Skip to content

feat(exceptions): expose __cause__ as original_exception - #1362

Draft
Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
release-candidate/v1from
devin/1791839092-original-exception-cause
Draft

Aaron ("AJ") Steers (aaronsteers) wants to merge 3 commits into
release-candidate/v1from
devin/1791839092-original-exception-cause

Conversation

@aaronsteers

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

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_exception is now a read-only view of __cause__. str() still prints Caused by: only when a call site opts in by passing original_exception=.

Changes

  • original_exception is now a typed descriptor field (_CauseDescriptor, repr=False). The constructor kwarg still works: it sets __cause__ and marks the cause as opted in. Reading err.original_exception returns err.__cause__, so a plain raise X from e now exposes e too. Subclass __post_init__ signatures are unchanged.
  • __str__ renders Caused by: only for an opted-in cause, which keeps today's output. Causes are never in repr().
  • destination_smoke_tests._sanitize_error and _extract_trace_error_from_log walk only opted-in causes, as before this PR.
  • The 5 call sites that pass original_exception= switch from from None to from ex, because from None would reset __cause__. _executors/base.py is unchanged: it passes an exception captured on another thread.
  • New unit tests in tests/unit_tests/test_exceptions.py.

Review Spotlight

Reviewers with limited time, please review first:

Risks

  • The 5 switched call sites now show the inner exception in tracebacks ("direct cause of"), where from None hid it before. Their str() already included it via Caused by:.
  • repr() no longer shows an explicitly passed original_exception (it did before). That keeps plain from e causes, which may quote secrets, out of repr().
  • Two existing safety tests are unchanged and pass: test_create_connection_400_raises_input_error_without_api_message and test_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)

Co-Authored-By: AJ Steers <aj@airbyte.io>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This PyAirbyte Version

You 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 Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /fix-pr - Fixes most formatting and linting issues
  • /uv-lock - Updates uv.lock file
  • /test-pr - Runs tests with the updated PyAirbyte
  • /prerelease - Builds and publishes a prerelease version to PyPI
📚 Show Repo Guidance

Helpful Resources

Community Support

Questions? Join the #pyairbyte channel in our Slack workspace.

📝 Edit this welcome message.

@github-code-quality

github-code-quality Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-no-creds

The overall line coverage in commit 37377fe in the devin/1791839092-ori... branch is 79%. The line coverage in commit d9f652f in the release-candidate/v1 branch is 65%.

Show a line coverage summary of the most impacted files.
File release-candidate/v1 d9f652f devin/1791839092-ori... 37377fe +/-
airbyte/mcp/cloud.py 52% 83% +31%
airbyte/_util/api_util.py 36% 68% +32%
airbyte/cloud/connectors.py 34% 75% +41%
airbyte/mcp/_datadog.py 0% 50% +50%
airbyte/mcp/_error_handling.py 0% 89% +89%
airbyte/_direct...ctors/models.py 0% 94% +94%
airbyte/mcp/_telemetry.py 0% 94% +94%
airbyte/mcp/_sso_auth.py 0% 96% +96%
airbyte/cloud/models.py 0% 97% +97%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%

Python / code-coverage/pytest

The overall line coverage in commit 37377fe in the devin/1791839092-ori... branch is 81%. The line coverage in commit d9f652f in the release-candidate/v1 branch is 71%.

Show a line coverage summary of the most impacted files.
File release-candidate/v1 d9f652f devin/1791839092-ori... 37377fe +/-
airbyte/mcp/cloud.py 52% 83% +31%
airbyte/mcp/_datadog.py 0% 50% +50%
airbyte/mcp/_error_handling.py 0% 89% +89%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/mcp/_user_identity.py 0% 93% +93%
airbyte/_direct...ctors/models.py 0% 94% +94%
airbyte/mcp/_telemetry.py 0% 94% +94%
airbyte/mcp/_sso_auth.py 0% 96% +96%
airbyte/cloud/models.py 0% 97% +97%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%

Python / code-coverage/pytest-unit

The overall line coverage in commit 37377fe in the devin/1791839092-ori... branch is 76%. Line coverage data for the release-candidate/v1 branch is not yet available.

Show a line coverage summary of the most covered files.
File release-candidate/v1 devin/1791839092-ori... 37377fe +/-
airbyte/cloud/models.py — 97% —
airbyte/mcp/_sso_auth.py — 96% —
airbyte/_direct...ctors/models.py — 94% —
airbyte/exceptions.py — 91% —
airbyte/cloud/client.py — 90% —
airbyte/mcp/cloud.py — 83% —
airbyte/progress.py — 80% —
airbyte/cloud/connectors.py — 75% —
airbyte/_util/api_util.py — 68% —
airbyte/shared/...ql_processor.py — 68% —

Updated October 12, 2026 21:59 UTC

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_exception to __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.

Comment thread tests/unit_tests/test_exceptions.py Outdated
Comment on lines +200 to +206
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ 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.

Written by Devin

Comment thread airbyte/exceptions.py Outdated
context: dict[str, Any] | None = None
message: str | None = None
original_exception: Exception | None = None
original_exception: InitVar[BaseException | None] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ 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.

Written by Devin

Comment thread airbyte/exceptions.py Outdated
Comment on lines +786 to +787
# Attach after dataclass creation so the property does not become the InitVar default.
AirbyteLibError.original_exception = property(_get_original_exception)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☑️ 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.

Written by Devin

devin-ai-integration Bot and others added 2 commits October 12, 2026 21:43
…use traversal opt-in

Co-Authored-By: AJ Steers <aj@airbyte.io>
Co-Authored-By: AJ Steers <aj@airbyte.io>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants