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. 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 # Nested Configurations before the doc generation using the block below
** [[file:../my/reddit][my.reddit]] ** [[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}') error(f'{click.style("FAIL", fg="red")}: {m:<50} loading failed{vw}')
# check that this is an import error in particular, not because # check that this is an import error in particular, not because
# of a ModuleNotFoundError because some dependency wasnt installed # of a ModuleNotFoundError because some dependency wasnt installed
if type(e) == ImportError: if isinstance(e, (ImportError, AttributeError)):
warn_my_config_import_error(e) warn_my_config_import_error(e)
if verbose: if verbose:
tb(e) 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 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') T = TypeVar('T')
@ -150,23 +150,43 @@ def error_to_json(e: Exception) -> Json:
return {'error': estr} 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, If the user tried to import something from my.config but it failed,
possibly due to missing the config block in my.config? possibly due to missing the config block in my.config?
Returns True if it matched a possible config error
""" """
import re import re
import click import click
if err.name != 'my.config': if type(err) == ImportError:
return if err.name != 'my.config':
# parse name that user attempted to import return False
em = re.match(r"cannot import name '(\w+)' from 'my.config'", str(err)) # parse name that user attempted to import
if em is not None: em = re.match(r"cannot import name '(\w+)' from 'my.config'", str(err))
section_name = em.group(1) if em is not None:
click.echo(click.style(f"""\ section_name = em.group(1)
You may be missing the '{section_name}' section from your config. click.echo(click.style(f"""\
See https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#private-configuration-myconfig\ You may be missing the '{section_name}' section from your config.
See {MODULE_SETUP_URL}\
""", fg='yellow'), err=True) """, 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: 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 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 my.core.warnings import medium, warn
from functools import wraps from functools import wraps
@ -44,7 +44,7 @@ def import_source(
try: try:
res = factory_func(*args, **kwargs) res = factory_func(*args, **kwargs)
yield from res yield from res
except ImportError as err: except (ImportError, AttributeError) as err:
from . import core_config as CC from . import core_config as CC
from .error import warn_my_config_import_error from .error import warn_my_config_import_error
suppressed_in_conf = False 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") medium(f"Module {factory_func.__qualname__} could not be imported, or isn't configured properly")
else: else:
medium(f"Module {module_name} ({factory_func.__qualname__}) could not be imported, or isn't configured properly") 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: class core:
disabled_modules = [{repr(module_name)}] disabled_modules = [{repr(module_name)}]
""") """)
# explicitly check if this is a ImportError, and didn't fail # try to check if this is a config error or based on dependencies not being installed
# due to a module not being installed if isinstance(err, (ImportError, AttributeError)):
if type(err) == ImportError: matched_config_err = warn_my_config_import_error(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 yield from default
return wrapper return wrapper
return decorator return decorator