"""Utility functions"""
# pylint: disable=maybe-no-member
import wx
from itertools import tee
import os
import platform
import subprocess
import sys
from typing import Any, Callable, Dict, Iterable, List, Tuple, Type, TypeVar
from dataclasses import is_dataclass
from pathlib import Path
[docs]def convert_position(fn):
"""Decorator that converts the event position to one that is relative to the receiver."""
def ret(self, evt):
if self is not evt.EventObject:
client_pos = evt.GetPosition() # get raw position
screen_pos = evt.EventObject.ClientToScreen(client_pos) # convert to screen position
relative_pos = self.ScreenToClient(screen_pos) # convert to receiver position
# call function
copy = evt.Clone()
copy.SetPosition(relative_pos)
copy.foreign = True
fn(self, copy)
evt.Skip()
else:
copy = evt
copy.foreign = False
fn(self, copy)
return ret
[docs]def no_rzeros(num: float, precision: int) -> str:
"""Returns string of the num with the given precision, but with trailing zeros removed."""
assert precision > 0
fmt = '{:.' + str(precision) + 'f}'
return fmt.format(num).rstrip('0').rstrip('.')
[docs]def on_msw() -> bool:
"""Returns whether we are running on Windows."""
return os.name == 'nt'
[docs]def get_local_path(relative_path):
"""Get path relative to the executable, or the working dir if not bundled."""
return os.path.abspath(relative_path)
# def get_bundled_path(relative_path):
# """Given a path relative to the application bundle, return the absolute path.
# Specifically for files bundled with the application, e.g. resources.
# """
# if hasattr(sys, '_MEIPASS'):
# # PyInstaller creates a temp folder and stores path in _MEIPASS
# base_path = getattr(sys, '_MEIPASS')
# else:
# base_path = os.path.abspath(".")
# return os.path.join(base_path, relative_path)
[docs]def resource_path(relative_path):
"""Get absolute path to resource, works for dev and PyInstaller."""
source_path = Path(__file__).resolve()
source_dir = source_path.parent
return source_dir.joinpath('resources').joinpath(relative_path).as_posix()
[docs]def start_file(abs_path: str):
# Tell OS to open the file for editing. From https://stackoverflow.com/a/435669/9171534
if platform.system() == 'Darwin': # macOS
subprocess.call(('open', abs_path))
elif platform.system() == 'Windows': # Windows
os.startfile(abs_path)
else: # linux variants
subprocess.call(('xdg-open', abs.path))
[docs]def pairwise(iterable: Iterable) -> Iterable:
"""s -> (s0,s1), (s1,s2), (s2, s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
[docs]def rgba_to_wx_colour(rgb: int, alpha: float) -> wx.Colour:
"""Given RGBA color, return wx.Colour.
Args:
rgb: RGB color in hex format.
alpha: The opacity of the color, ranging from 0.0 to 1.0.
"""
b = rgb & 0xff
g = (rgb >> 8) & 0xff
r = (rgb >> 16) & 0xff
return wx.Colour(r, g, b, int(alpha * 255))
[docs]def opacity_mul(color: wx.Colour, fraction: float) -> wx.Colour:
return wx.Colour(color.Red(), color.Green(), color.Blue(), int(color.Alpha() * fraction))
[docs]def change_opacity(color: wx.Colour, new_op: int):
return wx.Colour(color.Red(), color.Green(), color.Blue(), new_op)
[docs]def int_round(n: float) -> int:
return int(round(round(n, 2)))
[docs]def even_round(n: float) -> int:
"""Round to the nearest even integer"""
return int(round(n / 2)) * 2
T = TypeVar('T')
[docs]def gchain(*iterables: Iterable[T]) -> Iterable[Tuple[int, T]]:
# chain('ABC', 'DEF') --> (0,A) (0,B) (0,C) (1,D) (1,E) (1,F)
i = 0
for it in iterables:
for element in it:
yield (i, element)
i += 1
# Force dataclass to use kwargs, from https://gist.github.com/mikeholler/4be180627d3f8fceb55704b729464adb#file-dataclass_utils-py
_T = TypeVar("_T")
_Self = TypeVar("_Self")
_VarArgs = List[Any]
_KWArgs = Dict[str, Any]
def _kwarg_only_init_wrapper(
self: _Self,
init: Callable[..., None],
*args: _VarArgs,
**kwargs: _KWArgs
) -> None:
if len(args) > 0:
raise TypeError(
f"{type(self).__name__}.__init__(self, ...) only allows keyword arguments. Found the "
f"following positional arguments: {args}"
)
init(self, **kwargs)
[docs]def require_kwargs_on_init(cls: Type[_T]) -> Type[_T]:
"""
Force a dataclass's init function to only work if called with keyword arguments.
If parameters are not positional-only, a TypeError is thrown with a helpful message.
This function may only be used on dataclasses.
This works by wrapping the __init__ function and dynamically replacing it. Therefore,
stacktraces for calls to the new __init__ might look a bit strange. Fear not though,
all is well.
Note: although this may be used as a decorator, this is not advised as IDEs will no longer
suggest parameters in the constructor. Instead, this is the recommended usage::
from dataclasses import dataclass
@dataclass
class Foo:
bar: str
require_kwargs_on_init(Foo)
"""
if cls is None:
raise TypeError("Cannot call with cls=None")
if not is_dataclass(cls):
raise TypeError(
f"This decorator only works on dataclasses. {cls.__name__} is not a dataclass."
)
original_init = cls.__init__
def new_init(self: _Self, *args: _VarArgs, **kwargs: _KWArgs) -> None:
_kwarg_only_init_wrapper(self, original_init, *args, **kwargs)
# noinspection PyTypeHints
cls.__init__ = new_init # type: ignore
return cls