Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
Izalia Mae | e0ffa9d840 | ||
Izalia Mae | 5ddb1cc5d8 | ||
Izalia Mae | 80c872b4f9 | ||
Izalia Mae | 3dfdc50ed7 | ||
Izalia Mae | f3a05aefeb | ||
Izalia Mae | 93a7c06e68 | ||
Izalia Mae | 666ac69032 | ||
Izalia Mae | 6b716741e5 | ||
Izalia Mae | 9f7a1bd67f | ||
Izalia Mae | 765eb828d0 | ||
Izalia Mae | 5781ffaae5 | ||
Izalia Mae | 548a7902eb |
|
@ -1,11 +1,8 @@
|
|||
__software__ = "Barkshark Lib"
|
||||
"Name of the library"
|
||||
|
||||
__version_info__ = (0, 1, 2)
|
||||
"Version of the library"
|
||||
|
||||
__version_info__ = (0, 1, 3)
|
||||
__version__ = ".".join(str(v) for v in __version_info__)
|
||||
"Version of the library in string form"
|
||||
__author__ = "Zoey Mae"
|
||||
__homepage__ = "https://git.barkshark.xyz/barkshark/blib"
|
||||
|
||||
|
||||
from .application import Application
|
||||
|
@ -66,8 +63,8 @@ from .misc import (
|
|||
HttpDate,
|
||||
JsonBase,
|
||||
NamedTuple,
|
||||
RunData
|
||||
|
||||
RunData,
|
||||
StaticProperty
|
||||
)
|
||||
|
||||
from .objects import (
|
||||
|
|
|
@ -39,7 +39,7 @@ class Application(Object):
|
|||
]
|
||||
"List of signals to handle while the loop is running"
|
||||
|
||||
self.event = asyncio.Event()
|
||||
self._shutdown = asyncio.Event()
|
||||
Application.set(self)
|
||||
|
||||
|
||||
|
@ -139,13 +139,13 @@ class Application(Object):
|
|||
self.loop.stop()
|
||||
self.loop = None
|
||||
|
||||
self.event.clear()
|
||||
self._shutdown.clear()
|
||||
|
||||
|
||||
def quit(self, exit_code: int = 0) -> None:
|
||||
"Tell the application to quit"
|
||||
|
||||
self.event.set()
|
||||
self._shutdown.set()
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
|
@ -160,7 +160,7 @@ class Application(Object):
|
|||
return self.exit_code
|
||||
|
||||
self.exit_code = None
|
||||
self.event.clear()
|
||||
self._shutdown.clear()
|
||||
|
||||
asyncio.run(self.handle_run())
|
||||
self.loop = None
|
||||
|
@ -181,7 +181,7 @@ class Application(Object):
|
|||
self.loop = asyncio.get_running_loop()
|
||||
self.startup.emit() # pylint: disable=no-member
|
||||
|
||||
while not self.event.is_set():
|
||||
while not self._shutdown.is_set():
|
||||
await self.handle_loop()
|
||||
|
||||
self.shutdown.emit() # pylint: disable=no-member
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from .errors import FileError
|
||||
from .path import File
|
||||
|
||||
|
||||
class Icon(dict[int, Any]):
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Icon(dict[int, T]):
|
||||
"Represents an icon"
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
|
||||
def __init__(self, name: str, icons: dict[int, Any] | None = None) -> None:
|
||||
def __init__(self, name: str, icons: dict[int, T] | None = None) -> None:
|
||||
"""
|
||||
Create a new ``Icon`` object
|
||||
|
||||
|
@ -37,40 +40,63 @@ class Icon(dict[int, Any]):
|
|||
|
||||
|
||||
@property
|
||||
def small(self) -> Any:
|
||||
def small(self) -> T:
|
||||
"Return a 16x16 icon if available"
|
||||
|
||||
return self[16]
|
||||
|
||||
|
||||
@property
|
||||
def normal(self) -> Any:
|
||||
def normal(self) -> T:
|
||||
"Return a 32x32 icon if available"
|
||||
|
||||
return self[32]
|
||||
|
||||
|
||||
@property
|
||||
def large(self) -> Any:
|
||||
def large(self) -> T:
|
||||
"Return a 64x64 icon if available"
|
||||
|
||||
return self[64]
|
||||
|
||||
|
||||
@property
|
||||
def extra_large(self) -> Any:
|
||||
def extra_large(self) -> T:
|
||||
"Return a 128x128 icon if available"
|
||||
|
||||
return self[128]
|
||||
|
||||
|
||||
@property
|
||||
def scalable(self) -> Any:
|
||||
def humungous(self) -> T:
|
||||
return self[256]
|
||||
|
||||
|
||||
@property
|
||||
def ginormous(self) -> T:
|
||||
return self[512]
|
||||
|
||||
|
||||
@property
|
||||
def chonker(self) -> T:
|
||||
return self[1024]
|
||||
|
||||
|
||||
@property
|
||||
def scalable(self) -> T:
|
||||
"Return a scalable (usually svg) icon if available"
|
||||
|
||||
return self[0]
|
||||
|
||||
|
||||
def get_minimum(self, size: int) -> T:
|
||||
for icon_size in self.sizes:
|
||||
if size < icon_size:
|
||||
return self[icon_size]
|
||||
|
||||
raise KeyError(size)
|
||||
|
||||
|
||||
class IconTheme(ABC):
|
||||
"Base class for icon themes"
|
||||
|
||||
|
@ -78,14 +104,14 @@ class IconTheme(ABC):
|
|||
"Name of the icon theme"
|
||||
|
||||
@abstractmethod
|
||||
def get_icon(self, name: str) -> Icon: ...
|
||||
def get_icon(self, name: str) -> Icon[Any]: ...
|
||||
|
||||
|
||||
class IconThemes(dict[str, IconTheme]):
|
||||
"Represets a collection of icon themes"
|
||||
|
||||
|
||||
def __init__(self, base_path: str = "/usr/share/icons", default: str = "hicolor") -> None:
|
||||
def __init__(self, base_paths: list[str] | None = None, default: str = "hicolor") -> None:
|
||||
"""
|
||||
Create a new ``IconThemes`` object
|
||||
|
||||
|
@ -94,7 +120,13 @@ class IconThemes(dict[str, IconTheme]):
|
|||
:raises ValueError: When the default theme cannot be found
|
||||
"""
|
||||
|
||||
self.base_path: File = File(base_path).resolve()
|
||||
if base_paths is None:
|
||||
base_paths = [
|
||||
"/usr/share/icons",
|
||||
"~/.icons"
|
||||
]
|
||||
|
||||
self.base_paths: list[File] = [File(path).resolve() for path in base_paths]
|
||||
"Filesystem path to search for icon themes"
|
||||
|
||||
self.themes: dict[str, IconTheme] = {}
|
||||
|
@ -106,7 +138,7 @@ class IconThemes(dict[str, IconTheme]):
|
|||
self.load(default)
|
||||
|
||||
|
||||
def get_icon(self, theme: str, name: str) -> Icon:
|
||||
def get_icon(self, theme: str, name: str) -> Icon[Any]:
|
||||
"""
|
||||
Get an icon. If the icon cannot be found in the specified theme, the default theme
|
||||
will get searched.
|
||||
|
@ -135,12 +167,13 @@ class IconThemes(dict[str, IconTheme]):
|
|||
themes: dict[str, IconTheme] = {}
|
||||
fallback: IconTheme | None = None
|
||||
|
||||
for path in self.base_path.glob():
|
||||
if path.isdir:
|
||||
themes[path.name] = FileIconTheme(path)
|
||||
for base_path in self.base_paths:
|
||||
for path in base_path.glob():
|
||||
if path.isdir:
|
||||
themes[path.name] = FileIconTheme(path)
|
||||
|
||||
if path.name.lower() == default.lower():
|
||||
fallback = themes[path.name]
|
||||
if path.name.lower() == default.lower():
|
||||
fallback = themes[path.name]
|
||||
|
||||
if fallback is None:
|
||||
raise ValueError(f"Cannot find default theme: {default}")
|
||||
|
@ -166,7 +199,7 @@ class FileIconTheme(IconTheme):
|
|||
|
||||
self.path: File = File(base_path).resolve()
|
||||
self.name: str = name or self.path.name
|
||||
self._cache: dict[str, Icon | None] = {}
|
||||
self._cache: dict[str, Icon[str] | None] = {}
|
||||
|
||||
if not self.path.isdir:
|
||||
raise FileError.IsFile(self.path)
|
||||
|
@ -181,21 +214,27 @@ class FileIconTheme(IconTheme):
|
|||
self._cache.clear()
|
||||
|
||||
|
||||
def get_icon(self, name: str) -> Icon:
|
||||
def get_icon(self, name: str) -> Icon[str]:
|
||||
if name in self._cache:
|
||||
if (cached := self._cache[name]) is None:
|
||||
raise KeyError(name)
|
||||
|
||||
return cached
|
||||
|
||||
icon = Icon(name)
|
||||
icon: Icon[str] = Icon(name)
|
||||
|
||||
for path in self.path.glob(recursive = True, ext = ["png", "svg", "svgz"]):
|
||||
if name.lower() == path.stem.lower():
|
||||
size_str = path.split(self.name)[1].split("/")[1]
|
||||
path_parts = path.split(self.name)[1].lstrip("/").split("/")
|
||||
|
||||
if size_str != "scalable":
|
||||
icon[int(size_str.split("x")[0])] = path
|
||||
if "scalable" not in path_parts:
|
||||
for part in path_parts:
|
||||
try:
|
||||
icon[int(part.split("x")[0])] = path
|
||||
break
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
icon[0] = path
|
||||
|
@ -211,7 +250,7 @@ class FileIconTheme(IconTheme):
|
|||
class MemoryIconTheme(IconTheme):
|
||||
__slots__ = ("name", "icons",)
|
||||
|
||||
def __init__(self, name: str, *icons: Icon) -> None:
|
||||
def __init__(self, name: str, *icons: Icon[bytes]) -> None:
|
||||
"""
|
||||
Create a new ``MemoryIconTheme`` object
|
||||
|
||||
|
@ -220,10 +259,10 @@ class MemoryIconTheme(IconTheme):
|
|||
"""
|
||||
|
||||
self.name: str = name
|
||||
self.icons: dict[str, Icon] = {icon.name: icon for icon in icons}
|
||||
self.icons: dict[str, Icon[bytes]] = {icon.name: icon for icon in icons}
|
||||
|
||||
|
||||
def get_icon(self, name: str) -> Icon:
|
||||
def get_icon(self, name: str) -> Icon[bytes]:
|
||||
return self.icons[name]
|
||||
|
||||
|
||||
|
|
74
blib/misc.py
74
blib/misc.py
|
@ -10,7 +10,7 @@ import string
|
|||
import timeit
|
||||
import traceback
|
||||
|
||||
from collections.abc import Callable, Generator, Iterator, Sequence
|
||||
from collections.abc import Callable, Generator, Iterator, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
from functools import wraps
|
||||
|
@ -50,6 +50,7 @@ except ImportError:
|
|||
|
||||
|
||||
DictValueType = TypeVar("DictValueType")
|
||||
SPType = TypeVar("SPType")
|
||||
|
||||
TRUE_STR = ['on', 'y', 'yes', 'true', 'enable', 'enabled', '1']
|
||||
FALSE_STR = ['off', 'n', 'no', 'false', 'disable', 'disable', '0']
|
||||
|
@ -444,33 +445,49 @@ class DictProperty(Generic[DictValueType]):
|
|||
"Represents a key in a dict"
|
||||
|
||||
|
||||
def __init__(self, key: str) -> None:
|
||||
def __init__(self,
|
||||
key: str,
|
||||
deserializer: Callable[[str, Any], DictValueType] | None = None,
|
||||
serializer: Callable[[str, DictValueType], Any] | None = None) -> None:
|
||||
"""
|
||||
Create a new dict property
|
||||
|
||||
:param key: Name of the key to be handled by this ``Property``
|
||||
:param deserializer: Function that will convert a JSON value to a Python value
|
||||
:param serializer: Function that will convert a Python value to a JSON value
|
||||
"""
|
||||
|
||||
self.key: str = key
|
||||
self.deserializer: Callable[[str, Any], Any] | None = deserializer
|
||||
self.serializer: Callable[[str, Any], Any] | None = serializer
|
||||
|
||||
|
||||
def __get__(self,
|
||||
obj: dict[str, DictValueType | Any] | None,
|
||||
objtype: Any = None) -> Self | DictValueType:
|
||||
objtype: Any = None) -> DictValueType:
|
||||
|
||||
if obj is None:
|
||||
return self
|
||||
raise RuntimeError("No object for dict property")
|
||||
|
||||
try:
|
||||
return obj[self.key]
|
||||
value = obj[self.key]
|
||||
|
||||
except KeyError:
|
||||
objname = get_object_name(obj)
|
||||
raise AttributeError(f"'{objname}' has no attribute '{self.key}'") from None
|
||||
|
||||
if self.deserializer is None:
|
||||
return value
|
||||
|
||||
return self.deserializer(self.key, value) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def __set__(self, obj: dict[str, DictValueType | Any], value: DictValueType) -> None:
|
||||
obj[self.key] = value
|
||||
if self.serializer is None:
|
||||
obj[self.key] = value
|
||||
return
|
||||
|
||||
obj[self.key] = self.serializer(self.key, value)
|
||||
|
||||
|
||||
def __delete__(self, obj: dict[str, DictValueType | Any]) -> None:
|
||||
|
@ -641,7 +658,10 @@ class FileSize(int):
|
|||
"Converts a human-readable file size to bytes"
|
||||
|
||||
|
||||
def __new__(cls: type[Self], size: int | float, unit: FileSizeUnit = FileSizeUnit.B) -> Self:
|
||||
def __new__(cls: type[Self],
|
||||
size: int | float,
|
||||
unit: FileSizeUnit | str = FileSizeUnit.B) -> Self:
|
||||
|
||||
return int.__new__(cls, FileSizeUnit.parse(unit).multiply(size))
|
||||
|
||||
|
||||
|
@ -782,7 +802,7 @@ class JsonBase(dict[str, Any]):
|
|||
|
||||
|
||||
@classmethod
|
||||
def parse(cls: type[Self], data: str | bytes | dict[str, Any]) -> Self:
|
||||
def parse(cls: type[Self], data: str | bytes | Mapping[str, Any]) -> Self:
|
||||
"""
|
||||
Parse a JSON object
|
||||
|
||||
|
@ -822,7 +842,7 @@ class JsonBase(dict[str, Any]):
|
|||
"""
|
||||
|
||||
if not isinstance(value, (str, int, float, bool, dict, list, tuple, type(None))):
|
||||
print(f"Warning: Cannot properly convert value of type '{type(value).__name__}'")
|
||||
# print(f"Warning: Cannot properly convert value of type '{type(value).__name__}'")
|
||||
return str(value)
|
||||
|
||||
return value
|
||||
|
@ -910,3 +930,39 @@ class RunData:
|
|||
|
||||
total: float
|
||||
"Time it took for all runs"
|
||||
|
||||
|
||||
class StaticProperty(Generic[SPType]):
|
||||
"Decorator for turning a static method into a static property"
|
||||
|
||||
def __init__(self, func: Callable[..., SPType]) -> None:
|
||||
"""
|
||||
Create a new ``StaticProperty`` object
|
||||
|
||||
:param func: The decorated function
|
||||
"""
|
||||
|
||||
self._getter: Callable[[], SPType] = func
|
||||
self._setter: Callable[[Any], None] | None = None
|
||||
|
||||
|
||||
def __get__(self, obj: Any, cls: Any) -> SPType:
|
||||
return self._getter()
|
||||
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None:
|
||||
if self._setter is None:
|
||||
raise AttributeError("No setter is set")
|
||||
|
||||
self._setter(value)
|
||||
|
||||
|
||||
def setter(self, func: Callable[[Any], None]) -> Callable[[Any], None]:
|
||||
"""
|
||||
Add a function for setting the value
|
||||
|
||||
:param func: Function to decorate
|
||||
"""
|
||||
|
||||
self._setter = func
|
||||
return func
|
||||
|
|
|
@ -120,12 +120,13 @@ class Signal(list[SignalCallback]):
|
|||
print(f"WARNING: '{cbname}' was not connted to signal '{signame}'")
|
||||
|
||||
|
||||
async def handle_emit(self, *args: Any, **kwargs: Any) -> None:
|
||||
async def handle_emit(self, *args: Any, catch_errors: bool = True, **kwargs: Any) -> None:
|
||||
"""
|
||||
This gets called by :meth:`Signal.emit` as an :class:`asyncio.Task`.
|
||||
|
||||
:param args: Positional arguments to pass to all of the callbacks
|
||||
:param kwargs: Keyword arguments to pass to all of the callbacks
|
||||
:param catch_errors: Whether or not to handle exceptions raised from callbacks
|
||||
"""
|
||||
|
||||
if not self.callback:
|
||||
|
@ -133,10 +134,25 @@ class Signal(list[SignalCallback]):
|
|||
return
|
||||
|
||||
for callback in self:
|
||||
if await self.handle_callback(callback, *args, **kwargs):
|
||||
try:
|
||||
if await self.handle_callback(callback, *args, **kwargs):
|
||||
break
|
||||
|
||||
except Exception:
|
||||
if not catch_errors:
|
||||
raise
|
||||
|
||||
traceback.print_exc()
|
||||
break
|
||||
|
||||
await self.handle_callback(self.callback, *args, **kwargs)
|
||||
try:
|
||||
await self.handle_callback(self.callback, *args, **kwargs)
|
||||
|
||||
except Exception:
|
||||
if not catch_errors:
|
||||
raise
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
async def handle_callback(self,
|
||||
|
@ -157,10 +173,6 @@ class Signal(list[SignalCallback]):
|
|||
print(f"Callback '{callback.__name__}' timed out")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class Object:
|
||||
|
|
|
@ -152,7 +152,7 @@ class File(Path):
|
|||
:param dir_type: XDG name
|
||||
"""
|
||||
|
||||
return cls(XdgDir.parse(dir_type))
|
||||
return cls(XdgDir.parse(dir_type).path)
|
||||
|
||||
|
||||
@property
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
from urllib.parse import quote_plus, unquote, urlparse
|
||||
|
||||
from .enums import ProtocolPort
|
||||
from .misc import get_object_name, get_object_properties, get_top_domain
|
||||
|
@ -371,7 +371,7 @@ class Query(list[tuple[str, str]]):
|
|||
items = []
|
||||
|
||||
for key, value in self.items():
|
||||
items.append(f"{quote(key)}={quote(value)}")
|
||||
items.append(f"{quote_plus(key)}={quote_plus(value)}")
|
||||
|
||||
return "&".join(items)
|
||||
|
||||
|
|
|
@ -2,11 +2,6 @@ API
|
|||
===
|
||||
|
||||
|
||||
.. autodata:: blib.__software__
|
||||
.. autodata:: blib.__version__
|
||||
.. autodata:: blib.__version_info__
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
|
@ -98,6 +93,10 @@ Classes
|
|||
:show-inheritance:
|
||||
:exclude-members: append, remove
|
||||
|
||||
.. autoclass:: blib.StaticProperty
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: blib.Url
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -75,6 +75,5 @@ disallow_untyped_decorators = true
|
|||
warn_redundant_casts = true
|
||||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
follow_imports = "silent"
|
||||
strict = true
|
||||
implicit_reexport = true
|
||||
|
|
Loading…
Reference in a new issue