Skip to content

Commit

Permalink
feat(errors): add context in error reporting
Browse files Browse the repository at this point in the history
Errors contain context information, that is now added during parameter parsing.
  • Loading branch information
denisrosset committed Mar 16, 2022
1 parent a22b92e commit f00119a
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 13 deletions.
50 changes: 47 additions & 3 deletions src/configpile/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ def errors(self) -> Sequence[Err]:
"""
pass

@abstractmethod
def in_context(self, **contexts: Any) -> Err:
"""
Adds to this error information about the context in which it occurred
Returns:
Updated error
"""
pass

def pretty_print(self) -> None:
"""
Pretty prints an error on the console
Expand Down Expand Up @@ -96,7 +106,7 @@ def collect(*errs: Err) -> Optional[Err]:
return ManyErr(lst)

@staticmethod
def make(msg: str) -> Err:
def make(msg: str, **contexts: Any) -> Err:
"""
Creates a single error
Expand All @@ -106,7 +116,7 @@ def make(msg: str) -> Err:
Returns:
An error
"""
return Err1(msg)
return Err1(msg, [*contexts.items()])


@dataclass(frozen=True)
Expand Down Expand Up @@ -137,6 +147,9 @@ def space_prefix(s: str) -> str:
def errors(self) -> Sequence[Err]:
return self.errs

def in_context(self, **contexts: Any) -> Err:
return ManyErr([e.in_context(**contexts) for e in self.errs])


@dataclass(frozen=True)
class Err1(Err):
Expand All @@ -145,12 +158,17 @@ class Err1(Err):
"""

msg: str #: Error message
contexts: Sequence[Tuple[str, Any]] #: Contexts in which the error appears, from old to new

def errors(self) -> Sequence[Err]:
return [self]

def markdown(self) -> Sequence[str]:
return [self.msg]
c = [f"In {name}: {value}" for (name, value) in self.contexts]
return [*c, self.msg]

def in_context(self, **contexts: Any) -> Err:
return Err1(self.msg, [*self.contexts, *contexts.items()])


#: Ok value in our custom result type
Expand All @@ -164,6 +182,32 @@ def markdown(self) -> Sequence[str]:
Result = Union[T, Err]


@overload
def in_context(result: Optional[Err], **contexts: Any) -> Optional[Err]:
pass


@overload
def in_context(result: Result[T], **contexts: Any) -> Result[T]:
pass


def in_context(result: Union[T, Err, None], **contexts: Any) -> Union[T, Err, None]:
"""
Adds context to an error contained in a result type when possible
Args:
result: Result to enrich, if it contains an error
Returns:
Updated result
"""
if isinstance(result, Err):
return result.in_context(**contexts)
else:
return result


def collect_seq(seq: Sequence[Result[T]]) -> Result[Sequence[T]]:
ok: List[T] = []
errs: List[Err] = []
Expand Down
23 changes: 13 additions & 10 deletions src/configpile/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from typing_extensions import Annotated, get_args, get_origin, get_type_hints

from .arg import Arg, Expander, Param, Positional
from .errors import Err, Err1, Result
from .errors import Err, Result, in_context
from .types import ParamType
from .util import ClassDoc, filter_types_single

Expand Down Expand Up @@ -215,6 +215,7 @@ def handle(self, args: Sequence[str], state: State) -> Tuple[Sequence[str], Opti
handler = self.flags.get(flag)
if handler is not None:
next_args, err = handler.handle(args[1:], state)
err = in_context(err, flag=flag)
# TODO: add context
return next_args, err
else:
Expand Down Expand Up @@ -362,9 +363,9 @@ def process(self, ini_path: Path, state: State) -> Optional[Err]:
"""
errors: List[Err] = []
if not ini_path.exists():
return Err1(f"Config file {ini_path} does not exist")
return Err.make(f"Config file {ini_path} does not exist")
if not ini_path.is_file():
return Err1(f"Path {ini_path} is not a file")
return Err.make(f"Path {ini_path} is not a file")
parser = ConfigParser()
try:
with open(ini_path, "r") as file:
Expand All @@ -376,14 +377,16 @@ def process(self, ini_path: Path, state: State) -> Optional[Err]:
if key in self.kv_handlers:
res = self.kv_handlers[key].handle(value, state)
if isinstance(res, Err):
errors.append(res)
err = res
else:
if self.section_strict[section_name]:
errors.append(Err.make(f"Unknown key {key}"))
err = Err.make(f"Unknown key {key}")
if err is not None:
errors.append(err.in_context(ini_section=section_name))
except configparser.Error as e:
errors.append(Err.make(f"Parse error in {ini_path}"))
errors.append(Err.make(f"Parse error"))
except IOError as e:
errors.append(Err.make(f"IO Error in {ini_path}"))
errors.append(Err.make(f"IO Error"))
if errors:
return Err.collect(*errors)
else:
Expand Down Expand Up @@ -569,7 +572,7 @@ def process_config(self, cwd: Path, state: State) -> Optional[Err]:
for p in paths:
err = self.ini_processor.process(cwd / p, state)
if err is not None:
errors.append(err) # TODO: add context
errors.append(err.in_context(ini_file=p)) # TODO: add context
return Err.collect(*errors)

def process(
Expand Down Expand Up @@ -597,10 +600,10 @@ def process(
if handler is not None:
err = handler.handle(value, state)
if err is not None:
errors.append(err) # TODO: add context
errors.append(err.in_context(environment_variable=key)) # TODO: add context
err = self.process_config(cwd, state)
if err is not None:
errors.append(err)
errors.append(err.in_context(environment_variable=key))
# process command line arguments
rest_args: Sequence[str] = args
while rest_args:
Expand Down

0 comments on commit f00119a

Please sign in to comment.