Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 124 additions & 5 deletions src/miniflask/miniflask.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from .event import event, event_obj
from .state import state, as_is_callable, optional as optional_default, state_node
from .dummy import miniflask_dummy
from .util import getModulesAvail, EnumAction, get_relative_id
from .util import getModulesAvail, EnumAction, get_relative_id, Unit, UnitValue, make_unitvalue_argparse
from .util import (
highlight_error,
highlight_name,
Expand All @@ -41,7 +41,6 @@
get_varid_from_fuzzy,
get_full_base_module_name,
)

from .settings import listsettings


Expand Down Expand Up @@ -133,6 +132,7 @@ def __init__(self, *module_repositories, debug=False):
{}
) # saves the information of the respective variables (initial value, registration, etc.)
self._state_overwrites_list = []
self.units = {}
self.modules_loaded = {}
self.modules_ignored = []
self.modules_avail = getModulesAvail(self.module_repositories)
Expand Down Expand Up @@ -864,8 +864,8 @@ def register_defaults(
lambda state: somefunction(state["myvariable"]) + state["othervariable"]
lambda state: state["myvariable"] * 5 if "myvariable" in state else state["othervariable"]
lambda state: state["myvariable"] * state["othervariable"] if "myvariable" in state and "othervariable" in state else state["yetanothervariable"]
```

- [**Units**](../../08-API/02-miniflask-Instance/10-register_unit.md) (`Units`)
Units are custom numbers with multiple representations.

Note:
This method is the base method for variable registrations.
Expand Down Expand Up @@ -1038,7 +1038,9 @@ def _settings_parser_add(
kwarg["const"] = True

# define the actual arguments
if argtype in [int, str, float, str2bool, Enum]:
if argtype == UnitValue:
kwarg["type"] = make_unitvalue_argparse(val)
if argtype in [int, str, float, str2bool, Enum, UnitValue]:
self.settings_parser.add_argument("--" + varname, **kwarg)
else:
raise ValueError(
Expand All @@ -1060,9 +1062,126 @@ def _settings_parser_add(

return kwarg

def register_unit(self, unit_id, get_converter, set_converter, units):
r"""
Custom numbers with multiple representations.

Args:
- `unit_id`: Unique id to distinguish different units.
- `converter`: A method with the signature `(src_value: Number, src_unit: str, dest_unit: str)`
- `units`: A list of lists. Each list specifies synonymes for units.

Examples:

**First, we define `time` to be a unit with four different representations with getter/setter functions to convert between these.**:
(Note that you can save arbitrary data to `unitvalue.data` and define yourself how units behave).
```python


# define how to transform data when initializing a unit
def init_time_unit(data):
units = list(data.keys())
assert len(units) == 1, f"Unitvalue should contain exactly one data point, but found {units}."
unit = units[0]
value = list(data.values())[0]
data = {"computation_list": []}
if unit == "m":
return {"s": 60 * value, **data}
elif unit == "h":
return {"s": 60 * 60 * value, **data}
elif unit == "d":
return {"s": 24 * 60 * 60 * value, **data}
raise ValueError(f"Could not determine initial values from {repr(data)}.")


def get_time_unit(unitvalue, targetunit):

# we use seconds as base unit und convert any start to that value
if "s" not in unitvalue.data:
unitvalue.data = init_time_unit(unitvalue.data)

unitvalue.data["computation_list"].append(targetunit)

if targetunit == "s":
return unitvalue.data["s"]
if targetunit == "m":
return unitvalue.data["s"] / 60
elif targetunit == "h":
return unitvalue.data["s"] / 60 / 60
elif targetunit == "d":
return unitvalue.data["s"] / 60 / 60 / 24

raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}")


def set_time_unit(unitvalue, targetunit, newvalue):

# we use seconds as base unit und convert any start to that value
if "s" not in unitvalue.data:
unitvalue.data = init_time_unit(unitvalue.data)

unitvalue.data["computation_list"].append(targetunit)

if targetunit == "s":
unitvalue.data["s"] += newvalue
return
elif targetunit == "m":
unitvalue.data["s"] += 60 * newvalue
return
elif targetunit == "h":
unitvalue.data["s"] += 60 * 60 * newvalue
return
elif targetunit == "d":
unitvalue.data["s"] += 24 * 60 * 60 * newvalue
return

raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}")

time = mf.register_unit("time", get_time_unit, set_time_unit, [
["m", "minute", "minutes"],
["h", "hour", "hours"],
["s", "second", "seconds"],
["d", "day", "days"]
])
```

**In CLI, we can overwrite this unit using**:
- `--time 1.5d` (1.5 days)
- `--time 24h` (24 hour)
- `--time 500m` (500 minutes)

**Using `u in v` Notation, we can specify the base unit to convert all calculations to.**:
- `--time 12h in d` (0.5 days)
- `--time 1.5d in h` (18 hours)

**To register a unit, pass it to `register_defaults` as follows**:
```python
mf.register_defaults({
"time": time(6.0, "h"),
"time2": time(6.0, "h in d"), # in notation works here as well
})
```

**Querying other representations can be done easily, once defined:**
```python
state["time"].unit # the base unit name
state["time"].second
state["time"].minute
state["time"].hour
state["time"].day
```
""" # noqa: W291
if unit_id in self.units:
raise ValueError(f"Unit with name {unit_id} is already defined.")

u = Unit(unit_id, get_converter, set_converter, units)
self.units[unit_id] = u
return u

# ======= #
# runtime #
# ======= #

def stop_parse(self):
r"""
Stops loading of any new modules immediately and halts any further executions.
Expand Down
4 changes: 2 additions & 2 deletions src/miniflask/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from colored import attr, fg

from ..util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event
from ..util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event, UnitValue


html_module = lambda x: x # noqa: E731 no-lambda
Expand Down Expand Up @@ -76,7 +76,7 @@ def listsettings(mf, state, asciicodes=True):
k_hidden[-1] = color_module(k_hidden[-1])

is_lambda = mf.state_registrations[k_orig][-1].fn is not None
value_str = attr_fn('dim') + "λ ⟶ " + attr_fn('reset') + str(v_precli) if is_lambda else v_precli.str(asciicodes=False) if hasattr(v_precli, 'str') else str(v_precli)
value_str = attr_fn('dim') + "λ ⟶ " + attr_fn('reset') + str(v_precli) if is_lambda or isinstance(v_precli, UnitValue) else v_precli.str(asciicodes=False) if hasattr(v_precli, 'str') else str(v_precli)
append = "" if not overwritten else " ⟶ " + color_val_overwrite(str(v))
text_list.append("│".join(k_hidden) + (" " * (max_k_len - k_len)) + " = " + color_val(value_str) + append)

Expand Down
90 changes: 90 additions & 0 deletions src/miniflask/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from os import walk, path
from pathlib import Path
from pkgutil import resolve_name
from dataclasses import dataclass
from colored import attr, fg


Expand Down Expand Up @@ -168,3 +169,92 @@ def __call__(self, parser, namespace, values, option_string=None, return_enum=Fa
if return_enum:
return enum
setattr(namespace, self.dest, enum)


# ===== #
# Units #
# ===== #

# base unit class
# (used to save the unit endings and the converter functions)
class Unit: # pylint: disable=too-few-public-methods

def __init__(self, name, get_converter, set_converter, units):
self.name = name
self.get_converter = get_converter
self.set_converter = set_converter

# units is a list of lists
# - the first element of each list the canonical ending for each unit-form
# - using the unit-endings, we define a dict that always translates to the first/canonical unit-ending
# - in case a unit-ending has been used twice, throw an error
self.units = {}
for keys in units:
k_uid = keys[0]
for k in keys:
if k in self.units:
raise ValueError(f"All Unit-Synonymes must be unique, but {k} has been used already.")
self.units[k] = k_uid
Comment thread
da-h marked this conversation as resolved.

def __call__(self, value, unit, data=None):
if data is None:
data = {}
return UnitValue(self, {self.units[unit]: value, **data})


# unit value class
# (used to actually save the value)
@dataclass
class UnitValue:
_unitclass: Unit
data: dict

def __getattr__(self, name):
if name in self.data:
return self.data[name]
name = self._unitclass.units[name]
if name in self.data:
return self.data[name]
return self._unitclass.get_converter(self, self._unitclass.units[name])

def __setattr__(self, name, val):
if name in ["_unitclass", "data"]:
object.__setattr__(self, name, val)
elif name in self.data:
self.data[name] = val
else:
name = self._unitclass.units[name]
self._unitclass.set_converter(self, name, val)

def __str__(self):
return f"{', '.join(str(v)+str(k) for k,v in self.data.items())}"

def __repr__(self):
return "UnitValue(" + f"{', '.join(str(v)+str(k) for k,v in self.data.items())}" + ")"


# factory for objects
# (used by argparse to convert strings to unit values)
def make_unitvalue_argparse(unitvalue):
class UnitValueArgparse: # pylint: disable=too-few-public-methods
def __new__(cls, string):

# iterate over units, to check which format has been used for the given string
units = sorted(unitvalue._unitclass.units.keys(), key=len, reverse=True)
for u in units:
if string.endswith(u):

# convert string to number
number_str = string[:-len(u)]
try:
number = int(number_str)
except ValueError:
number = float(number_str)

# construct unit from argument
return UnitValue(unitvalue._unitclass, {unitvalue._unitclass.units[u]: number})

units_str = ",".join(units)
raise ValueError(f"I do not know how to convert {string} to a Unit. Possible endings are {units_str}.")

return UnitValueArgparse
Empty file.
Empty file.
73 changes: 73 additions & 0 deletions tests/state/units/modules/defineunits/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@


# define how to transform data when initializing a unit
def init_time_unit(data):
units = list(data.keys())
assert len(units) == 1, f"Unitvalue should contain exactly one data point, but found {units}."
unit = units[0]
value = list(data.values())[0]
data = {"computation_list": []}
if unit == "m":
return {"s": 60 * value, **data}
if unit == "h":
return {"s": 60 * 60 * value, **data}
if unit == "d":
return {"s": 24 * 60 * 60 * value, **data}
raise ValueError(f"Could not determine initial values from {repr(data)}.")


def get_time_unit(unitvalue, targetunit):

# we use seconds as base unit und convert any start to that value
if "s" not in unitvalue.data:
unitvalue.data = init_time_unit(unitvalue.data)

unitvalue.data["computation_list"].append(targetunit)

if targetunit == "s":
return unitvalue.data["s"]
if targetunit == "m":
return unitvalue.data["s"] / 60
if targetunit == "h":
return unitvalue.data["s"] / 60 / 60
if targetunit == "d":
return unitvalue.data["s"] / 60 / 60 / 24

raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}")


def set_time_unit(unitvalue, targetunit, newvalue):

# we use seconds as base unit und convert any start to that value
if "s" not in unitvalue.data:
unitvalue.data = init_time_unit(unitvalue.data)

unitvalue.data["computation_list"].append(targetunit)

if targetunit == "s":
unitvalue.data["s"] += newvalue
return
if targetunit == "m":
unitvalue.data["s"] += 60 * newvalue
return
if targetunit == "h":
unitvalue.data["s"] += 60 * 60 * newvalue
return
if targetunit == "d":
unitvalue.data["s"] += 24 * 60 * 60 * newvalue
return

raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}")


def register(mf):
time = mf.register_unit("time", get_time_unit, set_time_unit, [
["m", "minute", "minutes"],
["h", "hour", "hours"],
["s", "second", "seconds"],
["d", "day", "days"]
])
mf.register_defaults({
"time": time(6.0, "h"),
"time2": time(6.0, "d"),
})
Empty file.
Loading