Repository navigation
feat(exceptions): expose __cause__ as original_exception - #1362
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
Conversation
Co-Authored-By: AJ Steers <aj@airbyte.io>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This PyAirbyte VersionYou can test this version of PyAirbyte using the following: # Run PyAirbyte CLI from this branch:
uvx --from 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1791839092-original-exception-cause' pyairbyte --help
# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1791839092-original-exception-cause'PR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
📚 Show Repo GuidanceHelpful ResourcesCommunity SupportQuestions? Join the #pyairbyte channel in our Slack workspace. |
Code Coverage OverviewLanguages: Python Python / code-coverage/pytest-no-credsThe overall line coverage in commit 37377fe in the Show a line coverage summary of the most impacted files.
Python / code-coverage/pytestThe overall line coverage in commit 37377fe in the Show a line coverage summary of the most impacted files.
Python / code-coverage/pytest-unitThe overall line coverage in commit 37377fe in the Show a line coverage summary of the most covered files.
Updated |
There was a problem hiding this comment.
🟡 Changes recommended
The inherited InitVar breaks downstream subclasses, remains inaccessible to type checkers, and enables unsafe raw cause traversal.
3 open findings
What changed in this PR
Exposes Python exception causes through AirbyteLibError.original_exception while preserving opt-in string rendering.
Changes:
- Maps
original_exceptionto__cause__. - Updates five raise sites to retain explicit causes.
- Adds tests for cause access, rendering, subclasses, and sanitization.
| File | Description |
|---|---|
airbyte/exceptions.py |
Implements cause-backed exception access and rendering. |
airbyte/_connector_base.py |
Preserves causes when wrapping connector failures. |
airbyte/destinations/base.py |
Preserves destination write failure causes. |
airbyte/secrets/hydration.py |
Preserves mask-parsing failure causes. |
airbyte/sources/util.py |
Preserves invalid-number parsing causes. |
tests/unit_tests/test_exceptions.py |
Tests cause-backed exception behavior. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| def test_sanitize_error_walks_raise_from_chain() -> None: | ||
| inner = AirbyteCloudError(message="inner") | ||
|
|
||
| with pytest.raises(AirbyteCloudError) as exc_info: | ||
| raise AirbyteCloudError(message="outer") from inner | ||
|
|
||
| assert _sanitize_error(exc_info.value) == "AirbyteCloudError: inner" |
There was a problem hiding this comment.
☑️ Resolved in 1a731d0. Valid. _sanitize_error and _extract_trace_error_from_log now walk only causes passed explicitly via original_exception= (_get_opted_in_original_exception), which matches pre-PR behavior. A new test covers a plain raise ... from ValueError("password=hunter2") and checks that hunter2 is absent from both _sanitize_error() and str(). Follow-up 37377fe also keeps causes out of repr(), which the dataclass-generated __repr__ was leaking the same way.
| context: dict[str, Any] | None = None | ||
| message: str | None = None | ||
| original_exception: Exception | None = None | ||
| original_exception: InitVar[BaseException | None] = None |
There was a problem hiding this comment.
☑️ Resolved in 1a731d0. Valid. I dropped the InitVar and made original_exception a typed descriptor field (_CauseDescriptor), so the generated __init__ calls __post_init__() with no arguments again. The three in-tree overrides are back to def __post_init__(self) -> None. A new test covers an external @dataclass subclass with a no-arg __post_init__, both with and without the kwarg.
| # Attach after dataclass creation so the property does not become the InitVar default. | ||
| AirbyteLibError.original_exception = property(_get_original_exception) |
There was a problem hiding this comment.
☑️ Resolved in 1a731d0. Valid. The descriptor's overloaded __get__ makes err.original_exception type as BaseException | None (and as None on the class). The module-level property(...) attachment is gone, and the tests now read err.original_exception directly with no getattr. Pyrefly: 0 errors.
…use traversal opt-in Co-Authored-By: AJ Steers <aj@airbyte.io>
Co-Authored-By: AJ Steers <aj@airbyte.io>



Overview
👉 TL;DR: Lets code read the exception an Airbyte error was raised from through one standard place, without changing what those errors print.
Specifically,
AirbyteLibError.original_exceptionis now a read-only view of__cause__.str()still printsCaused by:only when a call site opts in by passingoriginal_exception=.Changes
original_exceptionis now a typed descriptor field (_CauseDescriptor,repr=False). The constructor kwarg still works: it sets__cause__and marks the cause as opted in. Readingerr.original_exceptionreturnserr.__cause__, so a plainraise X from enow exposesetoo. Subclass__post_init__signatures are unchanged.__str__rendersCaused by:only for an opted-in cause, which keeps today's output. Causes are never inrepr().destination_smoke_tests._sanitize_errorand_extract_trace_error_from_logwalk only opted-in causes, as before this PR.original_exception=switch fromfrom Nonetofrom ex, becausefrom Nonewould reset__cause__._executors/base.pyis unchanged: it passes an exception captured on another thread.tests/unit_tests/test_exceptions.py.Review Spotlight
Reviewers with limited time, please review first:
exceptions.py_CauseDescriptor: cause view + opt-in flagdestination_smoke_tests.py_get_opted_in_original_exception: opt-in-only chain walkRisks
from Nonehid it before. Theirstr()already included it viaCaused by:.repr()no longer shows an explicitly passedoriginal_exception(it did before). That keeps plainfrom ecauses, which may quote secrets, out ofrepr().test_create_connection_400_raises_input_error_without_api_messageandtest_validate_config_never_repeats_a_hydrated_secret. Printing every__cause__would have broken both (raw API body, hydrated secret), which is why rendering stays opt-in.Follow-ups
Link to Devin session: https://app.devin.ai/sessions/ee7d810f00b143079bbd6e552e9ec4c7
Open in Devin Desktop: https://app.devin.ai/desktop/session/ee7d810f00b143079bbd6e552e9ec4c7?variant=devin
Requested by: Aaron ("AJ") Steers (@aaronsteers)