From 16c777b45ac15f541539a4fd1ca5deb5dfc8233c Mon Sep 17 00:00:00 2001 From: Sean Breckenridge Date: Mon, 21 Mar 2022 17:33:57 -0700 Subject: [PATCH] my.config: catch possible nested config errors --- doc/MODULES.org | 2 ++ my/core/__main__.py | 2 +- my/core/error.py | 42 +++++++++++++++++++++++++++++++----------- my/core/source.py | 18 +++++++++++------- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/doc/MODULES.org b/doc/MODULES.org index 1f31931..a160ecb 100644 --- a/doc/MODULES.org +++ b/doc/MODULES.org @@ -63,6 +63,8 @@ The config snippets below are meant to be modified accordingly and *pasted into You don't have to set up all modules at once, it's recommended to do it gradually, to get the feel of how HPI works. +For an extensive/complex example, you can check out ~@seanbreckenridge~'s [[https://github.com/seanbreckenridge/dotfiles/blob/master/.config/my/my/config/__init__.py][config]] + # Nested Configurations before the doc generation using the block below ** [[file:../my/reddit][my.reddit]] diff --git a/my/core/__main__.py b/my/core/__main__.py index 2c5a2c1..22068a6 100644 --- a/my/core/__main__.py +++ b/my/core/__main__.py @@ -245,7 +245,7 @@ def modules_check(*, verbose: bool, list_all: bool, quick: bool, for_modules: Li error(f'{click.style("FAIL", fg="red")}: {m:<50} loading failed{vw}') # check that this is an import error in particular, not because # of a ModuleNotFoundError because some dependency wasnt installed - if type(e) == ImportError: + if isinstance(e, (ImportError, AttributeError)): warn_my_config_import_error(e) if verbose: tb(e) diff --git a/my/core/error.py b/my/core/error.py index 04c3e9f..7037399 100644 --- a/my/core/error.py +++ b/my/core/error.py @@ -4,7 +4,7 @@ See https://beepb00p.xyz/mypy-error-handling.html#kiss for more detail """ from itertools import tee -from typing import Union, TypeVar, Iterable, List, Tuple, Type, Optional, Callable, Any +from typing import Union, TypeVar, Iterable, List, Tuple, Type, Optional, Callable, Any, cast T = TypeVar('T') @@ -150,23 +150,43 @@ def error_to_json(e: Exception) -> Json: return {'error': estr} -def warn_my_config_import_error(err: ImportError) -> None: +MODULE_SETUP_URL = 'https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#private-configuration-myconfig' + +def warn_my_config_import_error(err: Union[ImportError, AttributeError]) -> bool: """ If the user tried to import something from my.config but it failed, possibly due to missing the config block in my.config? + + Returns True if it matched a possible config error """ import re import click - if err.name != 'my.config': - return - # parse name that user attempted to import - em = re.match(r"cannot import name '(\w+)' from 'my.config'", str(err)) - if em is not None: - section_name = em.group(1) - click.echo(click.style(f"""\ - You may be missing the '{section_name}' section from your config. - See https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#private-configuration-myconfig\ + if type(err) == ImportError: + if err.name != 'my.config': + return False + # parse name that user attempted to import + em = re.match(r"cannot import name '(\w+)' from 'my.config'", str(err)) + if em is not None: + section_name = em.group(1) + click.echo(click.style(f"""\ +You may be missing the '{section_name}' section from your config. +See {MODULE_SETUP_URL}\ """, fg='yellow'), err=True) + return True + elif type(err) == AttributeError: + # test if user had a nested config block missing + # https://github.com/karlicoss/HPI/issues/223 + if hasattr(err, 'obj') and hasattr(err, "name"): + config_obj = cast(object, getattr(err, 'obj')) # the object that caused the attribute error + # e.g. active_browser for my.browser + nested_block_name = err.name # type: ignore[attr-defined] + if config_obj.__module__ == 'my.config': + click.secho(f"You're likely missing the nested config block for '{getattr(config_obj, '__name__', str(config_obj))}.{nested_block_name}'.\nSee {MODULE_SETUP_URL} or check the module.py file for an example", fg='yellow', err=True) + return True + else: + click.echo(f"Unexpected error... {err}", err=True) + return False + def test_datetime_errors() -> None: diff --git a/my/core/source.py b/my/core/source.py index 25ffa44..07ead1e 100644 --- a/my/core/source.py +++ b/my/core/source.py @@ -3,7 +3,7 @@ Decorator to gracefully handle importing a data source, or warning and yielding nothing (or a default) when its not available """ -from typing import Any, Iterator, TypeVar, Callable, Optional, Iterable, Any +from typing import Any, Iterator, TypeVar, Callable, Optional, Iterable, Any, cast from my.core.warnings import medium, warn from functools import wraps @@ -44,7 +44,7 @@ def import_source( try: res = factory_func(*args, **kwargs) yield from res - except ImportError as err: + except (ImportError, AttributeError) as err: from . import core_config as CC from .error import warn_my_config_import_error suppressed_in_conf = False @@ -55,15 +55,19 @@ def import_source( medium(f"Module {factory_func.__qualname__} could not be imported, or isn't configured properly") else: medium(f"Module {module_name} ({factory_func.__qualname__}) could not be imported, or isn't configured properly") - warn(f"""To hide this message, add {module_name} to your core config disabled_modules, like: + warn(f"""If you don't want to use this module, to hide this message, add '{module_name}' to your core config disabled_modules in your config, like: class core: disabled_modules = [{repr(module_name)}] """) - # explicitly check if this is a ImportError, and didn't fail - # due to a module not being installed - if type(err) == ImportError: - warn_my_config_import_error(err) + # try to check if this is a config error or based on dependencies not being installed + if isinstance(err, (ImportError, AttributeError)): + matched_config_err = warn_my_config_import_error(err) + # if we determined this wasn't a config error, and it was an attribute error + # it could be *any* attribute error -- we should raise this since its otherwise a fatal error + # from some code in the module failing + if not matched_config_err and isinstance(err, AttributeError): + raise err yield from default return wrapper return decorator