dbx-patch 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbx_patch/__about__.py +2 -0
- dbx_patch/__init__.py +41 -0
- dbx_patch/base_patch.py +223 -0
- dbx_patch/cli.py +44 -0
- dbx_patch/install_sitecustomize.py +286 -0
- dbx_patch/models.py +94 -0
- dbx_patch/patch_dbx.py +428 -0
- dbx_patch/patches/__init__.py +1 -0
- dbx_patch/patches/autoreload_hook_patch.py +290 -0
- dbx_patch/patches/post_import_hook_verify.py +95 -0
- dbx_patch/patches/python_path_hook_patch.py +189 -0
- dbx_patch/patches/sys_path_init_patch.py +182 -0
- dbx_patch/patches/wsfs_import_hook_patch.py +406 -0
- dbx_patch/patches/wsfs_path_finder_patch.py +161 -0
- dbx_patch/pth_processor.py +351 -0
- dbx_patch/py.typed +0 -0
- dbx_patch/utils/logger.py +200 -0
- dbx_patch/utils/runtime_version.py +95 -0
- dbx_patch-0.1.0.dist-info/METADATA +133 -0
- dbx_patch-0.1.0.dist-info/RECORD +23 -0
- dbx_patch-0.1.0.dist-info/WHEEL +4 -0
- dbx_patch-0.1.0.dist-info/entry_points.txt +2 -0
- dbx_patch-0.1.0.dist-info/licenses/LICENSE +21 -0
dbx_patch/__about__.py
ADDED
dbx_patch/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dbx_patch.__about__ import __version__
|
|
2
|
+
from dbx_patch.install_sitecustomize import (
|
|
3
|
+
check_sitecustomize_status,
|
|
4
|
+
install_sitecustomize,
|
|
5
|
+
uninstall_sitecustomize,
|
|
6
|
+
)
|
|
7
|
+
from dbx_patch.models import (
|
|
8
|
+
ApplyPatchesResult,
|
|
9
|
+
PatchResult,
|
|
10
|
+
PthProcessingResult,
|
|
11
|
+
RemovePatchesResult,
|
|
12
|
+
SitecustomizeStatus,
|
|
13
|
+
StatusResult,
|
|
14
|
+
VerifyResult,
|
|
15
|
+
)
|
|
16
|
+
from dbx_patch.patch_dbx import (
|
|
17
|
+
check_patch_status,
|
|
18
|
+
patch_and_install,
|
|
19
|
+
patch_dbx,
|
|
20
|
+
remove_all_patches,
|
|
21
|
+
verify_editable_installs,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"check_patch_status",
|
|
27
|
+
"remove_all_patches",
|
|
28
|
+
"verify_editable_installs",
|
|
29
|
+
"install_sitecustomize",
|
|
30
|
+
"uninstall_sitecustomize",
|
|
31
|
+
"check_sitecustomize_status",
|
|
32
|
+
"patch_dbx",
|
|
33
|
+
"patch_and_install",
|
|
34
|
+
"ApplyPatchesResult",
|
|
35
|
+
"PatchResult",
|
|
36
|
+
"PthProcessingResult",
|
|
37
|
+
"RemovePatchesResult",
|
|
38
|
+
"SitecustomizeStatus",
|
|
39
|
+
"StatusResult",
|
|
40
|
+
"VerifyResult",
|
|
41
|
+
]
|
dbx_patch/base_patch.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Base classes for DBX-Patch interface.
|
|
2
|
+
|
|
3
|
+
Provides abstract base classes and singleton metaclass for all patches.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABCMeta, abstractmethod
|
|
7
|
+
import contextlib
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from dbx_patch.models import PatchResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SingletonMeta(ABCMeta):
|
|
15
|
+
"""Thread-safe singleton metaclass for patch classes.
|
|
16
|
+
|
|
17
|
+
Ensures only one instance of each patch class exists, with thread-safe
|
|
18
|
+
creation in notebook environments. Inherits from ABCMeta to support ABC.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_instances: dict[type, Any] = {}
|
|
22
|
+
_lock: threading.Lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
"""Get or create the singleton instance."""
|
|
26
|
+
if cls not in cls._instances:
|
|
27
|
+
with cls._lock:
|
|
28
|
+
# Double-check pattern
|
|
29
|
+
if cls not in cls._instances:
|
|
30
|
+
instance = super().__call__(*args, **kwargs)
|
|
31
|
+
cls._instances[cls] = instance
|
|
32
|
+
return cls._instances[cls]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def reset_instance(mcs, cls: type) -> None:
|
|
36
|
+
"""Reset singleton instance for testing.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
cls: The class whose singleton instance should be reset
|
|
40
|
+
"""
|
|
41
|
+
with mcs._lock:
|
|
42
|
+
if cls in mcs._instances:
|
|
43
|
+
del mcs._instances[cls]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BasePatch(metaclass=SingletonMeta):
|
|
47
|
+
"""Abstract base class for all Databricks runtime patches.
|
|
48
|
+
|
|
49
|
+
Provides a unified interface for applying, removing, and checking patch status.
|
|
50
|
+
All patches follow the singleton pattern to ensure consistent global state.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
_is_applied: Whether the patch has been applied
|
|
54
|
+
_original_target: Reference to original function/method for restoration
|
|
55
|
+
_cached_editable_paths: Set of cached editable install paths (if applicable)
|
|
56
|
+
_logger: Cached logger instance
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, verbose: bool = True) -> None:
|
|
60
|
+
"""Initialize the patch (called only once due to singleton).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
verbose: Enable verbose logging
|
|
64
|
+
"""
|
|
65
|
+
self._is_applied: bool = False
|
|
66
|
+
self._original_target: Any = None
|
|
67
|
+
self._cached_editable_paths: set[str] = set()
|
|
68
|
+
self._verbose: bool = verbose
|
|
69
|
+
self._logger: Any = None
|
|
70
|
+
|
|
71
|
+
def _get_logger(self) -> Any:
|
|
72
|
+
"""Get lazily-initialized logger instance.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Logger instance or None if unavailable
|
|
76
|
+
"""
|
|
77
|
+
if self._logger is None:
|
|
78
|
+
with contextlib.suppress(Exception):
|
|
79
|
+
from dbx_patch.utils.logger import get_logger
|
|
80
|
+
|
|
81
|
+
self._logger = get_logger()
|
|
82
|
+
return self._logger
|
|
83
|
+
|
|
84
|
+
def _detect_editable_paths(self) -> set[str]:
|
|
85
|
+
"""Detect editable install paths from pth_processor.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Set of absolute paths to editable install directories
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
from dbx_patch.pth_processor import get_editable_install_paths
|
|
92
|
+
|
|
93
|
+
return get_editable_install_paths()
|
|
94
|
+
except Exception:
|
|
95
|
+
return set()
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def patch(self) -> PatchResult:
|
|
99
|
+
"""Apply the patch to the Databricks runtime.
|
|
100
|
+
|
|
101
|
+
This method should:
|
|
102
|
+
1. Check if already applied (return early if so)
|
|
103
|
+
2. Verify target modules/functions exist
|
|
104
|
+
3. Apply the patch (monkey-patch, wrap, register, etc.)
|
|
105
|
+
4. Update internal state (_is_applied, _original_target, etc.)
|
|
106
|
+
5. Return PatchResult with operation details
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
PatchResult indicating success/failure and details
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def remove(self) -> bool:
|
|
115
|
+
"""Remove the patch and restore original behavior.
|
|
116
|
+
|
|
117
|
+
This method should:
|
|
118
|
+
1. Check if patch is applied (return early if not)
|
|
119
|
+
2. Restore original functions/methods from _original_target
|
|
120
|
+
3. Clean up any registered callbacks or hooks
|
|
121
|
+
4. Reset internal state (_is_applied = False, etc.)
|
|
122
|
+
5. Return True if successful, False otherwise
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if patch was removed successfully, False otherwise
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def is_applied(self) -> bool:
|
|
131
|
+
"""Check if the patch is currently applied.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if patch is applied, False otherwise
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def refresh_paths(self) -> int:
|
|
139
|
+
"""Refresh cached editable install paths (optional, override if needed).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Number of editable paths detected
|
|
143
|
+
"""
|
|
144
|
+
self._cached_editable_paths = self._detect_editable_paths()
|
|
145
|
+
return len(self._cached_editable_paths)
|
|
146
|
+
|
|
147
|
+
def get_editable_paths(self) -> set[str]:
|
|
148
|
+
"""Get current cached editable paths (optional, override if needed).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Set of editable install paths
|
|
152
|
+
"""
|
|
153
|
+
return self._cached_editable_paths.copy()
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def reset(cls) -> None:
|
|
157
|
+
"""Reset the singleton instance for testing purposes."""
|
|
158
|
+
SingletonMeta.reset_instance(cls)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class BaseVerification(metaclass=SingletonMeta):
|
|
162
|
+
"""Abstract base class for verification-only patches.
|
|
163
|
+
|
|
164
|
+
Verification patches don't modify runtime behavior - they only check
|
|
165
|
+
compatibility and report findings.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
_is_verified: Whether verification has been performed
|
|
169
|
+
_logger: Cached logger instance
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, verbose: bool = True) -> None:
|
|
173
|
+
"""Initialize the verification (called only once due to singleton).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
verbose: Enable verbose logging
|
|
177
|
+
"""
|
|
178
|
+
self._is_verified: bool = False
|
|
179
|
+
self._verbose: bool = verbose
|
|
180
|
+
self._logger: Any = None
|
|
181
|
+
|
|
182
|
+
def _get_logger(self) -> Any:
|
|
183
|
+
"""Get lazily-initialized logger instance.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Logger instance or None if unavailable
|
|
187
|
+
"""
|
|
188
|
+
if self._logger is None:
|
|
189
|
+
with contextlib.suppress(Exception):
|
|
190
|
+
from dbx_patch.utils.logger import get_logger
|
|
191
|
+
|
|
192
|
+
self._logger = get_logger()
|
|
193
|
+
return self._logger
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
def verify(self) -> PatchResult:
|
|
197
|
+
"""Verify compatibility with Databricks runtime.
|
|
198
|
+
|
|
199
|
+
This method should:
|
|
200
|
+
1. Check if already verified (return early if so)
|
|
201
|
+
2. Import and inspect target modules/hooks
|
|
202
|
+
3. Verify they won't interfere with editable installs
|
|
203
|
+
4. Update internal state (_is_verified = True)
|
|
204
|
+
5. Return PatchResult with verification details
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
PatchResult indicating compatibility status
|
|
208
|
+
"""
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def is_verified(self) -> bool:
|
|
213
|
+
"""Check if verification has been performed.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if verified, False otherwise
|
|
217
|
+
"""
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def reset(cls) -> None:
|
|
222
|
+
"""Reset the singleton instance for testing purposes."""
|
|
223
|
+
SingletonMeta.reset_instance(cls)
|
dbx_patch/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
from dbx_patch.__about__ import __version__
|
|
4
|
+
from dbx_patch.patch_dbx import check_patch_status, patch_dbx, remove_all_patches, verify_editable_installs
|
|
5
|
+
|
|
6
|
+
# Module-level logger
|
|
7
|
+
_logger = None
|
|
8
|
+
with contextlib.suppress(Exception):
|
|
9
|
+
from dbx_patch.utils.logger import get_logger
|
|
10
|
+
|
|
11
|
+
_logger = get_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
if _logger:
|
|
16
|
+
_logger.info(f"dbx-patch v{__version__}!")
|
|
17
|
+
|
|
18
|
+
# Allow running as a script
|
|
19
|
+
import argparse
|
|
20
|
+
|
|
21
|
+
parser = argparse.ArgumentParser(description="DBX-Patch for editable installs")
|
|
22
|
+
parser.add_argument("--apply", action="store_true", help="Apply all patches")
|
|
23
|
+
parser.add_argument("--verify", action="store_true", help="Verify configuration")
|
|
24
|
+
parser.add_argument("--status", action="store_true", help="Check patch status")
|
|
25
|
+
parser.add_argument("--remove", action="store_true", help="Remove all patches")
|
|
26
|
+
parser.add_argument("--quiet", action="store_true", help="Suppress output")
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
if args.apply:
|
|
31
|
+
patch_dbx()
|
|
32
|
+
elif args.verify:
|
|
33
|
+
verify_editable_installs()
|
|
34
|
+
elif args.status:
|
|
35
|
+
check_patch_status()
|
|
36
|
+
elif args.remove:
|
|
37
|
+
remove_all_patches()
|
|
38
|
+
else:
|
|
39
|
+
# Default: apply patches
|
|
40
|
+
patch_dbx()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Install sitecustomize.py to Auto-Apply Patches on Startup.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to install a sitecustomize.py file that
|
|
4
|
+
automatically applies all dbx-patch fixes when Python starts. This solves
|
|
5
|
+
the timing issue where sys_path_init and WsfsImportHook are loaded before
|
|
6
|
+
any notebook code runs.
|
|
7
|
+
|
|
8
|
+
Why sitecustomize.py?
|
|
9
|
+
- Python automatically imports sitecustomize.py during interpreter initialization
|
|
10
|
+
- It runs BEFORE sys_path_init.py and import hooks are installed
|
|
11
|
+
- This is the ONLY way to patch the import system early enough
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from dbx_patch.install_sitecustomize import install_sitecustomize
|
|
15
|
+
install_sitecustomize()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from dbx_patch.models import SitecustomizeStatus
|
|
22
|
+
from dbx_patch.utils.logger import PatchLogger
|
|
23
|
+
|
|
24
|
+
logger = PatchLogger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_site_packages_path() -> Path | None:
|
|
28
|
+
"""Get the first writable site-packages directory.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Path to site-packages directory, or None if not found
|
|
32
|
+
"""
|
|
33
|
+
for path_str in sys.path:
|
|
34
|
+
if "site-packages" in path_str:
|
|
35
|
+
path = Path(path_str)
|
|
36
|
+
if path.exists() and path.is_dir():
|
|
37
|
+
# Check if writable
|
|
38
|
+
try:
|
|
39
|
+
test_file = path / ".dbx_patch_write_test"
|
|
40
|
+
test_file.touch()
|
|
41
|
+
test_file.unlink()
|
|
42
|
+
return path
|
|
43
|
+
except (OSError, PermissionError):
|
|
44
|
+
continue
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_sitecustomize_content() -> str:
|
|
49
|
+
"""Generate the sitecustomize.py content.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Python code to be written to sitecustomize.py
|
|
53
|
+
"""
|
|
54
|
+
return """\"\"\"Auto-apply dbx-patch on Python startup.
|
|
55
|
+
|
|
56
|
+
This file is automatically loaded by Python during interpreter initialization.
|
|
57
|
+
It applies all dbx-patch fixes BEFORE sys_path_init and import hooks are loaded.
|
|
58
|
+
|
|
59
|
+
Generated by: dbx-patch
|
|
60
|
+
DO NOT EDIT MANUALLY - Use dbx_patch.install_sitecustomize() to update
|
|
61
|
+
\"\"\"
|
|
62
|
+
|
|
63
|
+
import sys
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _apply_dbx_patch() -> None:
|
|
67
|
+
\"\"\"Apply dbx-patch fixes silently during startup.\"\"\"
|
|
68
|
+
try:
|
|
69
|
+
# Import and apply all patches
|
|
70
|
+
from dbx_patch.patch_dbx import patch_dbx
|
|
71
|
+
|
|
72
|
+
# Apply silently (no output to avoid cluttering startup)
|
|
73
|
+
patch_dbx(force_refresh=False)
|
|
74
|
+
|
|
75
|
+
except ImportError:
|
|
76
|
+
# dbx-patch not installed, skip silently
|
|
77
|
+
pass
|
|
78
|
+
except Exception as e:
|
|
79
|
+
# Log error but don't break Python startup
|
|
80
|
+
print(f"Warning: dbx-patch auto-apply failed: {e}", file=sys.stderr)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Apply patches immediately on import
|
|
84
|
+
_apply_dbx_patch()
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def install_sitecustomize(force: bool = True, restart_python: bool = True) -> bool:
|
|
89
|
+
"""Install sitecustomize.py to auto-apply patches on Python startup.
|
|
90
|
+
|
|
91
|
+
This is the RECOMMENDED way to use dbx-patch because:
|
|
92
|
+
1. Patches are applied BEFORE sys_path_init runs
|
|
93
|
+
2. Import hooks are patched BEFORE they're installed
|
|
94
|
+
3. No need to manually call patch_dbx() in every notebook
|
|
95
|
+
4. Works automatically for all Python processes on the cluster
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
force: If True, overwrite existing sitecustomize.py
|
|
99
|
+
restart_python: If True, automatically restart Python using dbutils.library.restartPython()
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if installation succeeded, False otherwise
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
# Run once per cluster (e.g., in init script or first notebook):
|
|
106
|
+
from dbx_patch.install_sitecustomize import install_sitecustomize
|
|
107
|
+
install_sitecustomize()
|
|
108
|
+
|
|
109
|
+
# Python will restart automatically if running in Databricks
|
|
110
|
+
# After restart, editable installs will work automatically!
|
|
111
|
+
"""
|
|
112
|
+
with logger.section("Installing sitecustomize.py for auto-apply"):
|
|
113
|
+
# Find site-packages
|
|
114
|
+
site_packages = get_site_packages_path()
|
|
115
|
+
if site_packages is None:
|
|
116
|
+
logger.error("Could not find writable site-packages directory")
|
|
117
|
+
with logger.indent():
|
|
118
|
+
logger.info("Make sure you have write permissions to site-packages")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
sitecustomize_path = site_packages / "sitecustomize.py"
|
|
122
|
+
|
|
123
|
+
# Check if already exists
|
|
124
|
+
if sitecustomize_path.exists() and not force:
|
|
125
|
+
logger.warning(f"sitecustomize.py already exists: {sitecustomize_path}")
|
|
126
|
+
with logger.indent():
|
|
127
|
+
logger.info("Use force=True to overwrite")
|
|
128
|
+
logger.info("Or manually merge the content")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Backup existing file if it exists
|
|
132
|
+
if sitecustomize_path.exists():
|
|
133
|
+
backup_path = site_packages / "sitecustomize.py.backup"
|
|
134
|
+
logger.info(f"Backing up existing file to: {backup_path}")
|
|
135
|
+
try:
|
|
136
|
+
sitecustomize_path.rename(backup_path)
|
|
137
|
+
except OSError as e:
|
|
138
|
+
logger.error(f"Failed to backup existing file: {e}") # noqa: TRY400
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# Write new sitecustomize.py
|
|
142
|
+
try:
|
|
143
|
+
content = get_sitecustomize_content()
|
|
144
|
+
sitecustomize_path.write_text(content, encoding="utf-8")
|
|
145
|
+
|
|
146
|
+
logger.success(f"sitecustomize.py installed: {sitecustomize_path}")
|
|
147
|
+
logger.blank()
|
|
148
|
+
logger.info("✅ Installation complete!")
|
|
149
|
+
|
|
150
|
+
# Try to restart Python automatically if in Databricks environment
|
|
151
|
+
if restart_python:
|
|
152
|
+
logger.blank()
|
|
153
|
+
logger.info("Attempting to restart Python kernel...")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Try to access dbutils (available in Databricks notebooks)
|
|
157
|
+
# dbutils is injected by Databricks and available as a variable
|
|
158
|
+
try:
|
|
159
|
+
logger.info("Restarting Python kernel via dbutils.library.restartPython()...")
|
|
160
|
+
|
|
161
|
+
dbutils.library.restartPython() # ty:ignore[unresolved-reference] # noqa: F821
|
|
162
|
+
|
|
163
|
+
except Exception:
|
|
164
|
+
# Not in Databricks environment
|
|
165
|
+
logger.blank()
|
|
166
|
+
logger.warning("Not running in Databricks environment")
|
|
167
|
+
logger.info("Next steps:")
|
|
168
|
+
with logger.indent():
|
|
169
|
+
logger.info("1. Restart your Python kernel/notebook manually")
|
|
170
|
+
logger.info("2. Editable installs will work automatically")
|
|
171
|
+
logger.info("3. No need to call patch_dbx() anymore!")
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
# Failed to restart, provide manual instructions
|
|
175
|
+
logger.blank()
|
|
176
|
+
logger.warning(f"Could not restart Python automatically: {e}")
|
|
177
|
+
logger.info("Next steps:")
|
|
178
|
+
with logger.indent():
|
|
179
|
+
logger.info("1. Restart your Python kernel/notebook manually")
|
|
180
|
+
logger.info("2. Editable installs will work automatically")
|
|
181
|
+
logger.info("3. No need to call patch_dbx() anymore!")
|
|
182
|
+
else:
|
|
183
|
+
logger.blank()
|
|
184
|
+
logger.info("Next steps:")
|
|
185
|
+
with logger.indent():
|
|
186
|
+
logger.info("1. Restart your Python kernel/notebook")
|
|
187
|
+
logger.info("2. Editable installs will work automatically")
|
|
188
|
+
logger.info("3. No need to call patch_dbx() anymore!")
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
except OSError as e:
|
|
193
|
+
logger.error(f"Failed to write sitecustomize.py: {e}") # noqa: TRY400
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def uninstall_sitecustomize() -> bool:
|
|
198
|
+
"""Remove the auto-apply sitecustomize.py.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if uninstallation succeeded, False otherwise
|
|
202
|
+
"""
|
|
203
|
+
logger.debug("uninstall_sitecustomize() called")
|
|
204
|
+
|
|
205
|
+
with logger.section("Uninstalling sitecustomize.py"):
|
|
206
|
+
site_packages = get_site_packages_path()
|
|
207
|
+
if site_packages is None:
|
|
208
|
+
logger.error("Could not find site-packages directory")
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
sitecustomize_path = site_packages / "sitecustomize.py"
|
|
212
|
+
|
|
213
|
+
if not sitecustomize_path.exists():
|
|
214
|
+
logger.info("sitecustomize.py does not exist, nothing to uninstall")
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
# Check if it's our file
|
|
218
|
+
try:
|
|
219
|
+
content = sitecustomize_path.read_text(encoding="utf-8")
|
|
220
|
+
if "dbx-patch" not in content and "dbx_patch" not in content:
|
|
221
|
+
logger.warning("sitecustomize.py exists but wasn't created by dbx-patch")
|
|
222
|
+
with logger.indent():
|
|
223
|
+
logger.info("Skipping removal for safety")
|
|
224
|
+
logger.info("Manual removal required if needed")
|
|
225
|
+
return False
|
|
226
|
+
except OSError as e:
|
|
227
|
+
logger.error(f"Failed to read sitecustomize.py: {e}") # noqa: TRY400
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Remove the file
|
|
231
|
+
try:
|
|
232
|
+
sitecustomize_path.unlink()
|
|
233
|
+
logger.success("sitecustomize.py removed")
|
|
234
|
+
|
|
235
|
+
# Restore backup if it exists
|
|
236
|
+
backup_path = site_packages / "sitecustomize.py.backup"
|
|
237
|
+
if backup_path.exists():
|
|
238
|
+
backup_path.rename(sitecustomize_path)
|
|
239
|
+
logger.info("Restored backup file")
|
|
240
|
+
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
except OSError as e:
|
|
244
|
+
logger.error(f"Failed to remove sitecustomize.py: {e}") # noqa: TRY400
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def check_sitecustomize_status() -> SitecustomizeStatus:
|
|
249
|
+
"""Check if sitecustomize.py is installed and active.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
SitecustomizeStatus with installation information
|
|
253
|
+
"""
|
|
254
|
+
logger.debug("check_sitecustomize_status() called")
|
|
255
|
+
|
|
256
|
+
site_packages = get_site_packages_path()
|
|
257
|
+
if site_packages is None:
|
|
258
|
+
logger.warning("Could not find site-packages directory")
|
|
259
|
+
return SitecustomizeStatus(
|
|
260
|
+
installed=False,
|
|
261
|
+
path=None,
|
|
262
|
+
is_dbx_patch=False,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
sitecustomize_path = site_packages / "sitecustomize.py"
|
|
266
|
+
installed = sitecustomize_path.exists()
|
|
267
|
+
is_dbx_patch = False
|
|
268
|
+
|
|
269
|
+
if installed:
|
|
270
|
+
try:
|
|
271
|
+
content = sitecustomize_path.read_text(encoding="utf-8")
|
|
272
|
+
is_dbx_patch = "dbx-patch" in content or "dbx_patch" in content
|
|
273
|
+
except OSError:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
logger.info("sitecustomize.py status:")
|
|
277
|
+
with logger.indent():
|
|
278
|
+
logger.info(f"Installed: {installed}")
|
|
279
|
+
logger.info(f"Path: {sitecustomize_path}")
|
|
280
|
+
logger.info(f"Created by dbx-patch: {is_dbx_patch}")
|
|
281
|
+
|
|
282
|
+
return SitecustomizeStatus(
|
|
283
|
+
installed=installed,
|
|
284
|
+
path=str(sitecustomize_path) if sitecustomize_path else None,
|
|
285
|
+
is_dbx_patch=is_dbx_patch,
|
|
286
|
+
)
|
dbx_patch/models.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Data models for DBX-Patch results and status.
|
|
2
|
+
|
|
3
|
+
Provides strongly-typed dataclasses for all function return values.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PthProcessingResult:
|
|
11
|
+
"""Results from processing .pth files."""
|
|
12
|
+
|
|
13
|
+
site_dirs_scanned: int
|
|
14
|
+
pth_files_found: int
|
|
15
|
+
paths_extracted: list[str]
|
|
16
|
+
egg_link_paths: list[str]
|
|
17
|
+
metadata_paths: list[str]
|
|
18
|
+
paths_added: int
|
|
19
|
+
total_editable_paths: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PatchResult:
|
|
24
|
+
"""Generic patch operation result."""
|
|
25
|
+
|
|
26
|
+
success: bool
|
|
27
|
+
already_patched: bool
|
|
28
|
+
function_found: bool = True
|
|
29
|
+
hook_found: bool = True
|
|
30
|
+
editable_paths_count: int = 0
|
|
31
|
+
editable_paths: list[str] = field(default_factory=list)
|
|
32
|
+
error: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ApplyPatchesResult:
|
|
37
|
+
"""Results from applying all patches."""
|
|
38
|
+
|
|
39
|
+
sys_path_init_patch: PatchResult | None
|
|
40
|
+
pth_processing: PthProcessingResult | None
|
|
41
|
+
wsfs_hook_patch: PatchResult | None
|
|
42
|
+
wsfs_path_finder_patch: PatchResult | None
|
|
43
|
+
python_path_hook_patch: PatchResult | None
|
|
44
|
+
autoreload_hook_patch: PatchResult | None
|
|
45
|
+
overall_success: bool
|
|
46
|
+
editable_paths: list[str]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class VerifyResult:
|
|
51
|
+
"""Results from verifying editable install configuration."""
|
|
52
|
+
|
|
53
|
+
editable_paths: list[str]
|
|
54
|
+
paths_in_sys_path: list[str]
|
|
55
|
+
wsfs_hook_patched: bool
|
|
56
|
+
wsfs_path_finder_patched: bool
|
|
57
|
+
python_path_hook_patched: bool
|
|
58
|
+
autoreload_hook_patched: bool
|
|
59
|
+
importable_packages: list[str]
|
|
60
|
+
status: str # 'ok', 'warning', or 'error'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class StatusResult:
|
|
65
|
+
"""Current patch status."""
|
|
66
|
+
|
|
67
|
+
sys_path_init_patched: bool
|
|
68
|
+
wsfs_hook_patched: bool
|
|
69
|
+
wsfs_path_finder_patched: bool
|
|
70
|
+
python_path_hook_patched: bool
|
|
71
|
+
autoreload_hook_patched: bool
|
|
72
|
+
editable_paths_count: int
|
|
73
|
+
pth_files_processed: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class RemovePatchesResult:
|
|
78
|
+
"""Results from removing all patches."""
|
|
79
|
+
|
|
80
|
+
sys_path_init_unpatched: bool
|
|
81
|
+
wsfs_hook_unpatched: bool
|
|
82
|
+
wsfs_path_finder_unpatched: bool
|
|
83
|
+
python_path_hook_unpatched: bool
|
|
84
|
+
autoreload_hook_unpatched: bool
|
|
85
|
+
success: bool
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class SitecustomizeStatus:
|
|
90
|
+
"""Status of sitecustomize.py installation."""
|
|
91
|
+
|
|
92
|
+
installed: bool
|
|
93
|
+
path: str | None
|
|
94
|
+
is_dbx_patch: bool
|