Commit d01f89a
authored
Make WorkspaceClient.dbutils lazy via cached_property (#1470)
## Summary
Makes `WorkspaceClient.dbutils` a `functools.cached_property` so
consumers that never read it pay no construction cost — and, on Spark
Connect runtimes, never touch the legacy `SparkContext` path that
`databricks.sdk.runtime` materializes on import. Includes four
regression tests that lock in the contract.
## Why
`WorkspaceClient.__init__` used to call `_make_dbutils(self._config)`
eagerly, which on a cluster imports `databricks.sdk.runtime`. On a Spark
Connect (shared-access-mode) cluster, that import materializes a legacy
`SparkContext` and raises `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT`,
crashing the constructor before any API call. Downstream consumers that
never touch `.dbutils` (notably `dbt-databricks` Python models) hit this
for no reason — see #1463 and databricks/dbt-databricks#1252.
#1469 patches the runtime side as a defense-in-depth fallback (catch the
materialization failure, fall back to `RemoteDbUtils`). This PR is the
durable fix: callers that don't read `.dbutils` never trigger the build
at all, sidestepping the entire code path. The first read still calls
`_make_dbutils` once, lazily; subsequent reads hit the cached attribute
in `__dict__` at plain-attribute speed.
## What changed
`databricks/sdk/__init__.py` (generated from updated template):
- `from functools import cached_property` added to the imports.
- The eager `self._dbutils = _make_dbutils(self._config)` line is
removed from `__init__`.
- `@property def dbutils` (which returned the cached `self._dbutils`)
becomes `@cached_property def dbutils` that calls
`_make_dbutils(self._config)` on first access.
`_dbutils` was a private attribute with no external consumers (verified
across the codebase), so removing it does not break any public surface.
`tests/test_client.py` — four new tests:
- `test_dbutils_is_a_cached_property` — descriptor type check.
- `test_workspace_client_init_does_not_build_dbutils` — spies
`_make_dbutils`, constructs a `WorkspaceClient`, asserts the spy was
never called.
- `test_dbutils_first_access_builds_exactly_once` — first read invokes
`_make_dbutils` once (returns the spy's sentinel); second read still
shows `call_count == 1` and same identity.
-
`test_workspace_client_constructs_on_spark_connect_without_touching_runtime`
— fakes `dbruntime` to raise `CONTEXT_UNAVAILABLE_FOR_REMOTE_CLIENT` on
any namespace materialization; asserts `WorkspaceClient(config=...)`
succeeds and `databricks.sdk.runtime` is never imported during
construction. This is the strongest evidence that the dbt-databricks
failure mode is sidestepped by this change alone.
## How is this tested?
- 4/4 new tests pass locally (0.03s).
- Existing `tests/test_client.py` autospec tests untouched, still pass.
- The fourth test is the negative-space proof: asserts
`databricks.sdk.runtime` is *not* in `sys.modules` after
`WorkspaceClient(config=...)` — i.e., the constructor literally does not
reach for the runtime module.
NO_CHANGELOG=true1 parent c05cc6f commit d01f89a
2 files changed
Lines changed: 71 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
1 | 4 | | |
2 | 5 | | |
3 | 6 | | |
| |||
15 | 18 | | |
16 | 19 | | |
17 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
0 commit comments