my.config: catch possible nested config errors
This commit is contained in:
parent
e750666e30
commit
16c777b45a
4 changed files with 45 additions and 19 deletions
|
@ -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]]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue