-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Handle internal exceptions in postflight (#7242)
- Loading branch information
Showing
17 changed files
with
281 additions
and
177 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
kind: Fixes | ||
body: Handle internal exceptions | ||
time: 2023-03-29T11:32:03.259072-05:00 | ||
custom: | ||
Author: stu-k | ||
Issue: "7118" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,49 @@ | ||
TODO | ||
# Exception Handling | ||
|
||
## `requires.py` | ||
|
||
### `postflight` | ||
In the postflight decorator, the click command is invoked (i.e. `func(*args, **kwargs)`) and wrapped in a `try/except` block to handle any exceptions thrown. | ||
Any exceptions thrown from `postflight` are wrapped by custom exceptions from the `dbt.cli.exceptions` module (i.e. `ResultExit`, `ExceptionExit`) to instruct click to complete execution with a particular exit code. | ||
|
||
Some `dbt-core` handled exceptions have an attribute named `results` which contains results from running nodes (e.g. `FailFastError`). These are wrapped in the `ResultExit` exception to represent runs that have failed in a way that `dbt-core` expects. | ||
If the invocation of the command does not throw any exceptions but does not succeed, `postflight` will still raise the `ResultExit` exception to make use of the exit code. | ||
These exceptions produce an exit code of `1`. | ||
|
||
Exceptions wrapped with `ExceptionExit` may be thrown by `dbt-core` intentionally (i.e. an exception that inherits from `dbt.exceptions.Exception`) or unintentionally (i.e. exceptions thrown by the python runtime). In either case these are considered errors that `dbt-core` did not expect and are treated as genuine exceptions. | ||
These exceptions produce an exit code of `2`. | ||
|
||
If no exceptions are thrown from invoking the command and the command succeeds, `postflight` will not raise any exceptions. | ||
When no exceptions are raised an exit code of `0` is produced. | ||
|
||
## `main.py` | ||
|
||
### `dbtRunner` | ||
`dbtRunner` provides a programmatic interface for our click CLI and wraps the invocation of the click commands to handle any exceptions thrown. | ||
|
||
`dbtRunner.invoke` should ideally only ever return an instantiated `dbtRunnerResult` which contains the following fields: | ||
- `success`: A boolean representing whether the command invocation was successful | ||
- `result`: The optional result of the command invoked. This attribute can have many types, please see the definition of `dbtRunnerResult` for more information | ||
- `exception`: If an exception was thrown during command invocation it will be saved here, otherwise it will be `None`. Please note that the exceptions held in this attribute are not the exceptions thrown by `preflight` but instead the exceptions that `ResultExit` and `ExceptionExit` wrap | ||
|
||
Programmatic exception handling might look like the following: | ||
```python | ||
res = dbtRunner().invoke(["run"]) | ||
if not res.success: | ||
... | ||
if type(res.exception) == SomeExceptionType: | ||
... | ||
``` | ||
|
||
## `dbt/tests/util.py` | ||
|
||
### `run_dbt` | ||
In many of our functional and integration tests, we want to be sure that an invocation of `dbt` raises a certain exception. | ||
A common pattern for these assertions: | ||
```python | ||
class TestSomething: | ||
def test_something(self, project): | ||
with pytest.raises(SomeException): | ||
run_dbt(["run"]) | ||
``` | ||
To allow these tests to assert that exceptions have been thrown, the `run_dbt` function will raise any exceptions it recieves from the invocation of a `dbt` command. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from typing import Optional, IO | ||
|
||
from click.exceptions import ClickException | ||
from dbt.utils import ExitCodes | ||
|
||
|
||
class DbtUsageException(Exception): | ||
pass | ||
|
||
|
||
class DbtInternalException(Exception): | ||
pass | ||
|
||
|
||
class CliException(ClickException): | ||
"""The base exception class for our implementation of the click CLI. | ||
The exit_code attribute is used by click to determine which exit code to produce | ||
after an invocation.""" | ||
|
||
def __init__(self, exit_code: ExitCodes) -> None: | ||
self.exit_code = exit_code.value | ||
|
||
# the typing of _file is to satisfy the signature of ClickException.show | ||
# overriding this method prevents click from printing any exceptions to stdout | ||
def show(self, _file: Optional[IO] = None) -> None: | ||
pass | ||
|
||
|
||
class ResultExit(CliException): | ||
"""This class wraps any exception that contains results while invoking dbt, or the | ||
results of an invocation that did not succeed but did not throw any exceptions.""" | ||
|
||
def __init__(self, result) -> None: | ||
super().__init__(ExitCodes.ModelError) | ||
self.result = result | ||
|
||
|
||
class ExceptionExit(CliException): | ||
"""This class wraps any exception that does not contain results thrown while invoking dbt.""" | ||
|
||
def __init__(self, exception: Exception) -> None: | ||
super().__init__(ExitCodes.UnhandledError) | ||
self.exception = exception |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.