Option C oc_diagnostics
Loading...
Searching...
No Matches
migrate_to_oc_diagnostics.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tools/migrate_to_oc_diagnostics.py
4
5"""
6Migration Script: Migrate a project from embedded diagnostics to oc_diagnostics
7================================================================================
8
9Purpose
10-------
11Rewrite Python import paths and Markdown references in an existing project so
12that all usage of the old embedded ``diagnostics/`` package is replaced by the
13new standalone ``oc_diagnostics`` package.
14
15Responsibilities
16----------------
17- Walk the project tree, skipping common non-project directories
18 (``EXCLUDE_DIRS``).
19- Rewrite Python import statements in ``.py`` files using a curated list of
20 ``(pattern, replacement)`` pairs.
21- Rewrite documentation references in ``.md`` files.
22- Create a ``.bak`` file next to every modified file so changes are reversible.
23- Handle the old ``diagnostics/`` directory in one of two ways:
24 - **Default (backup):** rename ``diagnostics/`` to ``diagnostics.bak/``.
25 The original content is preserved; the active project no longer uses it.
26 - **``--delete-original``:** permanently delete ``diagnostics/`` with no
27 backup copy. Use only when a backup already exists.
28- Support ``--dry-run`` to preview all actions without modifying the filesystem.
29
30Diagnostics
31-----------
32Domain: MIGRATE
33Levels:
34 L2 — lifecycle
35 L3 — semantic details
36 L4 — deep tracing
37
38Contracts
39---------
40- Must not modify files outside the target project root.
41- Must skip directories in ``EXCLUDE_DIRS`` to avoid corrupting virtual
42 environments, VCS metadata, caches, and third-party packages.
43- In ``--dry-run`` mode no files must be written, renamed, or deleted.
44- Every modified file must have a ``.bak`` copy written before the rewrite.
45- The ``diagnostics/`` backup copy is created by default; ``--delete-original``
46 skips the backup and deletes instead.
47
48Run this script from the root of the project being migrated:
49
50 python tools/migrate_to_oc_diagnostics.py [--dry-run] [--delete-original]
51"""
52
53import argparse
54import os
55import re
56import shutil
57
58
59# ---------------------------------------------------------------------------
60# DIRECTORIES TO SKIP DURING TREE WALK
61# ---------------------------------------------------------------------------
62
63EXCLUDE_DIRS = {
64 ".git",
65 "__pycache__",
66 "venv",
67 "env",
68 ".venv",
69 ".env",
70 "node_modules",
71 "diagnostics.bak", # skip our own backup folder
72 ".tox",
73 ".mypy_cache",
74 ".pytest_cache",
75 "dist",
76 "build",
77 "*.egg-info",
78}
79
80
81# ---------------------------------------------------------------------------
82# PYTHON IMPORT REPLACEMENTS
83# ---------------------------------------------------------------------------
84
85PYTHON_REPLACEMENTS = [
86 # unified_debug
87 (r"from\s+diagnostics\.unified_debug\s+import\s+_dbg",
88 r"from oc_diagnostics import _dbg"),
89
90 (r"from\s+diagnostics\.unified_debug\s+import\s+DEBUG_LEVELS",
91 r"from oc_diagnostics import DEBUG_LEVELS"),
92
93 # relative unified_debug
94 (r"from\s+\.\s*diagnostics\.unified_debug\s+import\s+_dbg",
95 r"from oc_diagnostics import _dbg"),
96
97 (r"from\s+\.\s*diagnostics\.unified_debug\s+import\s+DEBUG_LEVELS",
98 r"from oc_diagnostics import DEBUG_LEVELS"),
99
100 # instrumentation middleware
101 (r"from\s+diagnostics\.instrumentation_middleware\s+import\s+apply_instrumentation",
102 r"from oc_diagnostics.instrumentation_middleware import apply_instrumentation"),
103
104 (r"from\s+diagnostics\.instrumentation_middleware\s+import\s+enable_instrumentation_middleware",
105 r"from oc_diagnostics.instrumentation_middleware import enable_instrumentation_middleware"),
106
107 # relative instrumentation middleware
108 (r"from\s+\.\s*diagnostics\.instrumentation_middleware\s+import\s+apply_instrumentation",
109 r"from oc_diagnostics.instrumentation_middleware import apply_instrumentation"),
110
111 (r"from\s+\.\s*diagnostics\.instrumentation_middleware\s+import\s+enable_instrumentation_middleware",
112 r"from oc_diagnostics.instrumentation_middleware import enable_instrumentation_middleware"),
113
114 # id instrumentation
115 (r"from\s+diagnostics\.id_instrumentation\s+import\s+instrument_ids",
116 r"from oc_diagnostics.id_instrumentation import instrument_ids"),
117
118 (r"from\s+\.\s*diagnostics\.id_instrumentation\s+import\s+instrument_ids",
119 r"from oc_diagnostics.id_instrumentation import instrument_ids"),
120
121 # UI event logger
122 (r"from\s+diagnostics\.ui_event_logger\s+import\s+ui_event_logger",
123 r"from oc_diagnostics.ui_event_logger import ui_event_logger"),
124
125 (r"from\s+\.\s*diagnostics\.ui_event_logger\s+import\s+ui_event_logger",
126 r"from oc_diagnostics.ui_event_logger import ui_event_logger"),
127]
128
129
130# ---------------------------------------------------------------------------
131# MARKDOWN REPLACEMENTS
132# ---------------------------------------------------------------------------
133
134MD_REPLACEMENTS = [
135 (r"\bdiagnostics/", r"oc_diagnostics/"),
136 (r"DIAGNOSTICS_README\.md", r"REFERENCE.md"),
137 (r"DIAGNOSTICS_INTEGRATION_GUIDE_README\.md", r"INTEGRATION_GUIDE.md"),
138 (r"from diagnostics\.", r"from oc_diagnostics."),
139 (r"`diagnostics/`", r"`oc_diagnostics/`"),
140]
141
142
143# ---------------------------------------------------------------------------
144# FILE PROCESSING
145# ---------------------------------------------------------------------------
146
147def _should_exclude(dirpath: str) -> bool:
148 """Return True if this directory should be skipped during the tree walk."""
149 parts = set(os.path.normpath(dirpath).split(os.sep))
150 return bool(parts & EXCLUDE_DIRS)
151
152
153def rewrite_file(path: str, replacements: list, dry_run: bool) -> bool:
154 """Rewrite a file using a list of (pattern, replacement) pairs.
155
156 Returns True if the file would be (or was) modified.
157 Creates a ``.bak`` backup before writing when not in dry-run mode.
158 """
159 try:
160 with open(path, "r", encoding="utf-8") as f:
161 original = f.read()
162 except (OSError, UnicodeDecodeError):
163 return False
164
165 updated = original
166 for pattern, repl in replacements:
167 updated = re.sub(pattern, repl, updated)
168
169 if updated == original:
170 return False
171
172 if not dry_run:
173 backup = path + ".bak"
174 with open(backup, "w", encoding="utf-8") as f:
175 f.write(original)
176 with open(path, "w", encoding="utf-8") as f:
177 f.write(updated)
178
179 return True
180
181
182def migrate_project(root: str = ".", dry_run: bool = False) -> list[str]:
183 """Walk *root* and rewrite all Python and Markdown files.
184
185 Returns the list of files that were (or would be) modified.
186 """
187 modified = []
188
189 for dirpath, dirnames, filenames in os.walk(root):
190 # Prune excluded directories in-place so os.walk skips their subtrees.
191 dirnames[:] = [
192 d for d in dirnames
193 if d not in EXCLUDE_DIRS and not d.endswith(".egg-info")
194 ]
195
196 if _should_exclude(dirpath):
197 continue
198
199 for filename in filenames:
200 full = os.path.join(dirpath, filename)
201
202 if filename.endswith(".py"):
203 if rewrite_file(full, PYTHON_REPLACEMENTS, dry_run):
204 modified.append(full)
205
206 elif filename.endswith(".md"):
207 if rewrite_file(full, MD_REPLACEMENTS, dry_run):
208 modified.append(full)
209
210 return modified
211
212
213# ---------------------------------------------------------------------------
214# HANDLE OLD diagnostics/ FOLDER
215# ---------------------------------------------------------------------------
216
217def handle_old_diagnostics(delete_original: bool, dry_run: bool) -> str:
218 """Handle the old ``diagnostics/`` directory.
219
220 Default (backup): rename ``diagnostics/`` → ``diagnostics.bak/``.
221 With ``--delete-original``: permanently delete ``diagnostics/``.
222
223 Returns a human-readable status string.
224 """
225 old_path = os.path.join(".", "diagnostics")
226 backup_path = os.path.join(".", "diagnostics.bak")
227
228 if not os.path.exists(old_path):
229 return "No diagnostics/ folder found (already removed or never present)."
230
231 if delete_original:
232 action = f"Deleted diagnostics/ (no backup created)."
233 if not dry_run:
234 shutil.rmtree(old_path)
235 else:
236 action = f"Renamed diagnostics/ → diagnostics.bak/ (backup preserved)."
237 if not dry_run:
238 if os.path.exists(backup_path):
239 shutil.rmtree(backup_path)
240 shutil.move(old_path, backup_path)
241
242 return action
243
244
245# ---------------------------------------------------------------------------
246# MAIN
247# ---------------------------------------------------------------------------
248
249if __name__ == "__main__":
250 parser = argparse.ArgumentParser(
251 description="Migrate a project from embedded diagnostics/ to oc_diagnostics.",
252 )
253 parser.add_argument(
254 "--dry-run",
255 action="store_true",
256 help="Preview all changes without modifying the filesystem.",
257 )
258 parser.add_argument(
259 "--delete-original",
260 action="store_true",
261 help=(
262 "Delete diagnostics/ without creating a backup. "
263 "Default behavior renames it to diagnostics.bak/ instead."
264 ),
265 )
266 args = parser.parse_args()
267
268 dry_prefix = "[DRY RUN] " if args.dry_run else ""
269
270 print(f"\n{dry_prefix}Migrating project to oc_diagnostics...\n")
271 if args.dry_run:
272 print(" (No files will be modified.)\n")
273
274 modified_files = migrate_project(".", dry_run=args.dry_run)
275
276 if modified_files:
277 print(f"{dry_prefix}Would update:" if args.dry_run else "Updated the following files:\n")
278 for f in modified_files:
279 print(" -", f)
280 if not args.dry_run:
281 print("\n (.bak backups created alongside each modified file)\n")
282 else:
283 print("No Python or Markdown files required changes.\n")
284
285 # Handle old diagnostics/ directory
287 delete_original=args.delete_original,
288 dry_run=args.dry_run,
289 )
290 print(f"{dry_prefix}{result}\n")
291
292 print(f"{dry_prefix}Migration {'preview' if args.dry_run else 'complete'}.\n")
bool rewrite_file(str path, list replacements, bool dry_run)
list[str] migrate_project(str root=".", bool dry_run=False)
str handle_old_diagnostics(bool delete_original, bool dry_run)