From d14b384f8258bc90d7203e1b029350f28f5c49dd Mon Sep 17 00:00:00 2001 From: Moritz Lell Date: Tue, 9 Dec 2025 21:26:07 +0100 Subject: [PATCH] Render ProgrammingError, CompilationError and ParserError alway user-friendly Change `render_exception()` so that it does not show a stack trace if no AST node information is provided to CompilationError. Also, do not print a stack trace for `ProgrammingError`s, as these are also errors committed by the user and therefore should have a nice error message without a stack trace. --- beanquery/compiler.py | 13 +++++++------ beanquery/shell.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/beanquery/compiler.py b/beanquery/compiler.py index 6e6c442e..4a4ce0b2 100644 --- a/beanquery/compiler.py +++ b/beanquery/compiler.py @@ -113,7 +113,7 @@ def _select(self, node: ast.Select): # should never trigger if the compilation environment does not # contain any aggregate. if c_where is not None and is_aggregate(c_where): - raise CompilationError('aggregates are not allowed in WHERE clause') + raise CompilationError('aggregates are not allowed in WHERE clause', node = node.where_clause) # Combine FROM and WHERE clauses if c_from_expr is not None: @@ -190,10 +190,10 @@ def _compile_from(self, node): # Check that the FROM clause does not contain aggregates. if c_expression is not None and is_aggregate(c_expression): - raise CompilationError('aggregates are not allowed in FROM clause') + raise CompilationError('aggregates are not allowed in FROM clause', node) if node.open and node.close and node.open > node.close: - raise CompilationError('CLOSE date must follow OPEN date') + raise CompilationError('CLOSE date must follow OPEN date', node) # Apply OPEN, CLOSE, and CLEAR clauses. if node.open is not None or node.close is not None or node.clear is not None: @@ -228,13 +228,13 @@ def _compile_targets(self, targets): # Check for mixed aggregates and non-aggregates. if columns and aggregates: - raise CompilationError('mixed aggregates and non-aggregates are not allowed') + raise CompilationError('mixed aggregates and non-aggregates are not allowed', target) # Check for aggregates of aggregates. for aggregate in aggregates: for child in aggregate.childnodes(): if is_aggregate(child): - raise CompilationError('aggregates of aggregates are not allowed') + raise CompilationError('aggregates of aggregates are not allowed', target) return c_targets @@ -414,7 +414,8 @@ def _compile_group_by(self, group_by, c_targets): # Check if the new expression is an aggregate. aggregate = is_aggregate(c_expr) if aggregate: - raise CompilationError(f'GROUP-BY expressions may not be aggregates: "{column}"') + _, agg = get_columns_and_aggregates(c_expr) + raise CompilationError(f'GROUP-BY expressions may not be aggregates: "{column}"', column) # Attempt to reconcile the expression with one of the existing # target expressions. diff --git a/beanquery/shell.py b/beanquery/shell.py index c4675bf1..053d2585 100644 --- a/beanquery/shell.py +++ b/beanquery/shell.py @@ -72,12 +72,15 @@ def render_location(text, pos, endpos, lineno, indent, strip, out): # FIXME: move the error formatting into the exception classes themselves def render_exception(exc, indent='| ', strip=True): - if isinstance(exc, (beanquery.CompilationError, beanquery.ParseError)) and exc.parseinfo: + if isinstance(exc, beanquery.ProgrammingError): + return str(exc) + if isinstance(exc, (beanquery.CompilationError, beanquery.ParseError)): out = [str(exc)] - pos = exc.parseinfo.pos - endpos = exc.parseinfo.endpos - lineno = exc.parseinfo.line - render_location(exc.parseinfo.tokenizer.text, pos, endpos, lineno, indent, strip, out) + if exc.parseinfo: + pos = exc.parseinfo.pos + endpos = exc.parseinfo.endpos + lineno = exc.parseinfo.line + render_location(exc.parseinfo.tokenizer.text, pos, endpos, lineno, indent, strip, out) return '\n'.join(out) return '\n' + traceback.format_exc()