Compare commits

...

12 commits
0.1.2 ... main

9 changed files with 166 additions and 64 deletions

View file

@ -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 (

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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