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 ADDED
@@ -0,0 +1,2 @@
1
+ # file generated by dynamic versioning during build when enabled
2
+ __version__ = "0.1.0"
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
+ ]
@@ -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