From 2453af03704454f7cfed126560bc08c21781f68e Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <-> Date: Wed, 6 Nov 2019 11:04:02 +0100 Subject: [PATCH] 0.10.1 - Openhab Tests log to own file - Serialized rule (un-) loading (fixes #80) - MyPy fixes - Added documentation for set_file_validator - added func get_path for FileEvents - Gracefully shutdown aiohttp on shutdown of HABApp --- .gitignore | 1 + HABApp/__main__.py | 6 +- HABApp/__version__.py | 2 +- HABApp/config/config.py | 6 +- HABApp/core/events/habapp_events.py | 11 +++- HABApp/mqtt/mqtt_connection.py | 3 +- HABApp/mqtt/mqtt_interface.py | 3 +- HABApp/openhab/definitions/values.py | 15 +++-- HABApp/openhab/http_connection.py | 8 +-- HABApp/openhab/items/map_items.py | 3 +- HABApp/openhab/oh_connection.py | 9 ++- HABApp/parameters/__init__.py | 3 +- HABApp/parameters/parameter_files.py | 13 ++-- HABApp/parameters/parameters.py | 21 ++++-- HABApp/rule/interfaces/rule_subprocess.py | 16 +++-- HABApp/rule/rule.py | 2 + HABApp/rule/scheduler/reoccurring_cb.py | 4 +- HABApp/rule_manager/rule_file.py | 6 +- HABApp/rule_manager/rule_manager.py | 65 +++++++++++-------- HABApp/util/multimode_item.py | 12 ++-- _doc/parameters.rst | 39 ++++++++++- conf_testing/logging.yml | 15 +++++ conf_testing/rules/bench_rule.py | 2 +- conf_testing/rules/test_openhab_item_funcs.py | 2 +- setup.py | 21 ++++-- tests/test_rule/test_process.py | 15 +++++ tests/test_utils/test_timeframe.py | 2 + 27 files changed, 209 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index 0a3c9c29..00293648 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.mypy_cache .idea __pycache__ /conf diff --git a/HABApp/__main__.py b/HABApp/__main__.py index 7cb67004..47781b8b 100644 --- a/HABApp/__main__.py +++ b/HABApp/__main__.py @@ -91,7 +91,6 @@ def main() -> typing.Union[int, str]: if args.NoMQTTConnectionErrors is True: HABApp.mqtt.MqttInterface._RAISE_CONNECTION_ERRORS = False - loop = None log = logging.getLogger('HABApp') # if installed we use uvloop because it seems to be much faster (untested) @@ -113,7 +112,7 @@ def main() -> typing.Union[int, str]: # https://docs.python.org/3/library/asyncio-subprocess.html#subprocess-and-threads asyncio.get_child_watcher() - loop = asyncio.get_event_loop() + loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() loop.set_debug(True) loop.slow_callback_duration = 0.02 @@ -142,6 +141,9 @@ def shutdown_handler(sig, frame): print(e) return str(e) finally: + # Sleep to allow underlying connections of aiohttp to close + # https://aiohttp.readthedocs.io/en/stable/client_advanced.html#graceful-shutdown + loop.run_until_complete(asyncio.sleep(1)) loop.close() return 0 diff --git a/HABApp/__version__.py b/HABApp/__version__.py index 22ec42ba..5097cb67 100644 --- a/HABApp/__version__.py +++ b/HABApp/__version__.py @@ -1 +1 @@ -__VERSION__ = '0.10.0' +__VERSION__ = '0.10.1' diff --git a/HABApp/config/config.py b/HABApp/config/config.py index 5515b326..d410844a 100644 --- a/HABApp/config/config.py +++ b/HABApp/config/config.py @@ -17,10 +17,10 @@ _yaml_param = ruamel.yaml.YAML(typ='safe') _yaml_param.default_flow_style = False -_yaml_param.default_style = False -_yaml_param.width = 1000000 +_yaml_param.default_style = False # type: ignore +_yaml_param.width = 1000000 # type: ignore _yaml_param.allow_unicode = True -_yaml_param.sort_base_mapping_type_on_output = False +_yaml_param.sort_base_mapping_type_on_output = False # type: ignore log = logging.getLogger('HABApp.Config') diff --git a/HABApp/core/events/habapp_events.py b/HABApp/core/events/habapp_events.py index 63512252..9d193bca 100644 --- a/HABApp/core/events/habapp_events.py +++ b/HABApp/core/events/habapp_events.py @@ -11,9 +11,12 @@ class RequestFileLoadEvent: def from_path(cls, folder: Path, file: Path) -> 'RequestFileLoadEvent': return cls(str(file.relative_to(folder))) - def __init__(self, name: str = None): + def __init__(self, name: str): self.filename: str = name + def get_path(self, parent_folder: Path) -> Path: + return parent_folder / self.filename + def __repr__(self): return f'<{self.__class__.__name__} filename: {self.filename}>' @@ -28,9 +31,13 @@ class RequestFileUnloadEvent: def from_path(cls, folder: Path, file: Path) -> 'RequestFileUnloadEvent': return cls(str(file.relative_to(folder))) - def __init__(self, name: str = None): + def __init__(self, name: str): self.filename: str = name + def get_path(self, parent_folder: Path) -> Path: + return parent_folder / self.filename + + def __repr__(self): return f'<{self.__class__.__name__} filename: {self.filename}>' diff --git a/HABApp/mqtt/mqtt_connection.py b/HABApp/mqtt/mqtt_connection.py index 23dcde20..8eb4d7f7 100644 --- a/HABApp/mqtt/mqtt_connection.py +++ b/HABApp/mqtt/mqtt_connection.py @@ -1,4 +1,5 @@ import logging +import typing import ujson import paho.mqtt.client as mqtt @@ -24,7 +25,7 @@ def __init__(self, mqtt_config: MqttConfig, shutdown_helper: ShutdownHelper): self.client: mqtt.Client = None - self.subscriptions = [] + self.subscriptions: typing.List[typing.Tuple[str, int]] = [] # config changes self.__config = mqtt_config diff --git a/HABApp/mqtt/mqtt_interface.py b/HABApp/mqtt/mqtt_interface.py index 38fca901..030b74fa 100644 --- a/HABApp/mqtt/mqtt_interface.py +++ b/HABApp/mqtt/mqtt_interface.py @@ -105,8 +105,7 @@ def unsubscribe(self, topic: str) -> int: return result - -MQTT_INTERFACE: MqttInterface = None +MQTT_INTERFACE: MqttInterface def get_mqtt_interface(connection=None, config=None) -> MqttInterface: diff --git a/HABApp/openhab/definitions/values.py b/HABApp/openhab/definitions/values.py index 33a0b8d1..449d1a1c 100644 --- a/HABApp/openhab/definitions/values.py +++ b/HABApp/openhab/definitions/values.py @@ -1,3 +1,4 @@ +import typing from HABApp.core.events import ComplexEventValue @@ -16,9 +17,9 @@ def __str__(self): class PercentValue(ComplexEventValue): def __init__(self, value: str): - value = float(value) - assert 0 <= value <= 100, f'{value} ({type(value)})' - super().__init__(value) + percent = float(value) + assert 0 <= percent <= 100, f'{percent} ({type(percent)})' + super().__init__(percent) def __str__(self): return f'{self.value}%' @@ -47,14 +48,16 @@ def __str__(self): class QuantityValue(ComplexEventValue): def __init__(self, value: str): - val, unit = value.split(' ') + str_val, unit = value.split(' ') + try: - val = int(val) + val: typing.Union[int, float] = int(str_val) except ValueError: - val = float(val) + val = float(str_val) super().__init__(val) self.unit = unit + def __str__(self): return f'{self.value} {self.unit}' diff --git a/HABApp/openhab/http_connection.py b/HABApp/openhab/http_connection.py index 501ded4d..58ca0cc9 100644 --- a/HABApp/openhab/http_connection.py +++ b/HABApp/openhab/http_connection.py @@ -103,8 +103,8 @@ def _is_disconnect_exception(self, e) -> bool: self.__set_offline(str(e)) return True - async def _check_http_response(self, future, additional_info="", - accept_404=False) -> typing.Optional[ ClientResponse]: + async def _check_http_response(self, future: aiohttp.client._RequestContextManager, additional_info="", + accept_404=False) -> ClientResponse: try: resp = await future except Exception as e: @@ -112,8 +112,7 @@ async def _check_http_response(self, future, additional_info="", log.log(logging.WARNING if is_disconnect else logging.ERROR, f'"{e}" ({type(e)})') if is_disconnect: raise OpenhabDisconnectedError() - else: - return None + raise # Server Errors if openhab is not ready yet if resp.status >= 500: @@ -290,6 +289,7 @@ async def async_get_items(self) -> typing.Optional[list]: if not isinstance(e, (OpenhabDisconnectedError, OpenhabNotReadyYet)): for l in traceback.format_exc().splitlines(): log.error(l) + return None async def async_get_item(self, item_name: str) -> dict: fut = self.__session.get(self.__get_openhab_url('rest/items/{:s}', item_name)) diff --git a/HABApp/openhab/items/map_items.py b/HABApp/openhab/items/map_items.py index a6b095cf..d3bbeded 100644 --- a/HABApp/openhab/items/map_items.py +++ b/HABApp/openhab/items/map_items.py @@ -1,4 +1,5 @@ import datetime +import typing from HABApp.core.items import Item from . import SwitchItem, ContactItem, RollershutterItem, DimmerItem, ColorItem, NumberItem @@ -8,7 +9,7 @@ def map_items(name, openhab_type : str, openhab_value : str): assert isinstance(openhab_type, str), type(openhab_type) assert isinstance(openhab_value, str), type(openhab_value) - value = openhab_value + value: typing.Optional[str] = openhab_value if openhab_value == 'NULL' or openhab_value == 'UNDEF': value = None diff --git a/HABApp/openhab/oh_connection.py b/HABApp/openhab/oh_connection.py index 72d7be1a..6fabcdc8 100644 --- a/HABApp/openhab/oh_connection.py +++ b/HABApp/openhab/oh_connection.py @@ -14,7 +14,6 @@ log = logging.getLogger('HABApp.openhab.Connection') - class OpenhabConnection(HttpConnectionEventHandler): def __init__(self, config, shutdown): @@ -99,11 +98,11 @@ async def async_ping(self): - def on_sse_event(self, event: dict): + def on_sse_event(self, event_dict: dict): try: # Lookup corresponding OpenHAB event - event = get_event(event) + event = get_event(event_dict) # Events which change the ItemRegistry if isinstance(event, (HABApp.openhab.events.ItemAddedEvent, HABApp.openhab.events.ItemUpdatedEvent)): @@ -140,7 +139,7 @@ def on_sse_event(self, event: dict): @PrintException - async def update_all_items(self) -> int: + async def update_all_items(self): try: data = await self.connection.async_get_items() @@ -176,4 +175,4 @@ async def update_all_items(self) -> int: log.error(e) for l in traceback.format_exc().splitlines(): log.error(l) - return 0 + return None diff --git a/HABApp/parameters/__init__.py b/HABApp/parameters/__init__.py index ca89781d..3393e751 100644 --- a/HABApp/parameters/__init__.py +++ b/HABApp/parameters/__init__.py @@ -1,2 +1,3 @@ -from .parameter import Parameter \ No newline at end of file +from .parameter import Parameter +from .parameters import set_file_validator diff --git a/HABApp/parameters/parameter_files.py b/HABApp/parameters/parameter_files.py index 17803558..2751650f 100644 --- a/HABApp/parameters/parameter_files.py +++ b/HABApp/parameters/parameter_files.py @@ -1,6 +1,7 @@ import logging import threading import traceback +import typing import ruamel.yaml @@ -11,14 +12,14 @@ _yml_setup = ruamel.yaml.YAML() _yml_setup.default_flow_style = False -_yml_setup.default_style = False -_yml_setup.width = 1000000 +_yml_setup.default_style = False # type: ignore +_yml_setup.width = 1000000 # type: ignore _yml_setup.allow_unicode = True -_yml_setup.sort_base_mapping_type_on_output = False +_yml_setup.sort_base_mapping_type_on_output = False # type: ignore LOCK = threading.Lock() HABAPP_PARAM_TOPIC = 'HABApp.Parameters' -CONFIG = None +CONFIG = None # type: typing.Optional[HABApp.config.Config] def setup_param_files(config, folder_watcher): @@ -56,7 +57,7 @@ def setup_param_files(config, folder_watcher): def load_file(event: HABApp.core.events.habapp_events.RequestFileLoadEvent): - path = CONFIG.directories.param / event.filename + path = event.get_path(CONFIG.directories.param) with LOCK: # serialize to get proper error messages try: @@ -75,7 +76,7 @@ def load_file(event: HABApp.core.events.habapp_events.RequestFileLoadEvent): def unload_file(event: HABApp.core.events.habapp_events.RequestFileUnloadEvent): - path = CONFIG.directories.param / event.filename + path = event.get_path(CONFIG.directories.param) with LOCK: # serialize to get proper error messages try: diff --git a/HABApp/parameters/parameters.py b/HABApp/parameters/parameters.py index f5a8f3fd..d1ecdcb1 100644 --- a/HABApp/parameters/parameters.py +++ b/HABApp/parameters/parameters.py @@ -24,23 +24,31 @@ def get_parameter_file(file: str): return _PARAMETERS[file] -def set_file_validator(file: str, validator: typing.Any, allow_extra_keys=True): +def set_file_validator(filename: str, validator: typing.Any, allow_extra_keys=True): + """Add a validator for the parameter file. If the file is already loaded this will reload the file. + + :param filename: filename which shall be validated (without extension) + :param validator: Description of file content - see the library + `voluptuous `_ for examples. + Use `None` to remove validator. + :param allow_extra_keys: Allow additional keys in the file structure + """ # Remove validator if validator is None: - _VALIDATORS.pop(file, None) + _VALIDATORS.pop(filename, None) return # Set validator - old_validator = _VALIDATORS.get(file) - _VALIDATORS[file] = new_validator = voluptuous.Schema( + old_validator = _VALIDATORS.get(filename) + _VALIDATORS[filename] = new_validator = voluptuous.Schema( validator, required=True, extra=(voluptuous.ALLOW_EXTRA if allow_extra_keys else voluptuous.PREVENT_EXTRA) ) # todo: move this to file handling so we get the extension if old_validator != new_validator: HABApp.core.EventBus.post_event( - HABAPP_PARAM_TOPIC, HABApp.core.events.habapp_events.RequestFileLoadEvent(file + '.yml') + HABAPP_PARAM_TOPIC, HABApp.core.events.habapp_events.RequestFileLoadEvent(filename + '.yml') ) @@ -50,7 +58,8 @@ def add_parameter(file: str, *keys, default_value): if file not in _PARAMETERS: save = True - _PARAMETERS[file] = param = {} + param: typing.Dict[str, typing.Any] = {} + _PARAMETERS[file] = param else: param = _PARAMETERS[file] diff --git a/HABApp/rule/interfaces/rule_subprocess.py b/HABApp/rule/interfaces/rule_subprocess.py index 65c322d2..75ba0fd5 100644 --- a/HABApp/rule/interfaces/rule_subprocess.py +++ b/HABApp/rule/interfaces/rule_subprocess.py @@ -1,16 +1,17 @@ import asyncio +import typing class FinishedProcessInfo: """Information about the finished process.""" - def __init__(self, returncode: int, stdout: str, stderr: str): + def __init__(self, returncode: int, stdout: typing.Optional[str], stderr: typing.Optional[str]): self.returncode: int = returncode - self.stdout: str = stdout - self.stderr: str = stderr + self.stdout: typing.Optional[str] = stdout + self.stderr: typing.Optional[str] = stderr def __repr__(self): - return f'' + return f'' async def async_subprocess_exec(callback, program: str, *args, capture_output=True): @@ -29,10 +30,11 @@ async def async_subprocess_exec(callback, program: str, *args, capture_output=Tr stderr=asyncio.subprocess.PIPE if capture_output else None ) - stdout, stderr = await proc.communicate() - stdout = stdout.decode() - stderr = stderr.decode() + b_stdout, b_stderr = await proc.communicate() ret_code = proc.returncode + if capture_output: + stdout = b_stdout.decode() + stderr = b_stderr.decode() except asyncio.CancelledError: if proc is not None: proc.terminate() diff --git a/HABApp/rule/rule.py b/HABApp/rule/rule.py index 1f713b01..66ccd35b 100644 --- a/HABApp/rule/rule.py +++ b/HABApp/rule/rule.py @@ -21,11 +21,13 @@ log = logging.getLogger('HABApp.Rule') +# Func to log deprecation warnings def send_warnings_to_log(message, category, filename, lineno, file=None, line=None): log.warning('%s:%s: %s:%s' % (filename, lineno, category.__name__, message)) return +# Setup deprecation warnings warnings.simplefilter('default') warnings.showwarning = send_warnings_to_log diff --git a/HABApp/rule/scheduler/reoccurring_cb.py b/HABApp/rule/scheduler/reoccurring_cb.py index a21e7fbf..9ff80299 100644 --- a/HABApp/rule/scheduler/reoccurring_cb.py +++ b/HABApp/rule/scheduler/reoccurring_cb.py @@ -17,7 +17,7 @@ def __init__(self, time: TYPING_DATE_TIME, interval: typing.Union[int, datetime. assert isinstance(interval, datetime.timedelta), type(interval) self.time_interval = interval - def check_due(self, now: datetime): + def check_due(self, now: datetime.datetime): super().check_due(now) if self.is_due: self.next_call += self.time_interval @@ -40,7 +40,7 @@ def __init__(self, time: TYPING_DATE_TIME, weekdays: typing.List[int], callback, while not self.next_call.isoweekday() in self.weekdays: self.next_call += datetime.timedelta(days=1) - def check_due(self, now: datetime): + def check_due(self, now: datetime.datetime): super().check_due(now) if self.is_due: self.next_call += datetime.timedelta(days=1) diff --git a/HABApp/rule_manager/rule_file.py b/HABApp/rule_manager/rule_file.py index e182bf15..80c0cd17 100644 --- a/HABApp/rule_manager/rule_file.py +++ b/HABApp/rule_manager/rule_file.py @@ -18,11 +18,11 @@ def __init__(self, rule_manager, path: Path): assert isinstance(rule_manager, RuleManager) self.rule_manager = rule_manager - self.path = path + self.path: Path = path - self.rules = {} + self.rules = {} # type: typing.Dict[str, HABApp.Rule] - self.class_ctr = collections.defaultdict(lambda : 1) + self.class_ctr: typing.Dict[str, int] = collections.defaultdict(lambda: 1) def suggest_rule_name(self, obj) -> str: diff --git a/HABApp/rule_manager/rule_manager.py b/HABApp/rule_manager/rule_manager.py index 7c87d57a..5c272dd4 100644 --- a/HABApp/rule_manager/rule_manager.py +++ b/HABApp/rule_manager/rule_manager.py @@ -26,8 +26,8 @@ def __init__(self, parent): self.files: typing.Dict[str, RuleFile] = {} # serialize loading - self.__file_load_lock = threading.Lock() - self.__rulefiles_lock = threading.Lock() + self.__load_lock = threading.Lock() + self.__files_lock = threading.Lock() # Processing self.__process_last_sec = 60 @@ -96,7 +96,7 @@ async def process_scheduled_events(self): # remember sec self.__process_last_sec = now.second - with self.__rulefiles_lock: + with self.__files_lock: for file in self.files.values(): assert isinstance(file, RuleFile), type(file) for rule in file.iterrules(): @@ -136,33 +136,38 @@ def get_rule(self, rule_name): @PrintException - def request_file_unload(self, event: HABApp.core.events.habapp_events.RequestFileUnloadEvent): - - path = self.runtime.config.directories.rules / event.filename + def request_file_unload(self, event: HABApp.core.events.habapp_events.RequestFileUnloadEvent, request_lock=True): + path = event.get_path(self.runtime.config.directories.rules) path_str = str(path) - # Only unload already loaded files - if path_str not in self.files: - log.warning(f'Rule file {path} is not yet loaded and therefore can not be unloaded') - return None - try: - with self.__file_load_lock: - log.debug(f'Removing file: {path}') - if path_str in self.files: - with self.__rulefiles_lock: - rule = self.files.pop(path_str) - rule.unload() + if request_lock: + self.__load_lock.acquire() + + # Only unload already loaded files + with self.__files_lock: + already_loaded = path_str in self.files + if not already_loaded: + log.warning(f'Rule file {path} is not yet loaded and therefore can not be unloaded') + return None + + log.debug(f'Removing file: {path}') + with self.__files_lock: + rule = self.files.pop(path_str) + rule.unload() except Exception: log.error(f"Could not remove {path}!") for l in traceback.format_exc().splitlines(): log.error(l) return None + finally: + if request_lock: + self.__load_lock.release() @PrintException def request_file_load(self, event: HABApp.core.events.habapp_events.RequestFileLoadEvent): - path = self.runtime.config.directories.rules / event.filename + path = event.get_path(self.runtime.config.directories.rules) path_str = str(path) # Only load existing files @@ -170,23 +175,27 @@ def request_file_load(self, event: HABApp.core.events.habapp_events.RequestFileL log.warning(f'Rule file {path} does not exist and can not be loaded!') return None - # Unload if we have already loaded - if path_str in self.files: - self.request_file_unload(event) - try: # serialize loading - with self.__file_load_lock: - - log.debug(f'Loading file: {path}') - with self.__rulefiles_lock: - self.files[path_str] = file = RuleFile(self, path) - file.load() + self.__load_lock.acquire() + + # Unload if we have already loaded + with self.__files_lock: + already_loaded = path_str in self.files + if already_loaded: + self.request_file_unload(event, request_lock=False) + + log.debug(f'Loading file: {path}') + with self.__files_lock: + self.files[path_str] = file = RuleFile(self, path) + file.load() except Exception: log.error(f"Could not (fully) load {path}!") for l in traceback.format_exc().splitlines(): log.error(l) return None + finally: + self.__load_lock.release() log.debug(f'File {path_str} successfully loaded!') diff --git a/HABApp/util/multimode_item.py b/HABApp/util/multimode_item.py index 1aedb5dc..91fb19b5 100644 --- a/HABApp/util/multimode_item.py +++ b/HABApp/util/multimode_item.py @@ -21,7 +21,7 @@ class MultiModeValue: """ DISABLE_OPERATORS = { '>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le, - '==': operator.eq, '!=': operator.ne, None: None + '==': operator.eq, '!=': operator.ne, } def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None, auto_disable_after=None, @@ -45,9 +45,9 @@ def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None, assert isinstance(auto_disable_after, datetime.timedelta) or auto_disable_after is None, \ type(auto_disable_after) - assert auto_disable_on in MultiModeValue.DISABLE_OPERATORS, auto_disable_on + assert auto_disable_on is None or auto_disable_on in MultiModeValue.DISABLE_OPERATORS, auto_disable_on self.auto_disable_after: typing.Optional[datetime.timedelta] = auto_disable_after - self.auto_disable_on: str = auto_disable_on + self.auto_disable_on: typing.Optional[str] = auto_disable_on self.calc_value_func: typing.Optional[typing.Callable[[typing.Any, typing.Any], typing.Any]] = calc_value_func @@ -151,7 +151,7 @@ class MultiModeItem(Item): """ @classmethod - def get_create_item(cls, name: str, logger: logging.getLoggerClass() = None): + def get_create_item(cls, name: str, logger: logging.Logger = None): item = super().get_create_item(name, None) item.logger = logger return item @@ -164,7 +164,7 @@ def __init__(self, name: str, initial_value=None): self.__lock = Lock() - self.logger: typing.Optional[logging._loggerClass] = None + self.logger: typing.Optional[logging.Logger] = None def log(self, level, text, *args, **kwargs): if self.logger is not None: @@ -204,7 +204,7 @@ def create_mode( # make the lower priority known to the mode low = None - for priority, child in sorted(self.__values_by_prio.items()): # type: int, MultiModeValue + for _, child in sorted(self.__values_by_prio.items()): # type: int, MultiModeValue child._set_lower_priority_mode(low) low = child diff --git a/_doc/parameters.rst b/_doc/parameters.rst index 03270e32..5c554f8b 100644 --- a/_doc/parameters.rst +++ b/_doc/parameters.rst @@ -74,4 +74,41 @@ Changes in the file will be automatically picked up through :class:`~HABApp.para .. autoclass:: HABApp.parameters.Parameter :members: - .. automethod:: __init__ \ No newline at end of file + .. automethod:: __init__ + +Validation +------------------------------ +Since parameters used to provide flexible configuration for automation classes they can get quite complex and +error prone. Thus it is possible to provide a validator for a file which will check the files for constraints, +missing keys etc. when the file is loaded. + +.. autofunction:: HABApp.parameters.set_file_validator + +Example + +.. execute_code:: + :hide_output: + + # hide + from HABApp.parameters.parameters import _PARAMETERS + _PARAMETERS['param_file_testrule'] = {'min_value': 10, 'Rule A': {'subkey1': {'subkey2': ['a', 'b', 'c']}}} + # hide + + import HABApp + import voluptuous + + # Validator can even and should be specified before loading rules + + # allows a dict e.g. { 'key1': {'key2': '5}} + HABApp.parameters.set_file_validator('file1', {str: {str: int}}) + + # More complex example with an optional key: + validator = { + 'Test': int, + 'Key': { + 'mandatory': str, + voluptuous.Optional('optional'): int + } + } + HABApp.parameters.set_file_validator('file1', validator) + diff --git a/conf_testing/logging.yml b/conf_testing/logging.yml index f8312a7f..11b5b78b 100644 --- a/conf_testing/logging.yml +++ b/conf_testing/logging.yml @@ -25,6 +25,15 @@ handlers: formatter: HABApp_format level: DEBUG + HABApp_test_file: + class: logging.handlers.RotatingFileHandler + filename: 'tests.log' + maxBytes: 10_485_760 + backupCount: 3 + + formatter: HABApp_format + level: DEBUG + BufferEventFile: class: logging.handlers.MemoryHandler capacity: 0 @@ -57,3 +66,9 @@ loggers: handlers: - BufferEventFile propagate: False + + HABApp.Tests: + level: DEBUG + handlers: + - HABApp_test_file + propagate: False diff --git a/conf_testing/rules/bench_rule.py b/conf_testing/rules/bench_rule.py index ed9a6cfb..bab626af 100644 --- a/conf_testing/rules/bench_rule.py +++ b/conf_testing/rules/bench_rule.py @@ -41,7 +41,7 @@ def bench_stop(self, event): ts_start = time.time() while True: for k in self.item_list: - if self.get_item_state(k) != self.__b_val: + if HABApp.core.items.Item.get_item(k).value != self.__b_val: break else: break diff --git a/conf_testing/rules/test_openhab_item_funcs.py b/conf_testing/rules/test_openhab_item_funcs.py index 8d8b8e8c..3d27255d 100644 --- a/conf_testing/rules/test_openhab_item_funcs.py +++ b/conf_testing/rules/test_openhab_item_funcs.py @@ -6,7 +6,7 @@ from HABApp.openhab.items import SwitchItem, RollershutterItem, DimmerItem, ColorItem from HABAppTests import TestBaseRule, ItemWaiter, OpenhabTmpItem -log = logging.getLogger('HABApp.Test') +log = logging.getLogger('HABApp.Tests') @dataclasses.dataclass diff --git a/setup.py b/setup.py index 74a67a6e..b24824ba 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,20 @@ +import typing from pathlib import Path + import setuptools -# Load version number -version = {} -with open("HABApp/__version__.py") as fp: - exec(fp.read(), version) -assert version -assert version['__VERSION__'] -__VERSION__ = version['__VERSION__'] + +# Load version number without importing HABApp +def load_version() -> str: + version: typing.Dict[str, str] = {} + with open("HABApp/__version__.py") as fp: + exec(fp.read(), version) + assert version['__VERSION__'], version + return version['__VERSION__'] + + +__VERSION__ = load_version() + print(f'Version: {__VERSION__}') print('') diff --git a/tests/test_rule/test_process.py b/tests/test_rule/test_process.py index fb02e997..9eb69172 100644 --- a/tests/test_rule/test_process.py +++ b/tests/test_rule/test_process.py @@ -46,6 +46,21 @@ def test_run_func(self): self.assertEqual(self.ret.returncode, 0) self.assertTrue(self.ret.stdout.startswith('20')) + def test_run_func_no_cap(self): + self.rule.execute_subprocess( + self.set_ret, sys.executable, '-c', 'import datetime; print(datetime.datetime.now())', capture_output=False + ) + + # Test this call from __main__ to create thread save process watchers + if sys.platform != "win32": + asyncio.get_child_watcher() + + asyncio.get_event_loop().run_until_complete(asyncio.gather(asyncio.sleep(0.5))) + self.assertEqual(self.ret.returncode, 0) + self.assertEqual(self.ret.stdout, None) + self.assertEqual(self.ret.stderr, None) + + def test_exception(self): self.rule.execute_subprocess(self.set_ret, 'asdfasdf', capture_output=False) diff --git a/tests/test_utils/test_timeframe.py b/tests/test_utils/test_timeframe.py index 494ab486..e73f68ed 100644 --- a/tests/test_utils/test_timeframe.py +++ b/tests/test_utils/test_timeframe.py @@ -1,3 +1,5 @@ +# mypy: ignore-errors + import datetime import unittest