Skip to content
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

Remove dbt-core dependency #81

Merged
merged 12 commits into from
Oct 31, 2024
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to

## [Unreleased]

- **Breaking**: The rule `public_model_has_example_sql` has been renamed
`has_example_sql` and applies by default to all models.
- **Breaking**: Remove `dbt-core` from dependencies. Since it is not mandatory
for `dbt-score` to execute `dbt`, remove the dependency.
jochemvandooren marked this conversation as resolved.
Show resolved Hide resolved
- **Breaking**: Stop using `MultiOption` selection type.

## [0.6.0] - 2024-08-23

- **Breaking**: Improve error handling in CLI. Log messages are written in
Expand Down
4 changes: 2 additions & 2 deletions docs/get_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Installation of `dbt-score` is simple:
pip install dbt-score
```

If a virtual environment is used to run dbt, make sure to install `dbt-score` in
the same environment.
In order to run `dbt-score` with all its features, be sure to install
`dbt-score` in the same environment as `dbt-core`.

## Usage

Expand Down
1,446 changes: 811 additions & 635 deletions pdm.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ classifiers = [
]

dependencies = [
"dbt-core>=1.5",
jochemvandooren marked this conversation as resolved.
Show resolved Hide resolved
"click>=7.1.1, <9.0.0",
"tomli>=1.1.0; python_version<'3.11'",
]
Expand All @@ -37,6 +36,7 @@ dbt-score = "dbt_score.__main__:main"
[tool.pdm]
[tool.pdm.dev-dependencies]
dev = [
"dbt-core>=1.5",
"tox-pdm~=0.7.2",
"tox~=4.13",
]
Expand Down
9 changes: 5 additions & 4 deletions src/dbt_score/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

import click
from click.core import ParameterSource
from dbt.cli.options import MultiOption

from dbt_score.config import Config
from dbt_score.dbt_utils import DbtParseException, dbt_parse, get_default_manifest_path
from dbt_score.dbt_utils import (
DbtParseException,
dbt_parse,
get_default_manifest_path,
)
from dbt_score.lint import lint_dbt_project
from dbt_score.rule_catalog import display_catalog

Expand Down Expand Up @@ -48,8 +51,6 @@ def cli() -> None:
"--select",
"-s",
help="Specify the nodes to include.",
cls=MultiOption,
type=tuple,
multiple=True,
)
@click.option(
Expand Down
37 changes: 32 additions & 5 deletions src/dbt_score/dbt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import contextlib
import os
from functools import wraps
from pathlib import Path
from typing import Iterable, Iterator, cast
from typing import Any, Callable, Iterable, Iterator, cast

from dbt.cli.main import dbtRunner, dbtRunnerResult
# Conditionally import dbt objects.
try:
DBT_INSTALLED = True
from dbt.cli.main import dbtRunner, dbtRunnerResult # type: ignore
except ImportError:
DBT_INSTALLED = False


class DbtNotInstalledException(Exception):
"""Raised when trying to run dbt when dbt is not installed."""


class DbtParseException(Exception):
Expand All @@ -16,13 +26,29 @@ class DbtLsException(Exception):
"""Raised when dbt ls fails."""


def dbt_required(func: Callable[..., Any]) -> Callable[..., Any]:
"""Decorator for methods that require dbt to be installed."""

@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if not DBT_INSTALLED:
raise DbtNotInstalledException(
"This option requires dbt to be installed in the same Python"
"environment as dbt-score."
)
return func(*args, **kwargs)

return wrapper


@contextlib.contextmanager
def _disable_dbt_stdout() -> Iterator[None]:
with contextlib.redirect_stdout(None):
yield


def dbt_parse() -> dbtRunnerResult:
@dbt_required
def dbt_parse() -> "dbtRunnerResult":
"""Parse a dbt project.

Returns:
Expand All @@ -32,22 +58,23 @@ def dbt_parse() -> dbtRunnerResult:
DbtParseException: dbt parse failed.
"""
with _disable_dbt_stdout():
result: dbtRunnerResult = dbtRunner().invoke(["parse"])
result: "dbtRunnerResult" = dbtRunner().invoke(["parse"])

if not result.success:
raise DbtParseException("dbt parse failed.") from result.exception

return result


@dbt_required
def dbt_ls(select: Iterable[str] | None) -> Iterable[str]:
"""Run dbt ls."""
cmd = ["ls", "--resource-type", "model", "--output", "name"]
if select:
cmd += ["--select", *select]

with _disable_dbt_stdout():
result: dbtRunnerResult = dbtRunner().invoke(cmd)
result: "dbtRunnerResult" = dbtRunner().invoke(cmd)

if not result.success:
raise DbtLsException("dbt ls failed.") from result.exception
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ def test_lint_dbt_parse_exception(caplog):
assert "dbt failed to parse project" in caplog.text


def test_lint_dbt_not_installed(caplog, manifest_path):
"""Test lint with a valid manifest when dbt is not installed."""
runner = CliRunner()

with patch("dbt_score.dbt_utils.DBT_INSTALLED", new=False):
result = runner.invoke(lint, ["-m", manifest_path], catch_exceptions=False)
assert result.exit_code == 0


def test_lint_dbt_not_installed_v(caplog):
"""Test lint with dbt parse when dbt is not installed."""
runner = CliRunner()

with patch("dbt_score.dbt_utils.DBT_INSTALLED", new=False):
result = runner.invoke(lint, ["-p"])
assert result.exit_code == 2
assert "DbtNotInstalledException" in caplog.text


def test_lint_other_exception(manifest_path, caplog):
"""Test lint with an unexpected error."""
runner = CliRunner()
Expand Down