Compare commits
7 Commits
dpi-font-a
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
1abd47609b | |
|
|
e7a867126b | |
|
|
0a2059d00f | |
|
|
b71d0533b2 | |
|
|
c23d034935 | |
|
|
347a2d67f9 | |
|
|
b22c59e7a0 |
|
|
@ -586,7 +586,7 @@ async def open_price_feed(
|
||||||
fh,
|
fh,
|
||||||
instrument
|
instrument
|
||||||
)
|
)
|
||||||
) as (first, chan):
|
) as (chan, first):
|
||||||
yield chan
|
yield chan
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -653,7 +653,7 @@ async def open_order_feed(
|
||||||
fh,
|
fh,
|
||||||
instrument
|
instrument
|
||||||
)
|
)
|
||||||
) as (first, chan):
|
) as (chan, first):
|
||||||
yield chan
|
yield chan
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import tractor
|
||||||
|
|
||||||
from piker.brokers import open_cached_client
|
from piker.brokers import open_cached_client
|
||||||
from piker.log import get_logger, get_console_log
|
from piker.log import get_logger, get_console_log
|
||||||
from piker.data import ShmArray
|
from tractor.ipc._shm import ShmArray
|
||||||
from piker.brokers._util import (
|
from piker.brokers._util import (
|
||||||
BrokerError,
|
BrokerError,
|
||||||
DataUnavailable,
|
DataUnavailable,
|
||||||
|
|
|
||||||
|
|
@ -1529,7 +1529,7 @@ async def open_client_proxies() -> tuple[
|
||||||
# TODO: maybe this should be the default in tractor?
|
# TODO: maybe this should be the default in tractor?
|
||||||
key=tractor.current_actor().uid,
|
key=tractor.current_actor().uid,
|
||||||
|
|
||||||
) as (cache_hit, (clients, _)),
|
) as (cache_hit, (_, clients)),
|
||||||
|
|
||||||
AsyncExitStack() as stack
|
AsyncExitStack() as stack
|
||||||
):
|
):
|
||||||
|
|
@ -1718,7 +1718,7 @@ async def open_client_proxy(
|
||||||
open_aio_client_method_relay,
|
open_aio_client_method_relay,
|
||||||
client=client,
|
client=client,
|
||||||
event_consumers=event_table,
|
event_consumers=event_table,
|
||||||
) as (first, chan),
|
) as (chan, first),
|
||||||
|
|
||||||
trionics.collapse_eg(), # loose-ify
|
trionics.collapse_eg(), # loose-ify
|
||||||
trio.open_nursery() as relay_tn,
|
trio.open_nursery() as relay_tn,
|
||||||
|
|
|
||||||
|
|
@ -514,8 +514,8 @@ async def open_trade_event_stream(
|
||||||
recv_trade_updates,
|
recv_trade_updates,
|
||||||
client=client,
|
client=client,
|
||||||
) as (
|
) as (
|
||||||
_, # first pushed val
|
|
||||||
trade_event_stream,
|
trade_event_stream,
|
||||||
|
_, # first pushed val
|
||||||
):
|
):
|
||||||
task_status.started(trade_event_stream)
|
task_status.started(trade_event_stream)
|
||||||
# block forever to keep session trio-asyncio session
|
# block forever to keep session trio-asyncio session
|
||||||
|
|
|
||||||
|
|
@ -989,7 +989,7 @@ async def open_aio_quote_stream(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
contract=contract,
|
contract=contract,
|
||||||
|
|
||||||
) as (contract, from_aio):
|
) as (from_aio, contract):
|
||||||
|
|
||||||
assert contract
|
assert contract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ sharing live streams over a network.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .ticktools import iterticks
|
from .ticktools import iterticks
|
||||||
from ._sharedmem import (
|
from tractor.ipc._shm import (
|
||||||
maybe_open_shm_array,
|
|
||||||
attach_shm_array,
|
|
||||||
open_shm_array,
|
|
||||||
get_shm_token,
|
|
||||||
ShmArray,
|
ShmArray,
|
||||||
|
get_shm_token,
|
||||||
|
open_shm_ndarray as open_shm_array,
|
||||||
|
attach_shm_ndarray as attach_shm_array,
|
||||||
)
|
)
|
||||||
|
from ._sharedmem import maybe_open_shm_array
|
||||||
from ._source import (
|
from ._source import (
|
||||||
def_iohlcv_fields,
|
def_iohlcv_fields,
|
||||||
def_ohlcv_fields,
|
def_ohlcv_fields,
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ from msgspec import field
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.lib import recfunctions as rfn
|
from numpy.lib import recfunctions as rfn
|
||||||
|
|
||||||
from ._sharedmem import (
|
from tractor.ipc._shm import ShmArray
|
||||||
ShmArray,
|
|
||||||
)
|
|
||||||
from ._pathops import (
|
from ._pathops import (
|
||||||
path_arrays_from_ohlc,
|
path_arrays_from_ohlc,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,7 @@ from ._util import (
|
||||||
from ..service import maybe_spawn_daemon
|
from ..service import maybe_spawn_daemon
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._sharedmem import (
|
from tractor.ipc._shm import ShmArray
|
||||||
ShmArray,
|
|
||||||
)
|
|
||||||
from .feed import (
|
from .feed import (
|
||||||
_FeedsBus,
|
_FeedsBus,
|
||||||
Sub,
|
Sub,
|
||||||
|
|
@ -378,16 +376,16 @@ async def register_with_sampler(
|
||||||
# feed_is_live.is_set()
|
# feed_is_live.is_set()
|
||||||
# ^TODO? pass it in instead?
|
# ^TODO? pass it in instead?
|
||||||
):
|
):
|
||||||
from ._sharedmem import (
|
from tractor.ipc._shm import (
|
||||||
attach_shm_array,
|
attach_shm_ndarray,
|
||||||
_Token,
|
NDToken,
|
||||||
)
|
)
|
||||||
for period in shms_by_period:
|
for period in shms_by_period:
|
||||||
|
|
||||||
# load and register shm handles
|
# load and register shm handles
|
||||||
shm_token_msg = shms_by_period[period]
|
shm_token_msg = shms_by_period[period]
|
||||||
shm = attach_shm_array(
|
shm = attach_shm_ndarray(
|
||||||
_Token.from_msg(shm_token_msg),
|
NDToken.from_msg(shm_token_msg),
|
||||||
readonly=False,
|
readonly=False,
|
||||||
)
|
)
|
||||||
shms_by_period[period] = shm
|
shms_by_period[period] = shm
|
||||||
|
|
|
||||||
|
|
@ -1,622 +1,67 @@
|
||||||
# piker: trading gear for hackers
|
# piker: trading gear for hackers
|
||||||
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
# and/or modify it under the terms of the GNU Affero General
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
# Public License as published by the Free Software
|
||||||
# (at your option) any later version.
|
# 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,
|
# This program is distributed in the hope that it will be
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||||
# GNU Affero General Public License for more details.
|
# PURPOSE. See the GNU Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
NumPy compatible shared memory buffers for real-time IPC streaming.
|
Piker-specific shared memory helpers.
|
||||||
|
|
||||||
"""
|
Thin shim providing piker-only wrappers around
|
||||||
from __future__ import annotations
|
``tractor.ipc._shm``; all core types and functions
|
||||||
from sys import byteorder
|
are now imported directly from tractor throughout
|
||||||
import time
|
the codebase.
|
||||||
from typing import Optional
|
|
||||||
from multiprocessing.shared_memory import SharedMemory, _USE_POSIX
|
|
||||||
|
|
||||||
if _USE_POSIX:
|
'''
|
||||||
from _posixshmem import shm_unlink
|
|
||||||
|
|
||||||
# import msgspec
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.lib import recfunctions as rfn
|
|
||||||
import tractor
|
from tractor.ipc._shm import (
|
||||||
|
NDToken,
|
||||||
|
ShmArray,
|
||||||
|
_known_tokens,
|
||||||
|
_make_token as _tractor_make_token,
|
||||||
|
open_shm_ndarray,
|
||||||
|
attach_shm_ndarray,
|
||||||
|
)
|
||||||
|
|
||||||
from ._util import log
|
from ._util import log
|
||||||
from ._source import def_iohlcv_fields
|
|
||||||
from piker.types import Struct
|
|
||||||
|
|
||||||
|
|
||||||
def cuckoff_mantracker():
|
|
||||||
'''
|
|
||||||
Disable all ``multiprocessing``` "resource tracking" machinery since
|
|
||||||
it's an absolute multi-threaded mess of non-SC madness.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from multiprocessing import resource_tracker as mantracker
|
|
||||||
|
|
||||||
# Tell the "resource tracker" thing to fuck off.
|
|
||||||
class ManTracker(mantracker.ResourceTracker):
|
|
||||||
def register(self, name, rtype):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def unregister(self, name, rtype):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ensure_running(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# "know your land and know your prey"
|
|
||||||
# https://www.dailymotion.com/video/x6ozzco
|
|
||||||
mantracker._resource_tracker = ManTracker()
|
|
||||||
mantracker.register = mantracker._resource_tracker.register
|
|
||||||
mantracker.ensure_running = mantracker._resource_tracker.ensure_running
|
|
||||||
mantracker.unregister = mantracker._resource_tracker.unregister
|
|
||||||
mantracker.getfd = mantracker._resource_tracker.getfd
|
|
||||||
|
|
||||||
|
|
||||||
cuckoff_mantracker()
|
|
||||||
|
|
||||||
|
|
||||||
class SharedInt:
|
|
||||||
"""Wrapper around a single entry shared memory array which
|
|
||||||
holds an ``int`` value used as an index counter.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
shm: SharedMemory,
|
|
||||||
) -> None:
|
|
||||||
self._shm = shm
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self) -> int:
|
|
||||||
return int.from_bytes(self._shm.buf, byteorder)
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value) -> None:
|
|
||||||
self._shm.buf[:] = value.to_bytes(self._shm.size, byteorder)
|
|
||||||
|
|
||||||
def destroy(self) -> None:
|
|
||||||
if _USE_POSIX:
|
|
||||||
# We manually unlink to bypass all the "resource tracker"
|
|
||||||
# nonsense meant for non-SC systems.
|
|
||||||
name = self._shm.name
|
|
||||||
try:
|
|
||||||
shm_unlink(name)
|
|
||||||
except FileNotFoundError:
|
|
||||||
# might be a teardown race here?
|
|
||||||
log.warning(f'Shm for {name} already unlinked?')
|
|
||||||
|
|
||||||
|
|
||||||
class _Token(Struct, frozen=True):
|
|
||||||
'''
|
|
||||||
Internal represenation of a shared memory "token"
|
|
||||||
which can be used to key a system wide post shm entry.
|
|
||||||
|
|
||||||
'''
|
|
||||||
shm_name: str # this servers as a "key" value
|
|
||||||
shm_first_index_name: str
|
|
||||||
shm_last_index_name: str
|
|
||||||
dtype_descr: tuple
|
|
||||||
size: int # in struct-array index / row terms
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dtype(self) -> np.dtype:
|
|
||||||
return np.dtype(list(map(tuple, self.dtype_descr))).descr
|
|
||||||
|
|
||||||
def as_msg(self):
|
|
||||||
return self.to_dict()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_msg(cls, msg: dict) -> _Token:
|
|
||||||
if isinstance(msg, _Token):
|
|
||||||
return msg
|
|
||||||
|
|
||||||
# TODO: native struct decoding
|
|
||||||
# return _token_dec.decode(msg)
|
|
||||||
|
|
||||||
msg['dtype_descr'] = tuple(map(tuple, msg['dtype_descr']))
|
|
||||||
return _Token(**msg)
|
|
||||||
|
|
||||||
|
|
||||||
# _token_dec = msgspec.msgpack.Decoder(_Token)
|
|
||||||
|
|
||||||
# TODO: this api?
|
|
||||||
# _known_tokens = tractor.ActorVar('_shm_tokens', {})
|
|
||||||
# _known_tokens = tractor.ContextStack('_known_tokens', )
|
|
||||||
# _known_tokens = trio.RunVar('shms', {})
|
|
||||||
|
|
||||||
# process-local store of keys to tokens
|
|
||||||
_known_tokens = {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_shm_token(key: str) -> _Token:
|
|
||||||
"""Convenience func to check if a token
|
|
||||||
for the provided key is known by this process.
|
|
||||||
"""
|
|
||||||
return _known_tokens.get(key)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_token(
|
def _make_token(
|
||||||
key: str,
|
key: str,
|
||||||
size: int,
|
size: int,
|
||||||
dtype: Optional[np.dtype] = None,
|
|
||||||
) -> _Token:
|
|
||||||
'''
|
|
||||||
Create a serializable token that can be used
|
|
||||||
to access a shared array.
|
|
||||||
|
|
||||||
'''
|
|
||||||
dtype = def_iohlcv_fields if dtype is None else dtype
|
|
||||||
return _Token(
|
|
||||||
shm_name=key,
|
|
||||||
shm_first_index_name=key + "_first",
|
|
||||||
shm_last_index_name=key + "_last",
|
|
||||||
dtype_descr=tuple(np.dtype(dtype).descr),
|
|
||||||
size=size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ShmArray:
|
|
||||||
'''
|
|
||||||
A shared memory ``numpy`` (compatible) array API.
|
|
||||||
|
|
||||||
An underlying shared memory buffer is allocated based on
|
|
||||||
a user specified ``numpy.ndarray``. This fixed size array
|
|
||||||
can be read and written to by pushing data both onto the "front"
|
|
||||||
or "back" of a set index range. The indexes for the "first" and
|
|
||||||
"last" index are themselves stored in shared memory (accessed via
|
|
||||||
``SharedInt`` interfaces) values such that multiple processes can
|
|
||||||
interact with the same array using a synchronized-index.
|
|
||||||
|
|
||||||
'''
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
shmarr: np.ndarray,
|
|
||||||
first: SharedInt,
|
|
||||||
last: SharedInt,
|
|
||||||
shm: SharedMemory,
|
|
||||||
# readonly: bool = True,
|
|
||||||
) -> None:
|
|
||||||
self._array = shmarr
|
|
||||||
|
|
||||||
# indexes for first and last indices corresponding
|
|
||||||
# to fille data
|
|
||||||
self._first = first
|
|
||||||
self._last = last
|
|
||||||
|
|
||||||
self._len = len(shmarr)
|
|
||||||
self._shm = shm
|
|
||||||
self._post_init: bool = False
|
|
||||||
|
|
||||||
# pushing data does not write the index (aka primary key)
|
|
||||||
dtype = shmarr.dtype
|
|
||||||
if dtype.fields:
|
|
||||||
self._write_fields = list(shmarr.dtype.fields.keys())[1:]
|
|
||||||
else:
|
|
||||||
self._write_fields = None
|
|
||||||
|
|
||||||
# TODO: ringbuf api?
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _token(self) -> _Token:
|
|
||||||
return _Token(
|
|
||||||
shm_name=self._shm.name,
|
|
||||||
shm_first_index_name=self._first._shm.name,
|
|
||||||
shm_last_index_name=self._last._shm.name,
|
|
||||||
dtype_descr=tuple(self._array.dtype.descr),
|
|
||||||
size=self._len,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def token(self) -> dict:
|
|
||||||
"""Shared memory token that can be serialized and used by
|
|
||||||
another process to attach to this array.
|
|
||||||
"""
|
|
||||||
return self._token.as_msg()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def index(self) -> int:
|
|
||||||
return self._last.value % self._len
|
|
||||||
|
|
||||||
@property
|
|
||||||
def array(self) -> np.ndarray:
|
|
||||||
'''
|
|
||||||
Return an up-to-date ``np.ndarray`` view of the
|
|
||||||
so-far-written data to the underlying shm buffer.
|
|
||||||
|
|
||||||
'''
|
|
||||||
a = self._array[self._first.value:self._last.value]
|
|
||||||
|
|
||||||
# first, last = self._first.value, self._last.value
|
|
||||||
# a = self._array[first:last]
|
|
||||||
|
|
||||||
# TODO: eventually comment this once we've not seen it in the
|
|
||||||
# wild in a long time..
|
|
||||||
# XXX: race where first/last indexes cause a reader
|
|
||||||
# to load an empty array..
|
|
||||||
if len(a) == 0 and self._post_init:
|
|
||||||
raise RuntimeError('Empty array race condition hit!?')
|
|
||||||
|
|
||||||
return a
|
|
||||||
|
|
||||||
def ustruct(
|
|
||||||
self,
|
|
||||||
fields: Optional[list[str]] = None,
|
|
||||||
|
|
||||||
# type that all field values will be cast to
|
|
||||||
# in the returned view.
|
|
||||||
common_dtype: np.dtype = float,
|
|
||||||
|
|
||||||
) -> np.ndarray:
|
|
||||||
|
|
||||||
array = self._array
|
|
||||||
|
|
||||||
if fields:
|
|
||||||
selection = array[fields]
|
|
||||||
# fcount = len(fields)
|
|
||||||
else:
|
|
||||||
selection = array
|
|
||||||
# fcount = len(array.dtype.fields)
|
|
||||||
|
|
||||||
# XXX: manual ``.view()`` attempt that also doesn't work.
|
|
||||||
# uview = selection.view(
|
|
||||||
# dtype='<f16',
|
|
||||||
# ).reshape(-1, 4, order='A')
|
|
||||||
|
|
||||||
# assert len(selection) == len(uview)
|
|
||||||
|
|
||||||
u = rfn.structured_to_unstructured(
|
|
||||||
selection,
|
|
||||||
# dtype=float,
|
|
||||||
copy=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# unstruct = np.ndarray(u.shape, dtype=a.dtype, buffer=shm.buf)
|
|
||||||
# array[:] = a[:]
|
|
||||||
return u
|
|
||||||
# return ShmArray(
|
|
||||||
# shmarr=u,
|
|
||||||
# first=self._first,
|
|
||||||
# last=self._last,
|
|
||||||
# shm=self._shm
|
|
||||||
# )
|
|
||||||
|
|
||||||
def last(
|
|
||||||
self,
|
|
||||||
length: int = 1,
|
|
||||||
|
|
||||||
) -> np.ndarray:
|
|
||||||
'''
|
|
||||||
Return the last ``length``'s worth of ("row") entries from the
|
|
||||||
array.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return self.array[-length:]
|
|
||||||
|
|
||||||
def push(
|
|
||||||
self,
|
|
||||||
data: np.ndarray,
|
|
||||||
|
|
||||||
field_map: Optional[dict[str, str]] = None,
|
|
||||||
prepend: bool = False,
|
|
||||||
update_first: bool = True,
|
|
||||||
start: int | None = None,
|
|
||||||
|
|
||||||
) -> int:
|
|
||||||
'''
|
|
||||||
Ring buffer like "push" to append data
|
|
||||||
into the buffer and return updated "last" index.
|
|
||||||
|
|
||||||
NB: no actual ring logic yet to give a "loop around" on overflow
|
|
||||||
condition, lel.
|
|
||||||
|
|
||||||
'''
|
|
||||||
length = len(data)
|
|
||||||
|
|
||||||
if prepend:
|
|
||||||
index = (start or self._first.value) - length
|
|
||||||
|
|
||||||
if index < 0:
|
|
||||||
raise ValueError(
|
|
||||||
f'Array size of {self._len} was overrun during prepend.\n'
|
|
||||||
f'You have passed {abs(index)} too many datums.'
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
index = start if start is not None else self._last.value
|
|
||||||
|
|
||||||
end = index + length
|
|
||||||
|
|
||||||
if field_map:
|
|
||||||
src_names, dst_names = zip(*field_map.items())
|
|
||||||
else:
|
|
||||||
dst_names = src_names = self._write_fields
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._array[
|
|
||||||
list(dst_names)
|
|
||||||
][index:end] = data[list(src_names)][:]
|
|
||||||
|
|
||||||
# NOTE: there was a race here between updating
|
|
||||||
# the first and last indices and when the next reader
|
|
||||||
# tries to access ``.array`` (which due to the index
|
|
||||||
# overlap will be empty). Pretty sure we've fixed it now
|
|
||||||
# but leaving this here as a reminder.
|
|
||||||
if (
|
|
||||||
prepend
|
|
||||||
and update_first
|
|
||||||
and length
|
|
||||||
):
|
|
||||||
assert index < self._first.value
|
|
||||||
|
|
||||||
if (
|
|
||||||
index < self._first.value
|
|
||||||
and update_first
|
|
||||||
):
|
|
||||||
assert prepend, 'prepend=True not passed but index decreased?'
|
|
||||||
self._first.value = index
|
|
||||||
|
|
||||||
elif not prepend:
|
|
||||||
self._last.value = end
|
|
||||||
|
|
||||||
self._post_init = True
|
|
||||||
return end
|
|
||||||
|
|
||||||
except ValueError as err:
|
|
||||||
if field_map:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# should raise if diff detected
|
|
||||||
self.diff_err_fields(data)
|
|
||||||
raise err
|
|
||||||
|
|
||||||
def diff_err_fields(
|
|
||||||
self,
|
|
||||||
data: np.ndarray,
|
|
||||||
) -> None:
|
|
||||||
# reraise with any field discrepancy
|
|
||||||
our_fields, their_fields = (
|
|
||||||
set(self._array.dtype.fields),
|
|
||||||
set(data.dtype.fields),
|
|
||||||
)
|
|
||||||
|
|
||||||
only_in_ours = our_fields - their_fields
|
|
||||||
only_in_theirs = their_fields - our_fields
|
|
||||||
|
|
||||||
if only_in_ours:
|
|
||||||
raise TypeError(
|
|
||||||
f"Input array is missing field(s): {only_in_ours}"
|
|
||||||
)
|
|
||||||
elif only_in_theirs:
|
|
||||||
raise TypeError(
|
|
||||||
f"Input array has unknown field(s): {only_in_theirs}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: support "silent" prepends that don't update ._first.value?
|
|
||||||
def prepend(
|
|
||||||
self,
|
|
||||||
data: np.ndarray,
|
|
||||||
) -> int:
|
|
||||||
end = self.push(data, prepend=True)
|
|
||||||
assert end
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self._first._shm.close()
|
|
||||||
self._last._shm.close()
|
|
||||||
self._shm.close()
|
|
||||||
|
|
||||||
def destroy(self) -> None:
|
|
||||||
if _USE_POSIX:
|
|
||||||
# We manually unlink to bypass all the "resource tracker"
|
|
||||||
# nonsense meant for non-SC systems.
|
|
||||||
shm_unlink(self._shm.name)
|
|
||||||
|
|
||||||
self._first.destroy()
|
|
||||||
self._last.destroy()
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
# TODO: flush to storage backend like markestore?
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def open_shm_array(
|
|
||||||
size: int,
|
|
||||||
key: str | None = None,
|
|
||||||
dtype: np.dtype|None = None,
|
dtype: np.dtype|None = None,
|
||||||
append_start_index: int | None = None,
|
) -> NDToken:
|
||||||
readonly: bool = False,
|
'''
|
||||||
|
Wrap tractor's ``_make_token()`` with piker's
|
||||||
) -> ShmArray:
|
default dtype fallback to ``def_iohlcv_fields``.
|
||||||
'''Open a memory shared ``numpy`` using the standard library.
|
|
||||||
|
|
||||||
This call unlinks (aka permanently destroys) the buffer on teardown
|
|
||||||
and thus should be used from the parent-most accessor (process).
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# create new shared mem segment for which we
|
from ._source import def_iohlcv_fields
|
||||||
# have write permission
|
dtype = (
|
||||||
a = np.zeros(size, dtype=dtype)
|
def_iohlcv_fields
|
||||||
a['index'] = np.arange(len(a))
|
if dtype is None
|
||||||
|
else dtype
|
||||||
shm = SharedMemory(
|
|
||||||
name=key,
|
|
||||||
create=True,
|
|
||||||
size=a.nbytes
|
|
||||||
)
|
)
|
||||||
array = np.ndarray(
|
return _tractor_make_token(
|
||||||
a.shape,
|
|
||||||
dtype=a.dtype,
|
|
||||||
buffer=shm.buf
|
|
||||||
)
|
|
||||||
array[:] = a[:]
|
|
||||||
array.setflags(write=int(not readonly))
|
|
||||||
|
|
||||||
token = _make_token(
|
|
||||||
key=key,
|
key=key,
|
||||||
size=size,
|
size=size,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create single entry arrays for storing an first and last indices
|
|
||||||
first = SharedInt(
|
|
||||||
shm=SharedMemory(
|
|
||||||
name=token.shm_first_index_name,
|
|
||||||
create=True,
|
|
||||||
size=4, # std int
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
last = SharedInt(
|
|
||||||
shm=SharedMemory(
|
|
||||||
name=token.shm_last_index_name,
|
|
||||||
create=True,
|
|
||||||
size=4, # std int
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start the "real-time" updated section after 3-days worth of 1s
|
|
||||||
# sampled OHLC. this allows appending up to a days worth from
|
|
||||||
# tick/quote feeds before having to flush to a (tsdb) storage
|
|
||||||
# backend, and looks something like,
|
|
||||||
# -------------------------
|
|
||||||
# | | i
|
|
||||||
# _________________________
|
|
||||||
# <-------------> <------->
|
|
||||||
# history real-time
|
|
||||||
#
|
|
||||||
# Once fully "prepended", the history section will leave the
|
|
||||||
# ``ShmArray._start.value: int = 0`` and the yet-to-be written
|
|
||||||
# real-time section will start at ``ShmArray.index: int``.
|
|
||||||
|
|
||||||
# this sets the index to nearly 2/3rds into the the length of
|
|
||||||
# the buffer leaving at least a "days worth of second samples"
|
|
||||||
# for the real-time section.
|
|
||||||
if append_start_index is None:
|
|
||||||
append_start_index = round(size * 0.616)
|
|
||||||
|
|
||||||
last.value = first.value = append_start_index
|
|
||||||
|
|
||||||
shmarr = ShmArray(
|
|
||||||
array,
|
|
||||||
first,
|
|
||||||
last,
|
|
||||||
shm,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert shmarr._token == token
|
|
||||||
_known_tokens[key] = shmarr.token
|
|
||||||
|
|
||||||
# "unlink" created shm on process teardown by
|
|
||||||
# pushing teardown calls onto actor context stack
|
|
||||||
stack = tractor.current_actor(
|
|
||||||
err_on_no_runtime=False,
|
|
||||||
).lifetime_stack
|
|
||||||
if stack:
|
|
||||||
stack.callback(shmarr.close)
|
|
||||||
stack.callback(shmarr.destroy)
|
|
||||||
|
|
||||||
return shmarr
|
|
||||||
|
|
||||||
|
|
||||||
def attach_shm_array(
|
|
||||||
token: tuple[str, str, tuple[str, str]],
|
|
||||||
readonly: bool = True,
|
|
||||||
|
|
||||||
) -> ShmArray:
|
|
||||||
'''
|
|
||||||
Attach to an existing shared memory array previously
|
|
||||||
created by another process using ``open_shared_array``.
|
|
||||||
|
|
||||||
No new shared mem is allocated but wrapper types for read/write
|
|
||||||
access are constructed.
|
|
||||||
|
|
||||||
'''
|
|
||||||
token = _Token.from_msg(token)
|
|
||||||
key = token.shm_name
|
|
||||||
|
|
||||||
if key in _known_tokens:
|
|
||||||
assert _Token.from_msg(_known_tokens[key]) == token, "WTF"
|
|
||||||
|
|
||||||
# XXX: ugh, looks like due to the ``shm_open()`` C api we can't
|
|
||||||
# actually place files in a subdir, see discussion here:
|
|
||||||
# https://stackoverflow.com/a/11103289
|
|
||||||
|
|
||||||
# attach to array buffer and view as per dtype
|
|
||||||
_err: Optional[Exception] = None
|
|
||||||
for _ in range(3):
|
|
||||||
try:
|
|
||||||
shm = SharedMemory(
|
|
||||||
name=key,
|
|
||||||
create=False,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except OSError as oserr:
|
|
||||||
_err = oserr
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
|
||||||
if _err:
|
|
||||||
raise _err
|
|
||||||
|
|
||||||
shmarr = np.ndarray(
|
|
||||||
(token.size,),
|
|
||||||
dtype=token.dtype,
|
|
||||||
buffer=shm.buf
|
|
||||||
)
|
|
||||||
shmarr.setflags(write=int(not readonly))
|
|
||||||
|
|
||||||
first = SharedInt(
|
|
||||||
shm=SharedMemory(
|
|
||||||
name=token.shm_first_index_name,
|
|
||||||
create=False,
|
|
||||||
size=4, # std int
|
|
||||||
),
|
|
||||||
)
|
|
||||||
last = SharedInt(
|
|
||||||
shm=SharedMemory(
|
|
||||||
name=token.shm_last_index_name,
|
|
||||||
create=False,
|
|
||||||
size=4, # std int
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# make sure we can read
|
|
||||||
first.value
|
|
||||||
|
|
||||||
sha = ShmArray(
|
|
||||||
shmarr,
|
|
||||||
first,
|
|
||||||
last,
|
|
||||||
shm,
|
|
||||||
)
|
|
||||||
# read test
|
|
||||||
sha.array
|
|
||||||
|
|
||||||
# Stash key -> token knowledge for future queries
|
|
||||||
# via `maybe_opepn_shm_array()` but only after we know
|
|
||||||
# we can attach.
|
|
||||||
if key not in _known_tokens:
|
|
||||||
_known_tokens[key] = token
|
|
||||||
|
|
||||||
# "close" attached shm on actor teardown
|
|
||||||
if (actor := tractor.current_actor(
|
|
||||||
err_on_no_runtime=False,
|
|
||||||
)):
|
|
||||||
actor.lifetime_stack.callback(sha.close)
|
|
||||||
|
|
||||||
return sha
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_open_shm_array(
|
def maybe_open_shm_array(
|
||||||
key: str,
|
key: str,
|
||||||
|
|
@ -625,37 +70,37 @@ def maybe_open_shm_array(
|
||||||
append_start_index: int|None = None,
|
append_start_index: int|None = None,
|
||||||
readonly: bool = False,
|
readonly: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> tuple[ShmArray, bool]:
|
) -> tuple[ShmArray, bool]:
|
||||||
'''
|
'''
|
||||||
Attempt to attach to a shared memory block using a "key" lookup
|
Attempt to attach to a shared memory block
|
||||||
to registered blocks in the users overall "system" registry
|
using a "key" lookup to registered blocks in
|
||||||
(presumes you don't have the block's explicit token).
|
the user's overall "system" registry (presumes
|
||||||
|
you don't have the block's explicit token).
|
||||||
|
|
||||||
This function is meant to solve the problem of discovering whether
|
This is a thin wrapper around tractor's
|
||||||
a shared array token has been allocated or discovered by the actor
|
``maybe_open_shm_ndarray()`` preserving piker's
|
||||||
running in **this** process. Systems where multiple actors may seek
|
historical defaults (``readonly=False``,
|
||||||
to access a common block can use this function to attempt to acquire
|
``append_start_index=None``).
|
||||||
a token as discovered by the actors who have previously stored
|
|
||||||
a "key" -> ``_Token`` map in an actor local (aka python global)
|
|
||||||
variable.
|
|
||||||
|
|
||||||
If you know the explicit ``_Token`` for your memory segment instead
|
If you know the explicit ``NDToken`` for your
|
||||||
use ``attach_shm_array``.
|
memory segment instead use
|
||||||
|
``tractor.ipc._shm.attach_shm_ndarray()``.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
# see if we already know this key
|
# see if we already know this key
|
||||||
token = _known_tokens[key]
|
token = _known_tokens[key]
|
||||||
return (
|
return (
|
||||||
attach_shm_array(
|
attach_shm_ndarray(
|
||||||
token=token,
|
token=token,
|
||||||
readonly=readonly,
|
readonly=readonly,
|
||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.debug(f"Could not find {key} in shms cache")
|
log.debug(
|
||||||
|
f'Could not find {key} in shms cache'
|
||||||
|
)
|
||||||
if dtype:
|
if dtype:
|
||||||
token = _make_token(
|
token = _make_token(
|
||||||
key,
|
key,
|
||||||
|
|
@ -663,9 +108,18 @@ def maybe_open_shm_array(
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return attach_shm_array(token=token, **kwargs), False
|
return (
|
||||||
|
attach_shm_ndarray(
|
||||||
|
token=token,
|
||||||
|
**kwargs,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log.debug(f"Could not attach to shm with token {token}")
|
log.debug(
|
||||||
|
f'Could not attach to shm'
|
||||||
|
f' with token {token}'
|
||||||
|
)
|
||||||
|
|
||||||
# This actor does not know about memory
|
# This actor does not know about memory
|
||||||
# associated with the provided "key".
|
# associated with the provided "key".
|
||||||
|
|
@ -673,7 +127,7 @@ def maybe_open_shm_array(
|
||||||
# to fail if a block has been allocated
|
# to fail if a block has been allocated
|
||||||
# on the OS by someone else.
|
# on the OS by someone else.
|
||||||
return (
|
return (
|
||||||
open_shm_array(
|
open_shm_ndarray(
|
||||||
key=key,
|
key=key,
|
||||||
size=size,
|
size=size,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
|
|
@ -683,18 +137,20 @@ def maybe_open_shm_array(
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def try_read(
|
def try_read(
|
||||||
array: np.ndarray
|
array: np.ndarray,
|
||||||
|
) -> np.ndarray|None:
|
||||||
) -> Optional[np.ndarray]:
|
|
||||||
'''
|
'''
|
||||||
Try to read the last row from a shared mem array or ``None``
|
Try to read the last row from a shared mem
|
||||||
if the array read returns a zero-length array result.
|
array or ``None`` if the array read returns
|
||||||
|
a zero-length array result.
|
||||||
|
|
||||||
Can be used to check for backfilling race conditions where an array
|
Can be used to check for backfilling race
|
||||||
is currently being (re-)written by a writer actor but the reader is
|
conditions where an array is currently being
|
||||||
unaware and reads during the window where the first and last indexes
|
(re-)written by a writer actor but the reader
|
||||||
are being updated.
|
is unaware and reads during the window where
|
||||||
|
the first and last indexes are being updated.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
|
|
@ -702,14 +158,13 @@ def try_read(
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# XXX: race condition with backfilling shm.
|
# XXX: race condition with backfilling shm.
|
||||||
#
|
#
|
||||||
# the underlying issue is that a backfill (aka prepend) and subsequent
|
# the underlying issue is that a backfill
|
||||||
# shm array first/last index update could result in an empty array
|
# (aka prepend) and subsequent shm array
|
||||||
# read here since the indices may be updated in such a way that
|
# first/last index update could result in an
|
||||||
# a read delivers an empty array (though it seems like we
|
# empty array read here since the indices may
|
||||||
# *should* be able to prevent that?). also, as and alt and
|
# be updated in such a way that a read delivers
|
||||||
# something we need anyway, maybe there should be some kind of
|
# an empty array (though it seems like we
|
||||||
# signal that a prepend is taking place and this consumer can
|
# *should* be able to prevent that?).
|
||||||
# respond (eg. redrawing graphics) accordingly.
|
|
||||||
|
|
||||||
# the array read was emtpy
|
# the array read was empty
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ import pendulum
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from ._sharedmem import (
|
from tractor.ipc._shm import (
|
||||||
attach_shm_array,
|
|
||||||
ShmArray,
|
ShmArray,
|
||||||
_Token,
|
NDToken,
|
||||||
|
attach_shm_ndarray,
|
||||||
)
|
)
|
||||||
from piker.accounting import MktPair
|
from piker.accounting import MktPair
|
||||||
|
|
||||||
|
|
@ -64,11 +64,11 @@ class Flume(Struct):
|
||||||
'''
|
'''
|
||||||
mkt: MktPair
|
mkt: MktPair
|
||||||
first_quote: dict
|
first_quote: dict
|
||||||
_rt_shm_token: _Token
|
_rt_shm_token: NDToken
|
||||||
|
|
||||||
# optional since some data flows won't have a "downsampled" history
|
# optional since some data flows won't have a "downsampled" history
|
||||||
# buffer/stream (eg. FSPs).
|
# buffer/stream (eg. FSPs).
|
||||||
_hist_shm_token: _Token | None = None
|
_hist_shm_token: NDToken|None = None
|
||||||
|
|
||||||
# private shm refs loaded dynamically from tokens
|
# private shm refs loaded dynamically from tokens
|
||||||
_hist_shm: ShmArray | None = None
|
_hist_shm: ShmArray | None = None
|
||||||
|
|
@ -88,7 +88,7 @@ class Flume(Struct):
|
||||||
def rt_shm(self) -> ShmArray:
|
def rt_shm(self) -> ShmArray:
|
||||||
|
|
||||||
if self._rt_shm is None:
|
if self._rt_shm is None:
|
||||||
self._rt_shm = attach_shm_array(
|
self._rt_shm = attach_shm_ndarray(
|
||||||
token=self._rt_shm_token,
|
token=self._rt_shm_token,
|
||||||
readonly=self._readonly,
|
readonly=self._readonly,
|
||||||
)
|
)
|
||||||
|
|
@ -104,7 +104,7 @@ class Flume(Struct):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._hist_shm is None:
|
if self._hist_shm is None:
|
||||||
self._hist_shm = attach_shm_array(
|
self._hist_shm = attach_shm_ndarray(
|
||||||
token=self._hist_shm_token,
|
token=self._hist_shm_token,
|
||||||
readonly=self._readonly,
|
readonly=self._readonly,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,12 @@ import numpy as np
|
||||||
import tractor
|
import tractor
|
||||||
from tractor.msg import NamespacePath
|
from tractor.msg import NamespacePath
|
||||||
|
|
||||||
from ..data._sharedmem import (
|
from tractor.ipc._shm import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
maybe_open_shm_array,
|
NDToken,
|
||||||
attach_shm_array,
|
attach_shm_ndarray,
|
||||||
_Token,
|
|
||||||
)
|
)
|
||||||
|
from ..data._sharedmem import maybe_open_shm_array
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
@ -78,8 +78,8 @@ class Fsp:
|
||||||
# + the consuming fsp *to* the consumers output
|
# + the consuming fsp *to* the consumers output
|
||||||
# shm flow.
|
# shm flow.
|
||||||
_flow_registry: dict[
|
_flow_registry: dict[
|
||||||
tuple[_Token, str],
|
tuple[NDToken, str],
|
||||||
tuple[_Token, Optional[ShmArray]],
|
tuple[NDToken, Optional[ShmArray]],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -148,7 +148,7 @@ class Fsp:
|
||||||
# times as possible as per:
|
# times as possible as per:
|
||||||
# - https://github.com/pikers/piker/issues/359
|
# - https://github.com/pikers/piker/issues/359
|
||||||
# - https://github.com/pikers/piker/issues/332
|
# - https://github.com/pikers/piker/issues/332
|
||||||
maybe_array := attach_shm_array(dst_token)
|
maybe_array := attach_shm_ndarray(dst_token)
|
||||||
)
|
)
|
||||||
|
|
||||||
return maybe_array
|
return maybe_array
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ from ..log import (
|
||||||
)
|
)
|
||||||
from .. import data
|
from .. import data
|
||||||
from ..data.flows import Flume
|
from ..data.flows import Flume
|
||||||
from ..data._sharedmem import ShmArray
|
from tractor.ipc._shm import ShmArray
|
||||||
from ..data._sampling import (
|
from ..data._sampling import (
|
||||||
_default_delay_s,
|
_default_delay_s,
|
||||||
open_sample_stream,
|
open_sample_stream,
|
||||||
|
|
@ -49,7 +49,7 @@ from ..accounting import MktPair
|
||||||
from ._api import (
|
from ._api import (
|
||||||
Fsp,
|
Fsp,
|
||||||
_load_builtins,
|
_load_builtins,
|
||||||
_Token,
|
NDToken,
|
||||||
)
|
)
|
||||||
from ..toolz import Profiler
|
from ..toolz import Profiler
|
||||||
|
|
||||||
|
|
@ -414,7 +414,7 @@ async def cascade(
|
||||||
dst_flume_addr: dict,
|
dst_flume_addr: dict,
|
||||||
ns_path: NamespacePath,
|
ns_path: NamespacePath,
|
||||||
|
|
||||||
shm_registry: dict[str, _Token],
|
shm_registry: dict[str, NDToken],
|
||||||
|
|
||||||
zero_on_step: bool = False,
|
zero_on_step: bool = False,
|
||||||
loglevel: str|None = None,
|
loglevel: str|None = None,
|
||||||
|
|
@ -465,9 +465,9 @@ async def cascade(
|
||||||
# not sure how else to do it.
|
# not sure how else to do it.
|
||||||
for (token, fsp_name, dst_token) in shm_registry:
|
for (token, fsp_name, dst_token) in shm_registry:
|
||||||
Fsp._flow_registry[(
|
Fsp._flow_registry[(
|
||||||
_Token.from_msg(token),
|
NDToken.from_msg(token),
|
||||||
fsp_name,
|
fsp_name,
|
||||||
)] = _Token.from_msg(dst_token), None
|
)] = NDToken.from_msg(dst_token), None
|
||||||
|
|
||||||
fsp: Fsp = reg.get(
|
fsp: Fsp = reg.get(
|
||||||
NamespacePath(ns_path)
|
NamespacePath(ns_path)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ from numba import jit, float64, optional, int64
|
||||||
|
|
||||||
from ._api import fsp
|
from ._api import fsp
|
||||||
from ..data import iterticks
|
from ..data import iterticks
|
||||||
from ..data._sharedmem import ShmArray
|
from tractor.ipc._shm import ShmArray
|
||||||
|
|
||||||
|
|
||||||
@jit(
|
@jit(
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from tractor.trionics._broadcast import AsyncReceiver
|
||||||
|
|
||||||
from ._api import fsp
|
from ._api import fsp
|
||||||
from ..data import iterticks
|
from ..data import iterticks
|
||||||
from ..data._sharedmem import ShmArray
|
from tractor.ipc._shm import ShmArray
|
||||||
from ._momo import _wma
|
from ._momo import _wma
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ import typer
|
||||||
|
|
||||||
from piker.service import open_piker_runtime
|
from piker.service import open_piker_runtime
|
||||||
from piker.cli import cli
|
from piker.cli import cli
|
||||||
from piker.data import (
|
from tractor.ipc._shm import ShmArray
|
||||||
ShmArray,
|
|
||||||
)
|
|
||||||
from piker import tsp
|
from piker import tsp
|
||||||
from . import log
|
from . import log
|
||||||
from . import (
|
from . import (
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,8 @@ from pendulum import (
|
||||||
|
|
||||||
from piker import config
|
from piker import config
|
||||||
from piker import tsp
|
from piker import tsp
|
||||||
from piker.data import (
|
from tractor.ipc._shm import ShmArray
|
||||||
def_iohlcv_fields,
|
from piker.data import def_iohlcv_fields
|
||||||
ShmArray,
|
|
||||||
)
|
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
from . import TimeseriesNotFound
|
from . import TimeseriesNotFound
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,12 @@ from piker.brokers import NoData
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
MktPair,
|
MktPair,
|
||||||
)
|
)
|
||||||
from piker.log import get_logger
|
from piker.log import (
|
||||||
from ..data._sharedmem import (
|
get_logger,
|
||||||
maybe_open_shm_array,
|
get_console_log,
|
||||||
ShmArray,
|
|
||||||
)
|
)
|
||||||
|
from tractor.ipc._shm import ShmArray
|
||||||
|
from ..data._sharedmem import maybe_open_shm_array
|
||||||
from piker.data._source import (
|
from piker.data._source import (
|
||||||
def_iohlcv_fields,
|
def_iohlcv_fields,
|
||||||
)
|
)
|
||||||
|
|
@ -1386,6 +1387,10 @@ async def manage_history(
|
||||||
engages.
|
engages.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
get_console_log(
|
||||||
|
name=__name__,
|
||||||
|
level=loglevel,
|
||||||
|
)
|
||||||
# TODO: is there a way to make each shm file key
|
# TODO: is there a way to make each shm file key
|
||||||
# actor-tree-discovery-addr unique so we avoid collisions
|
# actor-tree-discovery-addr unique so we avoid collisions
|
||||||
# when doing tests which also allocate shms for certain instruments
|
# when doing tests which also allocate shms for certain instruments
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,6 @@ class Axis(pg.AxisItem):
|
||||||
self.pi = plotitem
|
self.pi = plotitem
|
||||||
self._dpi_font = _font
|
self._dpi_font = _font
|
||||||
|
|
||||||
# store for later recalculation on zoom
|
|
||||||
self._typical_max_str = typical_max_str
|
|
||||||
|
|
||||||
self.setTickFont(_font.font)
|
self.setTickFont(_font.font)
|
||||||
font_size = self._dpi_font.font.pixelSize()
|
font_size = self._dpi_font.font.pixelSize()
|
||||||
|
|
||||||
|
|
@ -159,41 +156,6 @@ class Axis(pg.AxisItem):
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate axis sizing after zoom change.'''
|
|
||||||
# IMPORTANT: tell Qt we're about to change geometry
|
|
||||||
self.prepareGeometryChange()
|
|
||||||
|
|
||||||
self._dpi_font = font
|
|
||||||
self.setTickFont(font.font)
|
|
||||||
font_size = font.font.pixelSize()
|
|
||||||
|
|
||||||
# recalculate text offset based on new font size
|
|
||||||
text_offset = None
|
|
||||||
if self.orientation in ('bottom',):
|
|
||||||
text_offset = floor(0.25 * font_size)
|
|
||||||
elif self.orientation in ('left', 'right'):
|
|
||||||
text_offset = floor(font_size / 2)
|
|
||||||
|
|
||||||
if text_offset:
|
|
||||||
self.setStyle(tickTextOffset=text_offset)
|
|
||||||
|
|
||||||
# recalculate bounding rect with new font
|
|
||||||
# Note: typical_max_str should be stored from init
|
|
||||||
if not hasattr(self, '_typical_max_str'):
|
|
||||||
self._typical_max_str = '100 000.000 ' # fallback default
|
|
||||||
self.typical_br = font._qfm.boundingRect(self._typical_max_str)
|
|
||||||
|
|
||||||
# Update PyQtGraph's internal text size tracking
|
|
||||||
# This is critical - PyQtGraph uses these internally for auto-expand
|
|
||||||
if self.orientation in ['left', 'right']:
|
|
||||||
self.textWidth = self.typical_br.width()
|
|
||||||
else:
|
|
||||||
self.textHeight = self.typical_br.height()
|
|
||||||
|
|
||||||
# resize axis to fit new font - this triggers PyQtGraph's auto-expand
|
|
||||||
self.size_to_values()
|
|
||||||
|
|
||||||
def txt_offsets(self) -> tuple[int, int]:
|
def txt_offsets(self) -> tuple[int, int]:
|
||||||
return tuple(self.style['tickTextOffset'])
|
return tuple(self.style['tickTextOffset'])
|
||||||
|
|
||||||
|
|
@ -294,14 +256,7 @@ class PriceAxis(Axis):
|
||||||
self._min_tick = size
|
self._min_tick = size
|
||||||
|
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
# Call PyQtGraph's internal width update mechanism
|
self.setWidth(self.typical_br.width())
|
||||||
# This respects autoExpandTextSpace and updates min/max constraints
|
|
||||||
self._updateWidth()
|
|
||||||
# tell Qt our preferred size changed so layout recalculates
|
|
||||||
self.updateGeometry()
|
|
||||||
# force parent plot item to recalculate its layout
|
|
||||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
|
||||||
self.pi.updateGeometry()
|
|
||||||
|
|
||||||
# XXX: drop for now since it just eats up h space
|
# XXX: drop for now since it just eats up h space
|
||||||
|
|
||||||
|
|
@ -345,14 +300,7 @@ class DynamicDateAxis(Axis):
|
||||||
}
|
}
|
||||||
|
|
||||||
def size_to_values(self) -> None:
|
def size_to_values(self) -> None:
|
||||||
# Call PyQtGraph's internal height update mechanism
|
self.setHeight(self.typical_br.height() + 1)
|
||||||
# This respects autoExpandTextSpace and updates min/max constraints
|
|
||||||
self._updateHeight()
|
|
||||||
# tell Qt our preferred size changed so layout recalculates
|
|
||||||
self.updateGeometry()
|
|
||||||
# force parent plot item to recalculate its layout
|
|
||||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
|
||||||
self.pi.updateGeometry()
|
|
||||||
|
|
||||||
def _indexes_to_timestrs(
|
def _indexes_to_timestrs(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ from ._cursor import (
|
||||||
Cursor,
|
Cursor,
|
||||||
ContentsLabel,
|
ContentsLabel,
|
||||||
)
|
)
|
||||||
from ..data._sharedmem import ShmArray
|
from tractor.ipc._shm import ShmArray
|
||||||
from ._ohlc import BarItems
|
from ._ohlc import BarItems
|
||||||
from ._curve import (
|
from ._curve import (
|
||||||
Curve,
|
Curve,
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,7 @@ from numpy import (
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
from piker.ui.qt import QLineF
|
from piker.ui.qt import QLineF
|
||||||
from ..data._sharedmem import (
|
from tractor.ipc._shm import ShmArray
|
||||||
ShmArray,
|
|
||||||
)
|
|
||||||
from ..data.flows import Flume
|
from ..data.flows import Flume
|
||||||
from ..data._formatters import (
|
from ..data._formatters import (
|
||||||
IncrementalFormatter,
|
IncrementalFormatter,
|
||||||
|
|
|
||||||
|
|
@ -203,9 +203,6 @@ def run_qtractor(
|
||||||
if is_windows:
|
if is_windows:
|
||||||
window.configure_to_desktop()
|
window.configure_to_desktop()
|
||||||
|
|
||||||
# install global keyboard shortcuts for UI zoom
|
|
||||||
window.install_global_zoom_filter()
|
|
||||||
|
|
||||||
# actually render to screen
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,6 @@ class Edit(QLineEdit):
|
||||||
self.sizeHint()
|
self.sizeHint()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate widget size.'''
|
|
||||||
self.dpi_font = font
|
|
||||||
self.setFont(font.font)
|
|
||||||
# tell Qt our size hint changed so it recalculates layout
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
self.selectAll()
|
self.selectAll()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
@ -248,14 +241,6 @@ class Selection(QComboBox):
|
||||||
icon_size = round(h * 0.75)
|
icon_size = round(h * 0.75)
|
||||||
self.setIconSize(QSize(icon_size, icon_size))
|
self.setIconSize(QSize(icon_size, icon_size))
|
||||||
|
|
||||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
|
||||||
'''Update font and recalculate widget size.'''
|
|
||||||
self.setFont(font.font)
|
|
||||||
# recalculate heights with new font
|
|
||||||
self.resize()
|
|
||||||
# tell Qt our size hint changed so it recalculates layout
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def set_items(
|
def set_items(
|
||||||
self,
|
self,
|
||||||
keys: list[str],
|
keys: list[str],
|
||||||
|
|
@ -446,39 +431,6 @@ class FieldsForm(QWidget):
|
||||||
self.fields[key] = select
|
self.fields[key] = select
|
||||||
return select
|
return select
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
from ._style import _font, _font_small
|
|
||||||
|
|
||||||
# update stored font size
|
|
||||||
self._font_size = _font_small.px_size - 2
|
|
||||||
|
|
||||||
# update all labels
|
|
||||||
for name, label in self.labels.items():
|
|
||||||
if hasattr(label, 'update_font'):
|
|
||||||
label.update_font(_font.font, self._font_size - 1)
|
|
||||||
|
|
||||||
# update all fields (edits, selects)
|
|
||||||
for key, field in self.fields.items():
|
|
||||||
# first check for our custom update_fonts method (Edit, Selection)
|
|
||||||
if hasattr(field, 'update_fonts'):
|
|
||||||
field.update_fonts(_font)
|
|
||||||
# then handle stylesheet updates for those without custom methods
|
|
||||||
elif hasattr(field, 'setStyleSheet'):
|
|
||||||
# regenerate stylesheet with new font size
|
|
||||||
field.setStyleSheet(
|
|
||||||
f"""QLineEdit {{
|
|
||||||
color : {hcolor('gunmetal')};
|
|
||||||
font-size : {self._font_size}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
field.setFont(_font.font)
|
|
||||||
|
|
||||||
# for Selection widgets that need style updates
|
|
||||||
if hasattr(field, 'set_style'):
|
|
||||||
field.set_style(color='gunmetal', font_size=self._font_size)
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_field_input(
|
async def handle_field_input(
|
||||||
|
|
||||||
|
|
@ -681,37 +633,6 @@ class FillStatusBar(QProgressBar):
|
||||||
self.setRange(0, int(slots))
|
self.setRange(0, int(slots))
|
||||||
self.setValue(value)
|
self.setValue(value)
|
||||||
|
|
||||||
def update_fonts(self, font_size: int) -> None:
|
|
||||||
'''Update font size after zoom change.'''
|
|
||||||
from ._style import _font_small
|
|
||||||
|
|
||||||
self.font_size = font_size
|
|
||||||
# regenerate stylesheet with new font size
|
|
||||||
self.setStyleSheet(
|
|
||||||
f"""
|
|
||||||
QProgressBar {{
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
font-size : {self.font_size - 2}px;
|
|
||||||
|
|
||||||
background-color: {hcolor('papas_special')};
|
|
||||||
color : {hcolor('papas_special')};
|
|
||||||
|
|
||||||
border: {self.border_px}px solid {hcolor('default_light')};
|
|
||||||
border-radius: 2px;
|
|
||||||
}}
|
|
||||||
QProgressBar::chunk {{
|
|
||||||
|
|
||||||
background-color: {hcolor('default_spotlight')};
|
|
||||||
color: {hcolor('bracket')};
|
|
||||||
|
|
||||||
border-radius: 2px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.setFont(_font_small.font)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_fill_status_bar(
|
def mk_fill_status_bar(
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,12 @@ from piker.fsp import (
|
||||||
dolla_vlm,
|
dolla_vlm,
|
||||||
flow_rates,
|
flow_rates,
|
||||||
)
|
)
|
||||||
from piker.data import (
|
from tractor.ipc._shm import (
|
||||||
Flume,
|
|
||||||
ShmArray,
|
ShmArray,
|
||||||
|
NDToken,
|
||||||
)
|
)
|
||||||
from piker.data._sharedmem import (
|
from piker.data import Flume
|
||||||
_Token,
|
from piker.data._sharedmem import try_read
|
||||||
try_read,
|
|
||||||
)
|
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
from piker.toolz import Profiler
|
from piker.toolz import Profiler
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
|
|
@ -382,7 +380,7 @@ class FspAdmin:
|
||||||
tuple,
|
tuple,
|
||||||
tuple[tractor.MsgStream, ShmArray]
|
tuple[tractor.MsgStream, ShmArray]
|
||||||
] = {}
|
] = {}
|
||||||
self._flow_registry: dict[_Token, str] = {}
|
self._flow_registry: dict[NDToken, str] = {}
|
||||||
|
|
||||||
# TODO: make this a `.src_flume` and add
|
# TODO: make this a `.src_flume` and add
|
||||||
# a `dst_flume`?
|
# a `dst_flume`?
|
||||||
|
|
|
||||||
|
|
@ -334,19 +334,3 @@ class FormatLabel(QLabel):
|
||||||
out = self.fmt_str.format(**fields)
|
out = self.fmt_str.format(**fields)
|
||||||
self.setText(out)
|
self.setText(out)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def update_font(
|
|
||||||
self,
|
|
||||||
font: QtGui.QFont,
|
|
||||||
font_size: int,
|
|
||||||
font_color: str = 'default_lightest',
|
|
||||||
) -> None:
|
|
||||||
'''Update font after zoom change.'''
|
|
||||||
self.setStyleSheet(
|
|
||||||
f"""QLabel {{
|
|
||||||
color : {hcolor(font_color)};
|
|
||||||
font-size : {font_size}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.setFont(font)
|
|
||||||
|
|
|
||||||
|
|
@ -178,26 +178,6 @@ class SettingsPane:
|
||||||
# encompasing high level namespace
|
# encompasing high level namespace
|
||||||
order_mode: OrderMode | None = None # typing: ignore # noqa
|
order_mode: OrderMode | None = None # typing: ignore # noqa
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
from ._style import _font_small
|
|
||||||
|
|
||||||
# update form fields
|
|
||||||
if self.form and hasattr(self.form, 'update_fonts'):
|
|
||||||
self.form.update_fonts()
|
|
||||||
|
|
||||||
# update fill status bar
|
|
||||||
if self.fill_bar and hasattr(self.fill_bar, 'update_fonts'):
|
|
||||||
self.fill_bar.update_fonts(_font_small.px_size)
|
|
||||||
|
|
||||||
# update labels with new fonts
|
|
||||||
if self.step_label:
|
|
||||||
self.step_label.setFont(_font_small.font)
|
|
||||||
if self.pnl_label:
|
|
||||||
self.pnl_label.setFont(_font_small.font)
|
|
||||||
if self.limit_label:
|
|
||||||
self.limit_label.setFont(_font_small.font)
|
|
||||||
|
|
||||||
def set_accounts(
|
def set_accounts(
|
||||||
self,
|
self,
|
||||||
names: list[str],
|
names: list[str],
|
||||||
|
|
|
||||||
|
|
@ -174,13 +174,6 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
self.setStyleSheet(f"font: {size}px")
|
self.setStyleSheet(f"font: {size}px")
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
self.set_font_size(_font.px_size)
|
|
||||||
self.setIndentation(_font.px_size)
|
|
||||||
self.setFont(_font.font)
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def resize_to_results(
|
def resize_to_results(
|
||||||
self,
|
self,
|
||||||
w: float | None = 0,
|
w: float | None = 0,
|
||||||
|
|
@ -637,29 +630,6 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
| align_flag.AlignLeft,
|
| align_flag.AlignLeft,
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_fonts(self) -> None:
|
|
||||||
'''Update font sizes after zoom change.'''
|
|
||||||
# regenerate label stylesheet with new font size
|
|
||||||
self.label.setStyleSheet(
|
|
||||||
f"""QLabel {{
|
|
||||||
color : {hcolor('default_lightest')};
|
|
||||||
font-size : {_font.px_size - 2}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.label.setFont(_font.font)
|
|
||||||
|
|
||||||
# update search bar and view fonts
|
|
||||||
if hasattr(self.bar, 'update_fonts'):
|
|
||||||
self.bar.update_fonts(_font)
|
|
||||||
elif hasattr(self.bar, 'setFont'):
|
|
||||||
self.bar.setFont(_font.font)
|
|
||||||
|
|
||||||
if hasattr(self.view, 'update_fonts'):
|
|
||||||
self.view.update_fonts()
|
|
||||||
|
|
||||||
self.updateGeometry()
|
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
self.show()
|
self.show()
|
||||||
self.bar.focus()
|
self.bar.focus()
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,9 @@ class DpiAwareFont:
|
||||||
self._font_inches: float = None
|
self._font_inches: float = None
|
||||||
self._screen = None
|
self._screen = None
|
||||||
|
|
||||||
def _set_qfont_px_size(
|
def _set_qfont_px_size(self, px_size: int) -> None:
|
||||||
self,
|
self._qfont.setPixelSize(px_size)
|
||||||
px_size: int,
|
|
||||||
) -> int:
|
|
||||||
self._qfont.setPixelSize(int(px_size))
|
|
||||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||||
return self.px_size
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def screen(self) -> QtGui.QScreen:
|
def screen(self) -> QtGui.QScreen:
|
||||||
|
|
@ -128,22 +124,17 @@ class DpiAwareFont:
|
||||||
|
|
||||||
return size
|
return size
|
||||||
|
|
||||||
def configure_to_dpi(
|
def configure_to_dpi(self, screen: QtGui.QScreen | None = None):
|
||||||
self,
|
|
||||||
screen: QtGui.QScreen | None = None,
|
|
||||||
zoom_level: float = 1.0,
|
|
||||||
) -> int:
|
|
||||||
'''
|
'''
|
||||||
Set an appropriately sized font size depending on the screen DPI
|
Set an appropriately sized font size depending on the screen DPI.
|
||||||
or scale the size according to `zoom_level`.
|
|
||||||
|
|
||||||
If we end up needing to generalize this more here there are
|
If we end up needing to generalize this more here there are resources
|
||||||
resources listed in the script in
|
listed in the script in ``snippets/qt_screen_info.py``.
|
||||||
``snippets/qt_screen_info.py``.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if self._font_size is not None:
|
if self._font_size is not None:
|
||||||
return self._set_qfont_px_size(self._font_size * zoom_level)
|
self._set_qfont_px_size(self._font_size)
|
||||||
|
return
|
||||||
|
|
||||||
# NOTE: if no font size set either in the [ui] section of the
|
# NOTE: if no font size set either in the [ui] section of the
|
||||||
# config or not yet computed from our magic scaling calcs,
|
# config or not yet computed from our magic scaling calcs,
|
||||||
|
|
@ -162,7 +153,7 @@ class DpiAwareFont:
|
||||||
ldpi = pdpi
|
ldpi = pdpi
|
||||||
|
|
||||||
mx_dpi = max(pdpi, ldpi)
|
mx_dpi = max(pdpi, ldpi)
|
||||||
# mn_dpi = min(pdpi, ldpi)
|
mn_dpi = min(pdpi, ldpi)
|
||||||
scale = round(ldpi/pdpi, ndigits=2)
|
scale = round(ldpi/pdpi, ndigits=2)
|
||||||
|
|
||||||
if mx_dpi <= 97: # for low dpi use larger font sizes
|
if mx_dpi <= 97: # for low dpi use larger font sizes
|
||||||
|
|
@ -171,7 +162,7 @@ class DpiAwareFont:
|
||||||
else: # hidpi use smaller font sizes
|
else: # hidpi use smaller font sizes
|
||||||
inches = _font_sizes['hi'][self._font_size_calc_key]
|
inches = _font_sizes['hi'][self._font_size_calc_key]
|
||||||
|
|
||||||
# dpi = mn_dpi
|
dpi = mn_dpi
|
||||||
|
|
||||||
mult = 1.0
|
mult = 1.0
|
||||||
|
|
||||||
|
|
@ -206,25 +197,24 @@ class DpiAwareFont:
|
||||||
# always going to hit that error in range mapping from inches:
|
# always going to hit that error in range mapping from inches:
|
||||||
# float to px size: int.
|
# float to px size: int.
|
||||||
self._font_inches = inches
|
self._font_inches = inches
|
||||||
font_size = math.floor(inches * pdpi)
|
font_size = math.floor(inches * dpi)
|
||||||
|
|
||||||
# apply zoom level multiplier
|
|
||||||
font_size = int(font_size * zoom_level)
|
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"screen:{screen.name()}\n"
|
f"screen:{screen.name()}\n"
|
||||||
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
|
||||||
f"zoom_level: {zoom_level}\n"
|
|
||||||
f"\nOur best guess font size is {font_size}\n"
|
f"\nOur best guess font size is {font_size}\n"
|
||||||
)
|
)
|
||||||
# apply the size
|
# apply the size
|
||||||
return self._set_qfont_px_size(font_size)
|
self._set_qfont_px_size(font_size)
|
||||||
|
|
||||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||||
if self.screen is None:
|
|
||||||
|
screen = self.screen
|
||||||
|
if screen is None:
|
||||||
raise RuntimeError("You must call .configure_to_dpi() first!")
|
raise RuntimeError("You must call .configure_to_dpi() first!")
|
||||||
|
|
||||||
unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value)
|
unscaled_br = self._qfm.boundingRect(value)
|
||||||
|
|
||||||
return QtCore.QRectF(
|
return QtCore.QRectF(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|
@ -238,22 +228,12 @@ _font = DpiAwareFont()
|
||||||
_font_small = DpiAwareFont(_font_size_key='small')
|
_font_small = DpiAwareFont(_font_size_key='small')
|
||||||
|
|
||||||
|
|
||||||
def _config_fonts_to_screen(
|
def _config_fonts_to_screen() -> None:
|
||||||
zoom_level: float = 1.0
|
'configure global DPI aware font sizes'
|
||||||
) -> int:
|
|
||||||
'''
|
|
||||||
Configure global DPI aware font size(s).
|
|
||||||
|
|
||||||
If `zoom_level` is provided we apply it to auto-calculated
|
|
||||||
DPI-aware font.
|
|
||||||
|
|
||||||
Return the new `DpiAwareFont.px_size`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
global _font, _font_small
|
global _font, _font_small
|
||||||
_font.configure_to_dpi(zoom_level=zoom_level)
|
_font.configure_to_dpi()
|
||||||
_font_small.configure_to_dpi(zoom_level=zoom_level)
|
_font_small.configure_to_dpi()
|
||||||
return _font.px_size
|
|
||||||
|
|
||||||
|
|
||||||
def get_fonts() -> tuple[
|
def get_fonts() -> tuple[
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
Qt main window singletons and stuff.
|
Qt main window singletons and stuff.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
|
@ -39,107 +38,15 @@ from piker.ui.qt import (
|
||||||
QScreen,
|
QScreen,
|
||||||
QCloseEvent,
|
QCloseEvent,
|
||||||
QSettings,
|
QSettings,
|
||||||
QEvent,
|
|
||||||
QObject,
|
|
||||||
)
|
)
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from . import _style
|
from ._style import _font_small, hcolor
|
||||||
from ._style import (
|
|
||||||
_font_small,
|
|
||||||
hcolor,
|
|
||||||
)
|
|
||||||
from ._widget import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GlobalZoomEventFilter(QObject):
|
|
||||||
'''
|
|
||||||
Application-level event filter for global UI zoom shortcuts.
|
|
||||||
|
|
||||||
This filter intercepts keyboard events BEFORE they reach widgets,
|
|
||||||
allowing us to implement global UI zoom shortcuts that take precedence
|
|
||||||
over widget-specific shortcuts.
|
|
||||||
|
|
||||||
Shortcuts:
|
|
||||||
- Ctrl+Shift+Plus/Equal: Zoom in
|
|
||||||
- Ctrl+Shift+Minus: Zoom out
|
|
||||||
- Ctrl+Shift+0: Reset zoom
|
|
||||||
|
|
||||||
'''
|
|
||||||
def __init__(self, main_window: MainWindow):
|
|
||||||
super().__init__()
|
|
||||||
self.main_window = main_window
|
|
||||||
|
|
||||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
|
||||||
'''
|
|
||||||
Filter keyboard events for global zoom shortcuts.
|
|
||||||
|
|
||||||
Returns True to filter out (consume) the event, False to pass through.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if event.type() == QEvent.Type.KeyPress:
|
|
||||||
key = event.key()
|
|
||||||
mods = event.modifiers()
|
|
||||||
|
|
||||||
# Mask out the KeypadModifier which Qt sometimes adds
|
|
||||||
mods = mods & ~Qt.KeyboardModifier.KeypadModifier
|
|
||||||
|
|
||||||
# Check if we have Ctrl+Shift (both required)
|
|
||||||
has_ctrl = bool(
|
|
||||||
mods
|
|
||||||
&
|
|
||||||
Qt.KeyboardModifier.ControlModifier
|
|
||||||
)
|
|
||||||
_has_shift = bool(
|
|
||||||
mods
|
|
||||||
&
|
|
||||||
Qt.KeyboardModifier.ShiftModifier
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only handle UI zoom if BOTH Ctrl and Shift are pressed
|
|
||||||
# For Plus key: user presses Cmd+Shift+Equal (which makes Plus)
|
|
||||||
# For Minus key: user presses Cmd+Shift+Minus
|
|
||||||
if (
|
|
||||||
has_ctrl
|
|
||||||
# and
|
|
||||||
# has_shift
|
|
||||||
):
|
|
||||||
# Zoom in: Ctrl+Shift+Plus
|
|
||||||
# Note: Plus key usually comes as Key_Equal with Shift modifier
|
|
||||||
if key in (
|
|
||||||
Qt.Key.Key_Plus,
|
|
||||||
Qt.Key.Key_Equal,
|
|
||||||
):
|
|
||||||
self.main_window.zoom_in()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Zoom out: Ctrl+Shift+Minus
|
|
||||||
# Note: On some keyboards Shift+Minus produces '_' (Underscore)
|
|
||||||
elif key in (
|
|
||||||
Qt.Key.Key_Minus,
|
|
||||||
Qt.Key.Key_Underscore,
|
|
||||||
):
|
|
||||||
self.main_window.zoom_out()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Reset zoom: Ctrl+Shift+0
|
|
||||||
# Note: On some keyboards Shift+0 produces ')' (ParenRight)
|
|
||||||
elif key in (
|
|
||||||
Qt.Key.Key_0,
|
|
||||||
Qt.Key.Key_ParenRight,
|
|
||||||
):
|
|
||||||
self.main_window.reset_zoom()
|
|
||||||
return True # consume event
|
|
||||||
|
|
||||||
# Pass through if only Ctrl (no Shift) - this goes to chart zoom
|
|
||||||
# Pass through all other events too
|
|
||||||
return False
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MultiStatus:
|
class MultiStatus:
|
||||||
|
|
||||||
bar: QStatusBar
|
bar: QStatusBar
|
||||||
|
|
@ -282,24 +189,6 @@ class MainWindow(QMainWindow):
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
log.debug('Restored window geometry from previous session')
|
log.debug('Restored window geometry from previous session')
|
||||||
|
|
||||||
# zoom level for UI scaling (1.0 = 100%, 1.5 = 150%, etc)
|
|
||||||
# Change this value to set the default startup zoom level
|
|
||||||
self._zoom_level: float = 1.0 # Start at 100% (normal)
|
|
||||||
self._min_zoom: float = 0.5
|
|
||||||
self._max_zoom: float = 3.0 # Reduced from 10.0 to prevent extreme cropping
|
|
||||||
self._zoom_step: float = 0.2 # 20% per keypress
|
|
||||||
|
|
||||||
# event filter for global zoom shortcuts
|
|
||||||
self._zoom_filter: GlobalZoomEventFilter | None = None
|
|
||||||
|
|
||||||
def install_global_zoom_filter(self) -> None:
|
|
||||||
'''Install application-level event filter for global UI zoom shortcuts.'''
|
|
||||||
if self._zoom_filter is None:
|
|
||||||
self._zoom_filter = GlobalZoomEventFilter(self)
|
|
||||||
app = QApplication.instance()
|
|
||||||
app.installEventFilter(self._zoom_filter)
|
|
||||||
log.info('Installed global zoom shortcuts: Ctrl+Shift+Plus/Minus/0')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode_label(self) -> QLabel:
|
def mode_label(self) -> QLabel:
|
||||||
|
|
||||||
|
|
@ -468,201 +357,6 @@ class MainWindow(QMainWindow):
|
||||||
self.godwidget.on_win_resize(event)
|
self.godwidget.on_win_resize(event)
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def zoom_in(self) -> None:
|
|
||||||
'''
|
|
||||||
Increase overall UI-widgets zoom level by scaling it the
|
|
||||||
global font sizes.
|
|
||||||
|
|
||||||
'''
|
|
||||||
new_zoom: float = min(
|
|
||||||
self._zoom_level + self._zoom_step,
|
|
||||||
self._max_zoom,
|
|
||||||
)
|
|
||||||
if new_zoom != self._zoom_level:
|
|
||||||
self._zoom_level = new_zoom
|
|
||||||
font_size: int = self._apply_zoom()
|
|
||||||
log.info(
|
|
||||||
f'Zoomed in UI\n'
|
|
||||||
f'zoom_step: {self._zoom_step!r}\n'
|
|
||||||
f'zoom_level(%): {self._zoom_level:.1%}\n'
|
|
||||||
f'font_size: {font_size!r}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def zoom_out(self) -> float:
|
|
||||||
'''
|
|
||||||
Decrease UI zoom level.
|
|
||||||
|
|
||||||
'''
|
|
||||||
new_zoom: float = max(self._zoom_level - self._zoom_step, self._min_zoom)
|
|
||||||
if new_zoom != self._zoom_level:
|
|
||||||
self._zoom_level = new_zoom
|
|
||||||
font_size: int = self._apply_zoom()
|
|
||||||
log.info(
|
|
||||||
f'Zoomed out UI\n'
|
|
||||||
f'zoom_step: {self._zoom_step!r}\n'
|
|
||||||
f'zoom_level(%): {self._zoom_level:.1%}\n'
|
|
||||||
f'font_size: {font_size!r}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return new_zoom
|
|
||||||
|
|
||||||
def reset_zoom(self) -> None:
|
|
||||||
'''
|
|
||||||
Reset UI zoom to 100%.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if self._zoom_level != 1.0:
|
|
||||||
self._zoom_level = 1.0
|
|
||||||
font_size: int = self._apply_zoom()
|
|
||||||
log.info(
|
|
||||||
f'Reset zoom level\n'
|
|
||||||
f'zoom_step: {self._zoom_step!r}\n'
|
|
||||||
f'zoom_level(%): {self._zoom_level:.1%}\n'
|
|
||||||
f'font_size: {font_size!r}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._zoom_level
|
|
||||||
|
|
||||||
def _apply_zoom(self) -> int:
|
|
||||||
'''
|
|
||||||
Apply current zoom level to all UI elements.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# reconfigure fonts with zoom multiplier
|
|
||||||
font_size: int = _style._config_fonts_to_screen(
|
|
||||||
zoom_level=self._zoom_level
|
|
||||||
)
|
|
||||||
|
|
||||||
# update status bar styling with new font size
|
|
||||||
if self._status_bar:
|
|
||||||
sb = self.statusBar()
|
|
||||||
sb.setStyleSheet((
|
|
||||||
f"color : {hcolor('gunmetal')};"
|
|
||||||
f"background : {hcolor('default_dark')};"
|
|
||||||
f"font-size : {_style._font_small.px_size}px;"
|
|
||||||
"padding : 0px;"
|
|
||||||
))
|
|
||||||
|
|
||||||
# force update of mode label if it exists
|
|
||||||
if self._status_label:
|
|
||||||
self._status_label.setFont(_style._font_small.font)
|
|
||||||
|
|
||||||
# update godwidget and its children
|
|
||||||
if self.godwidget:
|
|
||||||
# update search widget if it exists
|
|
||||||
if hasattr(self.godwidget, 'search') and self.godwidget.search:
|
|
||||||
self.godwidget.search.update_fonts()
|
|
||||||
|
|
||||||
# update order mode panes in all chart views
|
|
||||||
self._update_chart_order_panes()
|
|
||||||
|
|
||||||
# recursively update all other widgets with stylesheets
|
|
||||||
self._refresh_widget_fonts(self.godwidget)
|
|
||||||
self.godwidget.update()
|
|
||||||
|
|
||||||
return font_size
|
|
||||||
|
|
||||||
def _update_chart_order_panes(self) -> None:
|
|
||||||
'''
|
|
||||||
Update order entry panels in all charts.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if not self.godwidget:
|
|
||||||
return
|
|
||||||
|
|
||||||
# iterate through all linked splits (hist and rt)
|
|
||||||
for splits_name in ['hist_linked', 'rt_linked']:
|
|
||||||
splits = getattr(self.godwidget, splits_name, None)
|
|
||||||
if not splits:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# get main chart
|
|
||||||
chart = getattr(splits, 'chart', None)
|
|
||||||
if chart:
|
|
||||||
# update axes
|
|
||||||
self._update_chart_axes(chart)
|
|
||||||
|
|
||||||
# update order pane
|
|
||||||
if hasattr(chart, 'view'):
|
|
||||||
view = chart.view
|
|
||||||
if hasattr(view, 'order_mode') and view.order_mode:
|
|
||||||
order_mode = view.order_mode
|
|
||||||
if hasattr(order_mode, 'pane') and order_mode.pane:
|
|
||||||
order_mode.pane.update_fonts()
|
|
||||||
|
|
||||||
# also check subplots
|
|
||||||
subplots = getattr(splits, 'subplots', {})
|
|
||||||
for name, subplot_chart in subplots.items():
|
|
||||||
# update subplot axes
|
|
||||||
self._update_chart_axes(subplot_chart)
|
|
||||||
|
|
||||||
# update subplot order pane
|
|
||||||
if hasattr(subplot_chart, 'view'):
|
|
||||||
subplot_view = subplot_chart.view
|
|
||||||
if hasattr(subplot_view, 'order_mode') and subplot_view.order_mode:
|
|
||||||
subplot_order_mode = subplot_view.order_mode
|
|
||||||
if hasattr(subplot_order_mode, 'pane') and subplot_order_mode.pane:
|
|
||||||
subplot_order_mode.pane.update_fonts()
|
|
||||||
|
|
||||||
# resize all sidepanes to match main chart's sidepane width
|
|
||||||
# this ensures volume/subplot sidepanes match the main chart
|
|
||||||
if splits and hasattr(splits, 'resize_sidepanes'):
|
|
||||||
splits.resize_sidepanes()
|
|
||||||
|
|
||||||
def _update_chart_axes(self, chart) -> None:
|
|
||||||
'''Update axis fonts and sizing for a chart.'''
|
|
||||||
from . import _style
|
|
||||||
|
|
||||||
# update price axis (right side)
|
|
||||||
if hasattr(chart, 'pi') and chart.pi:
|
|
||||||
plot_item = chart.pi
|
|
||||||
# get all axes from plot item
|
|
||||||
for axis_name in ['left', 'right', 'bottom', 'top']:
|
|
||||||
axis = plot_item.getAxis(axis_name)
|
|
||||||
if axis and hasattr(axis, 'update_fonts'):
|
|
||||||
axis.update_fonts(_style._font)
|
|
||||||
|
|
||||||
# force plot item to recalculate its entire layout
|
|
||||||
plot_item.updateGeometry()
|
|
||||||
|
|
||||||
# force chart widget to update
|
|
||||||
if hasattr(chart, 'updateGeometry'):
|
|
||||||
chart.updateGeometry()
|
|
||||||
|
|
||||||
# trigger a full scene update
|
|
||||||
if hasattr(chart, 'update'):
|
|
||||||
chart.update()
|
|
||||||
|
|
||||||
def _refresh_widget_fonts(self, widget: QWidget) -> None:
|
|
||||||
'''
|
|
||||||
Recursively update font sizes in all child widgets.
|
|
||||||
|
|
||||||
This handles widgets that have font-size hardcoded in their stylesheets.
|
|
||||||
'''
|
|
||||||
from . import _style
|
|
||||||
|
|
||||||
# recursively process all children
|
|
||||||
for child in widget.findChildren(QWidget):
|
|
||||||
# skip widgets that have their own update_fonts method (handled separately)
|
|
||||||
if hasattr(child, 'update_fonts'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# update child's stylesheet if it has font-size
|
|
||||||
child_stylesheet = child.styleSheet()
|
|
||||||
if child_stylesheet and 'font-size' in child_stylesheet:
|
|
||||||
# for labels and simple widgets, regenerate stylesheet
|
|
||||||
# this is a heuristic - may need refinement
|
|
||||||
try:
|
|
||||||
child.setFont(_style._font.font)
|
|
||||||
except (AttributeError, RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# update child's font
|
|
||||||
try:
|
|
||||||
child.setFont(_style._font.font)
|
|
||||||
except (AttributeError, RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# singleton app per actor
|
# singleton app per actor
|
||||||
_qt_win: QMainWindow = None
|
_qt_win: QMainWindow = None
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ from PyQt6.QtCore import (
|
||||||
QSize,
|
QSize,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QItemSelectionModel,
|
QItemSelectionModel,
|
||||||
QObject,
|
|
||||||
pyqtBoundSignal,
|
pyqtBoundSignal,
|
||||||
pyqtRemoveInputHook,
|
pyqtRemoveInputHook,
|
||||||
QSettings,
|
QSettings,
|
||||||
|
|
|
||||||
|
|
@ -203,9 +203,8 @@ pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||||
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
||||||
|
|
||||||
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
||||||
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
|
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
|
||||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
||||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" }
|
|
||||||
# ------ goodboy ------
|
# ------ goodboy ------
|
||||||
# hackin dev-envs, usually there's something new he's hackin in..
|
# hackin dev-envs, usually there's something new he's hackin in..
|
||||||
# tractor = { path = "../tractor", editable = true }
|
# tractor = { path = "../tractor", editable = true }
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
#!env xonsh
|
|
||||||
'''
|
|
||||||
Compute the pxs-per-inch (PPI) naively for the local DE.
|
|
||||||
|
|
||||||
NOTE, currently this only supports the `sway`-TWM on wayland.
|
|
||||||
|
|
||||||
!TODO!
|
|
||||||
- [ ] support Xorg (and possibly other OSs as well?
|
|
||||||
- [ ] conver this to pure py code, dropping the `.xsh` specifics
|
|
||||||
instead for `subprocess` API calls?
|
|
||||||
- [ ] possibly unify all this with `./qt_screen_info.py` as part of
|
|
||||||
a "PPI config wizard" or something, but more then likely we'll
|
|
||||||
have lib-ified version inside modden/piker by then?
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
import math
|
|
||||||
import json
|
|
||||||
|
|
||||||
# XXX, xonsh part using "subprocess mode"
|
|
||||||
disp_infos: list[dict] = json.loads($(wlr-randr --json))
|
|
||||||
lappy: dict = disp_infos[0]
|
|
||||||
|
|
||||||
dims: dict[str, int] = lappy['physical_size']
|
|
||||||
w_cm: int = dims['width']
|
|
||||||
h_cm: int = dims['height']
|
|
||||||
|
|
||||||
# cm per inch
|
|
||||||
cpi: float = 25.4
|
|
||||||
|
|
||||||
# compute "diagonal" size (aka hypot)
|
|
||||||
diag_inches: float = math.sqrt((h_cm/cpi)**2 + (w_cm/cpi)**2)
|
|
||||||
|
|
||||||
# compute reso-hypot / inches-hypot
|
|
||||||
hi_res: dict[str, float|bool] = lappy['modes'][0]
|
|
||||||
w_px: int = hi_res['width']
|
|
||||||
h_px: int = hi_res['height']
|
|
||||||
|
|
||||||
diag_pxs: float = math.sqrt(h_px**2 + w_px**2)
|
|
||||||
unscaled_ppi: float = diag_pxs/diag_inches
|
|
||||||
|
|
||||||
# retrieve TWM info on the display (including scaling info)
|
|
||||||
sway_disp_info: dict = json.loads($(swaymsg -r -t get_outputs))[0]
|
|
||||||
scale: float = sway_disp_info['scale']
|
|
||||||
|
|
||||||
print(
|
|
||||||
f'output: {sway_disp_info["name"]!r}\n'
|
|
||||||
f'--- DIMENSIONS ---\n'
|
|
||||||
f'w_cm: {w_cm!r}\n'
|
|
||||||
f'h_cm: {h_cm!r}\n'
|
|
||||||
f'w_px: {w_px!r}\n'
|
|
||||||
f'h_cm: {h_px!r}\n'
|
|
||||||
f'\n'
|
|
||||||
f'--- DIAGONALS ---\n'
|
|
||||||
f'diag_inches: {diag_inches!r}\n'
|
|
||||||
f'diag_pxs: {diag_pxs!r}\n'
|
|
||||||
f'\n'
|
|
||||||
f'--- PPI-related-info ---\n'
|
|
||||||
f'(DE reported) scale: {scale!r}\n'
|
|
||||||
f'unscaled PPI: {unscaled_ppi!r}\n'
|
|
||||||
f'|_ =sqrt(h_px**2 + w_px**2) / sqrt(h_in**2 + w_in**2)\n'
|
|
||||||
f'scaled PPI: {unscaled_ppi/scale!r}\n'
|
|
||||||
f'|_ =unscaled_ppi/scale\n'
|
|
||||||
)
|
|
||||||
|
|
@ -31,8 +31,8 @@ Resource list for mucking with DPIs on multiple screens:
|
||||||
- https://doc.qt.io/qt-5/qguiapplication.html#screenAt
|
- https://doc.qt.io/qt-5/qguiapplication.html#screenAt
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import os
|
|
||||||
|
|
||||||
|
from pyqtgraph import QtGui
|
||||||
from PyQt6 import (
|
from PyQt6 import (
|
||||||
QtCore,
|
QtCore,
|
||||||
QtWidgets,
|
QtWidgets,
|
||||||
|
|
@ -43,11 +43,6 @@ from PyQt6.QtCore import (
|
||||||
QSize,
|
QSize,
|
||||||
QRect,
|
QRect,
|
||||||
)
|
)
|
||||||
from pyqtgraph import QtGui
|
|
||||||
|
|
||||||
|
|
||||||
# https://doc.qt.io/qt-6/highdpi.html#environment-variable-reference
|
|
||||||
os.environ['QT_USE_PHYSICAL_DPI'] = '1'
|
|
||||||
|
|
||||||
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
||||||
# must be set before creating the application
|
# must be set before creating the application
|
||||||
|
|
@ -63,22 +58,13 @@ if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE, inherits `QGuiApplication`
|
|
||||||
# https://doc.qt.io/qt-6/qapplication.html
|
|
||||||
# https://doc.qt.io/qt-6/qguiapplication.html
|
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
#
|
|
||||||
# ^TODO? various global DPI settings?
|
|
||||||
# [ ] DPI rounding policy,
|
|
||||||
# - https://doc.qt.io/qt-6/qt.html#HighDpiScaleFactorRoundingPolicy-enum
|
|
||||||
# - https://doc.qt.io/qt-6/qguiapplication.html#setHighDpiScaleFactorRoundingPolicy
|
|
||||||
|
|
||||||
window = QtWidgets.QMainWindow()
|
window = QtWidgets.QMainWindow()
|
||||||
main_widget = QtWidgets.QWidget()
|
main_widget = QtWidgets.QWidget()
|
||||||
window.setCentralWidget(main_widget)
|
window.setCentralWidget(main_widget)
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
_main_pxr: float = main_widget.devicePixelRatioF()
|
pxr: float = main_widget.devicePixelRatioF()
|
||||||
|
|
||||||
# explicitly get main widget and primary displays
|
# explicitly get main widget and primary displays
|
||||||
current_screen: QtGui.QScreen = app.screenAt(
|
current_screen: QtGui.QScreen = app.screenAt(
|
||||||
|
|
@ -91,13 +77,7 @@ for screen in app.screens():
|
||||||
name: str = screen.name()
|
name: str = screen.name()
|
||||||
model: str = screen.model().rstrip()
|
model: str = screen.model().rstrip()
|
||||||
size: QSize = screen.size()
|
size: QSize = screen.size()
|
||||||
geo: QRect = screen.geometry()
|
geo: QRect = screen.availableGeometry()
|
||||||
|
|
||||||
# device-pixel-ratio
|
|
||||||
# https://doc.qt.io/qt-6/highdpi.html
|
|
||||||
pxr: float = screen.devicePixelRatio()
|
|
||||||
|
|
||||||
unscaled_size: QSize = pxr * size
|
|
||||||
phydpi: float = screen.physicalDotsPerInch()
|
phydpi: float = screen.physicalDotsPerInch()
|
||||||
logdpi: float = screen.logicalDotsPerInch()
|
logdpi: float = screen.logicalDotsPerInch()
|
||||||
is_primary: bool = screen is primary_screen
|
is_primary: bool = screen is primary_screen
|
||||||
|
|
@ -108,12 +88,11 @@ for screen in app.screens():
|
||||||
f'|_primary: {is_primary}\n'
|
f'|_primary: {is_primary}\n'
|
||||||
f' _current: {is_current}\n'
|
f' _current: {is_current}\n'
|
||||||
f' _model: {model}\n'
|
f' _model: {model}\n'
|
||||||
f' _size: {size}\n'
|
f' _screen size: {size}\n'
|
||||||
f' _geometry: {geo}\n'
|
f' _screen geometry: {geo}\n'
|
||||||
f' _devicePixelRatio(): {pxr}\n'
|
f' _devicePixelRationF(): {pxr}\n'
|
||||||
f' _unscaled-size: {unscaled_size!r}\n'
|
f' _physical dpi: {phydpi}\n'
|
||||||
f' _physical-dpi: {phydpi}\n'
|
f' _logical dpi: {logdpi}\n'
|
||||||
f' _logical-dpi: {logdpi}\n'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# app-wide font info
|
# app-wide font info
|
||||||
|
|
@ -131,8 +110,8 @@ str_w: int = str_br.width()
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f'------ global font settings ------\n'
|
f'------ global font settings ------\n'
|
||||||
f'font dpi: {fontdpi!r}\n'
|
f'font dpi: {fontdpi}\n'
|
||||||
f'font height: {font_h!r}\n'
|
f'font height: {font_h}\n'
|
||||||
f'string bounding rect: {str_br!r}\n'
|
f'string bounding rect: {str_br}\n'
|
||||||
f'string width : {str_w!r}\n'
|
f'string width : {str_w}\n'
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import pytest
|
|
||||||
from piker.ui._style import DpiAwareFont
|
|
||||||
|
|
||||||
|
|
||||||
class MockScreen:
|
|
||||||
def __init__(self, pdpi, ldpi, name="MockScreen"):
|
|
||||||
self._pdpi = pdpi
|
|
||||||
self._ldpi = ldpi
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
def physicalDotsPerInch(self):
|
|
||||||
return self._pdpi
|
|
||||||
|
|
||||||
def logicalDotsPerInch(self):
|
|
||||||
return self._ldpi
|
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"pdpi, ldpi, expected_px",
|
|
||||||
[
|
|
||||||
(96, 96, 9), # normal DPI
|
|
||||||
(169, 96, 15), # HiDPI
|
|
||||||
(120, 96, 10), # mid-DPI
|
|
||||||
]
|
|
||||||
)
|
|
||||||
def test_font_px_size(pdpi, ldpi, expected_px):
|
|
||||||
font = DpiAwareFont()
|
|
||||||
font.configure_to_dpi(screen=MockScreen(pdpi, ldpi))
|
|
||||||
|
|
||||||
px = font.px_size
|
|
||||||
print(f"{pdpi}x{ldpi} DPI -> Computed pixel size: {px}")
|
|
||||||
|
|
||||||
assert px == expected_px
|
|
||||||
4
uv.lock
4
uv.lock
|
|
@ -1034,7 +1034,7 @@ requires-dist = [
|
||||||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||||
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
||||||
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
||||||
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
|
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=main" },
|
||||||
{ name = "trio", specifier = ">=0.27" },
|
{ name = "trio", specifier = ">=0.27" },
|
||||||
{ name = "trio-typing", specifier = ">=0.10.0" },
|
{ name = "trio-typing", specifier = ">=0.10.0" },
|
||||||
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
||||||
|
|
@ -1676,7 +1676,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tractor"
|
name = "tractor"
|
||||||
version = "0.1.0a6.dev0"
|
version = "0.1.0a6.dev0"
|
||||||
source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#566a17ba92469000a01fd18c709ea3e336e72796" }
|
source = { git = "https://github.com/goodboy/tractor.git?branch=main#e77198bb64f0467a50e251ed140daee439752354" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bidict" },
|
{ name = "bidict" },
|
||||||
{ name = "cffi" },
|
{ name = "cffi" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue