github2gerrit 0.1.10__py3-none-any.whl → 0.1.11__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.
@@ -0,0 +1,502 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+
4
+ """
5
+ Rich display utilities for enhanced CLI output.
6
+
7
+ Provides formatted output for PR information, progress tracking, and
8
+ operation status using Rich formatting library.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from datetime import UTC
15
+ from datetime import datetime
16
+ from datetime import timedelta
17
+ from typing import Any
18
+
19
+ from .rich_logging import RichDisplayContext
20
+ from .rich_logging import setup_rich_aware_logging
21
+
22
+
23
+ try:
24
+ from rich.console import Console
25
+ from rich.live import Live
26
+ from rich.table import Table
27
+ from rich.text import Text
28
+
29
+ RICH_AVAILABLE = True
30
+ except ImportError:
31
+ RICH_AVAILABLE = False
32
+
33
+ # Fallback classes for when Rich is not available
34
+ class Live: # type: ignore
35
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
36
+ pass
37
+
38
+ def start(self) -> None:
39
+ pass
40
+
41
+ def stop(self) -> None:
42
+ pass
43
+
44
+ def update(self, *args: Any) -> None:
45
+ pass
46
+
47
+ class Text: # type: ignore
48
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
49
+ pass
50
+
51
+ def append(self, *args: Any, **kwargs: Any) -> None:
52
+ pass
53
+
54
+ class Console: # type: ignore
55
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
56
+ pass
57
+
58
+ def print(self, *args: Any, **kwargs: Any) -> None:
59
+ print(*args)
60
+
61
+ class Table: # type: ignore
62
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
63
+ pass
64
+
65
+ def add_column(self, *args: Any, **kwargs: Any) -> None:
66
+ pass
67
+
68
+ def add_row(self, *args: Any, **kwargs: Any) -> None:
69
+ pass
70
+
71
+
72
+ __all__ = [
73
+ "RICH_AVAILABLE",
74
+ "DummyProgressTracker",
75
+ "G2GProgressTracker",
76
+ "ProgressTracker",
77
+ "console",
78
+ "display_pr_info",
79
+ ]
80
+
81
+ log = logging.getLogger("github2gerrit.rich_display")
82
+
83
+ # Global console instance
84
+ console = Console(markup=False) if RICH_AVAILABLE else Console()
85
+
86
+
87
+ def safe_console_print(
88
+ message: str,
89
+ *,
90
+ style: str = "white",
91
+ progress_tracker: Any = None,
92
+ err: bool = False,
93
+ ) -> None:
94
+ """
95
+ Safely print to console with proper progress tracker handling.
96
+
97
+ This function ensures that progress displays are properly suspended
98
+ and resumed around console output to prevent display corruption.
99
+
100
+ Args:
101
+ message: Message to print
102
+ style: Rich style for the message
103
+ progress_tracker: Optional progress tracker to suspend/resume
104
+ err: Whether to print to stderr
105
+ """
106
+ # Use Rich display context to manage logging interference
107
+ context_id = f"safe_console_print_{id(message)}"
108
+
109
+ with RichDisplayContext(context_id):
110
+ if progress_tracker:
111
+ progress_tracker.suspend()
112
+
113
+ try:
114
+ if RICH_AVAILABLE and not err:
115
+ console.print(message, style=style)
116
+ else:
117
+ print(
118
+ message, file=None if not err else __import__("sys").stderr
119
+ )
120
+ finally:
121
+ if progress_tracker:
122
+ progress_tracker.resume()
123
+
124
+
125
+ def safe_typer_echo(
126
+ message: str,
127
+ *,
128
+ progress_tracker: Any = None,
129
+ err: bool = False,
130
+ ) -> None:
131
+ """
132
+ Safely use typer.echo with proper progress tracker handling.
133
+
134
+ Args:
135
+ message: Message to print
136
+ progress_tracker: Optional progress tracker to suspend/resume
137
+ err: Whether to print to stderr
138
+ """
139
+ # Use Rich display context to manage logging interference
140
+ context_id = f"safe_typer_echo_{id(message)}"
141
+
142
+ with RichDisplayContext(context_id):
143
+ if progress_tracker:
144
+ progress_tracker.suspend()
145
+
146
+ try:
147
+ import typer
148
+
149
+ typer.echo(message, err=err)
150
+ finally:
151
+ if progress_tracker:
152
+ progress_tracker.resume()
153
+
154
+
155
+ def display_pr_info(
156
+ pr_info: dict[str, Any],
157
+ title: str = "",
158
+ progress_tracker: Any = None,
159
+ ) -> None:
160
+ """Display pull request information in a formatted table.
161
+
162
+ Args:
163
+ pr_info: Dictionary containing PR information
164
+ title: Optional table title
165
+ progress_tracker: Optional progress tracker to suspend/resume
166
+ """
167
+ # Use Rich display context to manage logging interference
168
+ context_id = f"pr_info_display_{id(pr_info)}"
169
+
170
+ if not RICH_AVAILABLE:
171
+ # Fallback display for when Rich is not available
172
+ with RichDisplayContext(context_id):
173
+ if progress_tracker:
174
+ progress_tracker.suspend()
175
+ print(f"\n=== {title or 'Pull Request Information'} ===")
176
+ for key, value in pr_info.items():
177
+ print(f"{key:15}: {value}")
178
+ print("=" * 50)
179
+ if progress_tracker:
180
+ progress_tracker.resume()
181
+ return
182
+
183
+ # Rich display with logging context
184
+ with RichDisplayContext(context_id):
185
+ table = Table(title=title)
186
+ table.add_column("Property", style="cyan")
187
+ table.add_column("Value", style="green")
188
+
189
+ # Add rows for each piece of PR information
190
+ for key, value in pr_info.items():
191
+ table.add_row(str(key), str(value))
192
+
193
+ if progress_tracker:
194
+ progress_tracker.suspend()
195
+ console.print(table)
196
+ if progress_tracker:
197
+ progress_tracker.resume()
198
+
199
+
200
+ class ProgressTracker:
201
+ """Base progress tracker for GitHub and Gerrit operations."""
202
+
203
+ def __init__(self, operation_type: str, target: str):
204
+ """Initialize progress tracker for an operation.
205
+
206
+ Args:
207
+ operation_type: Type of operation (e.g., "GitHub to Gerrit")
208
+ target: Target identifier (e.g., organization name, repository)
209
+ """
210
+ self.operation_type = operation_type
211
+ self.target = target
212
+ self.start_time = datetime.now(UTC)
213
+ self.console = console
214
+
215
+ # Progress counters
216
+ self.current_operation = "Initializing..."
217
+ self.errors_count = 0
218
+ self.warnings_count = 0
219
+
220
+ # Rich Live display
221
+ self.live: Live | None = None
222
+ self.rich_available = RICH_AVAILABLE
223
+ self._rich_initially_available = RICH_AVAILABLE
224
+ self.paused = False
225
+
226
+ # Initialize Rich-aware logging on first progress tracker creation
227
+ if RICH_AVAILABLE:
228
+ setup_rich_aware_logging()
229
+
230
+ # Fallback for when Rich is not available
231
+ self._last_display = ""
232
+ self._last_operation = ""
233
+
234
+ # Rich display context for managing logging interference
235
+ self._rich_context: RichDisplayContext | None = None
236
+
237
+ def start(self) -> None:
238
+ """Start the progress display with in-place updates."""
239
+ # Start Rich display context to manage logging interference
240
+ if self.rich_available:
241
+ context_id = f"progress_tracker_{id(self)}"
242
+ self._rich_context = RichDisplayContext(context_id)
243
+ self._rich_context.__enter__()
244
+
245
+ self.console.print(
246
+ "🔄 GitHub to Gerrit ", style="bold blue", end=""
247
+ )
248
+ self.console.print(f"for {self.target}", style="bold cyan")
249
+ else:
250
+ print(f"🔄 GitHub to Gerrit for {self.target}")
251
+ self._last_operation = ""
252
+
253
+ def stop(self) -> None:
254
+ """Stop the progress display."""
255
+ # Clean up Rich display context
256
+ if self._rich_context:
257
+ import contextlib
258
+
259
+ with contextlib.suppress(Exception):
260
+ self._rich_context.__exit__(None, None, None)
261
+ self._rich_context = None
262
+ self.paused = False
263
+
264
+ def suspend(self) -> None:
265
+ """Temporarily pause the display to allow clean printing."""
266
+ self.paused = True
267
+
268
+ def resume(self) -> None:
269
+ """Resume the display after it was suspended."""
270
+ self.paused = False
271
+
272
+ def update_operation(self, operation: str) -> None:
273
+ """Update the current operation description."""
274
+ self.current_operation = operation
275
+ if not self.paused and operation != self._last_operation:
276
+ if self.rich_available:
277
+ # Just print the new operation - don't try in-place updates
278
+ # with Rich
279
+ self.console.print(f"📋 {operation}", style="dim white")
280
+ else:
281
+ print(f"📋 {operation}")
282
+ self._last_operation = operation
283
+
284
+ def add_error(self, message: str | None = None) -> None:
285
+ """Increment the error counter."""
286
+ self.errors_count += 1
287
+ if message and not self.rich_available:
288
+ # Only log when Rich is not available to avoid breaking clean
289
+ # display
290
+ log.error("Progress tracker error: %s", message)
291
+ self._refresh_display()
292
+
293
+ def add_warning(self, message: str | None = None) -> None:
294
+ """Increment the warning counter."""
295
+ self.warnings_count += 1
296
+ if message and not self.rich_available:
297
+ # Only log when Rich is not available to avoid breaking clean
298
+ # display
299
+ log.warning("Progress tracker warning: %s", message)
300
+ self._refresh_display()
301
+
302
+ def _refresh_display(self) -> None:
303
+ """Refresh the display - no-op for simple print-based display."""
304
+
305
+ def _generate_display_text(self) -> Text:
306
+ """Generate the current progress display text."""
307
+ if not self.rich_available:
308
+ return Text()
309
+
310
+ text = Text()
311
+
312
+ # Main operation line
313
+ text.append("🔄 ", style="bold blue")
314
+ text.append(f"{self.operation_type}", style="bold cyan")
315
+ text.append(f" for {self.target}", style="white")
316
+
317
+ # Status counters
318
+ if self.errors_count > 0 or self.warnings_count > 0:
319
+ text.append(" | ", style="white")
320
+ if self.errors_count > 0:
321
+ text.append(f"{self.errors_count} errors", style="red")
322
+ if self.warnings_count > 0:
323
+ if self.errors_count > 0:
324
+ text.append(", ", style="white")
325
+ text.append(f"{self.warnings_count} warnings", style="yellow")
326
+
327
+ text.append("\n")
328
+
329
+ # Current operation line
330
+ text.append(f"📋 {self.current_operation}", style="dim white")
331
+
332
+ # Elapsed time
333
+ elapsed = datetime.now(UTC) - self.start_time
334
+ text.append(
335
+ f"\n⏱️ Elapsed: {self._format_duration(elapsed)}", style="dim blue"
336
+ )
337
+
338
+ return text
339
+
340
+ def _fallback_display(self) -> None:
341
+ """Fallback display method - disabled to prevent duplicates."""
342
+ # Completely disable fallback display to prevent duplicate output
343
+
344
+ def _format_duration(self, duration: timedelta) -> str:
345
+ """Format a duration for display."""
346
+ total_seconds = int(duration.total_seconds())
347
+ minutes = total_seconds // 60
348
+ seconds = total_seconds % 60
349
+
350
+ if minutes > 0:
351
+ return f"{minutes}m {seconds}s"
352
+ else:
353
+ return f"{seconds}s"
354
+
355
+ def get_summary(self) -> dict[str, Any]:
356
+ """Get a summary of the operation progress."""
357
+ elapsed = datetime.now(UTC) - self.start_time
358
+
359
+ return {
360
+ "operation_type": self.operation_type,
361
+ "target": self.target,
362
+ "errors_count": self.errors_count,
363
+ "warnings_count": self.warnings_count,
364
+ "elapsed_time": self._format_duration(elapsed),
365
+ "current_operation": self.current_operation,
366
+ }
367
+
368
+
369
+ class G2GProgressTracker(ProgressTracker):
370
+ """Specialized progress tracker for GitHub to Gerrit operations."""
371
+
372
+ def __init__(self, target: str):
373
+ super().__init__("GitHub to Gerrit", target)
374
+ self.prs_processed = 0
375
+ self.changes_submitted = 0
376
+ self.changes_updated = 0
377
+ self.duplicates_skipped = 0
378
+
379
+ def pr_processed(self) -> None:
380
+ """Mark that a PR was processed."""
381
+ self.prs_processed += 1
382
+ self._refresh_display()
383
+
384
+ def change_submitted(self) -> None:
385
+ """Mark that a new change was submitted to Gerrit."""
386
+ self.changes_submitted += 1
387
+ self._refresh_display()
388
+
389
+ def change_updated(self) -> None:
390
+ """Mark that an existing change was updated in Gerrit."""
391
+ self.changes_updated += 1
392
+ self._refresh_display()
393
+
394
+ def duplicate_skipped(self) -> None:
395
+ """Mark that a duplicate was skipped."""
396
+ self.duplicates_skipped += 1
397
+ self._refresh_display()
398
+
399
+ def _generate_display_text(self) -> Text:
400
+ """Generate G2G-specific display text."""
401
+ if not self.rich_available:
402
+ return Text()
403
+
404
+ text = Text()
405
+
406
+ # Main progress line
407
+ text.append("🔄 GitHub to Gerrit ", style="bold blue")
408
+ text.append(f"for {self.target}", style="bold cyan")
409
+
410
+ # Stats
411
+ if self.prs_processed > 0:
412
+ text.append(f" | {self.prs_processed} PRs processed", style="white")
413
+
414
+ if self.changes_submitted > 0:
415
+ text.append(
416
+ f" | {self.changes_submitted} new changes", style="green"
417
+ )
418
+
419
+ if self.changes_updated > 0:
420
+ text.append(f" | {self.changes_updated} updated", style="yellow")
421
+
422
+ if self.duplicates_skipped > 0:
423
+ text.append(
424
+ f" | {self.duplicates_skipped} duplicates skipped",
425
+ style="dim white",
426
+ )
427
+
428
+ # Error/warning counts
429
+ if self.errors_count > 0:
430
+ text.append(f" | {self.errors_count} errors", style="red")
431
+ if self.warnings_count > 0:
432
+ text.append(f" | {self.warnings_count} warnings", style="yellow")
433
+
434
+ text.append("\n")
435
+
436
+ # Current operation line
437
+ text.append(f"📋 {self.current_operation}", style="dim white")
438
+
439
+ # Elapsed time
440
+ elapsed = datetime.now(UTC) - self.start_time
441
+ text.append(
442
+ f"\n⏱️ Elapsed: {self._format_duration(elapsed)}", style="dim blue"
443
+ )
444
+
445
+ return text
446
+
447
+ def get_summary(self) -> dict[str, Any]:
448
+ """Get G2G-specific summary."""
449
+ summary = super().get_summary()
450
+ summary.update(
451
+ {
452
+ "prs_processed": self.prs_processed,
453
+ "changes_submitted": self.changes_submitted,
454
+ "changes_updated": self.changes_updated,
455
+ "duplicates_skipped": self.duplicates_skipped,
456
+ }
457
+ )
458
+ return summary
459
+
460
+
461
+ class DummyProgressTracker:
462
+ """A no-op progress tracker for when progress display is disabled."""
463
+
464
+ def __init__(self, operation_type: str, target: str):
465
+ self.operation_type = operation_type
466
+ self.target = target
467
+
468
+ def start(self) -> None:
469
+ pass
470
+
471
+ def stop(self) -> None:
472
+ pass
473
+
474
+ def suspend(self) -> None:
475
+ pass
476
+
477
+ def resume(self) -> None:
478
+ pass
479
+
480
+ def update_operation(self, operation: str) -> None:
481
+ pass
482
+
483
+ def add_error(self, message: str | None = None) -> None:
484
+ pass
485
+
486
+ def add_warning(self, message: str | None = None) -> None:
487
+ pass
488
+
489
+ def pr_processed(self) -> None:
490
+ pass
491
+
492
+ def change_submitted(self) -> None:
493
+ pass
494
+
495
+ def change_updated(self) -> None:
496
+ pass
497
+
498
+ def duplicate_skipped(self) -> None:
499
+ pass
500
+
501
+ def get_summary(self) -> dict[str, Any]:
502
+ return {"operation_type": self.operation_type, "target": self.target}