kopia lustrzana https://github.com/corrscope/corrscope
Use Mypy checking (#154)
Major changes: - LayoutConfig._calc_layout() improved - register_enum() replaced with DumpEnumAsStr (*very* iffy multiple inheritance) i configured mypy to not care about missing annotations... but most files are fully annotated via MonkeyType = big PR diff.pull/357/head
rodzic
e3cddd7e69
commit
9160958257
|
@ -189,3 +189,6 @@ setup.py
|
|||
# Corrscope pyinstaller version codes
|
||||
_version.py
|
||||
version.txt
|
||||
/annotations.json
|
||||
/monkeytype.sqlite3
|
||||
/err
|
||||
|
|
|
@ -46,6 +46,7 @@ build_script:
|
|||
test_script:
|
||||
- 'poetry run pytest --tb=short --cov=corrscope'
|
||||
- 'poetry run codecov'
|
||||
- 'poetry run mypy corrscope'
|
||||
|
||||
after_test:
|
||||
- 'if not "%pydir%"=="C:\Python37-x64" appveyor exit'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from os.path import abspath
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union, Dict, Any
|
||||
|
||||
import attr
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
|
@ -17,7 +17,7 @@ class ChannelConfig(DumpableAttrs):
|
|||
wav_path: str
|
||||
|
||||
# Supplying a dict inherits attributes from global trigger.
|
||||
trigger: Union[ITriggerConfig, dict, None] = attr.Factory(
|
||||
trigger: Union[ITriggerConfig, Dict[str, Any], None] = attr.Factory( # type: ignore
|
||||
dict
|
||||
) # TODO test channel-specific triggers
|
||||
# Multiplies how wide the window is, in milliseconds.
|
||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
|||
import sys
|
||||
from itertools import count
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple, Union, Iterator
|
||||
from typing import Optional, List, Tuple, Union, Iterator, cast
|
||||
|
||||
import click
|
||||
|
||||
|
@ -83,7 +83,7 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|||
@click.option('--profile', is_flag=True, help=
|
||||
'Debug: Write CProfiler snapshot')
|
||||
@click.version_option(corrscope.__version__)
|
||||
# fmt: on
|
||||
# fmt: on is ignored, because of https://github.com/ambv/black/issues/560
|
||||
def main(
|
||||
files: Tuple[str],
|
||||
# cfg
|
||||
|
@ -116,6 +116,8 @@ def main(
|
|||
show_gui = not any([write, play, render])
|
||||
|
||||
# Gather data for cfg: Config object.
|
||||
CfgOrPath = Union[Config, Path]
|
||||
|
||||
cfg_or_path: Union[Config, Path, None] = None
|
||||
cfg_dir: Optional[str] = None
|
||||
|
||||
|
@ -171,10 +173,11 @@ def main(
|
|||
cfg_dir = '.'
|
||||
|
||||
assert cfg_or_path is not None
|
||||
assert cfg_dir is not None
|
||||
if show_gui:
|
||||
def command():
|
||||
from corrscope import gui
|
||||
return gui.gui_main(cfg_or_path)
|
||||
return gui.gui_main(cast(CfgOrPath, cfg_or_path))
|
||||
|
||||
if profile:
|
||||
import cProfile
|
||||
|
@ -227,18 +230,20 @@ def main(
|
|||
except MissingFFmpegError as e:
|
||||
# Tell user how to install ffmpeg (__str__).
|
||||
print(e, file=sys.stderr)
|
||||
# fmt: on
|
||||
|
||||
|
||||
def get_profile_dump_name(prefix: str) -> str:
|
||||
now = datetime.datetime.now()
|
||||
now = now.strftime('%Y-%m-%d_T%H-%M-%S')
|
||||
now_date = datetime.datetime.now()
|
||||
now_str = now_date.strftime("%Y-%m-%d_T%H-%M-%S")
|
||||
|
||||
profile_dump_name = f'{prefix}-{PROFILE_DUMP_NAME}-{now}'
|
||||
profile_dump_name = f"{prefix}-{PROFILE_DUMP_NAME}-{now_str}"
|
||||
|
||||
# Write stats to unused filename
|
||||
for path in add_numeric_suffixes(profile_dump_name):
|
||||
if not Path(path).exists():
|
||||
return path
|
||||
assert False # never happens since add_numeric_suffixes is endless.
|
||||
|
||||
|
||||
def add_numeric_suffixes(s: str) -> Iterator[str]:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import pickle
|
||||
from enum import Enum
|
||||
from io import StringIO, BytesIO
|
||||
from typing import ClassVar, TYPE_CHECKING, Type, TypeVar, Set
|
||||
from typing import *
|
||||
|
||||
import attr
|
||||
from ruamel.yaml import (
|
||||
|
@ -13,6 +13,9 @@ from ruamel.yaml import (
|
|||
Node,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"yaml",
|
||||
"copy_config",
|
||||
|
@ -20,7 +23,7 @@ __all__ = [
|
|||
"KeywordAttrs",
|
||||
"Alias",
|
||||
"Ignored",
|
||||
"register_enum",
|
||||
"DumpEnumAsStr",
|
||||
"TypedEnumDump",
|
||||
"CorrError",
|
||||
"CorrWarning",
|
||||
|
@ -31,15 +34,18 @@ __all__ = [
|
|||
|
||||
|
||||
class MyYAML(YAML):
|
||||
def dump(self, data, stream=None, **kw):
|
||||
def dump(
|
||||
self, data: Any, stream: "Union[Path, TextIO, None]" = None, **kwargs
|
||||
) -> Optional[str]:
|
||||
""" Allow dumping to str. """
|
||||
inefficient = False
|
||||
if stream is None:
|
||||
inefficient = True
|
||||
stream = StringIO()
|
||||
YAML.dump(self, data, stream, **kw)
|
||||
YAML.dump(self, data, stream, **kwargs)
|
||||
if inefficient:
|
||||
return stream.getvalue()
|
||||
return cast(StringIO, stream).getvalue()
|
||||
return None
|
||||
|
||||
|
||||
class NoAliasRepresenter(RoundTripRepresenter):
|
||||
|
@ -49,7 +55,7 @@ class NoAliasRepresenter(RoundTripRepresenter):
|
|||
TODO test
|
||||
"""
|
||||
|
||||
def ignore_aliases(self, data):
|
||||
def ignore_aliases(self, data: Any) -> bool:
|
||||
if isinstance(data, Enum):
|
||||
return True
|
||||
return super().ignore_aliases(data)
|
||||
|
@ -109,7 +115,7 @@ class DumpableAttrs:
|
|||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __init_subclass__(cls, kw_only=False, always_dump: str = ""):
|
||||
def __init_subclass__(cls, kw_only: bool = False, always_dump: str = ""):
|
||||
cls.__always_dump = set(always_dump.split())
|
||||
del always_dump
|
||||
|
||||
|
@ -129,7 +135,7 @@ class DumpableAttrs:
|
|||
), f'Invalid always_dump="...{dump_field}" missing from class {cls.__name__}'
|
||||
|
||||
# SafeRepresenter.represent_yaml_object() uses __getstate__ to dump objects.
|
||||
def __getstate__(self):
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
""" Removes all fields with default values, but not found in
|
||||
self.always_dump. """
|
||||
|
||||
|
@ -157,8 +163,8 @@ class DumpableAttrs:
|
|||
continue
|
||||
# noinspection PyTypeChecker,PyUnresolvedReferences
|
||||
if (
|
||||
isinstance(field.default, attr.Factory)
|
||||
and field.default.factory() == value
|
||||
isinstance(field.default, attr.Factory) # type: ignore
|
||||
and field.default.factory() == value # type: ignore
|
||||
):
|
||||
continue
|
||||
|
||||
|
@ -167,7 +173,7 @@ class DumpableAttrs:
|
|||
return state
|
||||
|
||||
# SafeConstructor.construct_yaml_object() uses __setstate__ to load objects.
|
||||
def __setstate__(self, state):
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
""" Redirect `Alias(key)=value` to `key=value`.
|
||||
Then call the dataclass constructor (to validate parameters). """
|
||||
|
||||
|
@ -217,29 +223,30 @@ Ignored = object()
|
|||
|
||||
|
||||
# Setup Enum load/dump infrastructure
|
||||
SomeEnum = TypeVar("SomeEnum", bound=Enum)
|
||||
|
||||
|
||||
def register_enum(cls: Type):
|
||||
cls.to_yaml = _EnumMixin.to_yaml
|
||||
return _yaml_loadable(cls)
|
||||
|
||||
|
||||
class _EnumMixin:
|
||||
@classmethod
|
||||
def to_yaml(cls, representer: Representer, node: Enum):
|
||||
return representer.represent_str(node._name_)
|
||||
|
||||
|
||||
class TypedEnumDump:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
class DumpEnumAsStr(Enum):
|
||||
def __init_subclass__(cls):
|
||||
_yaml_loadable(cls)
|
||||
|
||||
@classmethod
|
||||
def to_yaml(cls: Type[Enum], representer: Representer, node: Enum):
|
||||
return representer.represent_scalar("!" + cls.__name__, node._name_)
|
||||
def to_yaml(cls, representer: Representer, node: Enum) -> Any:
|
||||
return representer.represent_str(node._name_) # type: ignore
|
||||
|
||||
|
||||
class TypedEnumDump(Enum):
|
||||
def __init_subclass__(cls):
|
||||
_yaml_loadable(cls)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls: Type[Enum], constructor: Constructor, node: Node):
|
||||
def to_yaml(cls, representer: Representer, node: Enum) -> Any:
|
||||
return representer.represent_scalar(
|
||||
"!" + cls.__name__, node._name_ # type: ignore
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, constructor: Constructor, node: Node) -> Enum:
|
||||
return cls[node.value]
|
||||
|
||||
|
||||
|
|
|
@ -1,33 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from enum import unique, IntEnum
|
||||
from enum import unique, Enum
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Optional, List, Union, TYPE_CHECKING, Callable
|
||||
from typing import Iterator
|
||||
from typing import Optional, List, Union, Callable, cast
|
||||
|
||||
import attr
|
||||
|
||||
from corrscope import outputs as outputs_
|
||||
from corrscope.channel import Channel, ChannelConfig
|
||||
from corrscope.config import KeywordAttrs, register_enum, CorrError
|
||||
from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError
|
||||
from corrscope.layout import LayoutConfig
|
||||
from corrscope.renderer import MatplotlibRenderer, RendererConfig
|
||||
from corrscope.triggers import ITriggerConfig, CorrelationTriggerConfig, PerFrameCache
|
||||
from corrscope.renderer import MatplotlibRenderer, RendererConfig, Renderer
|
||||
from corrscope.triggers import (
|
||||
ITriggerConfig,
|
||||
CorrelationTriggerConfig,
|
||||
PerFrameCache,
|
||||
CorrelationTrigger,
|
||||
)
|
||||
from corrscope.util import pushd, coalesce
|
||||
from corrscope.wave import Wave, Flatten
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from corrscope.triggers import CorrelationTrigger
|
||||
|
||||
|
||||
PRINT_TIMESTAMP = True
|
||||
|
||||
|
||||
@register_enum
|
||||
# Placing Enum before any other superclass results in errors.
|
||||
# Placing DumpEnumAsStr before IntEnum or (int, Enum) results in errors on Python 3.6:
|
||||
# - TypeError: object.__new__(BenchmarkMode) is not safe, use int.__new__()
|
||||
# I don't know *why* this works. It's magic.
|
||||
@unique
|
||||
class BenchmarkMode(IntEnum):
|
||||
class BenchmarkMode(int, DumpEnumAsStr, Enum):
|
||||
NONE = 0
|
||||
TRIGGER = 1
|
||||
RENDER = 2
|
||||
|
@ -89,7 +95,7 @@ class Config(
|
|||
show_internals: List[str] = attr.Factory(list)
|
||||
benchmark_mode: Union[str, BenchmarkMode] = BenchmarkMode.NONE
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
def __attrs_post_init__(self) -> None:
|
||||
# Cast benchmark_mode to enum.
|
||||
try:
|
||||
if not isinstance(self.benchmark_mode, BenchmarkMode):
|
||||
|
@ -186,7 +192,7 @@ class CorrScope:
|
|||
outputs: List[outputs_.Output]
|
||||
nchan: int
|
||||
|
||||
def _load_channels(self):
|
||||
def _load_channels(self) -> None:
|
||||
with pushd(self.arg.cfg_dir):
|
||||
# Tell user if master audio path is invalid.
|
||||
# (Otherwise, only ffmpeg uses the value of master_audio)
|
||||
|
@ -202,7 +208,7 @@ class CorrScope:
|
|||
self.nchan = len(self.channels)
|
||||
|
||||
@contextmanager
|
||||
def _load_outputs(self):
|
||||
def _load_outputs(self) -> Iterator[None]:
|
||||
with pushd(self.arg.cfg_dir):
|
||||
with ExitStack() as stack:
|
||||
self.outputs = [
|
||||
|
@ -211,13 +217,13 @@ class CorrScope:
|
|||
]
|
||||
yield
|
||||
|
||||
def _load_renderer(self):
|
||||
def _load_renderer(self) -> Renderer:
|
||||
renderer = MatplotlibRenderer(
|
||||
self.cfg.render, self.cfg.layout, self.nchan, self.cfg.channels
|
||||
)
|
||||
return renderer
|
||||
|
||||
def play(self):
|
||||
def play(self) -> None:
|
||||
if self.has_played:
|
||||
raise ValueError("Cannot call CorrScope.play() more than once")
|
||||
self.has_played = True
|
||||
|
@ -270,7 +276,7 @@ class CorrScope:
|
|||
if PRINT_TIMESTAMP:
|
||||
begin = time.perf_counter()
|
||||
|
||||
benchmark_mode = self.cfg.benchmark_mode
|
||||
benchmark_mode = cast(BenchmarkMode, self.cfg.benchmark_mode)
|
||||
not_benchmarking = not benchmark_mode
|
||||
|
||||
with self._load_outputs():
|
||||
|
@ -327,13 +333,13 @@ class CorrScope:
|
|||
|
||||
# region Display buffers, for debugging purposes.
|
||||
if extra_outputs.window:
|
||||
triggers: List["CorrelationTrigger"] = self.triggers
|
||||
triggers = cast(List[CorrelationTrigger], self.triggers)
|
||||
extra_outputs.window.render_frame(
|
||||
[trigger._prev_window for trigger in triggers]
|
||||
)
|
||||
|
||||
if extra_outputs.buffer:
|
||||
triggers: List["CorrelationTrigger"] = self.triggers
|
||||
triggers = cast(List[CorrelationTrigger], self.triggers)
|
||||
extra_outputs.buffer.render_frame(
|
||||
[trigger._buffer for trigger in triggers]
|
||||
)
|
||||
|
|
|
@ -3,13 +3,14 @@ import sys
|
|||
import traceback
|
||||
from pathlib import Path
|
||||
from types import MethodType
|
||||
from typing import Type, Optional, List, Any, Tuple, Callable, Union, Dict
|
||||
from typing import Optional, List, Any, Tuple, Callable, Union, Dict, Sequence
|
||||
|
||||
import PyQt5.QtCore as qc
|
||||
import PyQt5.QtWidgets as qw
|
||||
import attr
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import QModelIndex, Qt
|
||||
from PyQt5.QtCore import QVariant
|
||||
from PyQt5.QtGui import QKeySequence, QFont, QCloseEvent
|
||||
from PyQt5.QtWidgets import QShortcut
|
||||
|
||||
|
@ -216,7 +217,7 @@ class MainWindow(qw.QMainWindow):
|
|||
qw.QMessageBox.critical(self, "Error loading file", str(e))
|
||||
return
|
||||
|
||||
def load_cfg(self, cfg: Config, cfg_path: Optional[Path]):
|
||||
def load_cfg(self, cfg: Config, cfg_path: Optional[Path]) -> None:
|
||||
self._cfg_path = cfg_path
|
||||
self._any_unsaved = False
|
||||
self.load_title()
|
||||
|
@ -242,11 +243,11 @@ class MainWindow(qw.QMainWindow):
|
|||
|
||||
title_cache: str
|
||||
|
||||
def load_title(self):
|
||||
def load_title(self) -> None:
|
||||
self.title_cache = self.title
|
||||
self._update_unsaved_title()
|
||||
|
||||
def _update_unsaved_title(self):
|
||||
def _update_unsaved_title(self) -> None:
|
||||
if self.any_unsaved:
|
||||
undo_str = "*"
|
||||
else:
|
||||
|
@ -496,7 +497,9 @@ class CorrProgressDialog(qw.QProgressDialog):
|
|||
|
||||
|
||||
# *arg_types: type
|
||||
def run_on_ui_thread(bound_slot: MethodType, types: Tuple[type, ...]) -> Callable:
|
||||
def run_on_ui_thread(
|
||||
bound_slot: MethodType, types: Tuple[type, ...]
|
||||
) -> Callable[..., None]:
|
||||
""" Runs an object's slot on the object's own thread.
|
||||
It's terrible code but it works (as long as the slot has no return value).
|
||||
"""
|
||||
|
@ -565,7 +568,7 @@ def nrow_ncol_property(altered: str, unaltered: str) -> property:
|
|||
return property(get, set)
|
||||
|
||||
|
||||
def default_property(path: str, default) -> property:
|
||||
def default_property(path: str, default: Any) -> property:
|
||||
def getter(self: "ConfigModel"):
|
||||
val = rgetattr(self.cfg, path)
|
||||
if val is None:
|
||||
|
@ -600,6 +603,7 @@ def color2hex_maybe_property(path: str) -> property:
|
|||
return color2hex(color_attr)
|
||||
|
||||
def setter(self: "ConfigModel", val: str):
|
||||
color: Optional[str]
|
||||
if val:
|
||||
color = color2hex(val)
|
||||
else:
|
||||
|
@ -633,7 +637,7 @@ flatten_modes = {
|
|||
Flatten.DiffAvg: "DiffAvg: (L-R)/2",
|
||||
Flatten.Stereo: "Stereo (broken)",
|
||||
}
|
||||
assert set(flatten_modes.keys()) == set(Flatten.modes)
|
||||
assert set(flatten_modes.keys()) == set(Flatten.modes) # type: ignore
|
||||
|
||||
flatten_symbols = list(flatten_modes.keys())
|
||||
flatten_text = list(flatten_modes.values())
|
||||
|
@ -641,8 +645,8 @@ flatten_text = list(flatten_modes.values())
|
|||
|
||||
class ConfigModel(PresentationModel):
|
||||
cfg: Config
|
||||
combo_symbols: Dict[str, List[Symbol]] = {}
|
||||
combo_text: Dict[str, List[str]] = {}
|
||||
combo_symbols: Dict[str, Sequence[Symbol]] = {}
|
||||
combo_text: Dict[str, Sequence[str]] = {}
|
||||
|
||||
master_audio = path_fix_property("master_audio")
|
||||
|
||||
|
@ -751,7 +755,7 @@ class ChannelTableView(qw.QTableView):
|
|||
@attr.dataclass
|
||||
class Column:
|
||||
key: str
|
||||
cls: Union[Type, Callable]
|
||||
cls: Union[type, Callable[[str], Any]]
|
||||
default: Any
|
||||
|
||||
def _display_name(self) -> str:
|
||||
|
@ -787,7 +791,7 @@ class ChannelModel(qc.QAbstractTableModel):
|
|||
|
||||
cfg.trigger = trigger_dict
|
||||
|
||||
def triggers(self, row: int) -> dict:
|
||||
def triggers(self, row: int) -> Dict[str, Any]:
|
||||
trigger = self.channels[row].trigger
|
||||
assert isinstance(trigger, dict)
|
||||
return trigger
|
||||
|
@ -803,18 +807,17 @@ class ChannelModel(qc.QAbstractTableModel):
|
|||
Column("trigger__buffer_falloff", float, None),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _idx_of_key(col_data=col_data):
|
||||
return {col.key: idx for idx, col in enumerate(col_data)}
|
||||
|
||||
idx_of_key = _idx_of_key.__func__()
|
||||
idx_of_key = {}
|
||||
for idx, col in enumerate(col_data):
|
||||
idx_of_key[col.key] = idx
|
||||
del idx, col
|
||||
|
||||
def columnCount(self, parent: QModelIndex = ...) -> int:
|
||||
return len(self.col_data)
|
||||
|
||||
def headerData(
|
||||
self, section: int, orientation: Qt.Orientation, role=Qt.DisplayRole
|
||||
):
|
||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
|
||||
) -> Union[str, QVariant]:
|
||||
if role == Qt.DisplayRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
col = section
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import functools
|
||||
import operator
|
||||
from typing import Optional, List, Callable, Dict, Any, ClassVar, TYPE_CHECKING, Union
|
||||
from typing import (
|
||||
Optional,
|
||||
List,
|
||||
Callable,
|
||||
Dict,
|
||||
Any,
|
||||
ClassVar,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from PyQt5 import QtWidgets as qw, QtCore as qc
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
@ -36,8 +46,8 @@ class PresentationModel(qc.QObject):
|
|||
|
||||
# These fields are specific to each subclass, and assigned there.
|
||||
# Although less explicit, these can be assigned using __init_subclass__.
|
||||
combo_symbols: Dict[str, List[Symbol]]
|
||||
combo_text: Dict[str, List[str]]
|
||||
combo_symbols: Dict[str, Sequence[Symbol]]
|
||||
combo_text: Dict[str, Sequence[str]]
|
||||
edited = qc.pyqtSignal()
|
||||
|
||||
def __init__(self, cfg: DumpableAttrs):
|
||||
|
@ -45,7 +55,7 @@ class PresentationModel(qc.QObject):
|
|||
self.cfg = cfg
|
||||
self.update_widget: Dict[str, WidgetUpdater] = {}
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
try:
|
||||
# Custom properties
|
||||
return getattr(self, item)
|
||||
|
@ -70,7 +80,7 @@ class PresentationModel(qc.QObject):
|
|||
|
||||
|
||||
# TODO add tests for recursive operations
|
||||
def map_gui(view: "MainWindow", model: PresentationModel):
|
||||
def map_gui(view: "MainWindow", model: PresentationModel) -> None:
|
||||
"""
|
||||
Binding:
|
||||
- .ui <widget name="layout__nrows">
|
||||
|
@ -133,7 +143,7 @@ class BoundWidget(QWidget):
|
|||
# PresentationModel+set_model vs. cfg2gui+set_gui vs. widget
|
||||
# Feel free to improve the naming.
|
||||
|
||||
def cfg2gui(self):
|
||||
def cfg2gui(self) -> None:
|
||||
""" Update the widget without triggering signals.
|
||||
|
||||
When the presentation pmodel updates dependent widget 1,
|
||||
|
@ -153,7 +163,9 @@ class BoundWidget(QWidget):
|
|||
pass
|
||||
|
||||
|
||||
def blend_colors(color1: QColor, color2: QColor, ratio: float, gamma=2):
|
||||
def blend_colors(
|
||||
color1: QColor, color2: QColor, ratio: float, gamma: float = 2
|
||||
) -> QColor:
|
||||
""" Blends two colors in linear color space.
|
||||
Produces better results on both light and dark themes,
|
||||
than integer blending (which is too dark).
|
||||
|
@ -169,7 +181,7 @@ def blend_colors(color1: QColor, color2: QColor, ratio: float, gamma=2):
|
|||
return QColor.fromRgbF(*rgb_blend, 1.0)
|
||||
|
||||
|
||||
def model_setter(value_type: type) -> Callable:
|
||||
def model_setter(value_type: type) -> Callable[[Any], None]:
|
||||
@pyqtSlot(value_type)
|
||||
def set_model(self: BoundWidget, value):
|
||||
assert isinstance(value, value_type)
|
||||
|
@ -183,7 +195,7 @@ def model_setter(value_type: type) -> Callable:
|
|||
return set_model
|
||||
|
||||
|
||||
def alias(name: str):
|
||||
def alias(name: str) -> property:
|
||||
return property(operator.attrgetter(name))
|
||||
|
||||
|
||||
|
@ -208,7 +220,7 @@ class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget):
|
|||
|
||||
|
||||
class BoundComboBox(qw.QComboBox, BoundWidget):
|
||||
combo_symbols: List[Symbol]
|
||||
combo_symbols: Sequence[Symbol]
|
||||
symbol2idx: Dict[Symbol, int]
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
|
@ -228,7 +240,7 @@ class BoundComboBox(qw.QComboBox, BoundWidget):
|
|||
BoundWidget.bind_widget(self, model, path)
|
||||
|
||||
# combobox.index = pmodel.attr
|
||||
def set_gui(self, symbol: Symbol):
|
||||
def set_gui(self, symbol: Symbol) -> None:
|
||||
combo_index = self.symbol2idx[symbol]
|
||||
self.setCurrentIndex(combo_index)
|
||||
|
||||
|
@ -257,7 +269,7 @@ def behead(string: str, header: str) -> str:
|
|||
DUNDER = "__"
|
||||
|
||||
# https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288
|
||||
def rgetattr(obj, dunder_delim_path: str, *default):
|
||||
def rgetattr(obj: DumpableAttrs, dunder_delim_path: str, *default) -> Any:
|
||||
"""
|
||||
:param obj: Object
|
||||
:param dunder_delim_path: 'attr1__attr2__etc'
|
||||
|
@ -268,7 +280,7 @@ def rgetattr(obj, dunder_delim_path: str, *default):
|
|||
def _getattr(obj, attr):
|
||||
return getattr(obj, attr, *default)
|
||||
|
||||
attrs = dunder_delim_path.split(DUNDER)
|
||||
attrs: List[Any] = dunder_delim_path.split(DUNDER)
|
||||
return functools.reduce(_getattr, [obj] + attrs)
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class FileName:
|
|||
|
||||
|
||||
def _get_hist_name(
|
||||
func: Callable,
|
||||
func: Callable[..., Tuple[str, str]],
|
||||
history_dir: _gp.Ref[_gp.GlobalPrefs],
|
||||
parent: qw.QWidget,
|
||||
title: str,
|
||||
|
@ -37,7 +37,7 @@ def _get_hist_name(
|
|||
filter: str = ";;".join(filters)
|
||||
|
||||
# Call qw.QFileDialog.getXFileName[s].
|
||||
name, sel_filter = func(parent, title, dir_or_file, filter) # type: str, str
|
||||
name, sel_filter = func(parent, title, dir_or_file, filter)
|
||||
if not name:
|
||||
return None
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import html
|
||||
from typing import TypeVar, Iterable, Generic, Tuple
|
||||
from typing import TypeVar, Iterable, Generic, Tuple, Any, Optional
|
||||
|
||||
import matplotlib.colors
|
||||
import more_itertools
|
||||
from PyQt5.QtCore import QMutex
|
||||
from PyQt5.QtWidgets import QErrorMessage
|
||||
from PyQt5.QtWidgets import QErrorMessage, QWidget
|
||||
|
||||
from corrscope.config import CorrError
|
||||
|
||||
|
||||
def color2hex(color):
|
||||
def color2hex(color: Any) -> str:
|
||||
try:
|
||||
return matplotlib.colors.to_hex(color, keep_alpha=False)
|
||||
except ValueError:
|
||||
|
@ -33,7 +33,7 @@ class Locked(Generic[T]):
|
|||
self.lock.lock()
|
||||
return self.obj
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
def __exit__(self, *args) -> None:
|
||||
self.lock.unlock()
|
||||
|
||||
def set(self, value: T) -> T:
|
||||
|
@ -57,11 +57,11 @@ class TracebackDialog(QErrorMessage):
|
|||
</style>
|
||||
<body>%s</body>"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
QErrorMessage.__init__(self, parent)
|
||||
self.resize(self.w, self.h)
|
||||
|
||||
def showMessage(self, message, type=None):
|
||||
def showMessage(self, message: str, type: Any = None) -> None:
|
||||
message = self.template % (html.escape(message))
|
||||
QErrorMessage.showMessage(self, message, type)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional, TypeVar, Callable, List, Generic
|
||||
from typing import Optional, TypeVar, Callable, List, Generic, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
@ -11,7 +11,7 @@ class LayoutConfig(DumpableAttrs, always_dump="orientation"):
|
|||
nrows: Optional[int] = None
|
||||
ncols: Optional[int] = None
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
def __attrs_post_init__(self) -> None:
|
||||
if not self.nrows:
|
||||
self.nrows = None
|
||||
if not self.ncols:
|
||||
|
@ -45,7 +45,7 @@ class RendererLayout:
|
|||
f"{self.VALID_ORIENTATIONS}"
|
||||
)
|
||||
|
||||
def _calc_layout(self):
|
||||
def _calc_layout(self) -> Tuple[int, int]:
|
||||
"""
|
||||
Inputs: self.cfg, self.waves
|
||||
:return: (nrows, ncols)
|
||||
|
@ -58,17 +58,17 @@ class RendererLayout:
|
|||
raise ValueError("impossible cfg: nrows is None and true")
|
||||
ncols = ceildiv(self.nplots, nrows)
|
||||
else:
|
||||
ncols = cfg.ncols
|
||||
if ncols is None:
|
||||
if cfg.ncols is None:
|
||||
raise ValueError(
|
||||
"invalid LayoutConfig: nrows,ncols is None "
|
||||
"(__attrs_post_init__ not called?)"
|
||||
)
|
||||
ncols = cfg.ncols
|
||||
nrows = ceildiv(self.nplots, ncols)
|
||||
|
||||
return nrows, ncols
|
||||
|
||||
def arrange(self, region_factory: RegionFactory) -> List[Region]:
|
||||
def arrange(self, region_factory: RegionFactory[Region]) -> List[Region]:
|
||||
""" Generates an array of regions.
|
||||
|
||||
index, row, column are fed into region_factory in a row-major order [row][col].
|
||||
|
|
|
@ -3,7 +3,7 @@ import shlex
|
|||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
from os.path import abspath
|
||||
from typing import TYPE_CHECKING, Type, List, Union, Optional, ClassVar
|
||||
from typing import TYPE_CHECKING, Type, List, Union, Optional, ClassVar, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
@ -26,7 +26,7 @@ FFMPEG_QUIET = "-nostats -hide_banner -loglevel error".split()
|
|||
class IOutputConfig(DumpableAttrs):
|
||||
cls: "ClassVar[Type[Output]]"
|
||||
|
||||
def __call__(self, corr_cfg: "Config"):
|
||||
def __call__(self, corr_cfg: "Config") -> "Output":
|
||||
return self.cls(corr_cfg, cfg=self)
|
||||
|
||||
|
||||
|
@ -64,7 +64,9 @@ class Output(ABC):
|
|||
# Glue logic
|
||||
|
||||
|
||||
def register_output(config_t: Type[IOutputConfig]):
|
||||
def register_output(
|
||||
config_t: Type[IOutputConfig]
|
||||
) -> Callable[[Type[Output]], Type[Output]]:
|
||||
def inner(output_t: Type[Output]):
|
||||
config_t.cls = output_t
|
||||
return output_t
|
||||
|
@ -98,7 +100,7 @@ class _FFmpegProcess:
|
|||
if self.corr_cfg.master_audio:
|
||||
self.templates.append(cfg.audio_template) # audio
|
||||
|
||||
def popen(self, extra_args, bufsize, **kwargs) -> subprocess.Popen:
|
||||
def popen(self, extra_args: List[str], bufsize: int, **kwargs) -> subprocess.Popen:
|
||||
"""Raises FileNotFoundError if FFmpeg missing"""
|
||||
try:
|
||||
args = self._generate_args() + extra_args
|
||||
|
@ -130,7 +132,7 @@ def ffmpeg_input_audio(audio_path: str) -> List[str]:
|
|||
|
||||
|
||||
class PipeOutput(Output):
|
||||
def open(self, *pipeline: subprocess.Popen):
|
||||
def open(self, *pipeline: subprocess.Popen) -> None:
|
||||
""" Called by __init__ with a Popen pipeline to ffmpeg/ffplay. """
|
||||
if len(pipeline) == 0:
|
||||
raise TypeError("must provide at least one Popen argument to popens")
|
||||
|
@ -140,7 +142,7 @@ class PipeOutput(Output):
|
|||
# Python documentation discourages accessing popen.stdin. It's wrong.
|
||||
# https://stackoverflow.com/a/9886747
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Output:
|
||||
return self
|
||||
|
||||
def write_frame(self, frame: ByteBuffer) -> Optional[_Stop]:
|
||||
|
@ -161,7 +163,7 @@ class PipeOutput(Output):
|
|||
else:
|
||||
raise
|
||||
|
||||
def close(self, wait=True) -> int:
|
||||
def close(self, wait: bool = True) -> int:
|
||||
try:
|
||||
self._stream.close()
|
||||
# technically it should match the above exception handler,
|
||||
|
@ -183,7 +185,7 @@ class PipeOutput(Output):
|
|||
else:
|
||||
self.terminate()
|
||||
|
||||
def terminate(self):
|
||||
def terminate(self) -> None:
|
||||
# Calling self.close() is bad.
|
||||
# If exception occurred but ffplay continues running,
|
||||
# popen.wait() will prevent stack trace from showing up.
|
||||
|
|
|
@ -41,7 +41,7 @@ if TYPE_CHECKING:
|
|||
from corrscope.channel import ChannelConfig
|
||||
|
||||
|
||||
def default_color():
|
||||
def default_color() -> str:
|
||||
# import matplotlib.colors
|
||||
# colors = np.array([int(x, 16) for x in '1f 77 b4'.split()], dtype=float)
|
||||
# colors /= np.amax(colors)
|
||||
|
@ -63,7 +63,7 @@ class RendererConfig(DumpableAttrs, always_dump="*"):
|
|||
# Performance (skipped when recording to video)
|
||||
res_divisor: float = 1.0
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
def __attrs_post_init__(self) -> None:
|
||||
# round(np.int32 / float) == np.float32, but we want int.
|
||||
assert isinstance(self.width, (int, float))
|
||||
assert isinstance(self.height, (int, float))
|
||||
|
|
|
@ -33,7 +33,7 @@ class GlobalPrefs(DumpableAttrs, always_dump="*"):
|
|||
file_dir: str = ""
|
||||
|
||||
@property
|
||||
def file_dir_ref(self) -> Ref:
|
||||
def file_dir_ref(self) -> "Ref[GlobalPrefs]":
|
||||
return Ref(self, "file_dir")
|
||||
|
||||
# Most recent video rendered
|
||||
|
@ -41,7 +41,7 @@ class GlobalPrefs(DumpableAttrs, always_dump="*"):
|
|||
render_dir: str = "" # Set to "" whenever separate_render_dir=False.
|
||||
|
||||
@property
|
||||
def render_dir_ref(self) -> Ref:
|
||||
def render_dir_ref(self) -> "Ref[GlobalPrefs]":
|
||||
if self.separate_render_dir:
|
||||
return Ref(self, "render_dir")
|
||||
else:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
from typing import MutableMapping, List
|
||||
from pathlib import Path
|
||||
|
||||
from appdirs import user_data_dir
|
||||
|
@ -13,7 +13,7 @@ from corrscope.config import CorrError
|
|||
__all__ = ["appdata_dir", "PATH_dir", "get_ffmpeg_url", "MissingFFmpegError"]
|
||||
|
||||
|
||||
def prepend(dic: Dict[str, str], _key: List[str], prefix: str) -> None:
|
||||
def prepend(dic: MutableMapping[str, str], _key: List[str], prefix: str) -> None:
|
||||
""" Dubiously readable syntactic sugar for prepending to a string in a dict. """
|
||||
key = _key[0]
|
||||
dic[key] = prefix + dic[key]
|
||||
|
@ -36,7 +36,7 @@ def get_ffmpeg_url() -> str:
|
|||
# is_python_64 = sys.maxsize > 2 ** 32
|
||||
is_os_64 = platform.machine().endswith("64")
|
||||
|
||||
def url(os_ver):
|
||||
def url(os_ver: str) -> str:
|
||||
return f"https://ffmpeg.zeranoe.com/builds/{os_ver}/shared/ffmpeg-latest-{os_ver}-shared.zip"
|
||||
|
||||
if sys.platform == "win32" and is_os_64:
|
||||
|
@ -60,5 +60,5 @@ class MissingFFmpegError(CorrError):
|
|||
else:
|
||||
message += "Cannot download FFmpeg for your platform."
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Type, Tuple, Optional, ClassVar
|
||||
from typing import TYPE_CHECKING, Type, Tuple, Optional, ClassVar, Callable, Union
|
||||
|
||||
import attr
|
||||
import numpy as np
|
||||
|
@ -28,7 +28,9 @@ class ITriggerConfig(KeywordAttrs):
|
|||
return self.cls(wave, cfg=self, tsamp=tsamp, stride=stride, fps=fps)
|
||||
|
||||
|
||||
def register_trigger(config_t: Type[ITriggerConfig]):
|
||||
def register_trigger(
|
||||
config_t: Type[ITriggerConfig]
|
||||
) -> "Callable[[Type[Trigger]], Type[Trigger]]": # my god mypy-strict sucks
|
||||
""" @register_trigger(FooTriggerConfig)
|
||||
def FooTrigger(): ...
|
||||
"""
|
||||
|
@ -69,7 +71,7 @@ class Trigger(ABC):
|
|||
else:
|
||||
self.post = None
|
||||
|
||||
def time2tsamp(self, time: float):
|
||||
def time2tsamp(self, time: float) -> int:
|
||||
return round(time * self._wave.smp_s / self._stride)
|
||||
|
||||
@abstractmethod
|
||||
|
@ -122,7 +124,7 @@ class CorrelationTriggerConfig(ITriggerConfig):
|
|||
use_edge_trigger: bool
|
||||
# endregion
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self._validate_param("lag_prevention", 0, 1)
|
||||
self._validate_param("responsiveness", 0, 1)
|
||||
# TODO trigger_falloff >= 0
|
||||
|
@ -138,7 +140,7 @@ class CorrelationTriggerConfig(ITriggerConfig):
|
|||
else:
|
||||
self.post = ZeroCrossingTriggerConfig()
|
||||
|
||||
def _validate_param(self, key: str, begin, end):
|
||||
def _validate_param(self, key: str, begin: float, end: float) -> None:
|
||||
value = getattr(self, key)
|
||||
if not begin <= value <= end:
|
||||
raise CorrError(
|
||||
|
@ -174,10 +176,10 @@ class CorrelationTrigger(Trigger):
|
|||
self._windowed_step = self._calc_step()
|
||||
|
||||
# Will be overwritten on the first frame.
|
||||
self._prev_period = None
|
||||
self._prev_window = None
|
||||
self._prev_period: Optional[int] = None
|
||||
self._prev_window: Optional[np.ndarray] = None
|
||||
|
||||
def _calc_data_taper(self):
|
||||
def _calc_data_taper(self) -> np.ndarray:
|
||||
""" Input data window. Zeroes out all data older than 1 frame old.
|
||||
See https://github.com/nyanpasu64/corrscope/wiki/Correlation-Trigger
|
||||
"""
|
||||
|
@ -215,7 +217,7 @@ class CorrelationTrigger(Trigger):
|
|||
|
||||
return data_taper
|
||||
|
||||
def _calc_step(self):
|
||||
def _calc_step(self) -> np.ndarray:
|
||||
""" Step function used for approximate edge triggering. """
|
||||
|
||||
# Increasing buffer_falloff (width of history buffer)
|
||||
|
@ -304,7 +306,7 @@ class CorrelationTrigger(Trigger):
|
|||
|
||||
return trigger
|
||||
|
||||
def _is_window_invalid(self, period):
|
||||
def _is_window_invalid(self, period: int) -> bool:
|
||||
""" Returns True if pitch has changed more than `recalc_semitones`. """
|
||||
|
||||
prev = self._prev_period
|
||||
|
@ -354,7 +356,7 @@ class CorrelationTrigger(Trigger):
|
|||
# get_trigger()
|
||||
|
||||
|
||||
def calc_step(nsamp: int, peak: float, stdev: float):
|
||||
def calc_step(nsamp: int, peak: float, stdev: float) -> np.ndarray:
|
||||
""" Step function used for approximate edge triggering.
|
||||
TODO deduplicate CorrelationTrigger._calc_step() """
|
||||
N = nsamp
|
||||
|
@ -387,7 +389,7 @@ def get_period(data: np.ndarray) -> int:
|
|||
return int(peakX)
|
||||
|
||||
|
||||
def cosine_flat(n: int, diameter: int, falloff: int):
|
||||
def cosine_flat(n: int, diameter: int, falloff: int) -> np.ndarray:
|
||||
cosine = windows.hann(falloff * 2)
|
||||
left, right = cosine[:falloff], cosine[falloff:]
|
||||
|
||||
|
@ -410,7 +412,7 @@ def normalize_buffer(data: np.ndarray) -> None:
|
|||
data /= max(peak, MIN_AMPLITUDE)
|
||||
|
||||
|
||||
def lerp(x: np.ndarray, y: np.ndarray, a: float):
|
||||
def lerp(x: np.ndarray, y: np.ndarray, a: float) -> Union[np.ndarray, float]:
|
||||
return x * (1 - a) + y * a
|
||||
|
||||
|
||||
|
@ -530,7 +532,7 @@ class ZeroCrossingTrigger(PostTrigger):
|
|||
# ZeroCrossingTrigger is only used as a postprocessing trigger.
|
||||
# stride is only passed 1, for improved precision.
|
||||
|
||||
def get_trigger(self, index: int, cache: "PerFrameCache"):
|
||||
def get_trigger(self, index: int, cache: "PerFrameCache") -> int:
|
||||
# 'cache' is unused.
|
||||
tsamp = self._tsamp
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@ import sys
|
|||
from contextlib import contextmanager
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Callable, Tuple, TypeVar, Iterator, Union, Optional
|
||||
from typing import Callable, Tuple, TypeVar, Iterator, Union, Optional, Any, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def ceildiv(n, d):
|
||||
def ceildiv(n: int, d: int) -> int:
|
||||
return -(-n // d)
|
||||
|
||||
|
||||
|
@ -24,16 +24,16 @@ def coalesce(*args: Optional[T]) -> T:
|
|||
raise TypeError("coalesce() called with all None")
|
||||
|
||||
|
||||
def obj_name(obj) -> str:
|
||||
def obj_name(obj: Any) -> str:
|
||||
return type(obj).__name__
|
||||
|
||||
|
||||
# Adapted from https://github.com/numpy/numpy/issues/2269#issuecomment-14436725
|
||||
def find(
|
||||
a: "np.ndarray[T]",
|
||||
predicate: "Callable[[np.ndarray[T]], np.ndarray[bool]]",
|
||||
chunk_size=1024,
|
||||
) -> Iterator[Tuple[Tuple[int], T]]:
|
||||
a: "np.ndarray[float]",
|
||||
predicate: "Callable[[np.ndarray], np.ndarray[bool]]",
|
||||
chunk_size: int = 1024,
|
||||
) -> Iterator[Tuple[Tuple[int], float]]:
|
||||
"""
|
||||
Find the indices of array elements that match the predicate.
|
||||
|
||||
|
@ -88,11 +88,11 @@ def find(
|
|||
chunk = a[i0:i1]
|
||||
for idx in predicate(chunk).nonzero()[0]:
|
||||
yield (idx + i0,), chunk[idx]
|
||||
i0 = i1
|
||||
i0 = cast(int, i1)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pushd(new_dir: Union[Path, str]):
|
||||
def pushd(new_dir: Union[Path, str]) -> Iterator[None]:
|
||||
previous_dir = os.getcwd()
|
||||
os.chdir(str(new_dir))
|
||||
try:
|
||||
|
@ -101,5 +101,5 @@ def pushd(new_dir: Union[Path, str]):
|
|||
os.chdir(previous_dir)
|
||||
|
||||
|
||||
def perr(*args, **kwargs):
|
||||
def perr(*args, **kwargs) -> None:
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
|
|
@ -3,7 +3,7 @@ from bisect import bisect_left
|
|||
import numpy as np
|
||||
|
||||
|
||||
def correlate(in1, in2) -> np.ndarray:
|
||||
def correlate(in1: np.ndarray, in2: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Based on scipy.correlate.
|
||||
Assumed: mode='full', method='fft'
|
||||
|
@ -30,11 +30,11 @@ def correlate(in1, in2) -> np.ndarray:
|
|||
return ret
|
||||
|
||||
|
||||
def _reverse_and_conj(x):
|
||||
def _reverse_and_conj(x: np.ndarray) -> np.ndarray:
|
||||
return x[::-1].conj()
|
||||
|
||||
|
||||
def next_fast_len(target):
|
||||
def next_fast_len(target: int) -> int:
|
||||
"""
|
||||
Find the next fast size of input data to `fft`, for zero-padding, etc.
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@ import numpy
|
|||
import struct
|
||||
import warnings
|
||||
|
||||
from typing import Tuple, TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from io import BufferedReader
|
||||
|
||||
__all__ = [
|
||||
'WavFileWarning',
|
||||
|
@ -67,7 +70,9 @@ KNOWN_WAVE_FORMATS = (WAVE_FORMAT_PCM, WAVE_FORMAT_IEEE_FLOAT)
|
|||
# after the 'fmt ' id
|
||||
|
||||
|
||||
def _read_fmt_chunk(fid, is_big_endian):
|
||||
def _read_fmt_chunk(
|
||||
fid: "BufferedReader", is_big_endian: bool
|
||||
) -> Tuple[int, int, int, int, int, int, int]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
|
@ -133,8 +138,10 @@ def _read_fmt_chunk(fid, is_big_endian):
|
|||
|
||||
|
||||
# assumes file pointer is immediately after the 'data' id
|
||||
def _read_data_chunk(fid, format_tag, channels, bit_depth, is_big_endian,
|
||||
mmap=False):
|
||||
def _read_data_chunk(
|
||||
fid: "BufferedReader", format_tag: int, channels: int,
|
||||
bit_depth: int, is_big_endian: bool, mmap: bool = False
|
||||
) -> numpy.ndarray:
|
||||
if is_big_endian:
|
||||
fmt = '>I'
|
||||
else:
|
||||
|
@ -169,7 +176,7 @@ def _read_data_chunk(fid, format_tag, channels, bit_depth, is_big_endian,
|
|||
return data
|
||||
|
||||
|
||||
def _skip_unknown_chunk(fid, is_big_endian):
|
||||
def _skip_unknown_chunk(fid: "BufferedReader", is_big_endian: bool) -> None:
|
||||
if is_big_endian:
|
||||
fmt = '>I'
|
||||
else:
|
||||
|
@ -185,7 +192,7 @@ def _skip_unknown_chunk(fid, is_big_endian):
|
|||
fid.seek(size, 1)
|
||||
|
||||
|
||||
def _read_riff_chunk(fid):
|
||||
def _read_riff_chunk(fid: "BufferedReader") -> Tuple[int, bool]:
|
||||
str1 = fid.read(4) # File signature
|
||||
if str1 == b'RIFF':
|
||||
is_big_endian = False
|
||||
|
@ -208,7 +215,7 @@ def _read_riff_chunk(fid):
|
|||
return file_size, is_big_endian
|
||||
|
||||
|
||||
def read(filename, mmap=False):
|
||||
def read(filename: str, mmap: bool = False) -> Tuple[int, numpy.ndarray]:
|
||||
"""
|
||||
Open a WAV file
|
||||
|
||||
|
|
|
@ -33,8 +33,7 @@ THE POSSIBILITY OF SUCH DAMAGE.
|
|||
|
||||
from __future__ import division, print_function, absolute_import
|
||||
|
||||
import operator
|
||||
import warnings
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
@ -45,14 +44,14 @@ __all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
|
|||
'exponential', 'tukey']
|
||||
|
||||
|
||||
def _len_guards(M):
|
||||
def _len_guards(M: int) -> bool:
|
||||
"""Handle small or incorrect window lengths"""
|
||||
if int(M) != M or M < 0:
|
||||
raise ValueError('Window length M must be a non-negative integer')
|
||||
return M <= 1
|
||||
|
||||
|
||||
def _extend(M, sym):
|
||||
def _extend(M: int, sym: bool) -> Tuple[int, bool]:
|
||||
"""Extend window by 1 sample if needed for DFT-even symmetry"""
|
||||
if not sym:
|
||||
return M + 1, True
|
||||
|
@ -60,7 +59,7 @@ def _extend(M, sym):
|
|||
return M, False
|
||||
|
||||
|
||||
def _truncate(w, needed):
|
||||
def _truncate(w: np.ndarray, needed: bool) -> np.ndarray:
|
||||
"""Truncate window by 1 sample if needed for DFT-even symmetry"""
|
||||
if needed:
|
||||
return w[:-1]
|
||||
|
@ -68,7 +67,7 @@ def _truncate(w, needed):
|
|||
return w
|
||||
|
||||
|
||||
def general_cosine(M, a, sym=True):
|
||||
def general_cosine(M: int, a: List[float], sym: bool = True) -> np.ndarray:
|
||||
r"""
|
||||
Generic weighted sum of cosine terms window
|
||||
|
||||
|
@ -734,7 +733,7 @@ def bartlett(M, sym=True):
|
|||
return _truncate(w, needs_trunc)
|
||||
|
||||
|
||||
def hann(M, sym=True):
|
||||
def hann(M: int, sym: bool = True) -> np.ndarray:
|
||||
r"""
|
||||
Return a Hann window.
|
||||
|
||||
|
@ -958,7 +957,7 @@ def barthann(M, sym=True):
|
|||
return _truncate(w, needs_trunc)
|
||||
|
||||
|
||||
def general_hamming(M, alpha, sym=True):
|
||||
def general_hamming(M: int, alpha: float, sym: bool = True) -> np.ndarray:
|
||||
r"""Return a generalized Hamming window.
|
||||
|
||||
The generalized Hamming window is constructed by multiplying a rectangular
|
||||
|
@ -1126,7 +1125,7 @@ def hamming(M, sym=True):
|
|||
# def kaiser(M, beta, sym=True):
|
||||
|
||||
|
||||
def gaussian(M, std, sym=True):
|
||||
def gaussian(M: int, std: float, sym: bool = True) -> np.ndarray:
|
||||
r"""Return a Gaussian window.
|
||||
|
||||
Parameters
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import numpy as np
|
||||
|
||||
|
||||
def leftpad(data: np.ndarray, n: int):
|
||||
def leftpad(data: np.ndarray, n: int) -> np.ndarray:
|
||||
if not n > 0:
|
||||
raise ValueError(f"leftpad(n={n}) must be > 0")
|
||||
|
||||
|
@ -11,7 +11,7 @@ def leftpad(data: np.ndarray, n: int):
|
|||
return data
|
||||
|
||||
|
||||
def midpad(data: np.ndarray, n: int):
|
||||
def midpad(data: np.ndarray, n: int) -> np.ndarray:
|
||||
if not n > 0:
|
||||
raise ValueError(f"midpad(n={n}) must be > 0")
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ class Wave:
|
|||
data: "np.ndarray"
|
||||
"""2-D array of shape (nsamp, nchan)"""
|
||||
|
||||
_flatten: Flatten
|
||||
|
||||
@property
|
||||
def flatten(self) -> Flatten:
|
||||
"""
|
||||
|
@ -64,7 +66,7 @@ class Wave:
|
|||
@flatten.setter
|
||||
def flatten(self, flatten: Flatten) -> None:
|
||||
# Reject invalid modes (including Mono).
|
||||
if flatten not in Flatten.modes:
|
||||
if flatten not in Flatten.modes: # type: ignore
|
||||
# Flatten.Mono not in Flatten.modes.
|
||||
raise CorrError(
|
||||
f"Wave {self.wave_path} has invalid flatten mode {flatten} "
|
||||
|
@ -161,7 +163,7 @@ class Wave:
|
|||
|
||||
region_len = end - begin
|
||||
|
||||
def constrain(idx):
|
||||
def constrain(idx: int) -> int:
|
||||
delta = 0
|
||||
if idx < 0:
|
||||
delta = 0 - idx # delta > 0
|
||||
|
|
|
@ -177,6 +177,26 @@ version = "4.3.0"
|
|||
[package.dependencies]
|
||||
six = ">=1.0.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Optional static typing for Python"
|
||||
name = "mypy"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.660"
|
||||
|
||||
[package.dependencies]
|
||||
mypy_extensions = ">=0.4.0,<0.5.0"
|
||||
typed-ast = ">=1.2.0,<1.3.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
name = "mypy-extensions"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.4.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "NumPy: array processing for numbers, strings, records, and objects."
|
||||
|
@ -341,6 +361,14 @@ optional = false
|
|||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||
version = "1.12.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
name = "typed-ast"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
|
@ -350,7 +378,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
|
|||
version = "1.24.1"
|
||||
|
||||
[metadata]
|
||||
content-hash = "5f805ebc68759139df36aae10a903fd9d347b2f23248eb32182c5b48b48e2a1c"
|
||||
content-hash = "2776f62aa3fa7955bc6e7127743d24504efd279c21dfbdbc84ea45c71d5dbce8"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.hashes]
|
||||
|
@ -373,6 +401,8 @@ kiwisolver = ["0ee4ed8b3ae8f5f712b0aa9ebd2858b5b232f1b9a96b0943dceb34df2a223bc3"
|
|||
macholib = ["ac02d29898cf66f27510d8f39e9112ae00590adb4a48ec57b25028d6962b1ae1", "c4180ffc6f909bf8db6cd81cff4b6f601d575568f4d5dee148c830e9851eb9db"]
|
||||
matplotlib = ["16aa61846efddf91df623bbb4598e63be1068a6b6a2e6361cc802b41c7a286eb", "1975b71a33ac986bb39b6d5cfbc15c7b1f218f1134efb4eb3881839d6ae69984", "2b222744bd54781e6cc0b717fa35a54e5f176ba2ced337f27c5b435b334ef854", "317643c0e88fad55414347216362b2e229c130edd5655fea5f8159a803098468", "4269ce3d1b897d46fc3cc2273a0cc2a730345bb47e4456af662e6fca85c89dd7", "65214fd668975077cdf8d408ccf2b2d6bdf73b4e6895a79f8e99ce4f0b43fcdb", "74bc213ab8a92d86a0b304d9359d1e1d14168d4c6121b83862c9d8a88b89a738", "88949be0db54755995dfb0210d0099a8712a3c696c860441971354c3debfc4af", "8e1223d868be89423ec95ada5f37aa408ee64fe76ccb8e4d5f533699ba4c0e4a", "9fa00f2d7a552a95fa6016e498fdeb6d74df537853dda79a9055c53dfc8b6e1a", "c27fd46cab905097ba4bc28d5ba5289930f313fb1970c9d41092c9975b80e9b4", "c94b792af431f6adb6859eb218137acd9a35f4f7442cea57e4a59c54751c36af", "f4c12a01eb2dc16693887a874ba948b18c92f425c4d329639ece6d3bb8e631bb"]
|
||||
more-itertools = ["c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", "c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", "fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"]
|
||||
mypy = ["986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e", "cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4"]
|
||||
mypy-extensions = ["37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", "b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"]
|
||||
numpy = ["0df89ca13c25eaa1621a3f09af4c8ba20da849692dcae184cb55e80952c453fb", "154c35f195fd3e1fad2569930ca51907057ae35e03938f89a8aedae91dd1b7c7", "18e84323cdb8de3325e741a7a8dd4a82db74fde363dce32b625324c7b32aa6d7", "1e8956c37fc138d65ded2d96ab3949bd49038cc6e8a4494b1515b0ba88c91565", "23557bdbca3ccbde3abaa12a6e82299bc92d2b9139011f8c16ca1bb8c75d1e95", "24fd645a5e5d224aa6e39d93e4a722fafa9160154f296fd5ef9580191c755053", "36e36b6868e4440760d4b9b44587ea1dc1f06532858d10abba98e851e154ca70", "3d734559db35aa3697dadcea492a423118c5c55d176da2f3be9c98d4803fc2a7", "416a2070acf3a2b5d586f9a6507bb97e33574df5bd7508ea970bbf4fc563fa52", "4a22dc3f5221a644dfe4a63bf990052cc674ef12a157b1056969079985c92816", "4d8d3e5aa6087490912c14a3c10fbdd380b40b421c13920ff468163bc50e016f", "4f41fd159fba1245e1958a99d349df49c616b133636e0cf668f169bce2aeac2d", "561ef098c50f91fbac2cc9305b68c915e9eb915a74d9038ecf8af274d748f76f", "56994e14b386b5c0a9b875a76d22d707b315fa037affc7819cda08b6d0489756", "73a1f2a529604c50c262179fcca59c87a05ff4614fe8a15c186934d84d09d9a5", "7da99445fd890206bfcc7419f79871ba8e73d9d9e6b82fe09980bc5bb4efc35f", "99d59e0bcadac4aa3280616591fb7bcd560e2218f5e31d5223a2e12a1425d495", "a4cc09489843c70b22e8373ca3dfa52b3fab778b57cf81462f1203b0852e95e3", "a61dc29cfca9831a03442a21d4b5fd77e3067beca4b5f81f1a89a04a71cf93fa", "b1853df739b32fa913cc59ad9137caa9cc3d97ff871e2bbd89c2a2a1d4a69451", "b1f44c335532c0581b77491b7715a871d0dd72e97487ac0f57337ccf3ab3469b", "b261e0cb0d6faa8fd6863af26d30351fd2ffdb15b82e51e81e96b9e9e2e7ba16", "c857ae5dba375ea26a6228f98c195fec0898a0fd91bcf0e8a0cae6d9faf3eca7", "cf5bb4a7d53a71bb6a0144d31df784a973b36d8687d615ef6a7e9b1809917a9b", "db9814ff0457b46f2e1d494c1efa4111ca089e08c8b983635ebffb9c1573361f", "df04f4bad8a359daa2ff74f8108ea051670cafbca533bb2636c58b16e962989e", "ecf81720934a0e18526177e645cbd6a8a21bb0ddc887ff9738de07a1df5c6b61", "edfa6fba9157e0e3be0f40168eb142511012683ac3dc82420bee4a3f3981b30e"]
|
||||
pefile = ["4c5b7e2de0c8cb6c504592167acf83115cbbde01fe4a507c16a1422850e86cd6"]
|
||||
pluggy = ["447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", "bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"]
|
||||
|
@ -389,4 +419,5 @@ pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5
|
|||
requests = ["502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"]
|
||||
"ruamel.yaml" = ["046b997a0892eebd9ca97823102b330ab3a2da719a6df877b2f2a03e19fb878e", "0e7414b6b757ae64d5452a83fb197040a7835ff96fa228f7127c94d54041801d", "31d9a1c4d15c02d60c2ca1996dcaf1a6c91b986b72299384071cce40bd1278bf", "356ee57fa561243223d91cedb526fd057a91edb744825fe64b6fff23aa262324", "39ad6bad81d0c797452e5eeadf45da9c507c978894a5e4c3e8c6ce5ee8abe8db", "3dcede78252a4cf3820928fa75c5928a56cbaedd4c698ea2d1f04e6c36235d6a", "4329af449c314ea0565bb5c36dc52232ea9ca32297b58bb2340acf5767719ad6", "51d81999ae9cb138a8043908c9406b7bd258a4d616f99200627a81f1c9f85fcb", "53ea23234e82242267c4d32611e6907a64f0f2e03b77c19d270bd56b2e375f5b", "5a3bf78266761f352d61cb18ee91fa11a1970566238ec732e4ec8857bebdf91d", "5ec8f00fc23cca74dfc0529647bb16e5578e247c1ac5182be15dfb949d1304cb", "606d3f83ede7a3f76845de64bc8f376df40d166eca782a56a35b8d1961b214e1", "66af4e1a5f24534e9d63854ce028d5ccba642c387df6b1b9b39b7a0953d08135", "71baa4ca8a7e4e37991f28b17f75d64c56ef82163f79fc9d875712d04c531c5a", "7a32ab8866c04d844e1b7116118e7b8e38718a291802a60bb287964bcf4c0f47", "a29655dc08ddf64b22c462aad88e4f54b54f80fac2cd8edfe0dcf817987a2722", "c1de59975299c919058c54bb9309e430bdcea7239c76021cf082a841e6f8e43c", "c3af911ee65e406d7ab0655fc3f4124d5000fcadcb470fe07d37d975f77a1a4c", "cdd215fa38b15713378bccc99f3fd1eee06c328fe50b5b17775f7cf3bc047cac", "d0ad7a4f8cd8082d0593b87848a46c12fbfb5c5011fb15dfe58941aa2ee5e5e8", "e7e5fa36587e6e06b12a678314d2020f90928ea9522001ea7834e3f1e5ac3b98", "f34262929dfab42ce88e34e2bc525a36a5972563c5cac1582a20b575303039c4"]
|
||||
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
|
||||
typed-ast = ["023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe", "07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c", "153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2", "3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a", "3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7", "51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827", "52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33", "6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9", "64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032", "74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9", "7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2", "91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2", "9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062", "b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15", "ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357", "bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a", "c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824", "c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442", "f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1", "f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2", "fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"]
|
||||
urllib3 = ["61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"]
|
||||
|
|
|
@ -27,6 +27,7 @@ pywin32-ctypes = {version = "^0.2.0",platform = "win32"}
|
|||
coverage = "^4.5"
|
||||
pytest-cov = "^2.6"
|
||||
codecov = "^2.0"
|
||||
mypy = "^0.660.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
corr = 'corrscope.cli:main'
|
||||
|
|
|
@ -50,3 +50,11 @@ def report():
|
|||
def html():
|
||||
run("coverage.cmdline:main", "html")
|
||||
webbrowser.open("htmlcov/index.html")
|
||||
|
||||
|
||||
"""
|
||||
export MONKEYTYPE_TRACE_MODULES=corrscope
|
||||
monkeytype run `which pytest`
|
||||
// monkeytype run -m corrscope
|
||||
monkeytype list-modules | xargs -I % -n 1 sh -c 'monkeytype apply % 2>&1 | tail -n4'
|
||||
"""
|
||||
|
|
22
setup.cfg
22
setup.cfg
|
@ -1,7 +1,29 @@
|
|||
[tool:pytest]
|
||||
testpaths = tests
|
||||
xfail_strict=true
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
source =
|
||||
corrscope
|
||||
|
||||
[mypy-corrscope.utils.scipy.*]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy]
|
||||
;Pretty-print
|
||||
show_error_context = True
|
||||
|
||||
;Config
|
||||
ignore_missing_imports = True
|
||||
|
||||
;https://github.com/python/mypy/blob/master/mypy_self_check.ini
|
||||
check_untyped_defs = True
|
||||
warn_no_return = True
|
||||
strict_optional = True
|
||||
no_implicit_optional = True
|
||||
disallow_any_generics = True
|
||||
warn_redundant_casts = True
|
||||
warn_unused_configs = True
|
||||
show_traceback = True
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue