diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index f35f6bde..70aa2724 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -10,9 +10,9 @@ jobs: OS: ${{ matrix.os }} PYTHON: '3.9' steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@master + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Generate coverage report @@ -23,7 +23,7 @@ jobs: pip install -r requirements-test.txt pytest --cov=./pyedflib/ --cov-report=xml --cov-config=.coveragerc - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: . env_vars: OS,PYTHON diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 00000000..4f72daac --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,15 @@ +name: Lint +on: + pull_request: + branches: + - '*' +permissions: + contents: read +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + - name: Lint with Ruff + run: ruff check --output-format=github diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80d7d79e..562b903c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,9 +12,9 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install packages diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 92000c51..5b3e562c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.8' - name: Install deps @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v4 # Used to host cibuildwheel - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' - name: Install cibuildwheel diff --git a/demo/readEDFFile.py b/demo/readEDFFile.py index 37415fed..646dac10 100644 --- a/demo/readEDFFile.py +++ b/demo/readEDFFile.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -import os - import numpy as np import pyedflib diff --git a/demo/streamingEDFFile.py b/demo/streamingEDFFile.py index e4845bc0..1c76a20f 100644 --- a/demo/streamingEDFFile.py +++ b/demo/streamingEDFFile.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -import time - import matplotlib.animation as animation import matplotlib.pyplot as plt import numpy as np diff --git a/doc/source/conf.py b/doc/source/conf.py index b4786355..a9f78c78 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -9,12 +9,12 @@ # # All configuration values have a default; values that are commented out # serve to show the default. -import datetime import re -import jinja2.filters import numpy as np +import pyedflib + # FIXME: doctests need the str/repr formatting used in Numpy < 1.14. try: np.set_printoptions(legacy='1.13') @@ -63,8 +63,6 @@ # built documents. # # The short X.Y version. -import pyedflib - version = re.sub(r'\.dev0+.*$', r'.dev', pyedflib.__version__) release = pyedflib.__version__ diff --git a/pyedflib/__init__.py b/pyedflib/__init__.py index a401d952..7f08d031 100644 --- a/pyedflib/__init__.py +++ b/pyedflib/__init__.py @@ -1,5 +1,3 @@ -# flake8: noqa - # Copyright (c) 2019 - 2020 Simon Kern # Copyright (c) 2015 - 2020 Holger Nahrstaedt # Copyright (c) 2016-2017 The pyedflib Developers diff --git a/pyedflib/_extensions/_pyedflib.pyi b/pyedflib/_extensions/_pyedflib.pyi index 901f5429..4d4a8616 100644 --- a/pyedflib/_extensions/_pyedflib.pyi +++ b/pyedflib/_extensions/_pyedflib.pyi @@ -1,22 +1,61 @@ -from typing import List, Dict, Union, Optional import numpy as np -from datetime import datetime -__all__ = ['lib_version', 'CyEdfReader', 'set_patientcode', 'set_starttime_subsecond', - 'write_annotation_latin1', 'write_annotation_utf8', 'set_technician', 'EdfAnnotation', - 'get_annotation', 'read_int_samples', 'blockwrite_digital_samples', 'blockwrite_physical_samples', - 'set_recording_additional', 'write_physical_samples', 'set_patientname', 'set_physical_minimum', - 'read_physical_samples', 'close_file', 'set_physical_maximum', 'open_file_writeonly', - 'set_patient_additional', 'set_digital_maximum', 'set_birthdate', 'set_digital_minimum', - 'write_digital_samples', 'set_equipment', 'set_samples_per_record', 'set_admincode', 'set_label', - 'tell', 'rewind', 'set_sex', 'set_gender', 'set_physical_dimension', 'set_transducer', - 'set_prefilter', 'seek', 'set_startdatetime', 'set_datarecord_duration', - 'set_number_of_annotation_signals', 'open_errors', 'FILETYPE_EDFPLUS', - 'FILETYPE_EDF', 'FILETYPE_BDF', 'FILETYPE_BDFPLUS', 'write_errors', 'get_number_of_open_files', - 'get_handle', 'is_file_used', 'blockwrite_digital_short_samples', 'write_digital_short_samples'] +__all__ = [ + 'FILETYPE_BDF', + 'FILETYPE_BDFPLUS', + 'FILETYPE_EDF', + 'FILETYPE_EDFPLUS', + 'CyEdfReader', + 'EdfAnnotation', + 'blockwrite_digital_samples', + 'blockwrite_digital_short_samples', + 'blockwrite_physical_samples', + 'close_file', + 'get_annotation', + 'get_handle', + 'get_number_of_open_files', + 'is_file_used', + 'lib_version', + 'open_errors', + 'open_file_writeonly', + 'read_int_samples', + 'read_physical_samples', + 'rewind', + 'seek', + 'set_admincode', + 'set_birthdate', + 'set_datarecord_duration', + 'set_digital_maximum', + 'set_digital_minimum', + 'set_equipment', + 'set_gender', + 'set_label', + 'set_number_of_annotation_signals', + 'set_patient_additional', + 'set_patientcode', + 'set_patientname', + 'set_physical_dimension', + 'set_physical_maximum', + 'set_physical_minimum', + 'set_prefilter', + 'set_recording_additional', + 'set_samples_per_record', + 'set_sex', + 'set_startdatetime', + 'set_starttime_subsecond', + 'set_technician', + 'set_transducer', + 'tell', + 'write_annotation_latin1', + 'write_annotation_utf8', + 'write_digital_samples', + 'write_digital_short_samples', + 'write_errors', + 'write_physical_samples', +] -open_errors: Dict[int, str] -write_errors: Dict[int, str] +open_errors: dict[int, str] +write_errors: dict[int, str] FILETYPE_EDF: int FILETYPE_EDFPLUS: int @@ -35,7 +74,7 @@ class CyEdfReader: def check_open_ok(self, result: int) -> bool: ... def make_buffer(self) -> np.ndarray: ... def open(self, file_name: str, annotations_mode: int = ..., check_file_size: int = ...) -> bool: ... - def read_annotation(self) -> List[List[str]]: ... + def read_annotation(self) -> list[list[str]]: ... def _close(self) -> None: ... def read_digital_signal(self, signalnum: int, start: int, n: int, sigbuf: np.ndarray[np.int32_t]) -> None: ... def readsignal(self, signalnum: int, start: int, n: int, sigbuf: np.ndarray[np.float64_t]) -> None: ... @@ -118,19 +157,19 @@ class EdfAnnotation: duration: int annotation: str -def set_patientcode(handle: int, patientcode: Union[str, bytes]) -> int: ... -def write_annotation_latin1(handle: int, onset: int, duration: int, description: Union[str, bytes]) -> int: ... -def write_annotation_utf8(handle: int, onset: int, duration: int, description: Union[str, bytes]) -> int: ... -def set_technician(handle: int, technician: Union[str, bytes]) -> int: ... +def set_patientcode(handle: int, patientcode: str | bytes) -> int: ... +def write_annotation_latin1(handle: int, onset: int, duration: int, description: str | bytes) -> int: ... +def write_annotation_utf8(handle: int, onset: int, duration: int, description: str | bytes) -> int: ... +def set_technician(handle: int, technician: str | bytes) -> int: ... def get_annotation(handle: int, n: int, edf_annotation: EdfAnnotation) -> int: ... def read_int_samples(handle: int, edfsignal: int, n: int, buf: np.ndarray[np.int32_t]) -> int: ... def blockwrite_digital_samples(handle: int, buf: np.ndarray[np.int32_t]) -> int: ... def blockwrite_digital_short_samples(handle: int, buf: np.ndarray[np.int16_t]) -> int: ... def blockwrite_physical_samples(handle: int, buf: np.ndarray[np.float64_t]) -> int: ... -def set_recording_additional(handle: int, recording_additional: Union[str, bytes]) -> int: ... +def set_recording_additional(handle: int, recording_additional: str | bytes) -> int: ... def write_digital_short_samples(handle: int, buf: np.ndarray[np.int16_t]) -> int: ... def write_physical_samples(handle: int, buf: np.ndarray[np.float64_t]) -> int: ... -def set_patientname(handle: int, name: Union[str,bytes]) -> int: ... +def set_patientname(handle: int, name: str | bytes) -> int: ... def set_physical_minimum(handle: int, edfsignal: int, phys_min: float) -> int: ... def read_physical_samples(handle: int, edfsignal: int, n: int, buf: np.ndarray[np.float64_t]) -> int: ... def close_file(handle: int) -> int: ... @@ -139,26 +178,25 @@ def get_handle(file_number: int) -> int: ... def is_file_used(path: str) -> bool: ... def set_physical_maximum(handle: int, edfsignal: int, phys_max: float) -> int: ... def open_file_writeonly(path: str, filetype: int, number_of_signals: int) -> int: ... -def set_patient_additional(handle: int, patient_additional: Union[str, bytes]) -> int: ... +def set_patient_additional(handle: int, patient_additional: str | bytes) -> int: ... def set_digital_maximum(handle: int, edfsignal: int, dig_max: int) -> int: ... def set_birthdate(handle: int, birthdate_year: int, birthdate_month: int, birthdate_day: int) -> int: ... def set_digital_minimum(handle: int, edfsignal: int, dig_min: int) -> int: ... def write_digital_samples(handle: int, buf: np.ndarray[np.int32_t]) -> int: ... -def set_equipment(handle: int, equipment: Union[str, bytes]) -> int: ... +def set_equipment(handle: int, equipment: str | bytes) -> int: ... def set_samples_per_record(handle: int, edfsignal: int, smp_per_record: int) -> int: ... -def set_admincode(handle: int, admincode: Union[str, bytes]) -> int: ... -def set_label(handle: int, edfsignal: int, label: Union[str, bytes]) -> int: ... +def set_admincode(handle: int, admincode: str | bytes) -> int: ... +def set_label(handle: int, edfsignal: int, label: str | bytes) -> int: ... def tell(handle: int, edfsignal: int) -> int: ... def rewind(handle: int, edfsignal: int) -> None: ... -def set_sex(handle: int, sex: Optional[int]) -> int: ... +def set_sex(handle: int, sex: int | None) -> int: ... def set_gender(handle: int, gender: int) -> int: ... -def set_physical_dimension(handle: int, edfsignal: int, phys_dim: Union[str, bytes]) -> int: ... -def set_transducer(handle: int, edfsignal: int, transducer: Union[str, bytes]) -> int: ... -def set_prefilter(handle: int, edfsignal: int, prefilter: Union[str, bytes]) -> int: ... +def set_physical_dimension(handle: int, edfsignal: int, phys_dim: str | bytes) -> int: ... +def set_transducer(handle: int, edfsignal: int, transducer: str | bytes) -> int: ... +def set_prefilter(handle: int, edfsignal: int, prefilter: str | bytes) -> int: ... def seek(handle: int, edfsignal: int, offset: int, whence: int) -> int: ... def set_startdatetime(handle: int, startdate_year: int, startdate_month: int, startdate_day: int, starttime_hour: int, starttime_minute: int, starttime_second: int) -> int: ... def set_starttime_subsecond(handle: int, subsecond: int) -> int: ... -def set_datarecord_duration(handle: int, duration: Union[int, float]) -> int: ... +def set_datarecord_duration(handle: int, duration: int | float) -> int: ... def set_number_of_annotation_signals(handle: int, annot_signals: int) -> int: ... - diff --git a/pyedflib/data/_readers.py b/pyedflib/data/_readers.py index e38fcd13..7818e161 100644 --- a/pyedflib/data/_readers.py +++ b/pyedflib/data/_readers.py @@ -1,7 +1,5 @@ import os -import numpy as np - import pyedflib diff --git a/pyedflib/edfreader.py b/pyedflib/edfreader.py index b82fd207..9d346054 100644 --- a/pyedflib/edfreader.py +++ b/pyedflib/edfreader.py @@ -15,13 +15,13 @@ from ._extensions._pyedflib import CyEdfReader __all__ = [ - "EdfReader", - "DO_NOT_READ_ANNOTATIONS", - "READ_ANNOTATIONS", - "READ_ALL_ANNOTATIONS", "CHECK_FILE_SIZE", "DO_NOT_CHECK_FILE_SIZE", + "DO_NOT_READ_ANNOTATIONS", + "READ_ALL_ANNOTATIONS", + "READ_ANNOTATIONS", "REPAIR_FILE_SIZE_IF_WRONG", + "EdfReader", ] DO_NOT_READ_ANNOTATIONS = 0 @@ -504,7 +504,7 @@ def getSampleFrequency(self, chn: int) -> float: return self.samplefrequency(chn) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -553,7 +553,7 @@ def getLabel(self, chn: int) -> str: return self._convert_string(self.signal_label(chn).rstrip()) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -580,7 +580,7 @@ def getPrefilter(self, chn: int) -> str: return self._convert_string(self.prefilter(chn).rstrip()) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -608,7 +608,7 @@ def getPhysicalMaximum(self, chn: Optional[int] = None) -> Union[float, np.ndarr return self.physical_max(chn) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -641,7 +641,7 @@ def getPhysicalMinimum(self, chn: Optional[int] = None) -> Union[float, np.ndarr return self.physical_min(chn) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -674,7 +674,7 @@ def getDigitalMaximum(self, chn: Optional[int] = None) -> Union[int, np.ndarray] return self.digital_max(chn) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -707,7 +707,7 @@ def getDigitalMinimum(self, chn: Optional[int] = None) -> Union[int, np.ndarray] return self.digital_min(chn) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -739,7 +739,7 @@ def getTransducer(self, chn: int): return self._convert_string(self.transducer(chn).rstrip()) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -766,7 +766,7 @@ def getPhysicalDimension(self, chn: int) -> str: return self._convert_string(self.physical_dimension(chn).rstrip()) else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) @@ -826,7 +826,7 @@ def readSignal( return x else: raise IndexError( - "Trying to access channel {}, but only {} " "channels found".format( + "Trying to access channel {}, but only {} channels found".format( chn, self.signals_in_file ) ) diff --git a/pyedflib/edfwriter.py b/pyedflib/edfwriter.py index 9caadf9e..47116f03 100644 --- a/pyedflib/edfwriter.py +++ b/pyedflib/edfwriter.py @@ -5,7 +5,6 @@ # # See LICENSE for license details. -import sys import warnings from datetime import date, datetime from types import TracebackType @@ -65,12 +64,12 @@ def check_is_ascii(string: str) -> None: https://www.edfplus.info/specs/edfplus.html#header """ - if not all([ord(x)>32 and ord(x)<127 for x in string]): - warnings.warn('Invalid char: header entries should contain only ASCII'\ + if not all(ord(x)>32 and ord(x)<127 for x in string): + warnings.warn('Invalid char: header entries should contain only ASCII' ' characters and no spaces: "{}"'.format(string)) -def check_signal_header_correct(channels: List[Dict[str, Union[str, None, float]]], i: int, file_type: int) -> None: +def check_signal_header_correct(channels: List[Dict[str, Union[str, float, None]]], i: int, file_type: int) -> None: """ helper function to check if all entries in the channel dictionary are fine. @@ -88,27 +87,27 @@ def check_signal_header_correct(channels: List[Dict[str, Union[str, None, float] label = ch['label'] if len(ch['label'])>16: # type: ignore - warnings.warn('Label of channel {} is longer than 16 ASCII chars.'\ + warnings.warn('Label of channel {} is longer than 16 ASCII chars. ' 'The label will be truncated to "{}"'.format(i, ch['label'][:16] )) # type: ignore if len(ch['prefilter'])>80: # type: ignore - warnings.warn('prefilter of channel {} is longer than 80 ASCII chars.'\ + warnings.warn('prefilter of channel {} is longer than 80 ASCII chars. ' 'The label will be truncated to "{}"'.format(i, ch['prefilter'][:80] )) # type: ignore if len(ch['transducer'])>80: # type: ignore - warnings.warn('transducer of channel {} is longer than 80 ASCII chars.'\ + warnings.warn('transducer of channel {} is longer than 80 ASCII chars. ' 'The label will be truncated to "{}"'.format(i, ch['transducer'][:80] )) # type: ignore if len(ch['dimension'])>80: # type: ignore - warnings.warn('dimension of channel {} is longer than 8 ASCII chars.'\ + warnings.warn('dimension of channel {} is longer than 8 ASCII chars. ' 'The label will be truncated to "{}"'.format(i, ch['dimension'][:8] )) # type: ignore # these ones actually raise an exception dmin, dmax = (-8388608, 8388607) if file_type in (FILETYPE_BDFPLUS, FILETYPE_BDF) else (-32768, 32767) if ch['digital_min']dmax: # type: ignore - raise ValueError('Digital maximum for channel {} ({}) is {},'\ + raise ValueError('Digital maximum for channel {} ({}) is {}, ' 'but maximum allowed value is {}'.format(i, label, ch['digital_max'], dmax)) @@ -117,29 +116,29 @@ def check_signal_header_correct(channels: List[Dict[str, Union[str, None, float] # if we truncate the physical min before the dot, we potentitally # have all the signals incorrect by an order of magnitude. if len(str(ch['physical_min']))>8 and ch['physical_min'] < -99999999: # type: ignore - raise ValueError('Physical minimum for channel {} ({}) is {}, which has {} chars, '\ - 'however, EDF+ can only save 8 chars, critical precision loss is expected, '\ + raise ValueError('Physical minimum for channel {} ({}) is {}, which has {} chars, ' + 'however, EDF+ can only save 8 chars, critical precision loss is expected, ' 'please convert the signals to another dimesion (eg uV to mV)'.format(i, label, ch['physical_min'], len(str(ch['physical_min'])))) if len(str(ch['physical_max']))>8 and ch['physical_max'] > 99999999: # type: ignore - raise ValueError('Physical minimum for channel {} ({}) is {}, which has {} chars, '\ - 'however, EDF+ can only save 8 chars, critical precision loss is expected, '\ + raise ValueError('Physical minimum for channel {} ({}) is {}, which has {} chars, ' + 'however, EDF+ can only save 8 chars, critical precision loss is expected, ' 'please convert the signals to another dimesion (eg uV to mV).'.format(i, label, ch['physical_max'], len(str(ch['physical_max'])))) # if we truncate the physical min behind the dot, we just lose precision, # in this case only a warning is enough if len(str(ch['physical_min']))>8: - warnings.warn('Physical minimum for channel {} ({}) is {}, which has {} chars, '\ - 'however, EDF+ can only save 8 chars, will be truncated to {}, '\ + warnings.warn('Physical minimum for channel {} ({}) is {}, which has {} chars, ' + 'however, EDF+ can only save 8 chars, will be truncated to {}, ' 'some loss of precision is to be expected'.format(i, label, ch['physical_min'], len(str(ch['physical_min'])), str(ch['physical_min'])[:8])) if len(str(ch['physical_max']))>8: - warnings.warn('Physical maximum for channel {} ({}) is {}, which has {} chars, '\ - 'however, EDF+ can only save 8 chars, will be truncated to {}, '\ + warnings.warn('Physical maximum for channel {} ({}) is {}, which has {} chars, ' + 'however, EDF+ can only save 8 chars, will be truncated to {}, ' 'some loss of precision is to be expected.'.format(i, label, ch['physical_max'], len(str(ch['physical_max'])), @@ -366,10 +365,10 @@ def update_header(self) -> None: + len('Startdate') + 3 + 11 # 3 spaces 11 birthdate if patient_ident>80: - warnings.warn('Patient code, name, sex and birthdate combined must not be larger than 80 chars. ' + + warnings.warn('Patient code, name, sex and birthdate combined must not be larger than 80 chars. ' f'Currently has len of {patient_ident}. See https://www.edfplus.info/specs/edfplus.html#additionalspecs') if record_ident>80: - warnings.warn('Equipment, technician, admincode and recording_additional combined must not be larger than 80 chars. ' + + warnings.warn('Equipment, technician, admincode and recording_additional combined must not be larger than 80 chars. ' f'Currently has len of {record_ident}. See https://www.edfplus.info/specs/edfplus.html#additionalspecs') # all data records (i.e. blocks of data of a channel) have one singular @@ -384,8 +383,8 @@ def update_header(self) -> None: raise FutureWarning('Use of `sample_rate` is deprecated, use `sample_frequency` instead') sample_freqs = [ch['sample_frequency'] for ch in self.channels] - if not self._enforce_record_duration and not any([f is None for f in sample_freqs]): - assert all([isinstance(f, (float, int)) for f in sample_freqs]), \ + if not self._enforce_record_duration and not any(f is None for f in sample_freqs): + assert all(isinstance(f, (float, int)) for f in sample_freqs), \ f'{sample_freqs=} contains non int/float' self.record_duration = _calculate_record_duration(sample_freqs) @@ -946,9 +945,8 @@ def writeSamples(self, data_list: Union[List[np.ndarray], np.ndarray], digital: 'transfer to C order for compatibility with edflib.') data_list = np.ascontiguousarray(data_list) - if digital: - if any([not np.issubdtype(a.dtype, np.integer) for a in data_list]): - raise TypeError('Digital = True requires all signals in int') + if digital and any(not np.issubdtype(a.dtype, np.integer) for a in data_list): + raise TypeError('Digital = True requires all signals in int') # Check that all channels have different physical_minimum and physical_maximum for chan in self.channels: @@ -956,10 +954,8 @@ def writeSamples(self, data_list: Union[List[np.ndarray], np.ndarray], digital: 'In chan {} physical_min {} should be different from '\ 'physical_max {}'.format(chan['label'], chan['physical_min'], chan['physical_max']) - ind = [] + ind = [0 for i in np.arange(len(data_list))] notAtEnd = True - for i in np.arange(len(data_list)): - ind.append(0) sampleLength = 0 smp_per_record = np.zeros(len(data_list), dtype=np.int32) @@ -1046,7 +1042,7 @@ def get_smp_per_record(self, ch_idx: int) -> int: smp_per_record = fs*record_duration if not np.isclose(np.round(smp_per_record), np.round(smp_per_record, 6)): - warnings.warn(f'Sample frequency {fs} can not be represented accurately. \n' + - f'smp_per_record={smp_per_record}, record_duration={record_duration} seconds,' + + warnings.warn(f'Sample frequency {fs} can not be represented accurately. \n' + f'smp_per_record={smp_per_record}, record_duration={record_duration} seconds,' f'calculated sample_frequency will be {np.round(smp_per_record)/record_duration}') return int(np.round(smp_per_record)) diff --git a/pyedflib/highlevel.py b/pyedflib/highlevel.py index 522ba08b..b909ebb2 100644 --- a/pyedflib/highlevel.py +++ b/pyedflib/highlevel.py @@ -46,7 +46,7 @@ def tqdm(iterable: Iterable, *args: Any, **kwargs: Any) -> Iterable: try: from tqdm import tqdm as iterator return iterator(iterable, *args, **kwargs) - except: + except Exception: return iterable @@ -71,15 +71,15 @@ def _parse_date(string: str) -> datetime: for f in formats: try: return datetime.strptime(string, f) - except: + except Exception: pass try: import dateparser return dateparser.parse(string) - except: - print('dateparser is not installed. to convert strings to dates'\ - 'install via `pip install dateparser`.') - raise ValueError('birthdate must be datetime object or of format'\ + except Exception: + print('dateparser is not installed. to convert strings to dates' + ' install via `pip install dateparser`.') + raise ValueError('birthdate must be datetime object or of format' ' `%d-%m-%Y`, eg. `24-01-2020`') def dig2phys(signal: Union[np.ndarray, int], dmin: int, dmax: int, pmin: float, pmax: float) -> Union[np.ndarray, float]: @@ -194,7 +194,7 @@ def make_header( """ - if not birthdate=='' and isinstance(birthdate, str): + if birthdate != '' and isinstance(birthdate, str): birthdate = _parse_date(birthdate) if startdate is None: now = datetime.now() @@ -210,7 +210,7 @@ def make_header( sex = gender warnings.warn("Parameter 'gender' is deprecated, use 'sex' instead.", DeprecationWarning, stacklevel=2) elif sex != gender: - raise ValueError("Defined both parameters 'sex' and 'gender', with different values: {sex} != {gender}") + raise ValueError(f"Defined both parameters 'sex' and 'gender', with different values: {sex} != {gender}") gender = sex local = locals() @@ -374,9 +374,10 @@ def read_edf( """ assert (ch_nrs is None) or (ch_names is None), \ 'names xor numbers should be supplied' - if ch_nrs is not None and not isinstance(ch_nrs, list): ch_nrs = [ch_nrs] - if ch_names is not None and \ - not isinstance(ch_names, list): ch_names = [ch_names] + if ch_nrs is not None and not isinstance(ch_nrs, list): + ch_nrs = [ch_nrs] + if ch_names is not None and not isinstance(ch_names, list): + ch_names = [ch_names] with pyedflib.EdfReader(edf_file) as f: # see which channels we want to load @@ -387,7 +388,7 @@ def read_edf( if ch_names is not None: ch_nrs = [] for ch in ch_names: - if not ch.upper() in available_chs: + if ch.upper() not in available_chs: warnings.warn('{} is not in source file (contains {})'\ .format(ch, available_chs)) print('will be ignored.') @@ -413,8 +414,7 @@ def read_edf( signals = [] - for i,c in enumerate(tqdm(ch_nrs, desc='Reading Channels', - disable=not verbose)): + for c in tqdm(ch_nrs, desc='Reading Channels', disable=not verbose): signal = f.readSignal(c, digital=digital) signals.append(signal) @@ -520,15 +520,15 @@ def write_edf( else: # only warning if difference is larger than the rounding error (which is quite large as edf scales data between phys_min and phys_max using -dig_min and +dig_max) edf_accuracy = min([sig.max()/dmax, sig.min()/dmin]) if abs(pmin - sig.min()) < edf_accuracy: - warnings.warn(f'phys_min is {pmin}, but signal_min is {sig.min()} ' \ - 'for channel {label}', category=UserWarning) + warnings.warn(f'phys_min is {pmin}, but signal_min is {sig.min()} ' + f'for channel {label}', category=UserWarning) else: # difference is > edf_accuracy assert pmin<=sig.min(), \ 'phys_min is {}, but signal_min is {} ' \ 'for channel {}'.format(pmin, sig.min(), label) if abs(sig.max() - pmax) < edf_accuracy: - warnings.warn(f'phys_max is {pmax}, but signal_max is {sig.max()} ' \ - 'for channel {label}', category=UserWarning) + warnings.warn(f'phys_max is {pmax}, but signal_max is {sig.max()} ' + f'for channel {label}', category=UserWarning) else: assert pmax>=sig.max(), \ 'phys_max is {}, but signal_max is {} ' \ @@ -613,7 +613,7 @@ def read_edf_header( summary['channels'] = f.getSignalLabels() if read_annotations: annotations = f.read_annotation() - annotations = [[float(t)/10000000, d if d else -1, x.decode()] for t,d,x in annotations] + annotations = [[float(t)/10000000, d or -1, x.decode()] for t,d,x in annotations] summary['annotations'] = annotations del f return summary @@ -650,10 +650,12 @@ def compare_edf( for i, sigs in enumerate(zip(signals1, signals2)): s1, s2 = sigs - if np.array_equal(s1, s2): continue # early stopping + if np.array_equal(s1, s2): + continue # early stopping s1 = np.abs(s1) s2 = np.abs(s2) - if np.array_equal(s1, s2): continue # early stopping + if np.array_equal(s1, s2): + continue # early stopping close = np.mean(np.isclose(s1, s2)) assert close>0.99, 'Error, digital values of {}'\ ' and {} for ch {}: {} are not the same: {:.3f}'.format( @@ -673,10 +675,12 @@ def compare_edf( s2 = dig2phys(s2, dmin2, dmax2, pmin2, pmax2) # compare absolutes in case of inverted signals - if np.array_equal(s1, s2): continue # early stopping + if np.array_equal(s1, s2): + continue # early stopping s1 = np.abs(s1) s2 = np.abs(s2) - if np.array_equal(s1, s2): continue # early stopping + if np.array_equal(s1, s2): + continue # early stopping min_dist = np.abs(dig2phys(1, dmin1, dmax1, pmin1, pmax1)) close = np.mean(np.isclose(s1, s2, atol=min_dist)) assert close>0.99, 'Error, physical values of {}'\ @@ -723,16 +727,18 @@ def drop_channels( """ # convert to list if necessary - if isinstance(to_keep, (int, str)): to_keep = [to_keep] - if isinstance(to_drop, (int, str)): to_drop = [to_drop] + if isinstance(to_keep, (int, str)): + to_keep = [to_keep] + if isinstance(to_drop, (int, str)): + to_drop = [to_drop] # check all parameters are good assert to_keep is None or to_drop is None,'Supply only to_keep xor to_drop' if to_keep is not None: - assert all([isinstance(ch, (str, int)) for ch in to_keep]),\ + assert all(isinstance(ch, (str, int)) for ch in to_keep),\ 'channels must be int or string' if to_drop is not None: - assert all([isinstance(ch, (str, int)) for ch in to_drop]),\ + assert all(isinstance(ch, (str, int)) for ch in to_drop),\ 'channels must be int or string' assert os.path.exists(edf_source), \ f'source file {edf_source} does not exist' @@ -992,12 +998,14 @@ def rename_channels( signal, signal_header, _ = read_edf(edf_file, digital=True, ch_nrs=ch_nr, verbose=verbose) ch = signal_header[0]['label'] - if ch in mapping : - if verbose: print(f'{ch} to {mapping[ch]}') + if ch in mapping: + if verbose: + print(f'{ch} to {mapping[ch]}') ch = mapping[ch] signal_header[0]['label']=ch else: - if verbose: print(f'no mapping for {ch}, leave as it is') + if verbose: + print(f'no mapping for {ch}, leave as it is') signal_headers.append(signal_header[0]) signals.append(signal.squeeze()) @@ -1037,7 +1045,8 @@ def change_polarity( if new_file is None: new_file = os.path.splitext(edf_file)[0] + '.edf' - if isinstance(channels, str): channels=[channels] + if isinstance(channels, str): + channels=[channels] channels = [c.lower() for c in channels] signals, signal_headers, header = read_edf(edf_file, digital=True, @@ -1045,9 +1054,11 @@ def change_polarity( for i,sig in enumerate(signals): label = signal_headers[i]['label'].lower() if label in channels: - if verbose: print(f'inverting {label}') + if verbose: + print(f'inverting {label}') signals[i] = -sig write_edf(new_file, signals, signal_headers, header, digital=True, correct=False, verbose=verbose) - if verify: compare_edf(edf_file, new_file) + if verify: + compare_edf(edf_file, new_file) return True diff --git a/pyedflib/tests/test_edfreader.py b/pyedflib/tests/test_edfreader.py index 17bddc25..f9e30ad8 100644 --- a/pyedflib/tests/test_edfreader.py +++ b/pyedflib/tests/test_edfreader.py @@ -286,7 +286,7 @@ def test_EdfReader_Check_Size(self): # sample_rate was deprecated, check that it does not appear anymore sheads = f.getSignalHeaders() for shead in sheads: - assert not 'sample_rate' in shead + assert 'sample_rate' not in shead assert 'sample_frequency' in shead del f @@ -330,7 +330,7 @@ def test_BdfReader_Read_accented_file(self): # sample_rate was deprecated, check that it does not appear anymore sheads = f.getSignalHeaders() for shead in sheads: - assert not 'sample_rate' in shead + assert 'sample_rate' not in shead assert 'sample_frequency' in shead del f diff --git a/pyedflib/tests/test_edfwriter.py b/pyedflib/tests/test_edfwriter.py index b788e4a7..232bb2eb 100644 --- a/pyedflib/tests/test_edfwriter.py +++ b/pyedflib/tests/test_edfwriter.py @@ -7,14 +7,13 @@ # from numpy.testing import (assert_raises, run_module_suite, # assert_equal, assert_allclose, assert_almost_equal) import unittest -import warnings from datetime import date, datetime import numpy as np import pyedflib from pyedflib.edfreader import EdfReader, _debug_parse_header -from pyedflib.edfwriter import ChannelDoesNotExist, EdfWriter, WrongInputSize +from pyedflib.edfwriter import ChannelDoesNotExist, EdfWriter from pyedflib.edfwriter import _calculate_record_duration @@ -442,16 +441,14 @@ def test_SampleWriting(self): data1 = np.ones(500) * 0.1 data2 = np.ones(500) * 0.2 - data_list = [] - data_list.append(data1) - data_list.append(data2) + data_list = [data1, data2] f.writeSamples(data_list) f.close() f = pyedflib.EdfReader(self.bdfplus_data_file) data1_read = f.readSignal(0) data2_read = f.readSignal(1) - f._close + f._close() np.testing.assert_equal(len(data1), len(data1_read)) np.testing.assert_equal(len(data2), len(data2_read)) np.testing.assert_almost_equal(data1, data1_read) @@ -498,9 +495,7 @@ def test_SampleWritingContextManager(self): f.setSignalHeader(1,channel_info2) data1 = np.ones(500) * 0.1 data2 = np.ones(500) * 0.2 - data_list = [] - data_list.append(data1) - data_list.append(data2) + data_list = [data1, data2] f.writeSamples(data_list) with pyedflib.EdfReader(self.bdfplus_data_file) as f: @@ -534,9 +529,7 @@ def test_SampleWriting2(self): data1 = np.ones(500) * 0.1 data2 = np.ones(500) * 0.2 - data_list = [] - data_list.append(data1) - data_list.append(data2) + data_list = [data1, data2] f.writeSamples(data_list) del f @@ -572,9 +565,7 @@ def test_SampleWriting_digital(self): data1 = np.arange(500, dtype=float) data2 = np.arange(500, dtype=float) - data_list = [] - data_list.append(data1) - data_list.append(data2) + data_list = [data1, data2] with np.testing.assert_raises(TypeError): f.writeSamples(data_list, digital=True) f.close() @@ -876,7 +867,6 @@ def test_force_record_duration(self): samples_per_record = 256 sample_frequency = 256 sample_freq_exp = int(256*record_duration)/record_duration - record_count = 4 f = pyedflib.EdfWriter(self.edf_data_file, channel_count, file_type=pyedflib.FILETYPE_EDF) with self.assertWarns(UserWarning): @@ -924,16 +914,17 @@ def test_sample_rate_deprecation(self): digMax = 32767 digMin = -digMax - base_signal_header = lambda idx: { - 'label': f'test_label{idx}', - 'dimension': 'mV', - 'physical_min': physMin, - 'physical_max': physMax, - 'digital_min': digMin, - 'digital_max': digMax, - 'transducer': f'trans{idx}', - 'prefilter': f'pre{idx}' - } + def base_signal_header(idx): + return { + 'label': f'test_label{idx}', + 'dimension': 'mV', + 'physical_min': physMin, + 'physical_max': physMax, + 'digital_min': digMin, + 'digital_max': digMax, + 'transducer': f'trans{idx}', + 'prefilter': f'pre{idx}' + } f = pyedflib.EdfWriter(self.edf_data_file, channel_count, file_type=pyedflib.FILETYPE_EDF) f.setDatarecordDuration(record_duration) diff --git a/pyedflib/tests/test_highlevel.py b/pyedflib/tests/test_highlevel.py index c492bbde..aa643aaa 100644 --- a/pyedflib/tests/test_highlevel.py +++ b/pyedflib/tests/test_highlevel.py @@ -227,11 +227,11 @@ def test_assertion_dmindmax(self): sheaders = [highlevel.make_signal_header('ch1', sample_frequency=256)] sheaders[0]['physical_min'] = -200 sheaders[0]['physical_max'] = 200 - + # a large difference between phys min and values should result in error with self.assertRaises(AssertionError): highlevel.write_edf(self.edfplus_data_file, signals, sheaders, digital=False) - + # A small roundoff difference between phys min and values should result in warning # edf_accuracy is calculated as: max_signals / digital_max or min_signals / digital_min # (whichever is smallest). digital_max = 2^16 / 2 @@ -242,7 +242,7 @@ def test_assertion_dmindmax(self): with self.assertWarnsRegex(expected_warning=UserWarning, expected_regex="phys_min is.*"): highlevel.write_edf(self.edfplus_data_file, signals, sheaders, digital=False) - + # It would be nice to doublecheck the written data in the files here. # However, the (rather inaccurate) data rescaling of EDF files makes this tricky. diff --git a/pyproject.toml b/pyproject.toml index d546d3d0..68df3df8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,31 @@ requires = [ "cython" ] build-backend = "setuptools.build_meta" + +[tool.ruff] +target-version = "py38" + +[tool.ruff.lint] +extend-select = [ + "UP", # pyupgrade + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "PIE", # flake8-pie + "FLY", # flynt + "PERF", # Perflint + "RUF", # Ruff-specific rules +] +ignore = [ + "F841", # Local variable is assigned to but never used + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "ISC002", # Implicitly concatenated string literals over multiple lines + "PERF203", # `try`-`except` within a loop incurs performance overhead + "RUF003", + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF100", # Unused `noqa` directive +] + +[tool.ruff.lint.extend-per-file-ignores] +"**/__init__.py" = ["F401", "F403"] diff --git a/requirements-test.txt b/requirements-test.txt index 54a1eb0d..e1d6df41 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,11 +1,11 @@ pip setuptools wheel -flake8 nose coverage cython numpy pytest +ruff dateparser tqdm diff --git a/setup.py b/setup.py index b242b1af..8238cc2f 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ import setuptools from setuptools import Extension, setup +from setuptools.command.develop import develop try: from Cython.Build import cythonize @@ -69,7 +70,7 @@ def get_numpy_include(): import builtins builtins.__NUMPY_SETUP__ = False import numpy as np - except ImportError as e: + except ImportError: try: # Try to install numpy from setuptools import dist @@ -229,8 +230,6 @@ def write_version_py(filename='pyedflib/version.py'): for module, source, in zip(cython_modules, cython_sources) ] -from setuptools.command.develop import develop - class develop_build_clib(develop): """Ugly monkeypatching to get clib to build for development installs @@ -305,10 +304,12 @@ def install_for_development(self): "Programming Language :: C", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules" ], platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], diff --git a/util/authors.py b/util/authors.py index ed784ab1..b4736c99 100644 --- a/util/authors.py +++ b/util/authors.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- encoding:utf-8 -*- """ List the authors who contributed within a given revision interval:: @@ -14,7 +13,6 @@ # Author: Pauli Virtanen . This script is in the public domain. import collections -import io import optparse import os import re @@ -51,28 +49,27 @@ def analyze_line(line, names, disp=False): line = line.strip().decode() # Check the commit author name - m = re.match(u'^@@@([^@]*)@@@', line) + m = re.match(r'^@@@([^@]*)@@@', line) if m: name = m.group(1) line = line[m.end():] name = NAME_MAP.get(name, name) - if disp: - if name not in names: - stdout_b.write((" - Author: %s\n" % name).encode()) + if disp and name not in names: + stdout_b.write((" - Author: %s\n" % name).encode()) names.update((name,)) # Look for "thanks to" messages in the commit log m = re.search(r'([Tt]hanks to|[Cc]ourtesy of|Co-authored-by:) ([A-Z][A-Za-z]*? [A-Z][A-Za-z]*? [A-Z][A-Za-z]*|[A-Z][A-Za-z]*? [A-Z]\. [A-Z][A-Za-z]*|[A-Z][A-Za-z ]*? [A-Z][A-Za-z]*|[a-z0-9]+)($|\.| )', line) if m: name = m.group(2) - if name not in (u'this',): + if name != 'this': if disp: stdout_b.write(" - Log : %s\n" % line.strip().encode()) name = NAME_MAP.get(name, name) names.update((name,)) line = line[m.end():].strip() - line = re.sub(r'^(and|, and|, ) ', u'Thanks to ', line) + line = re.sub(r'^(and|, and|, ) ', 'Thanks to ', line) analyze_line(line.encode(), names) # Find all authors before the named range @@ -87,19 +84,16 @@ def analyze_line(line, names, disp=False): # Sort def name_key(fullname): - m = re.search(u' [a-z ]*[A-Za-z-]+$', fullname) + m = re.search(r' [a-z ]*[A-Za-z-]+$', fullname) if m: forename = fullname[:m.start()].strip() surname = fullname[m.start():].strip() else: forename = "" surname = fullname.strip() - if surname.startswith(u'van der '): - surname = surname[8:] - if surname.startswith(u'de '): - surname = surname[3:] - if surname.startswith(u'von '): - surname = surname[4:] + surname = surname.removeprefix('van der ') + surname = surname.removeprefix('de ') + surname = surname.removeprefix('von ') return (surname.lower(), forename.lower()) # generate set of all new authors @@ -108,7 +102,7 @@ def name_key(fullname): n_authors = list(new_authors) n_authors.sort(key=name_key) # Print some empty lines to separate - stdout_b.write(("\n\n").encode()) + stdout_b.write(b"\n\n") for author in n_authors: stdout_b.write(("- %s\n" % author).encode()) # return for early exit so we only print new authors @@ -142,19 +136,19 @@ def name_key(fullname): People with a "+" by their names contributed a patch for the first time. This list of names is automatically generated, and may not be fully complete. -""" % dict(count=len(authors))).encode()) +""" % {"count": len(authors)}).encode()) - stdout_b.write(("\nNOTE: Check this list manually! It is automatically generated " - "and some names\n may be missing.\n").encode()) + stdout_b.write(b"\nNOTE: Check this list manually! It is automatically generated " + b"and some names\n may be missing.\n") def load_name_map(filename): name_map = {} - with io.open(filename, 'r', encoding='utf-8') as f: + with open(filename, encoding='utf-8') as f: for line in f: line = line.strip() - if line.startswith(u"#") or not line: + if line.startswith("#") or not line: continue m = re.match(r'^(.*?)\s*<(.*?)>(.*?)\s*<(.*?)>\s*$', line) @@ -205,12 +199,12 @@ def __call__(self, command, *a, **kw): def pipe(self, command, *a, **kw): stdin = kw.pop('stdin', None) - p = self._call(command, a, dict(stdin=stdin, stdout=subprocess.PIPE), + p = self._call(command, a, {"stdin": stdin, "stdout": subprocess.PIPE}, call=False, **kw) return p.stdout def read(self, command, *a, **kw): - p = self._call(command, a, dict(stdout=subprocess.PIPE), + p = self._call(command, a, {"stdout": subprocess.PIPE}, call=False, **kw) out, err = p.communicate() if p.returncode != 0: @@ -222,8 +216,8 @@ def readlines(self, command, *a, **kw): return out.rstrip("\n").split("\n") def test(self, command, *a, **kw): - ret = self._call(command, a, dict(stdout=subprocess.PIPE, - stderr=subprocess.PIPE), + ret = self._call(command, a, {"stdout": subprocess.PIPE, + "stderr": subprocess.PIPE}, call=True, **kw) return (ret == 0) diff --git a/util/gh_lists.py b/util/gh_lists.py index e483ea56..6fa884e0 100644 --- a/util/gh_lists.py +++ b/util/gh_lists.py @@ -80,7 +80,7 @@ def get_issues(getter, project, milestone): milestones = get_milestones(getter, project) mid = milestones[milestone] - url = "https://api.github.com/repos/{project}/issues?milestone={mid}&state=closed&sort=created&direction=asc" + url = "https://api.github.com/repos/{project}/issues?milestone={mid}&state=closed&sort=created&direction=asc" # noqa: RUF027 url = url.format(project=project, mid=mid) raw_datas = [] @@ -89,20 +89,19 @@ def get_issues(getter, project, milestone): raw_datas.append(raw_data) if 'link' not in info: break - m = re.search('<(.*?)>; rel="next"', info['link']) + m = re.search(r'<(.*?)>; rel="next"', info['link']) if m: url = m.group(1) continue break - issues = [] - - for raw_data in raw_datas: - data = json.loads(raw_data) - for issue_data in data: - issues.append(Issue(issue_data['number'], - issue_data['title'], - issue_data['html_url'])) + issues = [ + Issue(issue_data['number'], + issue_data['title'], + issue_data['html_url']) + for raw_data in raw_datas + for issue_data in json.loads(raw_data) + ] return issues diff --git a/util/refguide_check.py b/util/refguide_check.py index 536fb15c..8c2b046c 100644 --- a/util/refguide_check.py +++ b/util/refguide_check.py @@ -119,10 +119,12 @@ def find_names(module, names_dict): res = re.match(pattern, line) if res is not None: name = res.group(1) - entry = '.'.join([module_name, name]) + entry = f'{module_name}.{name}' # noqa: F841 names_dict.setdefault(module_name, set()).add(name) break + # FIXME: this function doesn't return anything + def get_all_dict(module): """Return a copy of the __all__ dict with irrelevant items removed.""" @@ -180,13 +182,13 @@ def compare(all_dict, others, names, module_name): def is_deprecated(f): - with warnings.catch_warnings(record=True) as w: + with warnings.catch_warnings(record=True): warnings.simplefilter("error") try: f(**{"not a kwarg": None}) except DeprecationWarning: return True - except: + except Exception: pass return False @@ -240,12 +242,12 @@ def validate_rst_syntax(text, name, dots=True): output_dot('E') return False, f"ERROR: {name}: no documentation" - ok_unknown_items = set([ + ok_unknown_items = { 'mod', 'currentmodule', 'autosummary', 'data', 'obj', 'versionadded', 'versionchanged', 'module', 'class', 'ref', 'func', 'toctree', 'moduleauthor', 'sectionauthor', 'codeauthor', 'eq', - ]) + } # Run through docutils error_stream = io.StringIO() @@ -257,16 +259,16 @@ def resolve(name, is_label=False): docutils.core.publish_doctree( text, token, - settings_overrides = dict(halt_level=5, - traceback=True, - default_reference_context='title-reference', - default_role='emphasis', - link_base='', - resolve_name=resolve, - stylesheet_path='', - raw_enabled=0, - file_insertion_enabled=0, - warning_stream=error_stream)) + settings_overrides = {'halt_level': 5, + 'traceback': True, + 'default_reference_context': 'title-reference', + 'default_role': 'emphasis', + 'link_base': '', + 'resolve_name': resolve, + 'stylesheet_path': '', + 'raw_enabled': 0, + 'file_insertion_enabled': 0, + 'warning_stream': error_stream}) # Print errors, disregarding unimportant ones error_msg = error_stream.getvalue() @@ -280,11 +282,10 @@ def resolve(name, is_label=False): continue m = re.match(r'.*Unknown (?:interpreted text role|directive type) "(.*)".*$', lines[0]) - if m: - if m.group(1) in ok_unknown_items: - continue + if m and m.group(1) in ok_unknown_items: + continue - m = re.match(r'.*Error in "math" directive:.*unknown option: "label"', " ".join(lines), re.S) + m = re.match(r'.*Error in "math" directive:.*unknown option: "label"', " ".join(lines), re.DOTALL) if m: continue @@ -338,14 +339,14 @@ def check_rest(module, names, dots=True): else: try: text = str(get_doc_object(obj)) - except: + except Exception: import traceback results.append((full_name, False, "Error in docstring format!\n" + traceback.format_exc())) continue - m = re.search("([\x00-\x09\x0b-\x1f])", text) + m = re.search(r"([\x00-\x09\x0b-\x1f])", text) if m: msg = ("Docstring contains a non-printable character %r! " "Maybe forgot r\"\"\"?" % (m.group(1),)) @@ -428,7 +429,7 @@ def report_failure(self, out, test, example, got): example, got) class Checker(doctest.OutputChecker): - obj_pattern = re.compile('at 0x[0-9a-fA-F]+>') + obj_pattern = re.compile(r'at 0x[0-9a-fA-F]+>') vanilla = doctest.OutputChecker() rndm_markers = {'# random', '# Random', '#random', '#Random', "# may vary"} stopwords = {'plt.', '.hist', '.show', '.ylim', '.subplot(', @@ -476,7 +477,7 @@ def check_output(self, want, got, optionflags): try: a_want = eval(want, dict(self.ns)) a_got = eval(got, dict(self.ns)) - except: + except Exception: if not self.parse_namedtuples: return False # suppose that "want" is a tuple, and "got" is smth like @@ -598,7 +599,7 @@ def check_doctests(module, verbose, ns=None, finder = doctest.DocTestFinder() try: tests = finder.find(obj, name, globs=dict(ns)) - except: + except Exception: import traceback results.append((full_name, False, "Failed to get doctests!\n" + @@ -667,10 +668,10 @@ def check_doctests_testfile(fname, verbose, ns=None, full_name = fname text = open(fname).read() - PSEUDOCODE = set(['some_function', 'some_module', 'import example', + PSEUDOCODE = {'some_function', 'some_module', 'import example', 'ctypes.CDLL', # likely need compiling, skip it 'integrate.nquad(func,' # ctypes integrate tutotial - ]) + } # split the text into "blocks" and try to detect and omit pseudocode blocks. parser = doctest.DocTestParser()