patchon 0.1.0__cp313-cp313-win_amd64.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.
patchon/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Patchon: Run Python scripts with temporary source-file hot patches."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Native backend info
6
+ from ._native import NATIVE_BACKEND
7
+
8
+ # Utilities
9
+ from .cleanup import check_status, cleanup_all
10
+
11
+ # Core components
12
+ from .core import PatchSession
13
+ from .lock import EnvironmentLock
14
+
15
+ __all__ = [
16
+ "NATIVE_BACKEND",
17
+ "EnvironmentLock",
18
+ "PatchSession",
19
+ "__version__",
20
+ "check_status",
21
+ "cleanup_all",
22
+ ]
patchon/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Support for `python -m patchon`."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,65 @@
1
+ """Native code abstraction layer.
2
+
3
+ This module provides a clean abstraction over native implementations:
4
+ - _rust: Rust extension (high performance)
5
+ - _pure: Pure Python fallback
6
+
7
+ Usage:
8
+ from patchon._native import batch_copy_files, PatchSession
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ # Try to import Rust extension first
14
+ # Fallback to pure Python if not available
15
+
16
+ try:
17
+ from ._rust import (
18
+ PatchSession,
19
+ acquire_file_lock,
20
+ atomic_write_with_backup,
21
+ batch_copy_files,
22
+ batch_restore,
23
+ calculate_file_hash,
24
+ cleanup_stale_locks,
25
+ fast_file_copy,
26
+ is_process_alive,
27
+ release_file_lock,
28
+ restore_from_backup,
29
+ scan_python_files,
30
+ )
31
+
32
+ NATIVE_BACKEND = "rust"
33
+ except ImportError:
34
+ from ._pure import (
35
+ PatchSession,
36
+ acquire_file_lock,
37
+ atomic_write_with_backup,
38
+ batch_copy_files,
39
+ batch_restore,
40
+ calculate_file_hash,
41
+ cleanup_stale_locks,
42
+ fast_file_copy,
43
+ is_process_alive,
44
+ release_file_lock,
45
+ restore_from_backup,
46
+ scan_python_files,
47
+ )
48
+
49
+ NATIVE_BACKEND = "pure"
50
+
51
+ __all__ = [
52
+ "NATIVE_BACKEND",
53
+ "PatchSession",
54
+ "acquire_file_lock",
55
+ "atomic_write_with_backup",
56
+ "batch_copy_files",
57
+ "batch_restore",
58
+ "calculate_file_hash",
59
+ "cleanup_stale_locks",
60
+ "fast_file_copy",
61
+ "is_process_alive",
62
+ "release_file_lock",
63
+ "restore_from_backup",
64
+ "scan_python_files",
65
+ ]
@@ -0,0 +1,220 @@
1
+ """Pure Python fallback implementation.
2
+
3
+ Provides same interface as Rust extension but with pure Python implementation
4
+ for maximum compatibility.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import fcntl as _fcntl
10
+ import hashlib
11
+ import os
12
+ import shutil
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ FCNTL: Any = _fcntl
18
+
19
+ if TYPE_CHECKING:
20
+ pass
21
+
22
+
23
+ def fast_file_copy(src: str, dst: str) -> None:
24
+ """Copy file with metadata preservation."""
25
+ dst_path = Path(dst)
26
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
27
+ shutil.copy2(src, dst)
28
+
29
+
30
+ def batch_copy_files(operations: list[tuple[str, str]]) -> list[str | None]:
31
+ """Execute multiple copy operations."""
32
+ results: list[str | None] = []
33
+ for src, dst in operations:
34
+ try:
35
+ fast_file_copy(src, dst)
36
+ results.append(None)
37
+ except Exception as e:
38
+ results.append(str(e))
39
+ return results
40
+
41
+
42
+ def scan_python_files(dir: str, recursive: bool = True) -> list[str]:
43
+ """Scan directory for Python files."""
44
+ result = []
45
+ path = Path(dir)
46
+
47
+ if recursive:
48
+ for py_file in path.rglob("*.py"):
49
+ result.append(str(py_file))
50
+ else:
51
+ for py_file in path.glob("*.py"):
52
+ result.append(str(py_file))
53
+
54
+ return result
55
+
56
+
57
+ def calculate_file_hash(path: str) -> int:
58
+ """Calculate file hash for change detection."""
59
+ hasher = hashlib.blake2b(digest_size=8)
60
+ with Path(path).open("rb") as f:
61
+ for chunk in iter(lambda: f.read(8192), b""):
62
+ hasher.update(chunk)
63
+ return int.from_bytes(hasher.digest(), "big")
64
+
65
+
66
+ def atomic_write_with_backup(target: str, content: str, backup_dir: str | None = None) -> str | None:
67
+ """Atomically write file with backup creation."""
68
+ target_path = Path(target)
69
+ backup_path = None
70
+
71
+ if target_path.exists():
72
+ if backup_dir:
73
+ backup_path = Path(backup_dir) / f"{target_path.name}.{os.getpid()}.backup"
74
+ else:
75
+ backup_path = Path(tempfile.gettempdir()) / f"patchon_backup_{target_path.name}_{os.getpid()}"
76
+ shutil.copy2(target, backup_path)
77
+
78
+ # Atomic write
79
+ temp_path = target_path.with_suffix(".tmp")
80
+ temp_path.write_text(content)
81
+ temp_path.rename(target_path)
82
+
83
+ return str(backup_path) if backup_path else None
84
+
85
+
86
+ def restore_from_backup(backup_path: str, target: str) -> None:
87
+ """Restore file from backup."""
88
+ shutil.copy2(backup_path, target)
89
+
90
+
91
+ def batch_restore(backups: list[tuple[str, str]]) -> list[str | None]:
92
+ """Execute multiple restore operations."""
93
+ results: list[str | None] = []
94
+ for backup, target in backups:
95
+ try:
96
+ restore_from_backup(backup, target)
97
+ results.append(None)
98
+ except Exception as e:
99
+ results.append(f"Failed to restore {target}: {e}")
100
+ return results
101
+
102
+
103
+ def acquire_file_lock(lock_path: str, timeout_secs: int = 30) -> int:
104
+ """Acquire exclusive file lock."""
105
+ import time
106
+
107
+ start = time.time()
108
+ fd = os.open(lock_path, os.O_RDWR | os.O_CREAT)
109
+
110
+ while time.time() - start < timeout_secs:
111
+ try:
112
+ FCNTL.flock(fd, FCNTL.LOCK_EX | FCNTL.LOCK_NB)
113
+ return fd
114
+ except OSError:
115
+ time.sleep(0.01)
116
+
117
+ os.close(fd)
118
+ raise TimeoutError(f"Failed to acquire lock within {timeout_secs}s")
119
+
120
+
121
+ def release_file_lock(fd: int) -> None:
122
+ """Release file lock."""
123
+ FCNTL.flock(fd, FCNTL.LOCK_UN)
124
+ os.close(fd)
125
+
126
+
127
+ def is_process_alive(pid: int) -> bool:
128
+ """Check if process is still running."""
129
+ try:
130
+ os.kill(pid, 0)
131
+ return True
132
+ except (OSError, ProcessLookupError):
133
+ return False
134
+
135
+
136
+ def cleanup_stale_locks(lock_dir: str) -> int:
137
+ """Remove stale lock files."""
138
+ cleaned = 0
139
+ lock_path = Path(lock_dir)
140
+
141
+ if not lock_path.exists():
142
+ return 0
143
+
144
+ for lock_file in lock_path.glob("*.lock"):
145
+ try:
146
+ fd = os.open(lock_file, os.O_RDWR)
147
+ try:
148
+ FCNTL.flock(fd, FCNTL.LOCK_EX | FCNTL.LOCK_NB)
149
+ # If we can acquire the lock, it's stale
150
+ FCNTL.flock(fd, FCNTL.LOCK_UN)
151
+ Path(lock_file).unlink()
152
+ cleaned += 1
153
+ except Exception:
154
+ pass
155
+ finally:
156
+ os.close(fd)
157
+ except Exception:
158
+ pass
159
+
160
+ return cleaned
161
+
162
+
163
+ class PatchSession:
164
+ """Pure Python implementation of patch session."""
165
+
166
+ def __init__(self) -> None:
167
+ self._applied: dict[str, str] = {}
168
+ self._lock_fd: int | None = None
169
+
170
+ def apply_patches(self, patches: list[tuple[str, str]]) -> list[tuple[str, str | None]]:
171
+ """Apply patches and store backups."""
172
+ results = []
173
+ for source, target in patches:
174
+ content = Path(source).read_text()
175
+ backup = atomic_write_with_backup(target, content)
176
+ if backup:
177
+ self._applied[target] = backup
178
+ results.append((target, backup))
179
+ return results
180
+
181
+ def restore_all(self) -> list[tuple[str, bool]]:
182
+ """Restore all applied patches."""
183
+ results = []
184
+ for target, backup in self._applied.items():
185
+ try:
186
+ restore_from_backup(backup, target)
187
+ results.append((target, True))
188
+ except Exception:
189
+ results.append((target, False))
190
+ return results
191
+
192
+ def patch_count(self) -> int:
193
+ """Get number of applied patches."""
194
+ return len(self._applied)
195
+
196
+ def acquire_lock(self, lock_path: str) -> None:
197
+ """Acquire session lock."""
198
+ self._lock_fd = acquire_file_lock(lock_path)
199
+
200
+ def release_lock(self) -> None:
201
+ """Release session lock."""
202
+ if self._lock_fd is not None:
203
+ release_file_lock(self._lock_fd)
204
+ self._lock_fd = None
205
+
206
+
207
+ __all__ = [
208
+ "PatchSession",
209
+ "acquire_file_lock",
210
+ "atomic_write_with_backup",
211
+ "batch_copy_files",
212
+ "batch_restore",
213
+ "calculate_file_hash",
214
+ "cleanup_stale_locks",
215
+ "fast_file_copy",
216
+ "is_process_alive",
217
+ "release_file_lock",
218
+ "restore_from_backup",
219
+ "scan_python_files",
220
+ ]
@@ -0,0 +1,65 @@
1
+ """Rust extension wrapper.
2
+
3
+ This module wraps the Rust extension and provides:
4
+ 1. Python-friendly interface
5
+ 2. Type hints
6
+ 3. Error conversion
7
+ """
8
+
9
+ # mypy: ignore-errors
10
+ # This module imports from the compiled Rust extension which mypy cannot analyze
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from patchon._native._rust_ext import (
18
+ PatchSessionRust as PatchSession,
19
+ )
20
+ from patchon._native._rust_ext import (
21
+ acquire_file_lock,
22
+ atomic_write_with_backup,
23
+ batch_copy_files,
24
+ batch_restore,
25
+ calculate_file_hash,
26
+ cleanup_stale_locks,
27
+ fast_file_copy,
28
+ is_process_alive,
29
+ release_file_lock,
30
+ restore_from_backup,
31
+ scan_python_files,
32
+ )
33
+ else:
34
+ # Import from Rust module (runtime)
35
+ from patchon._native._rust_ext import ( # type: ignore[import-not-found]
36
+ PatchSessionRust as PatchSession,
37
+ )
38
+ from patchon._native._rust_ext import (
39
+ acquire_file_lock,
40
+ atomic_write_with_backup,
41
+ batch_copy_files,
42
+ batch_restore,
43
+ calculate_file_hash,
44
+ cleanup_stale_locks,
45
+ fast_file_copy,
46
+ is_process_alive,
47
+ release_file_lock,
48
+ restore_from_backup,
49
+ scan_python_files,
50
+ )
51
+
52
+ __all__ = [
53
+ "PatchSession",
54
+ "acquire_file_lock",
55
+ "atomic_write_with_backup",
56
+ "batch_copy_files",
57
+ "batch_restore",
58
+ "calculate_file_hash",
59
+ "cleanup_stale_locks",
60
+ "fast_file_copy",
61
+ "is_process_alive",
62
+ "release_file_lock",
63
+ "restore_from_backup",
64
+ "scan_python_files",
65
+ ]