From d0170982bf1f1f4e5e2f8e50a74923ffb296a4f7 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 1 May 2024 14:17:15 -0400 Subject: [PATCH 1/3] Add `piker.ui.qt` as a `PyQt6` shim module For the future, like if we ever get a `PyQt7` (or wtv else..), add a module which allows changing Qt binding lib imports from one spot for all other `.ui` submodules. In some sense this is like a shoddier, less dynamic version of how `pyqtgraph.Qt.__init__.py` supports multiple libs; it might actually make sense eventually to instead import from their shim layer instead? Included is a draft attempt at exposing a bunch of enums which under custom names: - while the specific grouping of values seem to always stay consistent, the root enum's seem to almost always get moved around in the `PyQtX` module namespace. - changing groupings and/or each top level enum's ns location can more simply be changed/re-orged from one spot. - allows `.ui` consumer code to use a name more relevant to `piker`'s usage of wtv UI component is being configured. --- piker/ui/qt.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 piker/ui/qt.py diff --git a/piker/ui/qt.py b/piker/ui/qt.py new file mode 100644 index 00000000..9dbb971c --- /dev/null +++ b/piker/ui/qt.py @@ -0,0 +1,104 @@ +# piker: trading gear for hackers +# Copyright (C) Tyler Goodlet (in stewardship for pikers) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +''' +Qt UI framework version shimming. + +Allow importing sub-pkgs from this module instead of worrying about +major version specifics, any enum moves or component renames. + +Code in `piker.ui.*` should always explicitlyimport directly from +this module like `from piker.ui.qt import ( ..` + +''' +from enum import EnumType + +from PyQt6 import ( + QtCore, + QtGui, + QtWidgets, +) +from PyQt6.QtCore import ( + Qt, + QCoreApplication, + QLineF, + QRectF, + # NOTE: for enums use the `.Type` subattr-space + QEvent, + QPointF, + QSize, + QModelIndex, + QItemSelectionModel, + pyqtBoundSignal, + pyqtRemoveInputHook, +) + +align_flag: EnumType = Qt.AlignmentFlag +txt_flag: EnumType = Qt.TextFlag +keys: EnumType = QEvent.Type +scrollbar_policy: EnumType = Qt.ScrollBarPolicy + +# ^-NOTE-^: handy snippet to discover enums: +# import enum +# [attr for attr_name in dir(QFrame) +# if (attr := getattr(QFrame, attr_name)) +# and isinstance(attr, enum.EnumType)] + +from PyQt6.QtGui import ( + QPainter, + QPainterPath, + QIcon, + QPixmap, + QColor, + QTransform, + QStandardItem, + QStandardItemModel, + QWheelEvent, + QScreen, + QCloseEvent, +) + +from PyQt6.QtWidgets import ( + QMainWindow, + QApplication, + QLabel, + QStatusBar, + QLineEdit, + QHBoxLayout, + QVBoxLayout, + QFormLayout, + QProgressBar, + QSizePolicy, + QStyledItemDelegate, + QStyleOptionViewItem, + QComboBox, + QWidget, + QFrame, + QSplitter, + QTreeView, + QStyle, + QGraphicsItem, + QGraphicsPathItem, + # QGraphicsView, + QStyleOptionGraphicsItem, + QGraphicsScene, + QGraphicsSceneMouseEvent, + QGraphicsProxyWidget, +) + +gs_keys: EnumType = QGraphicsSceneMouseEvent.Type +size_policy: EnumType = QtWidgets.QSizePolicy.Policy +px_cache_mode: EnumType = QGraphicsItem.CacheMode -- 2.34.1 From 1fd8654ca5bbbd2bb43bc8ca65c60a934c953403 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 1 May 2024 14:33:10 -0400 Subject: [PATCH 2/3] Port all `.ui*` submods to new `.ui.qt` imports This also officially moves the code base to using `PyQt6` including all necessary reference changes and enum namespace path moves. Also includes a small `.ui.order_mode` fix to cancel any `Order.price <= 0` and a name error fix with logging using `msg`, which is already used for the input order msg.. --- piker/ui/__init__.py | 9 ++++---- piker/ui/_anchors.py | 6 +++-- piker/ui/_annotate.py | 18 +++++++++++---- piker/ui/_app.py | 4 +++- piker/ui/_axes.py | 38 +++++++++++++++++--------------- piker/ui/_chart.py | 22 +++++++++---------- piker/ui/_cursor.py | 22 ++++++++++++++----- piker/ui/_curve.py | 28 +++++++++++++----------- piker/ui/_dataviz.py | 2 +- piker/ui/_display.py | 47 +++++++++++++++++++++++++++++++++------- piker/ui/_editors.py | 27 +++++++++++------------ piker/ui/_event.py | 32 ++++++++++++++++----------- piker/ui/_exec.py | 25 ++++++++++----------- piker/ui/_forms.py | 40 +++++++++++++++++++++------------- piker/ui/_icons.py | 24 +++++++++++++------- piker/ui/_interaction.py | 42 +++++++++++++++++------------------ piker/ui/_l1.py | 7 ++++-- piker/ui/_label.py | 25 ++++++++++++++------- piker/ui/_lines.py | 20 ++++++++++------- piker/ui/_ohlc.py | 8 +++---- piker/ui/_remote_ctl.py | 6 ++--- piker/ui/_render.py | 2 +- piker/ui/_search.py | 43 +++++++++++++++++++++--------------- piker/ui/_style.py | 8 +++++-- piker/ui/_window.py | 12 +++++----- piker/ui/order_mode.py | 26 +++++++++++++++------- 26 files changed, 327 insertions(+), 216 deletions(-) diff --git a/piker/ui/__init__.py b/piker/ui/__init__.py index 88771b2d..134e0da0 100644 --- a/piker/ui/__init__.py +++ b/piker/ui/__init__.py @@ -14,9 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -""" -Stuff for your eyes, aka super hawt Qt UI components. +''' +UI components built using `Qt` with major versions swapped in via +the import indirection in the `.qt` sub-mod. -Currently we only support PyQt5 due to this issue in Pyside2: -https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1313 -""" +''' diff --git a/piker/ui/_anchors.py b/piker/ui/_anchors.py index 5d8217c8..09bcf71d 100644 --- a/piker/ui/_anchors.py +++ b/piker/ui/_anchors.py @@ -21,8 +21,10 @@ Anchor funtions for UI placement of annotions. from __future__ import annotations from typing import Callable, TYPE_CHECKING -from PyQt5.QtCore import QPointF -from PyQt5.QtWidgets import QGraphicsPathItem +from piker.ui.qt import ( + QPointF, + QGraphicsPathItem, +) if TYPE_CHECKING: from ._chart import ChartPlotWidget diff --git a/piker/ui/_annotate.py b/piker/ui/_annotate.py index f3eeeb07..0dca9dcc 100644 --- a/piker/ui/_annotate.py +++ b/piker/ui/_annotate.py @@ -20,12 +20,22 @@ Annotations for ur faces. """ from typing import Callable -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QPointF, QRectF -from PyQt5.QtWidgets import QGraphicsPathItem -from pyqtgraph import Point, functions as fn, Color +from pyqtgraph import ( + Point, + functions as fn, + Color, +) import numpy as np +from piker.ui.qt import ( + QtCore, + QtGui, + QtWidgets, + QPointF, + QRectF, + QGraphicsPathItem, +) + def mk_marker_path( diff --git a/piker/ui/_app.py b/piker/ui/_app.py index 199ba656..5733e372 100644 --- a/piker/ui/_app.py +++ b/piker/ui/_app.py @@ -21,9 +21,11 @@ Main app startup and run. from functools import partial from types import ModuleType -from PyQt5.QtCore import QEvent import trio +from piker.ui.qt import ( + QEvent, +) from ..service import maybe_spawn_brokerd from . import _event from ._exec import run_qtractor diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py index 461842db..5eab5afe 100644 --- a/piker/ui/_axes.py +++ b/piker/ui/_axes.py @@ -25,9 +25,16 @@ from math import floor import polars as pl import pyqtgraph as pg -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QPointF +from piker.ui.qt import ( + QtCore, + QtGui, + QtWidgets, + QPointF, + txt_flag, + align_flag, + px_cache_mode, +) from . import _pg_overrides as pgo from ..accounting._mktinfo import float_digits from ._label import Label @@ -414,11 +421,15 @@ class AxisLabel(pg.GraphicsObject): super().__init__() self.setParentItem(parent) - self.setFlag(self.ItemIgnoresTransformations) + self.setFlag( + self.GraphicsItemFlag.ItemIgnoresTransformations + ) self.setZValue(100) # XXX: pretty sure this is faster - self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + self.setCacheMode( + px_cache_mode.DeviceCoordinateCache + ) self._parent = parent @@ -555,21 +566,14 @@ class AxisLabel(pg.GraphicsObject): return (self.rect.width(), self.rect.height()) -# _common_text_flags = ( -# QtCore.Qt.TextDontClip | -# QtCore.Qt.AlignCenter | -# QtCore.Qt.AlignTop | -# QtCore.Qt.AlignHCenter | -# QtCore.Qt.AlignVCenter -# ) class XAxisLabel(AxisLabel): _x_margin = 8 text_flags = ( - QtCore.Qt.TextDontClip - | QtCore.Qt.AlignCenter + align_flag.AlignCenter + | txt_flag.TextDontClip ) def size_hint(self) -> tuple[float, float]: @@ -626,10 +630,10 @@ class YAxisLabel(AxisLabel): _y_margin: int = 4 text_flags = ( - QtCore.Qt.AlignLeft - # QtCore.Qt.AlignHCenter - | QtCore.Qt.AlignVCenter - | QtCore.Qt.TextDontClip + align_flag.AlignLeft + | align_flag.AlignVCenter + # | align_flag.AlignHCenter + | txt_flag.TextDontClip ) def __init__( diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index e00ad70b..afcd7dd0 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -28,22 +28,20 @@ from typing import ( TYPE_CHECKING, ) -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import ( +import pyqtgraph as pg +import trio + +from piker.ui.qt import ( + QtCore, + QtWidgets, Qt, QLineF, - # QPointF, -) -from PyQt5.QtWidgets import ( QFrame, QWidget, QHBoxLayout, QVBoxLayout, QSplitter, ) -import pyqtgraph as pg -import trio - from ._axes import ( DynamicDateAxis, PriceAxis, @@ -570,8 +568,8 @@ class LinkedSplits(QWidget): # style? self.chart.setFrameStyle( - QFrame.StyledPanel | - QFrame.Plain + QFrame.Shape.StyledPanel | + QFrame.Shadow.Plain ) return self.chart @@ -689,8 +687,8 @@ class LinkedSplits(QWidget): cpw.plotItem.vb.linked = self cpw.setFrameStyle( - QtWidgets.QFrame.StyledPanel - # | QtWidgets.QFrame.Plain + QFrame.Shape.StyledPanel + # | QFrame.Shadow.Plain ) # don't show the little "autoscale" A label. diff --git a/piker/ui/_cursor.py b/piker/ui/_cursor.py index c14387e0..7675b2e0 100644 --- a/piker/ui/_cursor.py +++ b/piker/ui/_cursor.py @@ -28,9 +28,14 @@ from typing import ( import inspect import numpy as np import pyqtgraph as pg -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import QPointF, QRectF +from piker.ui.qt import ( + QPointF, + QRectF, + QtCore, + QtWidgets, + px_cache_mode, +) from ._style import ( _xaxis_at, hcolor, @@ -104,7 +109,9 @@ class LineDot(pg.CurvePoint): dot.setParentItem(self) # keep a static size - self.setFlag(self.ItemIgnoresTransformations) + self.setFlag( + self.GraphicsItemFlag.ItemIgnoresTransformations + ) def event( self, @@ -424,10 +431,10 @@ class Cursor(pg.GraphicsObject): # vertical and horizonal lines and a y-axis label vl = plot.addLine(x=0, pen=self.lines_pen, movable=False) - vl.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + vl.setCacheMode(px_cache_mode.DeviceCoordinateCache) hl = plot.addLine(y=0, pen=self.lines_pen, movable=False) - hl.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + hl.setCacheMode(px_cache_mode.DeviceCoordinateCache) hl.hide() yl = YAxisLabel( @@ -511,7 +518,10 @@ class Cursor(pg.GraphicsObject): plot=chart ) chart.addItem(cursor) - self.graphics[chart].setdefault('cursors', []).append(cursor) + self.graphics[chart].setdefault( + 'cursors', + [], + ).append(cursor) return cursor def mouseAction( diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index ff01c89d..dc368344 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -19,20 +19,21 @@ Fast, smooth, sexy curves. """ from contextlib import contextmanager as cm +from enum import EnumType from typing import Callable import numpy as np import pyqtgraph as pg -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import QGraphicsItem -from PyQt5.QtCore import ( + +from piker.ui.qt import ( + QtWidgets, + QGraphicsItem, Qt, QLineF, QRectF, -) -from PyQt5.QtGui import ( QPainter, QPainterPath, + px_cache_mode, ) from ._style import hcolor from ..log import get_logger @@ -42,15 +43,16 @@ from ..toolz.profile import ( ms_slower_then, ) - log = get_logger(__name__) +pen_style: EnumType = Qt.PenStyle + _line_styles: dict[str, int] = { - 'solid': Qt.PenStyle.SolidLine, - 'dash': Qt.PenStyle.DashLine, - 'dot': Qt.PenStyle.DotLine, - 'dashdot': Qt.PenStyle.DashDotLine, + 'solid': pen_style.SolidLine, + 'dash': pen_style.DashLine, + 'dot': pen_style.DotLine, + 'dashdot': pen_style.DashDotLine, } @@ -69,12 +71,12 @@ class FlowGraphic(pg.GraphicsObject): # XXX-NOTE-XXX: graphics caching B) # see explanation for different caching modes: # https://stackoverflow.com/a/39410081 - cache_mode: int = QGraphicsItem.DeviceCoordinateCache + cache_mode: int = px_cache_mode.DeviceCoordinateCache # XXX: WARNING item caching seems to only be useful # if we don't re-generate the entire QPainterPath every time # don't ever use this - it's a colossal nightmare of artefacts # and is disastrous for performance. - # QGraphicsItem.ItemCoordinateCache + # cache_mode.ItemCoordinateCache # TODO: still questions todo with coord-cacheing that we should # probably talk to a core dev about: # - if this makes trasform interactions slower (such as zooming) @@ -176,7 +178,7 @@ class FlowGraphic(pg.GraphicsObject): @cm def reset_cache(self) -> None: try: - none = QGraphicsItem.NoCache + none = px_cache_mode.NoCache log.debug( f'{self._name} -> CACHE DISABLE: {none}' ) diff --git a/piker/ui/_dataviz.py b/piker/ui/_dataviz.py index cf3d5509..36251e48 100644 --- a/piker/ui/_dataviz.py +++ b/piker/ui/_dataviz.py @@ -40,8 +40,8 @@ from numpy import ( ndarray, ) import pyqtgraph as pg -from PyQt5.QtCore import QLineF +from piker.ui.qt import QLineF from ..data._sharedmem import ( ShmArray, ) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 5cb89f54..46e1b922 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -57,6 +57,7 @@ from piker.toolz import ( Profiler, ) from piker.log import get_logger +from piker import config # from ..data._source import tf_in_1s from ._axes import YAxisLabel from ._chart import ( @@ -1231,6 +1232,8 @@ async def link_views_with_region( # region.sigRegionChangeFinished.connect(update_pi_from_region) +# NOTE: default is set to 60 FPS until the runtime delivers the +# discoverd hw value below. _quote_throttle_rate: int = 60 - 6 @@ -1272,26 +1275,54 @@ async def display_symbol_data( # TODO: ctl over update loop's maximum frequency. # - load this from a config.toml! # - allow dyanmic configuration from chart UI? + ( + conf, + path, + ) = config.load() + ui_conf: dict = conf['ui'] + global _quote_throttle_rate from ._window import main_window - display_rate = main_window().current_screen().refreshRate() - _quote_throttle_rate = floor(display_rate) - 6 + + display_rate: int = floor( + main_window().current_screen().refreshRate() + ) - 6 + + mx_redraw_rate: int = ui_conf.get( + 'max_redraw_rate', + _quote_throttle_rate, + ) + + if mx_redraw_rate < display_rate: + log.info( + 'Down-throttling redraw rate to config setting\n' + f'display FPS: {display_rate}\n' + 'max_redraw_rate: {max_redraw_rate}\n' + ) + else: + _quote_throttle_rate = display_rate # TODO: we should be able to increase this if we use some # `mypyc` speedups elsewhere? 22ish seems to be the sweet # spot for single-feed chart. num_of_feeds = len(fqmes) - mx: int = 22 - if num_of_feeds > 1: - # there will be more ctx switches with more than 1 feed so we - # max throttle down a bit more. - mx = 16 + # if num_of_feeds > 1: + + # there will be more ctx switches with more than 1 feed so we + # max throttle down a bit more. + mx_per_feed: int = ( + ui_conf.get( + 'per_feed_redraw_rate', + mx_redraw_rate, + ) + or 16 + ) # limit to at least display's FPS # avoiding needless Qt-in-guest-mode context switches cycles_per_feed = min( round(_quote_throttle_rate/num_of_feeds), - mx, + mx_per_feed, ) feed: Feed diff --git a/piker/ui/_editors.py b/piker/ui/_editors.py index 5158c507..9aba7978 100644 --- a/piker/ui/_editors.py +++ b/piker/ui/_editors.py @@ -32,24 +32,21 @@ from pyqtgraph import ( QtCore, QtWidgets, ) -from PyQt5.QtCore import ( - QPointF, - QRectF, -) -from PyQt5.QtGui import ( - QColor, - QTransform, -) -from PyQt5.QtWidgets import ( - QGraphicsProxyWidget, - QGraphicsScene, - QLabel, -) from pyqtgraph import functions as fn import numpy as np from piker.types import Struct +from piker.ui.qt import ( + Qt, + QPointF, + QRectF, + QGraphicsProxyWidget, + QGraphicsScene, + QLabel, + QColor, + QTransform, +) from ._style import ( hcolor, _font, @@ -316,7 +313,9 @@ class SelectRect(QtWidgets.QGraphicsRectItem): self.setZValue(1e9) label = self._label = QLabel() - label.setTextFormat(0) # markdown + label.setTextFormat( + Qt.TextFormat.MarkdownText + ) label.setFont(_font.font) label.setMargin(0) label.setAlignment( diff --git a/piker/ui/_event.py b/piker/ui/_event.py index 27b98c97..44797fa4 100644 --- a/piker/ui/_event.py +++ b/piker/ui/_event.py @@ -23,28 +23,29 @@ from typing import Callable import trio from tractor.trionics import gather_contexts -from PyQt5 import QtCore -from PyQt5.QtCore import QEvent, pyqtBoundSignal -from PyQt5.QtWidgets import QWidget -from PyQt5.QtWidgets import ( - QGraphicsSceneMouseEvent as gs_mouse, -) +from piker.ui.qt import ( + QtCore, + QWidget, + QEvent, + keys, + gs_keys, + pyqtBoundSignal, +) from piker.types import Struct MOUSE_EVENTS = { - gs_mouse.GraphicsSceneMousePress, - gs_mouse.GraphicsSceneMouseRelease, - QEvent.MouseButtonPress, - QEvent.MouseButtonRelease, + gs_keys.GraphicsSceneMousePress, + gs_keys.GraphicsSceneMouseRelease, + keys.MouseButtonPress, + keys.MouseButtonRelease, # QtGui.QMouseEvent, } # TODO: maybe consider some constrained ints down the road? # https://pydantic-docs.helpmanual.io/usage/types/#constrained-types - class KeyboardMsg(Struct): '''Unpacked Qt keyboard event data. @@ -114,7 +115,10 @@ class EventRelay(QtCore.QObject): # something to do with Qt internals and calling the # parent handler? - if etype in {QEvent.KeyPress, QEvent.KeyRelease}: + if etype in { + QEvent.Type.KeyPress, + QEvent.Type.KeyRelease, + }: msg = KeyboardMsg( event=ev, @@ -160,7 +164,9 @@ class EventRelay(QtCore.QObject): async def open_event_stream( source_widget: QWidget, - event_types: set[QEvent] = {QEvent.KeyPress}, + event_types: set[QEvent] = { + QEvent.Type.KeyPress, + }, filter_auto_repeats: bool = True, ) -> trio.abc.ReceiveChannel: diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 5b0655da..ba91e534 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -30,25 +30,22 @@ from typing import ( import platform import traceback -# Qt specific -import PyQt5 # noqa -from PyQt5.QtWidgets import ( - QWidget, - QMainWindow, - QApplication, -) -from PyQt5 import QtCore -from PyQt5.QtCore import ( - pyqtRemoveInputHook, - Qt, - QCoreApplication, -) import qdarkstyle from qdarkstyle import DarkPalette # import qdarkgraystyle # TODO: play with it import trio from outcome import Error +# Qt version-agnostic +from .qt import ( + QWidget, + QMainWindow, + QApplication, + QtCore, + pyqtRemoveInputHook, + Qt, + QCoreApplication, +) from ..service import ( maybe_open_pikerd, get_runtime_vars, @@ -150,7 +147,7 @@ def run_qtractor( # load dark theme stylesheet = qdarkstyle.load_stylesheet( - qt_api='pyqt5', + qt_api='pyqt6', palette=DarkPalette, ) app.setStyleSheet(stylesheet) diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index de64747e..504dd418 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -28,9 +28,15 @@ from typing import ( ) import trio -from PyQt5 import QtGui -from PyQt5.QtCore import QSize, QModelIndex, Qt, QEvent -from PyQt5.QtWidgets import ( + +from piker.ui.qt import ( + keys, + size_policy, + QtGui, + QSize, + QModelIndex, + Qt, + QEvent, QWidget, QLabel, QComboBox, @@ -39,7 +45,6 @@ from PyQt5.QtWidgets import ( QVBoxLayout, QFormLayout, QProgressBar, - QSizePolicy, QStyledItemDelegate, QStyleOptionViewItem, ) @@ -71,14 +76,14 @@ class Edit(QLineEdit): if width_in_chars: self._chars = int(width_in_chars) - x_size_policy = QSizePolicy.Fixed + x_size_policy = size_policy.Fixed else: # chart count which will be used to calculate # width of input field. self._chars: int = 6 # fit to surroundingn frame width - x_size_policy = QSizePolicy.Expanding + x_size_policy = size_policy.Expanding super().__init__(parent) @@ -86,7 +91,7 @@ class Edit(QLineEdit): # https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum self.setSizePolicy( x_size_policy, - QSizePolicy.Fixed, + size_policy.Fixed, ) self.setFont(font.font) @@ -180,11 +185,13 @@ class Selection(QComboBox): self._items: dict[str, int] = {} super().__init__(parent=parent) - self.setSizeAdjustPolicy(QComboBox.AdjustToContents) + self.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToContents, + ) # make line edit expand to surrounding frame self.setSizePolicy( - QSizePolicy.Expanding, - QSizePolicy.Fixed, + size_policy.Expanding, + size_policy.Fixed, ) view = self.view() view.setUniformItemSizes(True) @@ -308,8 +315,8 @@ class FieldsForm(QWidget): # size it as we specify self.setSizePolicy( - QSizePolicy.Expanding, - QSizePolicy.Expanding, + size_policy.Expanding, + size_policy.Expanding, ) # XXX: not sure why we have to create this here exactly @@ -416,8 +423,8 @@ class FieldsForm(QWidget): select.set_items(values) self.setSizePolicy( - QSizePolicy.Fixed, - QSizePolicy.Fixed, + size_policy.Fixed, + size_policy.Fixed, ) select.show() self.form.addRow(label, select) @@ -437,7 +444,10 @@ async def handle_field_input( async for kbmsg in recv_chan: - if kbmsg.etype in {QEvent.KeyPress, QEvent.KeyRelease}: + if kbmsg.etype in { + keys.KeyPress, + keys.KeyRelease, + }: event, etype, key, mods, txt = kbmsg.to_tuple() print(f'key: {kbmsg.key}, mods: {kbmsg.mods}, txt: {kbmsg.txt}') diff --git a/piker/ui/_icons.py b/piker/ui/_icons.py index feb0cbb6..c42a93b7 100644 --- a/piker/ui/_icons.py +++ b/piker/ui/_icons.py @@ -15,15 +15,18 @@ # along with this program. If not, see . ''' -``QIcon`` hackery. +`QIcon` hackery. + +Mostly dynamically loading pixmaps for use with `QGraphicsScene`. ''' -from PyQt5.QtWidgets import QStyle -from PyQt5.QtGui import ( - QIcon, QPixmap, QColor +from piker.ui.qt import ( + QSize, + QStyle, + QIcon, + QPixmap, + QColor, ) -from PyQt5.QtCore import QSize - from ._style import hcolor # https://www.pythonguis.com/faq/built-in-qicons-pyqt/ @@ -44,7 +47,8 @@ def mk_icons( size: QSize, ) -> dict[str, QIcon]: - '''This helper is indempotent. + ''' + This helper is indempotent. ''' global _icons, _icon_names @@ -56,7 +60,11 @@ def mk_icons( # load account selection using current style for name, icon_name in _icon_names.items(): - stdpixmap = getattr(QStyle, icon_name) + stdpixmap = getattr( + # https://www.pythonguis.com/faq/built-in-qicons-pyqt/ + QStyle.StandardPixmap, # pyqt/pyside6 + icon_name, + ) stdicon = style.standardIcon(stdpixmap) pixmap = stdicon.pixmap(size) diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 7c710506..9bd48139 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -36,23 +36,21 @@ import pyqtgraph as pg # this down the road.. Bo from pyqtgraph.GraphicsScene import mouseEvents as mevs # from pyqtgraph.GraphicsScene.mouseEvents import MouseDragEvent -from PyQt5.QtWidgets import QGraphicsSceneMouseEvent as gs_mouse -from PyQt5.QtGui import ( - QWheelEvent, -) -from PyQt5.QtCore import ( - Qt, - QEvent, -) from pyqtgraph import ( ViewBox, Point, QtCore, + functions as fn, ) -from pyqtgraph import functions as fn import numpy as np import trio +from piker.ui.qt import ( + QWheelEvent, + QGraphicsSceneMouseEvent as gs_mouse, + Qt, + QEvent, +) from ..log import get_logger from ..toolz import ( Profiler, @@ -81,22 +79,22 @@ if TYPE_CHECKING: log = get_logger(__name__) NUMBER_LINE = { - Qt.Key_1, - Qt.Key_2, - Qt.Key_3, - Qt.Key_4, - Qt.Key_5, - Qt.Key_6, - Qt.Key_7, - Qt.Key_8, - Qt.Key_9, - Qt.Key_0, + Qt.Key.Key_1, + Qt.Key.Key_2, + Qt.Key.Key_3, + Qt.Key.Key_4, + Qt.Key.Key_5, + Qt.Key.Key_6, + Qt.Key.Key_7, + Qt.Key.Key_8, + Qt.Key.Key_9, + Qt.Key.Key_0, } ORDER_MODE = { - Qt.Key_A, - Qt.Key_F, - Qt.Key_D, + Qt.Key.Key_A, + Qt.Key.Key_F, + Qt.Key.Key_D, } diff --git a/piker/ui/_l1.py b/piker/ui/_l1.py index 23162c70..8d29d90c 100644 --- a/piker/ui/_l1.py +++ b/piker/ui/_l1.py @@ -21,9 +21,12 @@ Double auction top-of-book (L1) graphics. from typing import Tuple import pyqtgraph as pg -from PyQt5 import QtCore, QtGui -from PyQt5.QtCore import QPointF +from piker.ui.qt import ( + QPointF, + QtCore, + QtGui, +) from ._axes import YAxisLabel from ._style import hcolor from ._pg_overrides import PlotItem diff --git a/piker/ui/_label.py b/piker/ui/_label.py index 1e010f18..0e90b7fe 100644 --- a/piker/ui/_label.py +++ b/piker/ui/_label.py @@ -25,10 +25,17 @@ from typing import ( ) import pyqtgraph as pg -from PyQt5 import QtGui, QtWidgets -from PyQt5.QtWidgets import QLabel, QSizePolicy -from PyQt5.QtCore import QPointF, QRectF, Qt +from piker.ui.qt import ( + px_cache_mode, + QtGui, + QtWidgets, + QLabel, + size_policy, + QPointF, + QRectF, + Qt, +) from ._style import ( DpiAwareFont, hcolor, @@ -78,7 +85,7 @@ class Label: self._x_offset = x_offset txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent) - txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + txt.setCacheMode(px_cache_mode.DeviceCoordinateCache) vb.scene().addItem(txt) @@ -103,7 +110,7 @@ class Label: self._anchor_func = self.txt.pos().x # not sure if this makes a diff - self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + self.txt.setCacheMode(px_cache_mode.DeviceCoordinateCache) # TODO: edit and selection support # https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum @@ -299,12 +306,14 @@ class FormatLabel(QLabel): """ ) self.setFont(_font.font) - self.setTextFormat(Qt.MarkdownText) # markdown + self.setTextFormat( + Qt.TextFormat.MarkdownText + ) self.setMargin(0) self.setSizePolicy( - QSizePolicy.Expanding, - QSizePolicy.Expanding, + size_policy.Expanding, + size_policy.Expanding, ) self.setAlignment( Qt.AlignVCenter | Qt.AlignLeft diff --git a/piker/ui/_lines.py b/piker/ui/_lines.py index a2ea5331..e1b6d3ed 100644 --- a/piker/ui/_lines.py +++ b/piker/ui/_lines.py @@ -27,20 +27,22 @@ from typing import ( ) import pyqtgraph as pg -from pyqtgraph import Point, functions as fn -from PyQt5 import ( +from pyqtgraph import ( + Point, + functions as fn, +) + +from piker.ui.qt import ( + px_cache_mode, QtCore, QtGui, -) -from PyQt5.QtWidgets import ( QGraphicsPathItem, QStyleOptionGraphicsItem, QGraphicsItem, QGraphicsScene, QWidget, + QPointF, ) -from PyQt5.QtCore import QPointF - from ._annotate import LevelMarker from ._anchors import ( vbr_left, @@ -140,7 +142,9 @@ class LevelLine(pg.InfiniteLine): self._right_end_sc: float = 0 # use px caching - self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) + self.setCacheMode( + px_cache_mode.DeviceCoordinateCache + ) def txt_offsets(self) -> tuple[int, int]: return 0, 0 @@ -211,7 +215,7 @@ class LevelLine(pg.InfiniteLine): ) -> None: if not called_from_on_pos_change: - last = self.value() + last: float = self.value() # if the position hasn't changed then ``.update_labels()`` # will not be called by a non-triggered `.on_pos_change()`, diff --git a/piker/ui/_ohlc.py b/piker/ui/_ohlc.py index c43926a1..34fa3362 100644 --- a/piker/ui/_ohlc.py +++ b/piker/ui/_ohlc.py @@ -20,16 +20,14 @@ Super fast OHLC sampling graphics types. from __future__ import annotations import numpy as np -from PyQt5 import ( + +from piker.ui.qt import ( QtGui, QtWidgets, -) -from PyQt5.QtCore import ( + QPainterPath, QLineF, QRectF, ) -from PyQt5.QtGui import QPainterPath - from ._curve import FlowGraphic from ..toolz import ( Profiler, diff --git a/piker/ui/_remote_ctl.py b/piker/ui/_remote_ctl.py index 0dddc5c5..ccc8d6e9 100644 --- a/piker/ui/_remote_ctl.py +++ b/piker/ui/_remote_ctl.py @@ -38,14 +38,14 @@ from tractor import ( Context, MsgStream, ) -from PyQt5.QtWidgets import ( - QGraphicsItem, -) from piker.log import get_logger from piker.types import Struct from piker.service import find_service from piker.brokers import SymbolNotFound +from piker.ui.qt import ( + QGraphicsItem, +) from ._display import DisplayState from ._interaction import ChartView from ._editors import SelectRect diff --git a/piker/ui/_render.py b/piker/ui/_render.py index bd3d1757..64fad999 100644 --- a/piker/ui/_render.py +++ b/piker/ui/_render.py @@ -30,8 +30,8 @@ from typing import ( import msgspec import numpy as np import pyqtgraph as pg -from PyQt5.QtGui import QPainterPath +from piker.ui.qt import QPainterPath from ..data._formatters import ( IncrementalFormatter, ) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index d6af5535..16b25a46 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -48,27 +48,24 @@ from pprint import pformat from rapidfuzz import process as fuzzy import trio from trio_typing import TaskStatus -from PyQt5 import QtCore -from PyQt5 import QtWidgets -from PyQt5.QtCore import ( + +from piker.ui.qt import ( + size_policy, + align_flag, Qt, + QtCore, + QtWidgets, QModelIndex, QItemSelectionModel, -) -from PyQt5.QtGui import ( # QLayout, QStandardItem, QStandardItemModel, -) -from PyQt5.QtWidgets import ( QWidget, QTreeView, # QListWidgetItem, # QAbstractScrollArea, # QStyledItemDelegate, ) - - from ..log import get_logger from ._style import ( _font, @@ -129,8 +126,8 @@ class CompleterView(QTreeView): # ux settings self.setSizePolicy( - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding, + size_policy.Expanding, + size_policy.Expanding, ) self.setItemsExpandable(True) self.setExpandsOnDoubleClick(False) @@ -567,8 +564,8 @@ class SearchWidget(QtWidgets.QWidget): # size it as we specify self.setSizePolicy( - QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Fixed, + size_policy.Fixed, + size_policy.Fixed, ) self.godwidget = godwidget @@ -592,14 +589,16 @@ class SearchWidget(QtWidgets.QWidget): }} """ ) - label.setTextFormat(3) # markdown + label.setTextFormat( + Qt.TextFormat.MarkdownText + ) label.setFont(_font.font) label.setMargin(4) label.setText("search:") label.show() label.setAlignment( - QtCore.Qt.AlignVCenter - | QtCore.Qt.AlignLeft + align_flag.AlignVCenter + | align_flag.AlignLeft ) self.bar_hbox.addWidget(label) @@ -617,9 +616,17 @@ class SearchWidget(QtWidgets.QWidget): self.vbox.addLayout(self.bar_hbox) - self.vbox.setAlignment(self.bar, Qt.AlignTop | Qt.AlignRight) + self.vbox.setAlignment( + self.bar, + align_flag.AlignTop + | align_flag.AlignRight, + ) self.vbox.addWidget(self.bar.view) - self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft) + self.vbox.setAlignment( + self.view, + align_flag.AlignTop + | align_flag.AlignLeft, + ) def focus(self) -> None: self.show() diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 2d17b62d..302d9d30 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -22,10 +22,14 @@ from typing import Dict import math import pyqtgraph as pg -from PyQt5 import QtCore, QtGui -from PyQt5.QtCore import Qt, QCoreApplication from qdarkstyle import DarkPalette +from .qt import ( + QtCore, + QtGui, + Qt, + QCoreApplication, +) from ..log import get_logger from .. import config diff --git a/piker/ui/_window.py b/piker/ui/_window.py index 0fc87c24..a15ecd24 100644 --- a/piker/ui/_window.py +++ b/piker/ui/_window.py @@ -27,16 +27,14 @@ from typing import ( ) import uuid -from PyQt5 import QtCore -from PyQt5.QtWidgets import ( +from piker.ui.qt import ( + Qt, + QtCore, QWidget, QMainWindow, QApplication, QLabel, QStatusBar, -) - -from PyQt5.QtGui import ( QScreen, QCloseEvent, ) @@ -197,7 +195,9 @@ class MainWindow(QMainWindow): """ # font-size : {font_size}px; ) - label.setTextFormat(3) # markdown + label.setTextFormat( + Qt.TextFormat.MarkdownText + ) label.setFont(_font_small.font) label.setMargin(2) label.setAlignment( diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index ea96e97a..d5720e8a 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -34,7 +34,6 @@ import uuid from bidict import bidict import tractor import trio -from PyQt5.QtCore import Qt from piker import config from piker.accounting import ( @@ -59,6 +58,7 @@ from piker.data import ( ) from piker.types import Struct from piker.log import get_logger +from piker.ui.qt import Qt from ._editors import LineEditor, ArrowEditor from ._lines import order_line, LevelLine from ._position import ( @@ -358,7 +358,7 @@ class OrderMode: send_msg: bool = True, order: Order | None = None, - ) -> Dialog | None: + ) -> Dialog|None: ''' Send execution order to EMS return a level line to represent the order on a chart. @@ -494,7 +494,7 @@ class OrderMode: uuid: str, order: Order | None = None, - ) -> Dialog: + ) -> Dialog | None: ''' Order submitted status event handler. @@ -515,6 +515,11 @@ class OrderMode: # if an order msg is provided update the line # **from** that msg. if order: + if order.price <= 0: + log.error(f'Order has 0 price, cancelling..\n{order}') + self.cancel_orders([order.oid]) + return None + line.set_level(order.price) self.on_level_change_update_next_order_info( level=order.price, @@ -1013,8 +1018,13 @@ async def process_trade_msg( ) -> tuple[Dialog, Status]: - fmsg = pformat(msg) - log.debug(f'Received order msg:\n{fmsg}') + # TODO: obvi once we're parsing to native struct instances we can + # drop the `pformat()` call Bo + fmtmsg: Struct | dict = msg + if not isinstance(msg, Struct): + fmtmsg: str = pformat(msg) + + log.debug(f'Received order msg:\n{fmtmsg}') name = msg['name'] if name in ( @@ -1030,7 +1040,7 @@ async def process_trade_msg( ): log.info( f'Loading position for `{fqme}`:\n' - f'{fmsg}' + f'{fmtmsg}' ) tracker = mode.trackers[msg['account']] tracker.live_pp.update_from_msg(msg) @@ -1072,7 +1082,7 @@ async def process_trade_msg( elif order.action != 'cancel': log.warning( - f'received msg for untracked dialog:\n{fmsg}' + f'received msg for untracked dialog:\n{fmtmsg}' ) assert msg.resp in ('open', 'dark_open'), f'Unknown msg: {msg}' @@ -1139,7 +1149,7 @@ async def process_trade_msg( req={'exec_mode': 'dark'}, ): # TODO: UX for a "pending" clear/live order - log.info(f'Dark order triggered for {fmsg}') + log.info(f'Dark order triggered for {fmtmsg}') case Status( resp='triggered', -- 2.34.1 From 129cf58d4168b9306e9c817cf43412311be81fe9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 May 2024 10:52:48 -0400 Subject: [PATCH 3/3] Bump deps for Py3.12, go PyQt6, tweak ruff rules Code base is already ported for `Qt6` so this removes the pyqt5 dep, adds latest pyqt6 as well as buncha other updates: - add `xonsh` and ptk as dev deps for those of us using wacky shells ;P - bump compiled deps as needed for python 3.12 (`numpy`, `numba`) - add `httpx` and drop `asks` since the latter is zombied and not compat with other libs on 3.12. - add `ruff` linting ignore rules for the new `.ui.qt` shim mod layer. - few other deps updates to latest versions. - add in the `keywords` and `classifiers` sections from the old `setup.py`. --- pyproject.toml | 98 ++++++++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index efaf8336..c97841e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,40 +20,31 @@ build-backend = "poetry.core.masonry.api" # ------ - ------ +[tool.ruff.lint] +# https://docs.astral.sh/ruff/settings/#lint_ignore +ignore = [] + +# https://docs.astral.sh/ruff/settings/#lint_per-file-ignores +"piker/ui/qt.py" = [ + "E402", + 'F401', # unused imports (without __all__ or blah as blah) + # "F841", # unused variable rules +] +# ignore-init-module-imports = false + +# ------ - ------ + [tool.poetry] name = "piker" version = "0.1.0.alpha0.dev0" description = "trading gear for hackers" -authors = ["Tyler Goodlet "] +authors = ["Tyler Goodlet "] license = "AGPLv3" readme = "README.rst" -# TODO: add meta-data from setup.py -# keywords=[ -# "async", -# "trading", -# "finance", -# "quant", -# "charting", -# ], -# classifiers=[ -# 'Development Status :: 3 - Alpha', -# 'License :: OSI Approved :: ', -# 'Operating System :: POSIX :: Linux', -# "Programming Language :: Python :: Implementation :: CPython", -# "Programming Language :: Python :: 3 :: Only", -# "Programming Language :: Python :: 3.10", -# "Programming Language :: Python :: 3.11", -# 'Intended Audience :: Financial and Insurance Industry', -# 'Intended Audience :: Science/Research', -# 'Intended Audience :: Developers', -# 'Intended Audience :: Education', -# ], - # ------ - ------ [tool.poetry.dependencies] -asks = "^3.0.0" async-generator = "^1.10" attrs = "^23.1.0" bidict = "^0.22.1" @@ -63,41 +54,40 @@ cython = "^3.0.0" greenback = "^1.1.1" ib-insync = "^0.9.86" msgspec = "^0.18.0" -numba = "^0.57.1" -numpy = "1.24" -pendulum = "^2.1.2" +numba = "^0.59.0" +numpy = "^1.25" polars = "^0.18.13" pygments = "^2.16.1" -python = "^3.10" +python = ">=3.11, <3.13" rich = "^13.5.2" # setuptools = "^68.0.0" tomli = "^2.0.1" tomli-w = "^1.0.0" -trio = "^0.22.2" trio-util = "^0.7.0" trio-websocket = "^0.10.3" typer = "^0.9.0" +rapidfuzz = "^3.5.2" +pdbp = "^1.5.0" +trio = "^0.24" +pendulum = "^3.0.0" +httpx = "^0.27.0" +[tool.poetry.dependencies.tractor] +develop = true +git = 'https://github.com/goodboy/tractor.git' +branch = 'asyncio_debugger_support' +# path = "../tractor" [tool.poetry.dependencies.asyncvnc] git = 'https://github.com/pikers/asyncvnc.git' branch = 'main' [tool.poetry.dependencies.tomlkit] +develop = true git = 'https://github.com/pikers/tomlkit.git' branch = 'piker_pin' -develop = true # path = "../tomlkit/" -[tool.poetry.dependencies.tractor] -git = 'https://github.com/goodboy/tractor.git' -branch = 'asyncio_debugger_support' -# branch = 'piker_pin' -develop = true -# path = '../tractor/' - -# ------ - ------ - [tool.poetry.group.uis] optional = true [tool.poetry.group.uis.dependencies] @@ -106,11 +96,10 @@ optional = true # rapidfuzz = {extras = ["speedup"], version = "^0.18.0"} rapidfuzz = "^3.2.0" qdarkstyle = ">=3.0.2" -pyqt5 = "^5.15.9" pyqtgraph = { git = 'https://github.com/pikers/pyqtgraph.git' } -pyqt6 = "^6.5.2" # ------ - ------ +pyqt6 = "^6.7.0" [tool.poetry.group.dev] optional = true @@ -118,6 +107,8 @@ optional = true # testing / CI pytest = "^6.0.0" elasticsearch = "^8.9.0" +xonsh = "^0.14.2" +prompt-toolkit = "3.0.40" # console ehancements and eventually remote debugging # extras/helpers. @@ -126,8 +117,6 @@ elasticsearch = "^8.9.0" # - xonsh + xxh # - rsyscall + pdbp # - actor runtime control console like BEAM/OTP -xonsh = "^0.14.0" # XXX: explicit env install for shell use w nix -prompt-toolkit = "^3.0.39" # ------ - ------ @@ -140,3 +129,26 @@ prompt-toolkit = "^3.0.39" piker = 'piker.cli:cli' pikerd = 'piker.cli:pikerd' ledger = 'piker.accounting.cli:ledger' + + +[project] +keywords=[ + "async", + "trading", + "finance", + "quant", + "charting", +] +classifiers=[ + 'Development Status :: 3 - Alpha', + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + 'Operating System :: POSIX :: Linux', + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + 'Intended Audience :: Financial and Insurance Industry', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', +] -- 2.34.1