my.config: catch possible nested config errors

This commit is contained in:
Sean Breckenridge 2022-03-21 17:33:57 -07:00 committed by karlicoss
parent e750666e30
commit 16c777b45a
4 changed files with 45 additions and 19 deletions

View file

@ -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]]

View file

@ -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)

View file

@ -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 type(err) == ImportError:
if err.name != 'my.config':
return
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 https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#private-configuration-myconfig\
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:

View file

@ -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