Skip to content

Commit

Permalink
Merge branch 'api_cleanup' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasjuhrich committed Sep 29, 2023
2 parents 69caa85 + 917e57e commit bd8ed85
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 401 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/sipa-ci .yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- uses: pre-commit/[email protected]
build:
runs-on: ubuntu-latest
Expand All @@ -21,7 +23,7 @@ jobs:
- name: Build the Docker image
run: docker build -t sipa:latest .
- name: Login to GitLab Registry
uses: docker/login-action@v1
uses: docker/login-action@v1
with:
registry: registry.agdsn.de
username: "github-actions"
Expand All @@ -39,7 +41,7 @@ jobs:
submodules: recursive
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.11'
cache: 'pip'
- name: print information about pip cache
run: pip cache info && pip cache list
Expand Down
245 changes: 66 additions & 179 deletions sipa/model/fancy_property.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,61 @@
from collections import namedtuple
import typing as t
from dataclasses import dataclass
from functools import wraps

from flask_babel import gettext
from abc import ABCMeta, abstractmethod
from abc import ABC, abstractmethod
from sipa.utils import argstr


Capabilities = namedtuple('capabilities', ['edit', 'delete'])
NO_CAPABILITIES = Capabilities(edit=False, delete=False)
class Capabilities(t.NamedTuple):
edit: bool
delete: bool

@classmethod
def edit_if(cls, condition: bool) -> t.Self:
return cls(edit=condition, delete=False)

class PropertyBase(metaclass=ABCMeta):
def __init__(self, name, value, raw_value, capabilities=NO_CAPABILITIES,
style=None, empty=False, description_url=None):
self.name = name
self.value = value
self.raw_value = raw_value
self.capabilities = capabilities
self.style = style
self.empty = empty or not value
self.description_url = description_url
@classmethod
def edit_delete_if(cls, condition: bool) -> t.Self:
return cls(edit=condition, delete=condition)

def __repr__(self):
return "{}.{}({})".format(__name__, type(self).__name__, argstr(
name=self.name,
value=self.value,
raw_value=self.raw_value,
capabilities=self.capabilities,
style=self.style,
empty=self.empty,
description_url=self.description_url
))

NO_CAPABILITIES = Capabilities(edit=False, delete=False)

TVal = t.TypeVar("TVal")
TRawVal = t.TypeVar("TRawVal")


STYLE = t.Literal[
"muted",
"primary",
"success",
"info",
"warning",
"danger",
"password",
]


@dataclass
class PropertyBase(ABC, t.Generic[TVal, TRawVal]):
name: str
value: TVal
raw_value: TRawVal = None
capabilities: Capabilities = NO_CAPABILITIES
style: STYLE | None = None
# TODO actually is not None due to post_init. More elegantly solved with
# separate `InitVar`
empty: bool | None = None
description_url: str | None = None

def __post_init__(self):
if self.empty is None:
self.empty = not bool(self.value)

@property
@abstractmethod
def supported(self):
def supported(self) -> bool:
pass

def __eq__(self, other):
Expand All @@ -60,7 +81,7 @@ def __bool__(self):
return not self.empty


class UnsupportedProperty(PropertyBase):
class UnsupportedProperty(PropertyBase[str, None]):
supported = False

def __init__(self, name):
Expand All @@ -84,28 +105,18 @@ def __eq__(self, other):
return False


class ActiveProperty(PropertyBase):
class ActiveProperty(PropertyBase[TVal, TRawVal]):
supported = True

def __init__(self, name, value=None, raw_value=None, capabilities=NO_CAPABILITIES,
style=None, empty=False, description_url=None):

# Enforce css classes
assert style in {None, 'muted', 'primary', 'success',
'info', 'warning', 'danger', 'password'}, \
"Style must be a valid text-class string"

super().__init__(
name=name,
value=(value if value else gettext("Nicht angegeben")),
raw_value=raw_value if raw_value is not None else value,
capabilities=capabilities,
style=(style if style # customly given style is most important
else 'muted' if empty or not value
else None),
empty=empty or not value,
description_url=description_url,
)
def __post_init__(self):
if self.empty is None:
self.empty = not bool(self.value)
if self.raw_value is None:
self.raw_value = self.value
if self.value is None:
self.value = gettext("Nicht angegeben")
if self.style is None:
self.style = "muted" if self.empty else None

def __repr__(self):
return "<{cls} {name}='{value}' [{empty}]>".format(
Expand All @@ -116,144 +127,20 @@ def __repr__(self):
)


def unsupported_prop(func):
return property(lambda self: UnsupportedProperty(name=func.__name__))


class active_prop(property):
"""A property-like class wrapping the getter with
:py:class:`ActiveProperty`
:py:class:`active_prop` automatically adds `edit` and `delete`
capabilities to the ActiveProperty object if `setter`/`deleter` is
invoked.
Example usage:
>>> class User:
... @active_prop
... def foo(self):
... return {'value': "Empty!!", 'empty': True, 'style': 'danger'}
...
... @active_prop
... def bar(self):
... return 0
...
... @bar.setter
... def bar(self):
... print("furiously setting things")
>>> User().foo
<ActiveProperty foo='Empty!!' [empty]>
>>> User().bar
<ActiveProperty bar='Nicht angegeben' [empty]>
>>> User().bar.capabilities
capabilities(edit=True, delete=False)
"""

def __init__(self, fget, fset=None, fdel=None, doc=None,
fake_setter=False):
"""Return a property object and wrap fget with `ActiveProperty`.
The first argument is the function given to `active_prop`
(perhaps as a decorator).
`fget` is always wrapped by `ActiveProperty`in a way depending
of the return value of `fget`. If it returns
- Something subscriptable to `'value'`: Pass `['value']` and
`['style']` (defaults to `None`) to `ActiveProperty`s
`__init__`, respectively.
- Something else: Pass it as `value` to `ActiveProperty`s
`__init__`.
"""
self.__raw_getter = fget
self.__fake_setter = fake_setter # only for the __repr__

@wraps(fget)
def wrapped_getter(*args, **kwargs):
result = fget(*args, **kwargs)
try:
value = result['value']
except TypeError:
# `KeyError` should not happen, since that means a
# dict would have been returned not including `value`,
# which would make no sense and likely is a mistake.
name = fget.__name__
value = result
raw_value = value
style = None
description_url = None
empty = None
tmp_readonly = False
else:
name = result.get('name', fget.__name__)
raw_value = result.get('raw_value', value)
style = result.get('style', None)
description_url = result.get('description_url', None)
empty = result.get('empty', None)
tmp_readonly = result.get('tmp_readonly', False)

return ActiveProperty(
name=name,
value=value,
raw_value=raw_value,
capabilities=Capabilities(
edit=(fset is not None or fake_setter),
delete=(fdel is not None),
) if not tmp_readonly else NO_CAPABILITIES,
style=style,
description_url=description_url,
empty=empty,
)

# Let `property` handle the initialization of `__get__`, `__set__` etc.
super().__init__(wrapped_getter, fset, fdel, doc)

def __repr__(self):
return "{}.{}({})".format(__name__, type(self).__name__, argstr(
fget=self.__raw_getter,
fset=self.fset,
fdel=self.fdel,
doc=self.__doc__,
fake_setter=self.__fake_setter,
))

def getter(self, func):
return type(self)(func, self.fset, self.fdel, self.__doc__)

def setter(self, func):
return type(self)(self.__raw_getter, func, self.fdel, self.__doc__)

def deleter(self, func):
return type(self)(self.__raw_getter, self.fset, func, self.__doc__)

def fake_setter(self):
return type(self)(self.__raw_getter, self.fset, self.fdel,
self.__doc__,
fake_setter=True)


def connection_dependent(func):
"""A decorator to “deactivate” the property if the user's not active.
"""
"""A decorator to “deactivate” the property if the user's not active."""

def _connection_dependent(self, *args, **kwargs):
@wraps(func)
def _connection_dependent(self, *args, **kwargs) -> ActiveProperty:
if not self.has_connection:
return {
'name': func.__name__,
'value': gettext("Nicht verfügbar"),
'empty': True,
'tmp_readonly': True,
}
return ActiveProperty(
name=func.__name__,
value=gettext("Nicht verfügbar"),
empty=True,
capabilities=NO_CAPABILITIES,
)

ret = func(self, *args, **kwargs)
try:
ret.update({'name': func.__name__})
except AttributeError:
ret = {'value': ret, 'name': func.__name__}

return ret

return _connection_dependent
2 changes: 1 addition & 1 deletion sipa/model/finance.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class BaseFinanceInformation(metaclass=ABCMeta):
"""

@property
def balance(self):
def balance(self) -> ActiveProperty[str, float]:
"""The current balance as a
:py:class:`~sipa.model.fancy_property.ActiveProperty`
Expand Down
Loading

0 comments on commit bd8ed85

Please sign in to comment.