Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3224583b1 | |||
| d1136178de | |||
| 00a99a8d2c | |||
| 68d9b36273 | |||
| 76f7c1fadd | |||
| 7e6e3f3beb | |||
| 320ff92c7d | |||
| 49d68cefa5 | |||
| 24c06479b6 | |||
| 62e79c03b6 | |||
| d14448293b | |||
| 332162775f | |||
| 18e0a8ecea | |||
| 3ab468205f | |||
| 8521692dc8 | |||
| 8fdfbebb75 | |||
| a6ffc2ebf4 | |||
| ca736c16e4 | |||
| a813d16c3d | |||
| 3f9d5e3652 |
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "dopt-basics"
|
name = "dopt-basics"
|
||||||
version = "0.1.3"
|
version = "0.2.6"
|
||||||
description = "basic cross-project tools for Python-based d-opt projects"
|
description = "basic cross-project tools for Python-based d-opt projects"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Florian Förster", email = "f.foerster@d-opt.com"},
|
{name = "Florian Förster", email = "f.foerster@d-opt.com"},
|
||||||
@@ -69,7 +69,7 @@ directory = "reports/coverage"
|
|||||||
|
|
||||||
|
|
||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.1.3"
|
current_version = "0.2.6"
|
||||||
parse = """(?x)
|
parse = """(?x)
|
||||||
(?P<major>0|[1-9]\\d*)\\.
|
(?P<major>0|[1-9]\\d*)\\.
|
||||||
(?P<minor>0|[1-9]\\d*)\\.
|
(?P<minor>0|[1-9]\\d*)\\.
|
||||||
|
|||||||
2
scripts/bump_patch.ps1
Normal file
2
scripts/bump_patch.ps1
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pdm run bump-my-version bump patch
|
||||||
|
pdm run bump-my-version show current_version
|
||||||
127
src/dopt_basics/batching.py
Normal file
127
src/dopt_basics/batching.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Collection
|
||||||
|
from typing import Generic, Never, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class Chunk(Generic[T]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
capacity: int,
|
||||||
|
overfill_once: bool = True,
|
||||||
|
cap_property: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.capacity = capacity
|
||||||
|
self.overfill = overfill_once
|
||||||
|
self.capacity_left: int = capacity
|
||||||
|
self.capacity_held: int = 0
|
||||||
|
self.full: bool = False
|
||||||
|
self._contents: list[T] = []
|
||||||
|
self.cap_property = cap_property
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self._contents.__repr__()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self._contents.__str__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contents(self) -> list[T]:
|
||||||
|
return self._contents
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._contents)
|
||||||
|
|
||||||
|
def __getitem__(
|
||||||
|
self,
|
||||||
|
idx: int,
|
||||||
|
) -> T:
|
||||||
|
return self._contents[idx]
|
||||||
|
|
||||||
|
def __raise_for_exceeded_capacity(self) -> Never:
|
||||||
|
raise ValueError("Item can not be added due to capacity limit.")
|
||||||
|
|
||||||
|
def _capacity_sufficient(
|
||||||
|
self,
|
||||||
|
size_to_add: int,
|
||||||
|
) -> bool:
|
||||||
|
if self.full:
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_size = len(self) + size_to_add
|
||||||
|
|
||||||
|
if self.overfill and (new_size > self.capacity):
|
||||||
|
return True
|
||||||
|
elif new_size > self.capacity:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _recalc_capacity(
|
||||||
|
self,
|
||||||
|
size_to_add: int,
|
||||||
|
) -> None:
|
||||||
|
self.capacity_held += size_to_add
|
||||||
|
self.capacity_left = max(self.capacity_left - size_to_add, 0)
|
||||||
|
if self.capacity_left == 0:
|
||||||
|
self.full = True
|
||||||
|
else:
|
||||||
|
self.full = False
|
||||||
|
|
||||||
|
def append(
|
||||||
|
self,
|
||||||
|
object_: T,
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = 1
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not hasattr(object_, self.cap_property):
|
||||||
|
raise AttributeError("Object does not posses the wanted property")
|
||||||
|
attr = getattr(object_, self.cap_property)
|
||||||
|
if not isinstance(attr, int):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = attr
|
||||||
|
|
||||||
|
if self._capacity_sufficient(size_to_add):
|
||||||
|
self._contents.append(object_)
|
||||||
|
self._recalc_capacity(size_to_add)
|
||||||
|
else:
|
||||||
|
self.__raise_for_exceeded_capacity()
|
||||||
|
|
||||||
|
def extend(
|
||||||
|
self,
|
||||||
|
collection: Collection[T],
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = len(collection)
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not all(hasattr(object_, self.cap_property) for object_ in collection):
|
||||||
|
raise AttributeError("Not all objects posses the wanted property")
|
||||||
|
if not all(
|
||||||
|
isinstance(getattr(object_, self.cap_property), int) for object_ in collection
|
||||||
|
):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = sum(getattr(object_, self.cap_property) for object_ in collection)
|
||||||
|
|
||||||
|
if self._capacity_sufficient(size_to_add):
|
||||||
|
self._contents.extend(collection)
|
||||||
|
self._recalc_capacity(size_to_add)
|
||||||
|
else:
|
||||||
|
self.__raise_for_exceeded_capacity()
|
||||||
|
|
||||||
|
def remove(
|
||||||
|
self,
|
||||||
|
object_: T,
|
||||||
|
) -> None:
|
||||||
|
size_to_add: int = 1
|
||||||
|
if self.cap_property is not None:
|
||||||
|
if not hasattr(object_, self.cap_property):
|
||||||
|
raise AttributeError("Object does not posses the wanted property")
|
||||||
|
attr = getattr(object_, self.cap_property)
|
||||||
|
if not isinstance(attr, int):
|
||||||
|
raise TypeError("Capacity property must be an integer")
|
||||||
|
size_to_add = attr
|
||||||
|
size_to_remove = (-1) * size_to_add
|
||||||
|
|
||||||
|
self._contents.remove(object_)
|
||||||
|
self._recalc_capacity(size_to_remove)
|
||||||
108
src/dopt_basics/cli.py
Normal file
108
src/dopt_basics/cli.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import typing as t
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import wraps
|
||||||
|
from itertools import cycle
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
P = t.ParamSpec("P")
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
# based on: https://stackoverflow.com/questions/22029562/python-how-to-make-simple-animated-loading-while-process-is-running
|
||||||
|
class LoadingAnimation:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
loading_txt: str,
|
||||||
|
ending_text: str,
|
||||||
|
timeout: float = 0.1,
|
||||||
|
force_ascii: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.loading_txt = loading_txt
|
||||||
|
self.ending_text = ending_text
|
||||||
|
self.timeout = timeout
|
||||||
|
self.done: bool = False
|
||||||
|
self.keyboard_interrupt: bool = False
|
||||||
|
|
||||||
|
self._isatty = sys.stdout.isatty()
|
||||||
|
self._do_animation = bool(sys.stdout.isatty())
|
||||||
|
|
||||||
|
self._animate_dots: list[str] = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
|
||||||
|
self._animate_ascii: list[str] = ["|", "/", "-", "\\"]
|
||||||
|
|
||||||
|
self.frames: list[str]
|
||||||
|
enc = (sys.stdout.encoding or "utf-8").lower()
|
||||||
|
if force_ascii:
|
||||||
|
self.frames = self._animate_ascii
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self._animate_dots[0].encode(enc)
|
||||||
|
self.frames = self._animate_dots
|
||||||
|
except Exception:
|
||||||
|
self.frames = self._animate_ascii
|
||||||
|
|
||||||
|
self._thread: Thread = Thread(target=self._animation)
|
||||||
|
|
||||||
|
def __enter__(self) -> t.Self:
|
||||||
|
self.start()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[Exception],
|
||||||
|
exc_value,
|
||||||
|
tb,
|
||||||
|
) -> None:
|
||||||
|
if exc_type is not None:
|
||||||
|
self.stop(interrupt=True)
|
||||||
|
if exc_type is KeyboardInterrupt:
|
||||||
|
self.keyboard_interrupt = True
|
||||||
|
print("Operation cancelled by user. (KeyboardInterrupt)", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def _animation(self) -> None:
|
||||||
|
for frame in cycle(self.frames):
|
||||||
|
if self.done:
|
||||||
|
break
|
||||||
|
print(f"\r{frame} {self.loading_txt}", flush=True, end="")
|
||||||
|
time.sleep(self.timeout)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
if self._do_animation:
|
||||||
|
self._thread.start()
|
||||||
|
else:
|
||||||
|
print(f"\r{self.loading_txt}", end="", flush=True)
|
||||||
|
|
||||||
|
def stop(
|
||||||
|
self,
|
||||||
|
interrupt: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if self.done:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.done = True
|
||||||
|
if self._do_animation:
|
||||||
|
self._thread.join()
|
||||||
|
cols = shutil.get_terminal_size((80, 20)).columns
|
||||||
|
print("\r" + " " * cols, end="\r", flush=True)
|
||||||
|
if interrupt:
|
||||||
|
return
|
||||||
|
print(f"{self.ending_text}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def default_loading(func: Callable[P, T]) -> Callable[P, T]:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||||
|
loading_txt: str = "Performing operation..."
|
||||||
|
ending_text: str = "Operation successful"
|
||||||
|
|
||||||
|
with LoadingAnimation(loading_txt, ending_text):
|
||||||
|
res = func(*args, **kwargs)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
return wrapper
|
||||||
19
src/dopt_basics/iteration.py
Normal file
19
src/dopt_basics/iteration.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from collections.abc import Iterator, Sequence
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def symmetric_iter(
|
||||||
|
x: Sequence[T],
|
||||||
|
) -> Iterator[T]:
|
||||||
|
d = deque(x)
|
||||||
|
|
||||||
|
for idx in range(len(d)):
|
||||||
|
if idx % 2 == 0:
|
||||||
|
yield d.popleft()
|
||||||
|
else:
|
||||||
|
yield d.pop()
|
||||||
76
src/dopt_basics/logging.py
Normal file
76
src/dopt_basics/logging.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses as dc
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
from pathlib import Path
|
||||||
|
from time import gmtime
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
# ** config
|
||||||
|
LOG_FILENAME: Final[str] = "dopt.log"
|
||||||
|
BASE_NAME: Final[str] = "dopt_base"
|
||||||
|
|
||||||
|
LOG_FMT: Final[str] = "%(asctime)s | lang_main:%(module)s:%(levelname)s | %(message)s"
|
||||||
|
LOG_DATE_FMT: Final[str] = "%Y-%m-%d %H:%M:%S +0000"
|
||||||
|
DEFAULT_LOGLEVEL_STDERR: Final[int] = logging.DEBUG
|
||||||
|
DEFAULT_LOGLEVEL_FILE: Final[int] = logging.DEBUG
|
||||||
|
# ** handlers
|
||||||
|
NULL_HANDLER = logging.NullHandler()
|
||||||
|
|
||||||
|
# ** loggers and configuration
|
||||||
|
BASE_LOGGER = logging.getLogger("dopt_base")
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass(eq=False, slots=True, kw_only=True)
|
||||||
|
class LoggingConfig:
|
||||||
|
enable_stderr: bool
|
||||||
|
enable_file: bool = False
|
||||||
|
logging_dir: Path | None = None
|
||||||
|
log_filename: str = LOG_FILENAME
|
||||||
|
file_max_bytes: int = 5_242_880
|
||||||
|
file_backup_count: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(
|
||||||
|
config: LoggingConfig,
|
||||||
|
) -> None:
|
||||||
|
# ** formatters
|
||||||
|
logging.Formatter.converter = gmtime
|
||||||
|
LOGGER_ALL_FORMATER = logging.Formatter(fmt=LOG_FMT, datefmt=LOG_DATE_FMT)
|
||||||
|
# ** handlers
|
||||||
|
if config.enable_stderr:
|
||||||
|
logger_all_handler_stderr = logging.StreamHandler()
|
||||||
|
logger_all_handler_stderr.setLevel(DEFAULT_LOGLEVEL_STDERR)
|
||||||
|
logger_all_handler_stderr.setFormatter(LOGGER_ALL_FORMATER)
|
||||||
|
else: # pragma: no cover
|
||||||
|
logger_all_handler_stderr = NULL_HANDLER
|
||||||
|
|
||||||
|
if config.enable_file and config.logging_dir is None:
|
||||||
|
raise ValueError("Logging path must be provided to write to log file")
|
||||||
|
elif config.enable_file:
|
||||||
|
if config.logging_dir is None:
|
||||||
|
raise ValueError("Logging directory must be provided if file writing is selected")
|
||||||
|
log_file_path = config.logging_dir / config.log_filename
|
||||||
|
logger_all_handler_file = logging.handlers.RotatingFileHandler(
|
||||||
|
log_file_path,
|
||||||
|
encoding="utf-8",
|
||||||
|
maxBytes=config.file_max_bytes,
|
||||||
|
backupCount=config.file_backup_count,
|
||||||
|
delay=True,
|
||||||
|
)
|
||||||
|
logger_all_handler_file.setLevel(DEFAULT_LOGLEVEL_FILE)
|
||||||
|
logger_all_handler_file.setFormatter(LOGGER_ALL_FORMATER)
|
||||||
|
else: # pragma: no cover
|
||||||
|
logger_all_handler_file = NULL_HANDLER
|
||||||
|
|
||||||
|
BASE_LOGGER.addHandler(logger_all_handler_stderr)
|
||||||
|
BASE_LOGGER.addHandler(logger_all_handler_file)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_logging() -> None:
|
||||||
|
handlers = tuple(BASE_LOGGER.handlers)
|
||||||
|
for handler in handlers:
|
||||||
|
BASE_LOGGER.removeHandler(handler)
|
||||||
|
|
||||||
|
BASE_LOGGER.addHandler(NULL_HANDLER)
|
||||||
201
src/dopt_basics/result_pattern.py
Normal file
201
src/dopt_basics/result_pattern.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses as dc
|
||||||
|
import inspect
|
||||||
|
import typing as t
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import wraps
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
|
P = t.ParamSpec("P")
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
# ** Exceptions for result wrappers
|
||||||
|
class WrapperAccessResultDespiteError(Exception):
|
||||||
|
"""wrapped results exception: raised if result is accessed, even though
|
||||||
|
there was an error in the underlying procedure"""
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass(kw_only=True, slots=True)
|
||||||
|
class Status:
|
||||||
|
code: int
|
||||||
|
description: str
|
||||||
|
message: str
|
||||||
|
ExceptionType: type[Exception] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusHandler:
|
||||||
|
__slots__ = ("logger", "_SUCCESS")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logger: Logger | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self._SUCCESS: t.Final[Status] = Status(
|
||||||
|
code=0, description="SUCCESS", message="operation executed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SUCCESS(self) -> Status:
|
||||||
|
return self._SUCCESS
|
||||||
|
|
||||||
|
def error_to_exception(
|
||||||
|
self,
|
||||||
|
error_state: Status,
|
||||||
|
) -> Exception:
|
||||||
|
if error_state.ExceptionType is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot construct exception from state where no exception is defined"
|
||||||
|
)
|
||||||
|
return error_state.ExceptionType(error_state.message)
|
||||||
|
|
||||||
|
def exception_to_error(
|
||||||
|
self,
|
||||||
|
exception: Exception,
|
||||||
|
code: int,
|
||||||
|
) -> Status:
|
||||||
|
doc_string = inspect.getdoc(exception)
|
||||||
|
description = doc_string if doc_string is not None else ""
|
||||||
|
message = str(exception)
|
||||||
|
return self.error_state(
|
||||||
|
code,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
exception=exception,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error_state(
|
||||||
|
code: int,
|
||||||
|
description: str,
|
||||||
|
message: str,
|
||||||
|
exception: Exception | None,
|
||||||
|
) -> Status:
|
||||||
|
if code == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Custom error codes must not be zero since this value "
|
||||||
|
"is reserved for successful operations"
|
||||||
|
)
|
||||||
|
elif code < 0:
|
||||||
|
raise ValueError("Custom error codes must not be smaller than zero")
|
||||||
|
|
||||||
|
exception_type: type[Exception] | None = None
|
||||||
|
if exception is not None:
|
||||||
|
exception_type = type(exception)
|
||||||
|
|
||||||
|
return Status(
|
||||||
|
code=code,
|
||||||
|
description=description,
|
||||||
|
message=message,
|
||||||
|
ExceptionType=exception_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def raise_for_status(
|
||||||
|
self,
|
||||||
|
state: Status,
|
||||||
|
) -> None:
|
||||||
|
if state == self.SUCCESS:
|
||||||
|
if self.logger is not None:
|
||||||
|
self.logger.info(
|
||||||
|
"[STATUS] Raise for status - SUCCESS. all good.", stack_info=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
exc = self.error_to_exception(error_state=state)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
class NotSet:
|
||||||
|
__slots__ = tuple()
|
||||||
|
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return ">Not set<"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}()"
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_HANDLER: t.Final[StatusHandler] = StatusHandler()
|
||||||
|
|
||||||
|
|
||||||
|
class ResultWrapper(t.Generic[T]):
|
||||||
|
__slots__ = ("_result", "status")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
result: T | NotSet,
|
||||||
|
exception: Exception | None,
|
||||||
|
code_on_error: int,
|
||||||
|
) -> None:
|
||||||
|
if isinstance(result, NotSet) and exception is None:
|
||||||
|
raise ValueError("[ResultWrapper] Set >NotSet< without exception")
|
||||||
|
elif not isinstance(result, NotSet) and exception is not None:
|
||||||
|
raise ValueError("[ResultWrapper] Set result with exception")
|
||||||
|
|
||||||
|
self._result = result
|
||||||
|
status: Status = STATUS_HANDLER.SUCCESS
|
||||||
|
if exception is not None:
|
||||||
|
status = STATUS_HANDLER.exception_to_error(exception, code=code_on_error)
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self) -> T:
|
||||||
|
if isinstance(self._result, NotSet):
|
||||||
|
raise WrapperAccessResultDespiteError(
|
||||||
|
"Can not access result because it is not set"
|
||||||
|
)
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Status(result: {self._result}, status: {self.status})"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def unwrap(self) -> T:
|
||||||
|
STATUS_HANDLER.raise_for_status(self.status)
|
||||||
|
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_result(
|
||||||
|
code_on_error: int,
|
||||||
|
logger: Logger | None = None,
|
||||||
|
) -> Callable[[Callable[P, T]], Callable[P, ResultWrapper[T]]]:
|
||||||
|
def wrap_result(func: Callable[P, T]) -> Callable[P, ResultWrapper[T]]:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultWrapper[T]:
|
||||||
|
wrapped_result: ResultWrapper[T]
|
||||||
|
try:
|
||||||
|
res = func(*args, **kwargs)
|
||||||
|
wrapped_result = ResultWrapper(
|
||||||
|
result=res, exception=None, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
wrapped_result = ResultWrapper(
|
||||||
|
result=NotSet(), exception=err, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
if logger is not None:
|
||||||
|
logger.info(
|
||||||
|
(
|
||||||
|
"[RESULT-WRAPPER] An exception in routine %s occurred - msg: %s, "
|
||||||
|
"stack trace:"
|
||||||
|
),
|
||||||
|
func.__name__,
|
||||||
|
str(err),
|
||||||
|
stack_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapped_result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrap_result
|
||||||
38
src/dopt_basics/system.py
Normal file
38
src/dopt_basics/system.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from dataclasses import fields, is_dataclass
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
|
||||||
|
def obj_size(
|
||||||
|
obj: Any,
|
||||||
|
seen: set[int] | None = None,
|
||||||
|
unit: Literal["b", "kb", "mb", "gb"] = "b",
|
||||||
|
) -> float:
|
||||||
|
size = sys.getsizeof(obj)
|
||||||
|
if seen is None:
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
obj_id = id(obj)
|
||||||
|
if obj_id in seen:
|
||||||
|
return 0
|
||||||
|
seen.add(obj_id)
|
||||||
|
|
||||||
|
if is_dataclass(obj):
|
||||||
|
for f in fields(obj):
|
||||||
|
size += obj_size(getattr(obj, f.name), seen)
|
||||||
|
elif isinstance(obj, dict):
|
||||||
|
size += sum(obj_size(v, seen) + obj_size(k, seen) for k, v in obj.items())
|
||||||
|
elif isinstance(obj, (list, tuple, set)):
|
||||||
|
size += sum(obj_size(i, seen) for i in obj)
|
||||||
|
|
||||||
|
match unit:
|
||||||
|
case "kb":
|
||||||
|
size /= 1024
|
||||||
|
case "mb":
|
||||||
|
size /= 1024 * 1024
|
||||||
|
case "gb":
|
||||||
|
size /= 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
return size
|
||||||
142
tests/test_batching.py
Normal file
142
tests/test_batching.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_basics.batching import Chunk
|
||||||
|
|
||||||
|
|
||||||
|
class _Item:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
size: int,
|
||||||
|
) -> None:
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base():
|
||||||
|
chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
assert chunk.capacity == 3
|
||||||
|
assert chunk.capacity_held == 0
|
||||||
|
assert chunk.capacity_left == 3
|
||||||
|
assert chunk.full is False
|
||||||
|
assert len(chunk) == 0
|
||||||
|
assert not chunk.contents
|
||||||
|
|
||||||
|
|
||||||
|
# TODO implement behaviour
|
||||||
|
# def test_Chunk_base_overfill_append():
|
||||||
|
# chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
# chunk.append(1)
|
||||||
|
# chunk.append(2)
|
||||||
|
# chunk.append(3)
|
||||||
|
# chunk.append(4) # allow once, then not again
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_no_overfill_append():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.append(1)
|
||||||
|
assert chunk[0] == 1
|
||||||
|
chunk.append(2)
|
||||||
|
chunk.append(3)
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.append(4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_overfill_extend():
|
||||||
|
chunk: Chunk[int] = Chunk(3)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
chunk.extend((3, 4))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.extend((5, 6))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_no_overfill_extend():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.extend((3, 4))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_base_remove():
|
||||||
|
chunk: Chunk[int] = Chunk(3, overfill_once=False)
|
||||||
|
|
||||||
|
chunk.extend((1, 2))
|
||||||
|
assert chunk.capacity_held == 2
|
||||||
|
assert chunk.capacity_left == 1
|
||||||
|
|
||||||
|
chunk.remove(2)
|
||||||
|
assert chunk.capacity_held == 1
|
||||||
|
assert chunk.capacity_left == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_success():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk[_Item] = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
i2 = _Item(4)
|
||||||
|
|
||||||
|
chunk.append(i1)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Item can not be added due to capacity limit"):
|
||||||
|
chunk.append(i2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Object does not posses the wanted property"):
|
||||||
|
chunk.append(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_append_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(4)
|
||||||
|
i1.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.append(i1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_extend_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Not all objects posses the wanted property"):
|
||||||
|
chunk.extend((i1, 2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_extend_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(2)
|
||||||
|
i2 = _Item(4)
|
||||||
|
i2.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.extend((i1, i2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_remove_fail_no_attribute():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError, match="Object does not posses the wanted property"):
|
||||||
|
chunk.remove(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_Chunk_property_remove_fail_wrong_type():
|
||||||
|
CAP_PROP = "size"
|
||||||
|
chunk: Chunk = Chunk(3, overfill_once=False, cap_property=CAP_PROP)
|
||||||
|
i1 = _Item(4)
|
||||||
|
i1.size = "wrong type" # type: ignore
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Capacity property must be an integer"):
|
||||||
|
chunk.remove(i1)
|
||||||
25
tests/test_iteration.py
Normal file
25
tests/test_iteration.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from dopt_basics import iteration
|
||||||
|
|
||||||
|
|
||||||
|
def test_symmetric_iter_1():
|
||||||
|
l1_in = [1, 2, 3, 4, 5, 6]
|
||||||
|
l1_out = [1, 6, 2, 5, 3, 4]
|
||||||
|
|
||||||
|
l1_calc = list(iteration.symmetric_iter(l1_in))
|
||||||
|
assert len(l1_in) == len(l1_calc)
|
||||||
|
assert len(l1_out) == len(l1_calc)
|
||||||
|
|
||||||
|
for truth, calc in zip(l1_out, l1_calc):
|
||||||
|
assert truth == calc
|
||||||
|
|
||||||
|
|
||||||
|
def test_symmetric_iter_2():
|
||||||
|
l1_in = [1, 2, 3, 4, 5]
|
||||||
|
l1_out = [1, 5, 2, 4, 3]
|
||||||
|
|
||||||
|
l1_calc = list(iteration.symmetric_iter(l1_in))
|
||||||
|
assert len(l1_in) == len(l1_calc)
|
||||||
|
assert len(l1_out) == len(l1_calc)
|
||||||
|
|
||||||
|
for truth, calc in zip(l1_out, l1_calc):
|
||||||
|
assert truth == calc
|
||||||
268
tests/test_result_pattern.py
Normal file
268
tests/test_result_pattern.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_basics import result_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def status_hdlr() -> result_pattern.StatusHandler:
|
||||||
|
return result_pattern.StatusHandler()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def dummy_logger() -> logging.Logger:
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
handlers = tuple(logger.handlers)
|
||||||
|
for handler in handlers:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
logger.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_NotSet():
|
||||||
|
not_set = result_pattern.NotSet()
|
||||||
|
# using slots, dynamic attribute generation should not be possible
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
not_set.test = "try to set value" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_properties(status_hdlr, dummy_logger):
|
||||||
|
assert status_hdlr.logger is None
|
||||||
|
assert status_hdlr._SUCCESS is not None
|
||||||
|
assert status_hdlr.SUCCESS is not None
|
||||||
|
assert isinstance(status_hdlr.SUCCESS, result_pattern.Status)
|
||||||
|
assert status_hdlr.SUCCESS.code == 0
|
||||||
|
assert status_hdlr.SUCCESS.description == "SUCCESS"
|
||||||
|
status_hdlr.logger = dummy_logger
|
||||||
|
assert status_hdlr.logger is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_FailCodeZero(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 0
|
||||||
|
with pytest.raises(ValueError, match="must not be zero since this value"):
|
||||||
|
status_hdlr.error_state(code, description, message, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_FailCodeNegative(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = -100
|
||||||
|
with pytest.raises(ValueError, match="must not be smaller than zero"):
|
||||||
|
status_hdlr.error_state(code, description, message, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_SuccessWithoutException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, None)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_state_generation_SuccessWithException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, exception)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_to_exception_FailWithoutException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, None)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is None
|
||||||
|
with pytest.raises(ValueError, match="state where no exception is defined"):
|
||||||
|
status_hdlr.error_to_exception(err_state)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_error_to_exception_SuccessWithException(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = status_hdlr.error_state(code, description, message, exception)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is ValueError
|
||||||
|
exc = status_hdlr.error_to_exception(err_state)
|
||||||
|
assert isinstance(exc, ValueError)
|
||||||
|
assert str(exc) == message
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_exception_to_error_Success(status_hdlr):
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
|
||||||
|
class TestException(Exception):
|
||||||
|
"""test"""
|
||||||
|
|
||||||
|
exc = TestException(message)
|
||||||
|
err_state = status_hdlr.exception_to_error(exc, code)
|
||||||
|
assert err_state.code == code
|
||||||
|
assert err_state.description == description
|
||||||
|
assert err_state.message == message
|
||||||
|
assert err_state.ExceptionType is TestException
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_raise_for_status_Success(dummy_logger):
|
||||||
|
status_hdlr = result_pattern.StatusHandler(dummy_logger)
|
||||||
|
state = status_hdlr.SUCCESS
|
||||||
|
|
||||||
|
assert status_hdlr.raise_for_status(state) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_raise_for_status_RaiseException(dummy_logger):
|
||||||
|
status_hdlr = result_pattern.StatusHandler(dummy_logger)
|
||||||
|
|
||||||
|
description: str = "test"
|
||||||
|
message: str = "this is a test case"
|
||||||
|
code: int = 100
|
||||||
|
exception: Exception = ValueError(message)
|
||||||
|
err_state = result_pattern.Status(
|
||||||
|
code=code, description=description, message=message, ExceptionType=type(exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(type(exception), match=message):
|
||||||
|
status_hdlr.raise_for_status(err_state)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailInitNotSetNoException():
|
||||||
|
error_code = 146
|
||||||
|
test_result = result_pattern.NotSet()
|
||||||
|
exception = None
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Set >NotSet< without exception"):
|
||||||
|
_: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailInitSetResultWithException():
|
||||||
|
error_code = 146
|
||||||
|
test_result = 10
|
||||||
|
message = "Test of error message"
|
||||||
|
exception = ValueError(message)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Set result with exception"):
|
||||||
|
_: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_Success(status_hdlr):
|
||||||
|
# successful operation
|
||||||
|
test_result = 10
|
||||||
|
error_code = 146
|
||||||
|
twrapper: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=None,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status == status_hdlr.SUCCESS
|
||||||
|
assert twrapper.result == test_result
|
||||||
|
assert twrapper.unwrap() == test_result
|
||||||
|
assert twrapper.status.code != error_code
|
||||||
|
assert twrapper.status.code == status_hdlr.SUCCESS.code
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class_FailureAccess(status_hdlr):
|
||||||
|
# test for no result
|
||||||
|
error_code = 146
|
||||||
|
test_result = result_pattern.NotSet()
|
||||||
|
message = "Test of error message"
|
||||||
|
exception = ValueError(message)
|
||||||
|
twrapper: result_pattern.ResultWrapper[int] = result_pattern.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status != status_hdlr.SUCCESS
|
||||||
|
assert twrapper.status.code == error_code
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
twrapper.result
|
||||||
|
with pytest.raises(type(exception), match=message):
|
||||||
|
_ = twrapper.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_ExceptionRaised():
|
||||||
|
MESSAGE = "Test case wrapped function decorator"
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_1() -> None:
|
||||||
|
raise ValueError(MESSAGE)
|
||||||
|
|
||||||
|
res = test_func_1()
|
||||||
|
assert isinstance(res, result_pattern.ResultWrapper)
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.message == MESSAGE
|
||||||
|
assert res.status.ExceptionType is ValueError
|
||||||
|
with pytest.raises(ValueError, match=MESSAGE):
|
||||||
|
res.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_Success(status_hdlr):
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "1")
|
||||||
|
assert res.result == 2
|
||||||
|
assert res.unwrap() == 2
|
||||||
|
assert res.status == status_hdlr.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_FailureZeroDivisionError():
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "0")
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
res.result
|
||||||
|
with pytest.raises(ZeroDivisionError):
|
||||||
|
res.unwrap()
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.ExceptionType is ZeroDivisionError
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_result_NotNone_FailureValueErrorWithLogger(dummy_logger):
|
||||||
|
error_code = 103
|
||||||
|
|
||||||
|
@result_pattern.wrap_result(error_code, logger=dummy_logger)
|
||||||
|
def test_func_2(x: str, y: str) -> int:
|
||||||
|
return int(int(x) / int(y))
|
||||||
|
|
||||||
|
res = test_func_2("2", "test")
|
||||||
|
with pytest.raises(result_pattern.WrapperAccessResultDespiteError):
|
||||||
|
res.result
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
res.unwrap()
|
||||||
|
assert res.status.code == error_code
|
||||||
|
assert res.status.ExceptionType is ValueError
|
||||||
56
tests/test_system.py
Normal file
56
tests/test_system.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import dataclasses as dc
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_basics import system
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass()
|
||||||
|
class _TData:
|
||||||
|
dic: dict[str, str]
|
||||||
|
dic2: dict[str, dict[str, int]]
|
||||||
|
lst: list[int]
|
||||||
|
tup: tuple[str, ...]
|
||||||
|
string: str
|
||||||
|
id_: int
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def TData() -> _TData:
|
||||||
|
return _TData(
|
||||||
|
{"test": "test", "test2": "test"},
|
||||||
|
{"test": {"prop1": 3, "prop2": 500}},
|
||||||
|
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||||
|
("test", "test", "test", "test", "test", "test", "test"),
|
||||||
|
"This is one test string",
|
||||||
|
1234,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def TData_real_size(TData) -> int:
|
||||||
|
return 1435
|
||||||
|
|
||||||
|
|
||||||
|
def test_obj_size(TData, TData_real_size):
|
||||||
|
ret = system.obj_size(TData)
|
||||||
|
|
||||||
|
assert ret == TData_real_size
|
||||||
|
|
||||||
|
|
||||||
|
def test_obj_size_kb(TData, TData_real_size):
|
||||||
|
ret = system.obj_size(TData, unit="kb")
|
||||||
|
|
||||||
|
assert ret == pytest.approx(TData_real_size / 1024)
|
||||||
|
|
||||||
|
|
||||||
|
def test_obj_size_mb(TData, TData_real_size):
|
||||||
|
ret = system.obj_size(TData, unit="mb")
|
||||||
|
|
||||||
|
assert ret == pytest.approx(TData_real_size / 1024**2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_obj_size_gb(TData, TData_real_size):
|
||||||
|
ret = system.obj_size(TData, unit="gb")
|
||||||
|
|
||||||
|
assert ret == pytest.approx(TData_real_size / 1024**3)
|
||||||
Reference in New Issue
Block a user