diff --git a/.gitmodules b/.gitmodules index aa219ff..794ff4b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "kucher/libraries/popcop"] path = kucher/libraries/popcop url = https://github.com/Zubax/popcop -[submodule "kucher/libraries/pyqtgraph"] - path = kucher/libraries/pyqtgraph - url = https://github.com/pyqtgraph/pyqtgraph-core [submodule "kucher/libraries/construct"] path = kucher/libraries/construct url = https://github.com/construct/construct @@ -13,3 +10,6 @@ [submodule "kucher/libraries/quamash"] path = kucher/libraries/quamash url = https://github.com/harvimt/quamash +[submodule "kucher/libraries/staticx"] + path = kucher/libraries/staticx + url = https://github.com/Zubax/staticx diff --git a/README.md b/README.md index a2a85b1..3159026 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,22 @@ The following command line options are available: * `--debug` - activates verbose logging; useful for troubleshooting. * `--profile` - creates a profile file after the application is closed. -* `--test` - run unit tests. - * `-k` - can be used in conjunction with `--test` to run a specific test. - Refer to the PyTest documentation for more information. - * Other options can be provided with `--test`; they will be passed directly to - the PyTest framework. + +### Running the unit tests + +From the root directory, on Linux: + +```bash +PYTHONPATH=kucher pytest # TODO: fix imports to make "PYTHONPATH=kucher" unnecessary +``` + +On Windows: + +```bash +set PYTHONPATH=kucher +pytest +``` + ### Getting the right version of Python @@ -87,3 +98,10 @@ pyenv global 3.6.4 If there was a warning that `sqlite3` has not been compiled, make sure to resolve it first before continuing - `sqlite3` is required by Kucher. Now run `python3 --version` and ensure that you have v3.6 as default. + +### CI artifacts + +The CI builds redistributable release binaries automatically. +Follow the CI status link from the [commits page](https://github.com/Zubax/kucher/commits/master) +to find the binaries for a particular commit. +The Linux binaries can be shipped directly; the Windows binaries must be signed first. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..81059c3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,57 @@ +environment: + matrix: + # Windows & python 3.6 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + PYTHON: "C:\\Python36-x64" + PYTHON_ARCH: "64" + + # Windows & python 3.7 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + PYTHON: "C:\\Python37-x64" + PYTHON_ARCH: "64" + + # Ubuntu & python 3.6 + - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + PYTHON: "3.6" + + # Ubuntu & python 3.7 + - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu + PYTHON: "3.7" + +stack: python %PYTHON% + +install: + - "git submodule update --init --recursive" + - cmd: "SET PATH=%PYTHON%;%PATH%" + - cmd: "SET PATH=C:\\Python36-x64\\Scripts;%PATH%" + - cmd: "SET PATH=C:\\Python37-x64\\Scripts;%PATH%" + - "python -V" + - "pip --version" + - cmd: "python -m pip install -r requirements.txt" + - cmd: "python -m pip install -r requirements-dev-windows.txt" + - sh: "pip install -r requirements.txt" + - sh: "pip install -r requirements-dev-linux.txt" + - sh: "sudo add-apt-repository -y ppa:deadsnakes/ppa " + - sh: "sudo apt-get update" + - sh: "sudo apt-get -y install python3.6-dev" + - sh: "sudo apt-get -y install python3.7-dev" + + +build: off + +test_script: + - cmd: "test_windows.bat" + # Plugins related to software display are missing on Linux. Appveyor can't handle display so we disable it. See: + # https://doc.qt.io/qt-5/embedded-linux.html + # https://github.com/ariya/phantomjs/issues/14376 + - sh: "export QT_QPA_PLATFORM=offscreen" + - sh: "bash test_linux.sh" + +after_test: + - cmd: "7z a zubax-kucher.7z *" + - sh: "7z a zubax-kucher.7z *" + - "appveyor PushArtifact zubax-kucher.7z" + - sh: "bash build_linux.sh" + - cmd: "build_windows.bat" + - cmd: "appveyor PushArtifact dist\\Kucher.exe" + - sh: "appveyor PushArtifact dist/Kucher" diff --git a/build_linux.sh b/build_linux.sh index 96356d0..f566c76 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -3,13 +3,22 @@ # Before running this, make sure all PIP dependencies are installed. # -sudo apt-get install patchelf -y +sudo apt-get install -y patchelf +sudo apt-get install -y gcc +sudo apt-get install -y scons +sudo apt-get install -y liblzma-dev +pip install backports.lzma pyinstaller --clean --noconfirm pyinstaller.spec || exit 2 +pushd kucher/libraries/staticx +scons +python setup.py install # https://github.com/JonathonReinhart/staticx/issues/79 -cd dist +popd +pushd dist mv Kucher Kucher.tmp staticx --loglevel DEBUG Kucher.tmp Kucher rm -rf *.tmp +popd diff --git a/build_windows.bat b/build_windows.bat new file mode 100644 index 0000000..b41a235 --- /dev/null +++ b/build_windows.bat @@ -0,0 +1,3 @@ +REM Before running this, make sure all PIP dependencies are installed. + +pyinstaller --clean --noconfirm pyinstaller.spec || exit 2 diff --git a/kucher/__init__.py b/kucher/__init__.py index 943d015..bba3eb4 100644 --- a/kucher/__init__.py +++ b/kucher/__init__.py @@ -19,8 +19,8 @@ if sys.version_info[:2] < (3, 6): raise ImportError('A newer version of Python is required') -from .version import * -from .main import main +from .version import * # noqa +from .main import main # noqa # # Configuring the third-party modules. @@ -33,9 +33,14 @@ os.path.join(THIRDPARTY_PATH_ROOT), os.path.join(THIRDPARTY_PATH_ROOT, 'popcop', 'python'), os.path.join(THIRDPARTY_PATH_ROOT, 'construct'), - os.path.join(THIRDPARTY_PATH_ROOT, 'dataclasses'), os.path.join(THIRDPARTY_PATH_ROOT, 'quamash'), ] +# 'dataclasses' module is included in Python libraries since version 3.7. For Python versions below, the dataclass +# module located in the 'libraries' directory will be used. It is not compatible with Python 3.7, so we only declare +# its path if Python version is below 3.7. Otherwise, the built-in module will be used by default. +if sys.version_info[:2] < (3, 7): + THIRDPARTY_PATH.append(os.path.join(THIRDPARTY_PATH_ROOT, 'dataclasses')) + for tp in THIRDPARTY_PATH: sys.path.insert(0, tp) diff --git a/kucher/libraries/pyqtgraph b/kucher/libraries/pyqtgraph deleted file mode 160000 index cc2d6b5..0000000 --- a/kucher/libraries/pyqtgraph +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cc2d6b54cb0cb9d18fcfa5cade098d0ddfa63bb7 diff --git a/kucher/libraries/staticx b/kucher/libraries/staticx new file mode 160000 index 0000000..daf5bbc --- /dev/null +++ b/kucher/libraries/staticx @@ -0,0 +1 @@ +Subproject commit daf5bbc88c3a879eda8e3c89ef8e234ad5123269 diff --git a/kucher/main.py b/kucher/main.py index 674d458..537073a 100644 --- a/kucher/main.py +++ b/kucher/main.py @@ -43,7 +43,7 @@ def main() -> int: import datetime from PyQt5.QtWidgets import QApplication from quamash import QEventLoop - from . import THIRDPARTY_PATH_ROOT, data_dir, version, resources + from . import data_dir, version, resources from .fuhrer import Fuhrer data_dir.init() @@ -70,20 +70,6 @@ def save_profile(): atexit.register(save_profile) prof.enable() - if '--test' in sys.argv: - if not os.environ.get('PYTHONASYNCIODEBUG'): - raise RuntimeError('PYTHONASYNCIODEBUG should be set while unit testing') - - import pytest - args = sys.argv[:] - args.remove('--test') - args.append('--ignore=' + THIRDPARTY_PATH_ROOT) - args.append('--capture=no') - args.append('--fulltrace') - args.append('-vv') - args.append('.') - return pytest.main(args) - # Configuring the event loop app = QApplication(sys.argv) loop = QEventLoop(app) diff --git a/kucher/model/device_model/__init__.py b/kucher/model/device_model/__init__.py index 2578f05..2eb1a50 100644 --- a/kucher/model/device_model/__init__.py +++ b/kucher/model/device_model/__init__.py @@ -119,9 +119,10 @@ def registers(self) -> typing.Dict[str, Register]: """ return self._conn.registers if self.is_connected else {} - async def connect(self, - port_name: str, - on_progress_report: typing.Optional[typing.Callable[[str, float], None]]=None) -> DeviceInfoView: + async def connect( + self, + port_name: str, + on_progress_report: typing.Optional[typing.Callable[[str, float], None]] = None) -> DeviceInfoView: await self.disconnect() assert not self._conn @@ -143,7 +144,7 @@ async def connect(self, return self._conn.device_info - async def disconnect(self, reason: str=None): + async def disconnect(self, reason: str = None): _logger.info('Explicit disconnect request; reason: %r', reason) if self._conn: # noinspection PyTypeChecker diff --git a/kucher/model/device_model/communicator/communicator.py b/kucher/model/device_model/communicator/communicator.py index f78f78c..0a92b61 100644 --- a/kucher/model/device_model/communicator/communicator.py +++ b/kucher/model/device_model/communicator/communicator.py @@ -211,8 +211,8 @@ async def send(self, message: AnyMessage): async def request(self, message_or_type: typing.Union[Message, MessageType, StandardMessageBase, StandardMessageType], - timeout: typing.Optional[typing.Union[float, int]]=None, - predicate: typing.Optional[typing.Callable[[AnyMessage], bool]]=None) ->\ + timeout: typing.Optional[typing.Union[float, int]] = None, + predicate: typing.Optional[typing.Callable[[AnyMessage], bool]] = None) ->\ typing.Optional[AnyMessage]: """ Sends a message, then awaits for a matching response. diff --git a/kucher/model/device_model/communicator/messages.py b/kucher/model/device_model/communicator/messages.py index c1822b7..445d38b 100644 --- a/kucher/model/device_model/communicator/messages.py +++ b/kucher/model/device_model/communicator/messages.py @@ -405,8 +405,8 @@ class Message: """ def __init__(self, message_type: MessageType, - fields: typing.Optional[typing.Mapping]=None, - timestamp: typing.Optional[float]=None): + fields: typing.Optional[typing.Mapping] = None, + timestamp: typing.Optional[float] = None): if not isinstance(message_type, MessageType): raise TypeError('Expected MessageType not %r' % message_type) diff --git a/kucher/model/device_model/connection.py b/kucher/model/device_model/connection.py index 5688d33..fcd8b00 100644 --- a/kucher/model/device_model/connection.py +++ b/kucher/model/device_model/connection.py @@ -122,10 +122,10 @@ async def request(self, MessageType, popcop.standard.MessageBase, typing.Type[popcop.standard.MessageBase]], - timeout: typing.Optional[typing.Union[float, int]]=None, + timeout: typing.Optional[typing.Union[float, int]] = None, predicate: typing.Optional[typing.Callable[[typing.Union[Message, popcop.standard.MessageBase]], - bool]]=None) ->\ + bool]] = None) ->\ typing.Union[Message, popcop.standard.MessageBase]: try: return await self._com.request(message_or_type, @@ -276,7 +276,7 @@ async def connect(event_loop: asyncio.AbstractEventLoop, begun_at = time.monotonic() progress = 0.0 - def report(stage: str, progress_increment: float=0.01): + def report(stage: str, progress_increment: float = 0.01): nonlocal progress assert progress_increment > 0 progress = min(1.0, progress + progress_increment) diff --git a/kucher/model/device_model/register.py b/kucher/model/device_model/register.py index f78b627..8640157 100644 --- a/kucher/model/device_model/register.py +++ b/kucher/model/device_model/register.py @@ -64,7 +64,7 @@ def __init__(self, flags: Flags, update_timestamp_device_time: Decimal, set_get_callback: SetGetCallback, - update_timestamp_monotonic: float=None): + update_timestamp_monotonic: float = None): self._name = str(name) self._cached_value = value self._default_value = default_value diff --git a/kucher/resources.py b/kucher/resources.py index 3dad9f1..10d6621 100644 --- a/kucher/resources.py +++ b/kucher/resources.py @@ -24,7 +24,7 @@ def get_absolute_path(*relative_path_items: str, check_existence=False) -> str: - out = os.path.abspath(os.path.join(PACKAGE_ROOT, *relative_path_items)).replace('\\','/') + out = os.path.abspath(os.path.join(PACKAGE_ROOT, *relative_path_items)).replace('\\', '/') if check_existence: if not os.path.exists(out): raise ValueError(f'The specified path does not exist: {out}') diff --git a/kucher/view/main_window/register_view_widget/__init__.py b/kucher/view/main_window/register_view_widget/__init__.py index 07e11eb..05066e6 100644 --- a/kucher/view/main_window/register_view_widget/__init__.py +++ b/kucher/view/main_window/register_view_widget/__init__.py @@ -108,7 +108,7 @@ def __init__(self, parent: QWidget): def add_action(callback: typing.Callable[[], None], icon_name: str, name: str, - shortcut: typing.Optional[str]=None): + shortcut: typing.Optional[str] = None): action = QAction(get_icon(icon_name), name, self) # noinspection PyUnresolvedReferences action.triggered.connect(callback) diff --git a/kucher/view/main_window/register_view_widget/model.py b/kucher/view/main_window/register_view_widget/model.py index 2b75121..05d0bfc 100644 --- a/kucher/view/main_window/register_view_widget/model.py +++ b/kucher/view/main_window/register_view_widget/model.py @@ -118,7 +118,7 @@ def default_data_handler(*_) -> QVariant: for r in self._registers: r.update_event.connect_weak(self, Model._on_register_update) - def iter_indices(self, root: QModelIndex=None) -> typing.Generator[QModelIndex, None, None]: + def iter_indices(self, root: QModelIndex = None) -> typing.Generator[QModelIndex, None, None]: """ Iterates over all indexes in this model starting from :param root:. Returns a generator of QModelIndex. """ @@ -137,7 +137,7 @@ def registers(self) -> typing.List[Register]: async def read(self, registers: typing.Iterable[Register], - progress_callback: typing.Optional[typing.Callable[[Register, int, int], None]]=None): + progress_callback: typing.Optional[typing.Callable[[Register, int, int], None]] = None): """ :param registers: which ones to read :param progress_callback: (register: Register, current_register_index: int, total_registers: int) -> None @@ -180,7 +180,7 @@ async def read(self, async def write(self, register_value_mapping: typing.Dict[Register, typing.Any], - progress_callback: typing.Optional[typing.Callable[[Register, int, int], None]]=None): + progress_callback: typing.Optional[typing.Callable[[Register, int, int], None]] = None): """ :param register_value_mapping: keys are registers, values are what to assign :param progress_callback: (register: Register, current_register_index: int, total_registers: int) -> None @@ -229,7 +229,7 @@ def get_register_from_index(index: QModelIndex) -> Register: def get_font() -> QFont: return get_monospace_font(small=True) - def index(self, row: int, column: int, parent: QModelIndex=None) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex = None) -> QModelIndex: if column >= self.columnCount(parent): return QModelIndex() @@ -251,7 +251,7 @@ def parent(self, index: QModelIndex) -> QModelIndex: else: return QModelIndex() - def rowCount(self, parent: QModelIndex=None) -> int: + def rowCount(self, parent: QModelIndex = None) -> int: # Only zero-column items have children, this is per the Qt's conventions. # http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html if parent is not None and parent.column() > 0: @@ -259,7 +259,7 @@ def rowCount(self, parent: QModelIndex=None) -> int: return len(self._resolve_parent_node(parent).children) - def columnCount(self, parent: QModelIndex=None) -> int: + def columnCount(self, parent: QModelIndex = None) -> int: return len(self._COLUMNS) def _data_display(self, index: QModelIndex) -> str: @@ -373,7 +373,7 @@ def _data_decoration(self, index: QModelIndex) -> typing.Union[QPixmap, QVariant return QVariant() - def data(self, index: QModelIndex, role: int=None): + def data(self, index: QModelIndex, role: int = None): """ This function is a major performance bottleneck. Look at the results of profiling, you'll see that it is invoked hundreds of times for every minor data change. Therefore, it is heavily optimized. @@ -382,7 +382,7 @@ def data(self, index: QModelIndex, role: int=None): """ return self._data_role_dispatch[role](index) - def setData(self, index: QModelIndex, value, role: int=None) -> bool: + def setData(self, index: QModelIndex, value, role: int = None) -> bool: # As per http://doc.qt.io/qt-5/model-view-programming.html if not index.isValid() or role != Qt.EditRole: return False @@ -424,7 +424,7 @@ def flags(self, index: QModelIndex) -> int: return out - def headerData(self, section: int, orientation: int, role: int=None): + def headerData(self, section: int, orientation: int, role: int = None): if orientation == Qt.Horizontal: if role == Qt.DisplayRole: return self._COLUMNS[section] @@ -508,7 +508,7 @@ class State(enum.Enum): state: State = State.DEFAULT message: str = '' - def set_state(self, state: '_Node.State', message: str=''): + def set_state(self, state: '_Node.State', message: str = ''): self.message = message self.state = self.State(state) diff --git a/kucher/view/main_window/register_view_widget/style_option_modifying_delegate.py b/kucher/view/main_window/register_view_widget/style_option_modifying_delegate.py index bd03306..91e7003 100644 --- a/kucher/view/main_window/register_view_widget/style_option_modifying_delegate.py +++ b/kucher/view/main_window/register_view_widget/style_option_modifying_delegate.py @@ -26,8 +26,8 @@ class StyleOptionModifyingDelegate(QStyledItemDelegate): def __init__(self, parent: QObject, *, - decoration_position: int=None, - decoration_alignment: int=None): + decoration_position: int = None, + decoration_alignment: int = None): super(StyleOptionModifyingDelegate, self).__init__(parent) self._decoration_position = decoration_position self._decoration_alignment = decoration_alignment diff --git a/kucher/view/main_window/telega_control_widget/__init__.py b/kucher/view/main_window/telega_control_widget/__init__.py index 2cef5af..6257db9 100644 --- a/kucher/view/main_window/telega_control_widget/__init__.py +++ b/kucher/view/main_window/telega_control_widget/__init__.py @@ -130,8 +130,8 @@ def on_general_status_update(self, timestamp: float, s: GeneralStatusView): def _make_monitored_quantity(value: float, - too_low: bool=False, - too_high: bool=False) -> MonitoredQuantity: + too_low: bool = False, + too_high: bool = False) -> MonitoredQuantity: mq = MonitoredQuantity(value) if too_low: diff --git a/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/phase_manipulation_widget.py b/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/phase_manipulation_widget.py index 0141171..c7d829c 100644 --- a/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/phase_manipulation_widget.py +++ b/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/phase_manipulation_widget.py @@ -89,7 +89,7 @@ def make_fat_label(text: str) -> QLabel: )) self.setLayout( - lay_out_horizontally(*(top_layout_items + [self._sync_checkbox, self._send_button])) + lay_out_horizontally(*(top_layout_items + [self._sync_checkbox, self._send_button])) ) def get_widget_name_and_icon_name(self): diff --git a/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/scalar_control_widget.py b/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/scalar_control_widget.py index df5c657..56f2728 100644 --- a/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/scalar_control_widget.py +++ b/kucher/view/main_window/telega_control_widget/control_widget/low_level_manipulation_control_widget/scalar_control_widget.py @@ -93,17 +93,17 @@ def __init__(self, self._frequency_gradient_control.value_change_event.connect(self._on_any_control_changed) self.setLayout( - lay_out_horizontally( - self._volt_per_hertz_control.spinbox, - self._frequency_gradient_control.spinbox, - self._target_frequency_control.spinbox, - make_button(self, - icon_name='clear-symbol', - tool_tip='Reset the target frequency to zero', - on_clicked=self._on_target_frequency_clear_button_clicked), - self._target_frequency_control.slider, - self._send_button, - ) + lay_out_horizontally( + self._volt_per_hertz_control.spinbox, + self._frequency_gradient_control.spinbox, + self._target_frequency_control.spinbox, + make_button(self, + icon_name='clear-symbol', + tool_tip='Reset the target frequency to zero', + on_clicked=self._on_target_frequency_clear_button_clicked), + self._target_frequency_control.slider, + self._send_button, + ) ) def get_widget_name_and_icon_name(self): diff --git a/kucher/view/main_window/telega_control_widget/task_specific_status_widget/fault_status_widget.py b/kucher/view/main_window/telega_control_widget/task_specific_status_widget/fault_status_widget.py index 608c971..0c9e892 100644 --- a/kucher/view/main_window/telega_control_widget/task_specific_status_widget/fault_status_widget.py +++ b/kucher/view/main_window/telega_control_widget/task_specific_status_widget/fault_status_widget.py @@ -85,7 +85,7 @@ def on_general_status_update(self, timestamp: float, s: GeneralStatusView): self._error_description_display.setText('(elaboration not available)') - def _make_display(self, tool_tip: str=''): + def _make_display(self, tool_tip: str = ''): o = QLineEdit(self) o.setReadOnly(True) o.setFont(get_monospace_font()) diff --git a/kucher/view/main_window/telega_control_widget/task_specific_status_widget/run_status_widget.py b/kucher/view/main_window/telega_control_widget/task_specific_status_widget/run_status_widget.py index a245cf6..58f7494 100644 --- a/kucher/view/main_window/telega_control_widget/task_specific_status_widget/run_status_widget.py +++ b/kucher/view/main_window/telega_control_widget/task_specific_status_widget/run_status_widget.py @@ -204,7 +204,7 @@ def _display_estimated_active_power(self, tssr: TaskSpecificStatusReport.Run): self._estimated_active_power_display.set(f'{active_power:.0f} W') - def _make_display(self, title: str, tooltip: str, with_comment: bool=False) -> ValueDisplayWidget: + def _make_display(self, title: str, tooltip: str, with_comment: bool = False) -> ValueDisplayWidget: return ValueDisplayWidget(self, title=title, with_comment=with_comment, @@ -216,7 +216,7 @@ class _DQDisplayWidget(QWidget): def __init__(self, parent: QWidget): super(_DQDisplayWidget, self).__init__(parent) - def make_label(text: str='') -> QLabel: + def make_label(text: str = '') -> QLabel: w = QLabel(text, self) w.setAlignment(Qt.AlignVCenter | Qt.AlignRight) font = QFont() diff --git a/kucher/view/monitored_quantity.py b/kucher/view/monitored_quantity.py index 2f5c476..9866748 100644 --- a/kucher/view/monitored_quantity.py +++ b/kucher/view/monitored_quantity.py @@ -28,7 +28,7 @@ class Alert(enum.Enum): def __init__(self, value: typing.Union[int, float], - alert: 'typing.Optional[MonitoredQuantity.Alert]'=None): + alert: 'typing.Optional[MonitoredQuantity.Alert]' = None): self.value = float(value) if value is not None else None self.alert = alert or self.Alert.NONE @@ -54,9 +54,9 @@ class DisplayParameters: def __init__(self, display_target: ValueDisplayWidget, format_string: str, - params_default: DisplayParameters=None, - params_when_low: DisplayParameters=None, - params_when_high: DisplayParameters=None): + params_default: DisplayParameters = None, + params_when_low: DisplayParameters = None, + params_when_high: DisplayParameters = None): self._display_target = display_target self._format_string = format_string self._params = { diff --git a/kucher/view/tool_window_manager.py b/kucher/view/tool_window_manager.py index cfde6c9..dcc1102 100644 --- a/kucher/view/tool_window_manager.py +++ b/kucher/view/tool_window_manager.py @@ -102,9 +102,9 @@ def register(self, factory: typing.Union[typing.Type[QWidget], typing.Callable[[ToolWindow], QWidget]], title: str, - icon_name: typing.Optional[str]=None, - allow_multiple_instances: bool=False, - shown_by_default: bool=False): + icon_name: typing.Optional[str] = None, + allow_multiple_instances: bool = False, + shown_by_default: bool = False): """ Adds the specified tool WIDGET (not window) to the set of known tools. If requested, it can be instantiated automatically at the time of application startup. @@ -176,8 +176,8 @@ def add_arrangement_rule(self, location=location)) def select_widgets(self, - widget_type: typing.Type[_WidgetTypeVar]=QWidget, - current_location: typing.Optional[ToolWindowLocation]=None) -> typing.List[_WidgetTypeVar]: + widget_type: typing.Type[_WidgetTypeVar] = QWidget, + current_location: typing.Optional[ToolWindowLocation] = None) -> typing.List[_WidgetTypeVar]: """ Returns a list of references to the root widgets of all existing tool windows which are instances of the specified type. This can be used to broadcast events and such. diff --git a/kucher/view/utils.py b/kucher/view/utils.py index 63d8c45..15f0e56 100644 --- a/kucher/view/utils.py +++ b/kucher/view/utils.py @@ -57,7 +57,7 @@ def get_icon(name: str) -> QIcon: @cached -def get_icon_pixmap(icon_name: str, width: int, height: int=None) -> QPixmap: +def get_icon_pixmap(icon_name: str, width: int, height: int = None) -> QPixmap: """ Caching wrapper around get_icon(...).pixmap(...). Every generated pixmap is cached permanently. @@ -107,12 +107,12 @@ def is_small_screen() -> bool: def make_button(parent: QWidget, - text: str='', - icon_name: typing.Optional[str]=None, - tool_tip: typing.Optional[str]=None, - checkable: bool=False, - checked: bool=False, - on_clicked: typing.Callable[[], None]=None) -> QPushButton: + text: str = '', + icon_name: typing.Optional[str] = None, + tool_tip: typing.Optional[str] = None, + checkable: bool = False, + checked: bool = False, + on_clicked: typing.Callable[[], None] = None) -> QPushButton: b = QPushButton(text, parent) b.setFocusPolicy(Qt.NoFocus) if icon_name: diff --git a/kucher/view/widgets/__init__.py b/kucher/view/widgets/__init__.py index a4c54bd..36c2de5 100644 --- a/kucher/view/widgets/__init__.py +++ b/kucher/view/widgets/__init__.py @@ -28,7 +28,7 @@ class WidgetBase(QWidget): def __init__(self, parent: typing.Optional[QWidget]): super(WidgetBase, self).__init__(parent) - def flash(self, message: str, *format_args, duration: typing.Optional[float]=None): + def flash(self, message: str, *format_args, duration: typing.Optional[float] = None): """ Shows the specified message in the status bar of the parent window. """ diff --git a/kucher/view/widgets/group_box_widget.py b/kucher/view/widgets/group_box_widget.py index a42192e..e00d5d2 100644 --- a/kucher/view/widgets/group_box_widget.py +++ b/kucher/view/widgets/group_box_widget.py @@ -26,7 +26,7 @@ class GroupBoxWidget(QGroupBox): def __init__(self, parent: QWidget, title: str, - icon_name: typing.Optional[str]=None): + icon_name: typing.Optional[str] = None): super(GroupBoxWidget, self).__init__(title, parent) # Changing icons is very expensive, so we store last set icon in order to avoid re-setting it diff --git a/kucher/view/widgets/spinbox_linked_with_slider.py b/kucher/view/widgets/spinbox_linked_with_slider.py index 4c610ea..9f58e50 100644 --- a/kucher/view/widgets/spinbox_linked_with_slider.py +++ b/kucher/view/widgets/spinbox_linked_with_slider.py @@ -68,10 +68,10 @@ class SliderOrientation(enum.IntEnum): # noinspection PyUnresolvedReferences def __init__(self, parent: QWidget, - minimum: float=0.0, - maximum: float=100.0, - step: float=1.0, - slider_orientation: SliderOrientation=SliderOrientation.VERTICAL): + minimum: float = 0.0, + maximum: float = 100.0, + step: float = 1.0, + slider_orientation: SliderOrientation = SliderOrientation.VERTICAL): self._events_suppression_depth = 0 # Instantiating the widgets @@ -222,10 +222,10 @@ def set_range(self, minimum: float, maximum: float): self.maximum = maximum def update_atomically(self, - minimum: typing.Optional[float]=None, - maximum: typing.Optional[float]=None, - step: typing.Optional[float]=None, - value: typing.Optional[float]=None): + minimum: typing.Optional[float] = None, + maximum: typing.Optional[float] = None, + step: typing.Optional[float] = None, + value: typing.Optional[float] = None): """ This function updates all of the parameters, and invokes the change event only once at the end, provided that the new value is different from the old value. diff --git a/kucher/view/widgets/tool_window.py b/kucher/view/widgets/tool_window.py index 57c3e02..015cc6b 100644 --- a/kucher/view/widgets/tool_window.py +++ b/kucher/view/widgets/tool_window.py @@ -28,8 +28,8 @@ class ToolWindow(QDockWidget): # noinspection PyArgumentList def __init__(self, parent: QWidget, - title: typing.Optional[str]=None, - icon_name: typing.Optional[str]=None): + title: typing.Optional[str] = None, + icon_name: typing.Optional[str] = None): super(QDockWidget, self).__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose) # This is required to stop background timers! diff --git a/kucher/view/widgets/value_display_group_widget.py b/kucher/view/widgets/value_display_group_widget.py index 08d0086..2deb85e 100644 --- a/kucher/view/widgets/value_display_group_widget.py +++ b/kucher/view/widgets/value_display_group_widget.py @@ -23,8 +23,8 @@ class ValueDisplayGroupWidget(GroupBoxWidget): def __init__(self, parent: QWidget, title: str, - icon_name: typing.Optional[str]=None, - with_comments: bool=False): + icon_name: typing.Optional[str] = None, + with_comments: bool = False): super(ValueDisplayGroupWidget, self).__init__(parent, title, icon_name) self._with_comments = with_comments @@ -35,8 +35,8 @@ def __init__(self, # noinspection PyArgumentList def create_value_display(self, title: str, - placeholder_text: typing.Optional[str]=None, - tooltip: typing.Optional[str]=None) -> ValueDisplayWidget: + placeholder_text: typing.Optional[str] = None, + tooltip: typing.Optional[str] = None) -> ValueDisplayWidget: inferior = ValueDisplayWidget(self, title, placeholder_text=placeholder_text, diff --git a/kucher/view/widgets/value_display_widget.py b/kucher/view/widgets/value_display_widget.py index f4e5a50..ec92add 100644 --- a/kucher/view/widgets/value_display_widget.py +++ b/kucher/view/widgets/value_display_widget.py @@ -39,9 +39,9 @@ class Style(enum.Enum): def __init__(self, parent: QWidget, title: str, - placeholder_text: typing.Optional[str]=None, - with_comment: bool=False, - tooltip: typing.Optional[str]=None): + placeholder_text: typing.Optional[str] = None, + with_comment: bool = False, + tooltip: typing.Optional[str] = None): super(ValueDisplayWidget, self).__init__(parent) self._placeholder_text = str(placeholder_text or '') @@ -84,9 +84,9 @@ def reset(self): def set(self, text: str, - style: 'typing.Optional[ValueDisplayWidget.Style]'=None, - comment: typing.Optional[str]=None, - icon_name: typing.Optional[str]=None): + style: 'typing.Optional[ValueDisplayWidget.Style]' = None, + comment: typing.Optional[str] = None, + icon_name: typing.Optional[str] = None): # TODO: handle style style = style or self.Style.NORMAL diff --git a/requirements-dev-linux.txt b/requirements-dev-linux.txt new file mode 100644 index 0000000..6c693fb --- /dev/null +++ b/requirements-dev-linux.txt @@ -0,0 +1,16 @@ +# +# Development-only dependencies (not required to run the application) +# + +# General build dependencies +setuptools +wheel + +# Test dependencies +pytest >= 3.3 +pycodestyle + +# Packaging tools +PyInstaller ~= 3.3 +patchelf-wrapper~=1.0 +staticx ~= 0.7 # Manual patching required, see https://github.com/JonathonReinhart/staticx/issues/79 diff --git a/requirements-dev-windows.txt b/requirements-dev-windows.txt new file mode 100644 index 0000000..022cf1d --- /dev/null +++ b/requirements-dev-windows.txt @@ -0,0 +1,14 @@ +# +# Development-only dependencies (not required to run the application) +# + +# General build dependencies +setuptools +wheel + +# Test dependencies +pytest >= 3.3 +pycodestyle + +# Packaging tools +PyInstaller ~= 3.3 diff --git a/requirements.txt b/requirements.txt index 6da6659..2f29a33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,26 +4,11 @@ # # https://packages.ubuntu.com/bionic/python3-pyqt5 -PyQt5 ~= 5.9 +# https://github.com/pyinstaller/pyinstaller/issues/4293 +PyQt5 == 5.12.2 # https://packages.ubuntu.com/bionic/python3-serial pyserial ~= 3.4 # https://packages.ubuntu.com/bionic/python3-numpy numpy ~= 1.13 - -# -# Development-only dependencies (not required to run the application) -# - -# General build dependencies -setuptools -wheel - -# Test dependencies -pytest >= 3.3 - -# Packaging tools -PyInstaller ~= 3.3 -patchelf-wrapper~=1.0 -staticx ~= 0.7 # Manual patching required, see https://github.com/JonathonReinhart/staticx/issues/79 diff --git a/setup.cfg b/setup.cfg index 2f476c7..57e5054 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,16 @@ [tool:pytest] -python_files=*.py -python_classes=_UnitTest -python_functions=_unittest +# https://docs.pytest.org/en/latest/pythonpath.html#invoking-pytest-versus-python-m-pytest +norecursedirs = + kucher/libraries +testpaths = kucher +python_files = *.py +python_classes = _UnitTest +python_functions = _unittest +addopts = --doctest-modules -v + +[pycodestyle] +# E221 multiple spaces before operator +# E241 multiple spaces after ':' +ignore = E221, E241 +max-line-length = 120 +exclude = kucher/libraries, staticx diff --git a/test_linux.sh b/test_linux.sh new file mode 100644 index 0000000..ae5e25b --- /dev/null +++ b/test_linux.sh @@ -0,0 +1,4 @@ +#!/bin/bash +PYTHONPATH=kucher pytest + +pycodestyle diff --git a/test_windows.bat b/test_windows.bat new file mode 100644 index 0000000..bbaf246 --- /dev/null +++ b/test_windows.bat @@ -0,0 +1,4 @@ +set PYTHONPATH=kucher +pytest + +pycodestyle