Skip to content

Feature/show full traceback #1855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ logger.warning("Something unexpected but not critical")
logger.error("Something went wrong")
```

**Exception Handling**:
EDSL provides simplified exception messages by default, but you can enable full tracebacks during development:

```python
# Option 1: Set environment variable before running your script
import os
os.environ["EDSL_SHOW_FULL_TRACEBACK"] = "True"

# Option 2: Add to your .env file
# EDSL_SHOW_FULL_TRACEBACK=True
```

**Flexibility**:
Choose whether to run surveys on your own computer or at the Expected Parrot server.

Expand Down
21 changes: 21 additions & 0 deletions docs/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ It can typically be remedied by reinstalling your virtual environment or install
Strategies for dealing with exceptions
--------------------------------------

Show full tracebacks
^^^^^^^^^^^^^^^^^^^

By default, EDSL simplifies exception messages by hiding the full Python traceback. This makes error messages cleaner and easier to read for most users.

When developing with EDSL or debugging issues, you may want to see the full traceback to better understand where the error is occurring. You can enable full tracebacks by setting the `EDSL_SHOW_FULL_TRACEBACK` environment variable:

.. code-block:: python

# Option 1: Set environment variable in your Python code
import os
os.environ["EDSL_SHOW_FULL_TRACEBACK"] = "True"

# Option 2: Add to your .env file
# EDSL_SHOW_FULL_TRACEBACK=True

The value is not case-sensitive and can be any of the following:

- "True", "1", "yes", "y" to show full tracebacks
- Any other value (including "False") to use the default behavior

Re-try the question
^^^^^^^^^^^^^^^^^^^

Expand Down
22 changes: 20 additions & 2 deletions edsl/base/base_exception.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import os
from IPython.core.interactiveshell import InteractiveShell
from IPython.display import HTML, display
from pathlib import Path
Expand Down Expand Up @@ -134,7 +135,7 @@ def _install_ipython_hook(cls):

# Wrap in a function so we can pass it to set_custom_exc.
def _ipython_custom_exc(shell, etype, evalue, tb, tb_offset=None):
if issubclass(etype, BaseException) and cls.suppress_traceback:
if issubclass(etype, BaseException) and not cls._should_show_full_traceback():
# Show custom message only if not silent
if not getattr(evalue, "silent", False):
# Try HTML display first; fall back to stderr
Expand Down Expand Up @@ -166,7 +167,7 @@ def _install_sys_excepthook(cls):
original_excepthook = sys.excepthook

def _custom_excepthook(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, BaseException) and cls.suppress_traceback:
if issubclass(exc_type, BaseException) and not cls._should_show_full_traceback():
# Show custom message only if not silent
if not getattr(exc_value, "silent", False):
# try:
Expand Down Expand Up @@ -202,3 +203,20 @@ def _in_ipython() -> bool:
return True
except NameError:
return False

@classmethod
def _should_show_full_traceback(cls) -> bool:
"""
Determines whether to show full tracebacks based on the EDSL_SHOW_FULL_TRACEBACK
environment variable.

Returns:
bool: True if full tracebacks should be shown, False otherwise
"""
# Check environment variable (can be set directly or via .env)
env_var = os.environ.get("EDSL_SHOW_FULL_TRACEBACK")
if env_var is not None:
return env_var.lower() in ("true", "1", "yes", "y")

# Default to the class attribute if nothing else is set
return not cls.suppress_traceback
4 changes: 4 additions & 0 deletions edsl/config/config_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ class MissingEnvironmentVariableError(BaseException):
"default": "10", # Change to a very low threshold (10 bytes) to test SQLite offloading
"info": "This config var determines the memory threshold in bytes before Results' SQLList offloads data to SQLite.",
},
"EDSL_SHOW_FULL_TRACEBACK": {
"default": "False",
"info": "This config var determines whether to show full tracebacks for exceptions. Set to True for development/debugging.",
},
}


Expand Down
39 changes: 30 additions & 9 deletions edsl/results/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,8 @@ def filter(self, expression: str) -> Results:
Args:
expression: A string containing a Python expression that evaluates to a boolean.
The expression is applied to each Result object individually.
Can be a multi-line string for better readability.
Supports template-style syntax with {{ field }} notation.

Returns:
A new Results object containing only the Result objects that satisfy the expression.
Expand All @@ -1631,6 +1633,8 @@ def filter(self, expression: str) -> Results:
- You can use comparison operators like '==', '!=', '>', '<', '>=', '<='
- You can use membership tests with 'in'
- You can use string methods like '.startswith()', '.contains()', etc.
- The expression can be a multi-line string for improved readability
- You can use template-style syntax with double curly braces: {{ field }}

Examples:
>>> r = Results.example()
Expand All @@ -1647,14 +1651,31 @@ def filter(self, expression: str) -> Results:
>>> r.filter("agent.status == 'Joyful'").select('agent.status')
Dataset([{'agent.status': ['Joyful', 'Joyful']}])

>>> # Using multi-line string for complex conditions
>>> r.filter('''
... how_feeling == 'Great'
... or how_feeling == 'Terrible'
... ''').select('how_feeling')
Dataset([{'answer.how_feeling': ['Great', 'Terrible']}])

>>> # Using template-style syntax with {{}}
>>> r.filter("{{ answer.how_feeling }} == 'Great'").select('how_feeling')
Dataset([{'answer.how_feeling': ['Great']}])

>>> # Common error: using = instead of ==
>>> try:
... r.filter("how_feeling = 'Great'")
... except Exception as e:
... print("ResultsFilterError: You must use '==' instead of '=' in the filter expression.")
ResultsFilterError: You must use '==' instead of '=' in the filter expression.
"""
if self.has_single_equals(expression):
# Normalize expression by removing extra whitespace and newlines
normalized_expression = ' '.join(expression.strip().split())

# Remove template-style syntax (double curly braces)
normalized_expression = normalized_expression.replace('{{', '').replace('}}', '')

if self.has_single_equals(normalized_expression):
raise ResultsFilterError(
"You must use '==' instead of '=' in the filter expression."
)
Expand All @@ -1671,8 +1692,8 @@ def filter(self, expression: str) -> Results:
# Process one result at a time
for result in self.data:
evaluator = self._create_evaluator(result)
result.check_expression(expression) # check expression
if evaluator.eval(expression):
result.check_expression(normalized_expression) # check expression
if evaluator.eval(normalized_expression):
filtered_results.append(
result
) # Use append method to add matching results
Expand All @@ -1692,12 +1713,12 @@ def filter(self, expression: str) -> Results:
)
except Exception as e:
raise ResultsFilterError(
f"""Error in filter. Exception:{e}.""",
f"""The expression you provided was: {expression}.""",
"""Please make sure that the expression is a valid Python expression that evaluates to a boolean.""",
"""For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'., """,
"""However, 'how_feeling = "Great"' is not a valid expression.""",
"""See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.""",
f"Error in filter. Exception:{e}.",
f"The expression you provided was: {expression}.",
"Please make sure that the expression is a valid Python expression that evaluates to a boolean.",
"For example, 'how_feeling == \"Great\"' is a valid expression, as is 'how_feeling in [\"Great\", \"Terrible\"]'.",
"However, 'how_feeling = \"Great\"' is not a valid expression.",
"See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.",
)

@classmethod
Expand Down
30 changes: 30 additions & 0 deletions failing_multiline_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from edsl import Results

def test_multiline_filter():
r = Results.example()
# Test the original failing case with multi-line strings and template syntax
filtered_results = r.filter("""
{{ answer.how_feeling}} == 'OK'
or {{ answer.how_feeling}} == 'Good'
""")

# Verify that only results with "OK" or "Good" feelings are returned
for result in filtered_results:
assert result["answer"]["how_feeling"] == "OK" or result["answer"]["how_feeling"] == "Good"

# Test with multi-line strings but without template syntax
filtered_results2 = r.filter("""
how_feeling == 'OK'
or how_feeling == 'Good'
""")

# Verify that both filters produce the same result
assert len(filtered_results) == len(filtered_results2)
assert set(hash(result) for result in filtered_results) == set(hash(result) for result in filtered_results2)

print("All tests passed!")
return filtered_results

# Run the tests and print the result
result = test_multiline_filter()
print(result)
58 changes: 58 additions & 0 deletions tests/base/test_BaseException_traceback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Tests for the BaseException traceback environment variable feature.
"""
import os
import sys
import pytest
from unittest.mock import patch
from edsl.base.base_exception import BaseException

class TestExampleException(BaseException):
"""An example exception for testing purposes."""
pass

def test_should_show_full_traceback_env_var():
"""Test that _should_show_full_traceback respects environment variable."""
# Test with env var set to True
with patch.dict(os.environ, {"EDSL_SHOW_FULL_TRACEBACK": "True"}):
assert TestExampleException._should_show_full_traceback() is True

# Test with env var set to False
with patch.dict(os.environ, {"EDSL_SHOW_FULL_TRACEBACK": "False"}):
assert TestExampleException._should_show_full_traceback() is False

# Test with env var set to other values that should be True
for true_value in ["true", "1", "yes", "y"]:
with patch.dict(os.environ, {"EDSL_SHOW_FULL_TRACEBACK": true_value}):
assert TestExampleException._should_show_full_traceback() is True

def test_should_show_full_traceback_env_from_config():
"""Test that _should_show_full_traceback respects environment variable values that would come from config."""
# Remove env var to ensure we're testing with a clean environment
with patch.dict(os.environ, clear=True):
# Test with env var set to True (as if populated from config)
with patch.dict(os.environ, {"EDSL_SHOW_FULL_TRACEBACK": "True"}):
assert TestExampleException._should_show_full_traceback() is True

# Test with env var set to False (as if populated from config)
with patch.dict(os.environ, {"EDSL_SHOW_FULL_TRACEBACK": "False"}):
assert TestExampleException._should_show_full_traceback() is False

def test_should_show_full_traceback_default():
"""Test that _should_show_full_traceback falls back to class attribute."""
# Remove env var to ensure we're testing the class attribute path
with patch.dict(os.environ, clear=True):
# Test with suppress_traceback=True (default)
assert TestExampleException._should_show_full_traceback() is False

# Test with suppress_traceback=False
original_value = TestExampleException.suppress_traceback
TestExampleException.suppress_traceback = False
try:
assert TestExampleException._should_show_full_traceback() is True
finally:
# Restore original value
TestExampleException.suppress_traceback = original_value

if __name__ == "__main__":
pytest.main(["-xvs", __file__])
26 changes: 26 additions & 0 deletions tests/results/test_Results.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ def test_filter(self):
.first(),
first_answer,
)

def test_multiline_filter(self):
"""Test multi-line filter support and template syntax."""
r = Results.example()

# Count the number of results with OK and Great feelings
ok_count = len(r.filter("how_feeling == 'OK'"))
great_count = len(r.filter("how_feeling == 'Great'"))

# Test multi-line filter
f1 = r.filter("""
how_feeling == 'OK'
or how_feeling == 'Great'
""")
self.assertEqual(len(f1), ok_count + great_count)

# Test template-style syntax with double curly braces
f2 = r.filter("{{ answer.how_feeling }} == 'OK'")
self.assertEqual(len(f2), ok_count)

# Test combination of multi-line and template syntax
f3 = r.filter("""
{{ answer.how_feeling }} == 'OK'
or {{ answer.how_feeling }} == 'Great'
""")
self.assertEqual(len(f3), ok_count + great_count)

def test_relevant_columns(self):
self.assertIn("answer.how_feeling", self.example_results.relevant_columns())
Expand Down