rxiv-maker 1.16.5__py3-none-any.whl → 1.16.7__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.
rxiv_maker/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "1.16.5"
3
+ __version__ = "1.16.7"
@@ -1,68 +1,18 @@
1
1
  """Upgrade command for rxiv-maker CLI."""
2
2
 
3
- import subprocess # nosec B404 - needed for executing upgrade commands
4
3
  import sys
5
4
 
6
5
  import click
7
- from henriqueslab_updater import force_update_check
6
+ from henriqueslab_updater import handle_upgrade_workflow
8
7
  from rich.console import Console
9
8
 
10
9
  from ... import __version__
11
- from ...utils.changelog_parser import fetch_and_format_changelog
12
- from ...utils.install_detector import detect_install_method, get_friendly_install_name, get_upgrade_command
13
- from ..interactive import prompt_confirm
10
+ from ...utils.install_detector import detect_install_method
11
+ from ...utils.rich_upgrade_notifier import RxivUpgradeNotifier
14
12
 
15
13
  console = Console()
16
14
 
17
15
 
18
- def _display_changelog(console: Console, current_version: str, latest_version: str) -> None:
19
- """Display changelog summary for version range.
20
-
21
- Args:
22
- console: Rich console for output
23
- current_version: Current installed version
24
- latest_version: Latest available version
25
- """
26
- console.print("\n📋 What's changing:", style="bold blue")
27
-
28
- # Fetch changelog summary
29
- summary, error = fetch_and_format_changelog(
30
- current_version=current_version,
31
- latest_version=latest_version,
32
- highlights_per_version=3,
33
- )
34
-
35
- if error:
36
- console.print(" Unable to fetch changelog details", style="dim yellow")
37
- console.print(
38
- f" View online: https://github.com/henriqueslab/rxiv-maker/releases/tag/v{latest_version}",
39
- style="dim blue",
40
- )
41
- return
42
-
43
- if summary:
44
- # Display the changelog with proper formatting
45
- for line in summary.split("\n"):
46
- if line.startswith("⚠️"):
47
- # Highlight breaking changes prominently
48
- console.print(line, style="bold red")
49
- elif line.startswith("What's New:"):
50
- console.print(line, style="bold cyan")
51
- elif line.startswith(" v"):
52
- # Version headers
53
- console.print(line, style="bold yellow")
54
- elif line.strip().startswith(("✨", "🔄", "🐛", "🗑️", "🔒", "📝")):
55
- # Change items
56
- console.print(f" {line.strip()}", style="white")
57
- elif line.strip().startswith("•"):
58
- # Breaking change items
59
- console.print(f" {line.strip()}", style="yellow")
60
- elif line.strip():
61
- console.print(f" {line}", style="dim")
62
- else:
63
- console.print(" No detailed changelog available", style="dim")
64
-
65
-
66
16
  @click.command()
67
17
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
68
18
  @click.option("--check-only", "-c", is_flag=True, help="Only check for updates, don't upgrade")
@@ -72,93 +22,31 @@ def upgrade(ctx: click.Context, yes: bool, check_only: bool) -> None:
72
22
 
73
23
  This command automatically detects how rxiv-maker was installed
74
24
  (Homebrew, pip, uv, pipx, etc.) and runs the appropriate upgrade command.
25
+
26
+ Examples:
27
+ rxiv upgrade # Check and upgrade with confirmation
28
+ rxiv upgrade --yes # Upgrade without confirmation
29
+ rxiv upgrade --check-only # Only check for updates
75
30
  """
76
- # Detect installation method
31
+ # Handle development installations specially
77
32
  install_method = detect_install_method()
78
- install_name = get_friendly_install_name(install_method)
79
-
80
- console.print(f"🔍 Detected installation method: {install_name}", style="blue")
81
-
82
- # Handle development installations
83
33
  if install_method == "dev":
84
34
  console.print("⚠️ Development installation detected", style="yellow")
85
35
  console.print(" To update, pull the latest changes from git:", style="yellow")
86
- console.print(" cd <repo> && git pull && uv sync", style="yellow")
36
+ console.print(" [cyan]cd <repo> && git pull && uv sync[/cyan]", style="yellow")
87
37
  sys.exit(0)
88
38
 
89
- # Check for updates
90
- console.print("🔍 Checking for updates...", style="blue")
91
- try:
92
- update_available, latest_version = force_update_check()
93
-
94
- if not update_available:
95
- console.print(f"✅ You already have the latest version ({__version__})", style="green")
96
- sys.exit(0)
97
-
98
- console.print(f"📦 Update available: {__version__} → {latest_version}", style="green")
99
-
100
- # Fetch and display changelog
101
- _display_changelog(console, __version__, latest_version)
102
-
103
- if check_only:
104
- upgrade_cmd = get_upgrade_command(install_method)
105
- console.print(f"\n Run: {upgrade_cmd}", style="blue")
106
- sys.exit(0)
107
-
108
- except Exception as e:
109
- console.print(f"⚠️ Could not check for updates: {e}", style="yellow")
110
- console.print(" Proceeding with upgrade attempt...", style="yellow")
111
- latest_version = "latest"
112
-
113
- # Get upgrade command
114
- upgrade_cmd = get_upgrade_command(install_method)
115
-
116
- # Show confirmation
117
- if not yes:
118
- console.print(f"\n📦 About to run: {upgrade_cmd}", style="blue")
119
- if not prompt_confirm("Do you want to continue?", default=True):
120
- console.print("❌ Upgrade cancelled", style="yellow")
121
- sys.exit(0)
122
-
123
- # Execute upgrade command directly
124
- console.print("\n🚀 Upgrading rxiv-maker...", style="blue")
125
- console.print(f" Running: {upgrade_cmd}", style="dim")
126
-
127
- try:
128
- # Split command for safer execution without shell=True
129
- import shlex
130
-
131
- cmd_parts = shlex.split(upgrade_cmd)
132
- result = subprocess.run(
133
- cmd_parts,
134
- check=True,
135
- capture_output=False,
136
- timeout=300,
137
- ) # nosec B603 - upgrade_cmd comes from trusted install_detector module
138
- success = result.returncode == 0
139
- error = None
140
- except subprocess.CalledProcessError as e:
141
- success = False
142
- error = f"Command failed with exit code {e.returncode}"
143
- except subprocess.TimeoutExpired:
144
- success = False
145
- error = "Upgrade timed out after 5 minutes"
146
- except Exception as e:
147
- success = False
148
- error = str(e)
149
-
150
- if success:
151
- console.print("\n✅ Upgrade completed successfully!", style="green")
152
- console.print(" Run 'rxiv --version' to verify the installation", style="blue")
39
+ # Use centralized upgrade workflow
40
+ notifier = RxivUpgradeNotifier(console)
41
+ success, error = handle_upgrade_workflow(
42
+ package_name="rxiv-maker",
43
+ current_version=__version__,
44
+ check_only=check_only,
45
+ skip_confirmation=yes,
46
+ notifier=notifier,
47
+ github_org="HenriquesLab",
48
+ github_repo="rxiv-maker",
49
+ )
153
50
 
154
- # Show what's new in the upgraded version
155
- if latest_version != "latest":
156
- console.print(f"\n🎉 What's new in v{latest_version}:", style="bold green")
157
- console.print(
158
- f" View full changelog: https://github.com/henriqueslab/rxiv-maker/releases/tag/v{latest_version}",
159
- style="blue",
160
- )
161
- else:
162
- console.print(f"\n❌ Upgrade failed: {error}", style="red")
163
- console.print(f" Try running manually: {upgrade_cmd}", style="yellow")
51
+ if not success and error:
164
52
  sys.exit(1)
@@ -187,13 +187,16 @@ class CheckInstallationCommand(BaseCommand):
187
187
  """Show next steps after dependency check."""
188
188
  if not missing_results:
189
189
  self.console.print("\n🚀 Next steps:", style="blue")
190
- self.console.print(" • Test PDF generation: rxiv pdf ../manuscript-rxiv-maker/MANUSCRIPT")
190
+ self.console.print(" • Get example manuscript: rxiv get-rxiv-preprint")
191
+ self.console.print(" • Navigate to manuscript: cd manuscript-rxiv-maker/MANUSCRIPT")
192
+ self.console.print(" • Generate PDF: rxiv pdf")
191
193
  return
192
194
 
193
195
  self.console.print("\n🔧 Next steps:", style="blue")
194
196
  self.console.print(" • Install missing dependencies shown above")
195
197
  self.console.print(" • Re-run: rxiv check-installation")
196
- self.console.print(" • Test PDF generation: rxiv pdf ../manuscript-rxiv-maker/MANUSCRIPT")
198
+ self.console.print(" • Get example manuscript: rxiv get-rxiv-preprint")
199
+ self.console.print(" • Generate PDF: cd manuscript-rxiv-maker/MANUSCRIPT && rxiv pdf")
197
200
 
198
201
  def _show_basic_results(self, results: dict) -> None:
199
202
  """Show basic installation results."""
@@ -250,31 +250,95 @@ class FigureGenerator:
250
250
  response = get_with_retry(mermaid_url, max_attempts=5, timeout=30)
251
251
  else:
252
252
  response = requests.get(mermaid_url, timeout=30)
253
- except Exception:
254
- # If retry fails, fall back to placeholder
255
- return self._create_fallback_mermaid_diagram(input_file, output_file)
253
+ except requests.Timeout:
254
+ # Timeout - likely diagram too complex or service slow
255
+ return self._create_fallback_mermaid_diagram(
256
+ input_file, output_file, reason="timeout", details="30s timeout exceeded"
257
+ )
258
+ except requests.HTTPError as e:
259
+ # HTTP error (400, 503, etc.) - extract status code
260
+ status_code = e.response.status_code if hasattr(e, "response") else "unknown"
261
+ if status_code == 400:
262
+ details = "syntax error or diagram too complex"
263
+ elif status_code == 503:
264
+ details = "service timeout (diagram too complex)"
265
+ else:
266
+ details = f"HTTP {status_code}"
267
+ return self._create_fallback_mermaid_diagram(
268
+ input_file, output_file, reason="http_error", details=details
269
+ )
270
+ except Exception as e:
271
+ # Network or other error during request
272
+ error_msg = str(e)
273
+ # Try to extract status code from error message if it's there
274
+ if "400" in error_msg:
275
+ details = "syntax error or diagram too complex"
276
+ elif "503" in error_msg:
277
+ details = "service timeout (diagram too complex)"
278
+ elif "429" in error_msg:
279
+ details = "rate limit exceeded"
280
+ else:
281
+ details = "connection error"
282
+ return self._create_fallback_mermaid_diagram(
283
+ input_file, output_file, reason="network_error", details=details
284
+ )
256
285
 
257
286
  if response.status_code == 200:
258
287
  with open(output_file, "wb") as f:
259
288
  f.write(response.content)
260
289
  return True
261
290
  else:
262
- print(f"mermaid.ink service returned status {response.status_code}")
263
- print(
264
- "💡 Tip: If this is a syntax error, check your Mermaid diagram at https://www.mermaidchart.com/"
291
+ # Determine failure reason from status code
292
+ if response.status_code == 400:
293
+ reason_msg = "syntax error or diagram too complex"
294
+ elif response.status_code == 429:
295
+ reason_msg = "rate limit exceeded"
296
+ elif response.status_code == 503:
297
+ reason_msg = "service timeout (diagram too complex)"
298
+ elif response.status_code >= 500:
299
+ reason_msg = "service unavailable"
300
+ else:
301
+ reason_msg = f"HTTP {response.status_code}"
302
+
303
+ return self._create_fallback_mermaid_diagram(
304
+ input_file, output_file, reason="http_error", details=reason_msg
265
305
  )
266
- return self._create_fallback_mermaid_diagram(input_file, output_file)
267
306
  else:
268
- print("requests library not available for Mermaid generation")
269
- return self._create_fallback_mermaid_diagram(input_file, output_file)
307
+ return self._create_fallback_mermaid_diagram(
308
+ input_file, output_file, reason="no_requests_lib", details="requests library not available"
309
+ )
270
310
 
271
311
  except Exception as e:
272
- print(f"mermaid.ink service error: {e}")
273
- print("💡 Tip: Check your Mermaid diagram syntax at https://www.mermaidchart.com/")
274
- return self._create_fallback_mermaid_diagram(input_file, output_file)
312
+ return self._create_fallback_mermaid_diagram(
313
+ input_file, output_file, reason="unexpected_error", details=str(e)
314
+ )
315
+
316
+ def _create_fallback_mermaid_diagram(
317
+ self, input_file: Path, output_file: Path, reason: str = "unknown", details: str = "service unavailable"
318
+ ) -> bool:
319
+ """Create a fallback placeholder diagram when Mermaid service is unavailable.
320
+
321
+ Args:
322
+ input_file: Source mermaid file
323
+ output_file: Output file path
324
+ reason: Failure reason category (timeout, http_error, network_error, etc.)
325
+ details: Detailed error message
326
+
327
+ Returns:
328
+ True if placeholder was created successfully
329
+ """
330
+ # Generate user-friendly warning message based on failure reason
331
+ if reason == "timeout":
332
+ warning_msg = f"diagram rendering timed out ({details})"
333
+ elif reason == "http_error":
334
+ warning_msg = f"{details}"
335
+ elif reason == "network_error":
336
+ warning_msg = f"network error: {details}"
337
+ elif reason == "no_requests_lib":
338
+ warning_msg = "requests library not available"
339
+ else:
340
+ warning_msg = f"{details}"
275
341
 
276
- def _create_fallback_mermaid_diagram(self, input_file: Path, output_file: Path) -> bool:
277
- """Create a fallback placeholder diagram when Mermaid service is unavailable."""
278
342
  try:
279
343
  if self.output_format.lower() == "svg":
280
344
  # Create SVG placeholder
@@ -294,6 +358,7 @@ class FigureGenerator:
294
358
  </svg>"""
295
359
  with open(output_file, "w", encoding="utf-8") as f:
296
360
  f.write(svg_content)
361
+ print(f"⚠️ Created placeholder SVG for {input_file.name} ({warning_msg})")
297
362
  return True
298
363
  elif self.output_format.lower() == "png":
299
364
  # Create minimal PNG placeholder (1x1 white pixel)
@@ -303,7 +368,7 @@ class FigureGenerator:
303
368
  )
304
369
  with open(output_file, "wb") as f:
305
370
  f.write(png_data)
306
- print(f"⚠️ Created placeholder PNG for {input_file.name} (mermaid.ink unavailable)")
371
+ print(f"⚠️ Created placeholder PNG for {input_file.name} ({warning_msg})")
307
372
  return True
308
373
  elif self.output_format.lower() == "pdf":
309
374
  # Create minimal PDF placeholder
@@ -374,20 +439,83 @@ startxref
374
439
  """
375
440
  with open(output_file, "w", encoding="utf-8") as f:
376
441
  f.write(pdf_content)
377
- print(f"⚠️ Created placeholder PDF for {input_file.name} (mermaid.ink unavailable)")
442
+ print(f"⚠️ Created placeholder PDF for {input_file.name} ({warning_msg})")
378
443
  return True
379
444
  else:
380
445
  # Fallback for other formats - create text file with warning
381
446
  with open(output_file.with_suffix(".txt"), "w", encoding="utf-8") as f:
382
447
  f.write(f"Mermaid diagram placeholder for {input_file.name}\n")
383
- f.write("mermaid.ink service unavailable - diagram generation failed\n")
448
+ f.write(f"Reason: {warning_msg}\n")
384
449
  f.write("\n💡 Tip: Check your Mermaid diagram syntax at https://www.mermaidchart.com/\n")
385
- print(f"⚠️ Created text placeholder for {input_file.name} (mermaid.ink unavailable)")
450
+ print(f"⚠️ Created text placeholder for {input_file.name} ({warning_msg})")
386
451
  return True
387
452
  except Exception as e:
388
453
  print(f"Failed to create fallback diagram: {e}")
389
454
  return False
390
455
 
456
+ def validate_mermaid_diagram(self, mmd_file: Path) -> tuple[bool, str, dict]:
457
+ """Validate a Mermaid diagram for mermaid.ink compatibility.
458
+
459
+ Args:
460
+ mmd_file: Path to .mmd file
461
+
462
+ Returns:
463
+ Tuple of (is_valid, message, details_dict)
464
+ - is_valid: True if diagram will render successfully
465
+ - message: Human-readable validation result
466
+ - details_dict: Dict with complexity metrics and suggestions
467
+ """
468
+ if not requests:
469
+ return False, "requests library not available for validation", {}
470
+
471
+ try:
472
+ # Read and analyze the diagram
473
+ with open(mmd_file, "r", encoding="utf-8") as f:
474
+ content = f.read().strip()
475
+
476
+ # Analyze complexity
477
+ details = {
478
+ "file_size": len(content),
479
+ "line_count": content.count("\n") + 1,
480
+ "subgraph_count": content.count("subgraph"),
481
+ "node_count": len(re.findall(r"\w+\[", content)),
482
+ "class_def_count": content.count("classDef"),
483
+ }
484
+
485
+ # Check for known problematic patterns
486
+ warnings = []
487
+ if details["file_size"] > 2500:
488
+ warnings.append(f"Large diagram ({details['file_size']} chars, limit ~2500)")
489
+ if details["subgraph_count"] > 5:
490
+ warnings.append(f"Many subgraphs ({details['subgraph_count']}, limit ~5)")
491
+ if details["class_def_count"] > 6:
492
+ warnings.append(f"Heavy styling ({details['class_def_count']} classes, limit ~6)")
493
+
494
+ # Test with mermaid.ink
495
+ encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
496
+ test_url = f"https://mermaid.ink/svg/{encoded}" # Use SVG for faster testing
497
+
498
+ try:
499
+ response = requests.get(test_url, timeout=10)
500
+ if response.status_code == 200:
501
+ if warnings:
502
+ msg = f"✓ Valid (but complex: {', '.join(warnings)})"
503
+ return True, msg, details
504
+ return True, "✓ Valid and will render successfully", details
505
+ elif response.status_code == 400:
506
+ return False, "✗ Syntax error or too complex for mermaid.ink", details
507
+ elif response.status_code == 503:
508
+ return False, "✗ Diagram too complex (service timeout)", details
509
+ else:
510
+ return False, f"✗ HTTP {response.status_code}", details
511
+ except requests.Timeout:
512
+ return False, "✗ Validation timeout (diagram likely too complex)", details
513
+ except Exception as e:
514
+ return False, f"✗ Network error: {str(e)[:50]}", details
515
+
516
+ except Exception as e:
517
+ return False, f"✗ Error reading diagram: {str(e)[:50]}", {}
518
+
391
519
  def _execute_python_files(self, progress=None, task_id=None, use_rich: bool = True):
392
520
  """Execute Python scripts to generate figures using local Python."""
393
521
  py_files = list(self.figures_dir.glob("*.py"))
@@ -0,0 +1,162 @@
1
+ """Rich console adapter for upgrade notifications in rxiv-maker."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+
7
+ class RxivUpgradeNotifier:
8
+ """Adapt Rich console to UpgradeNotifier protocol for rxiv-maker.
9
+
10
+ Integrates with rxiv-maker's changelog parser to show rich change summaries.
11
+ """
12
+
13
+ def __init__(self, console: Console):
14
+ """Initialize with Rich console instance.
15
+
16
+ Args:
17
+ console: Rich Console instance for styled output
18
+ """
19
+ self.console = console
20
+
21
+ def show_checking(self) -> None:
22
+ """Show 'checking for updates' message."""
23
+ self.console.print("🔍 Checking for updates...", style="blue")
24
+
25
+ def show_version_check(self, current: str, latest: str, available: bool) -> None:
26
+ """Show version check results.
27
+
28
+ Args:
29
+ current: Current installed version
30
+ latest: Latest available version
31
+ available: Whether an update is available
32
+ """
33
+ if available:
34
+ self.console.print(
35
+ f"📦 Update available: [cyan]v{current}[/cyan] → [green bold]v{latest}[/green bold]",
36
+ style="yellow",
37
+ )
38
+ else:
39
+ self.console.print(
40
+ f"✅ You already have the latest version ([cyan]v{current}[/cyan])",
41
+ style="green",
42
+ )
43
+
44
+ def show_update_info(self, current: str, latest: str, release_url: str) -> None:
45
+ """Show update available information with changelog integration.
46
+
47
+ Args:
48
+ current: Current version
49
+ latest: Latest version
50
+ release_url: URL to release notes
51
+ """
52
+ # Import here to avoid circular dependencies
53
+ from .changelog_parser import fetch_and_format_changelog
54
+
55
+ # Fetch and display changelog
56
+ summary, error = fetch_and_format_changelog(
57
+ current_version=current,
58
+ latest_version=latest,
59
+ highlights_per_version=3,
60
+ )
61
+
62
+ if summary and not error:
63
+ self.console.print("\n📋 What's changing:", style="bold blue")
64
+ # Display changelog - format_summary returns rich-formatted text
65
+ # Parse and display with proper styling
66
+ for line in summary.split("\n"):
67
+ if line.startswith("⚠️"):
68
+ # Highlight breaking changes prominently
69
+ self.console.print(line, style="bold red")
70
+ elif "What's New:" in line or "What's changing:" in line:
71
+ self.console.print(line, style="bold cyan")
72
+ elif line.strip().startswith("v"):
73
+ # Version headers
74
+ self.console.print(line, style="bold yellow")
75
+ elif line.strip().startswith(("✨", "🔄", "🐛", "🗑️", "🔒", "📝")):
76
+ # Change items with emojis
77
+ self.console.print(f" {line.strip()}", style="white")
78
+ elif line.strip().startswith("•"):
79
+ # Breaking change items
80
+ self.console.print(f" {line.strip()}", style="yellow")
81
+ elif line.strip():
82
+ self.console.print(f" {line}", style="dim")
83
+ else:
84
+ # Fallback if changelog unavailable
85
+ self.console.print(
86
+ f"\nView release notes: [link]{release_url}[/link]",
87
+ style="blue",
88
+ )
89
+
90
+ def show_installer_info(self, friendly_name: str, command: str) -> None:
91
+ """Show detected installer information.
92
+
93
+ Args:
94
+ friendly_name: Human-readable installer name
95
+ command: The upgrade command that will be executed
96
+ """
97
+ self.console.print()
98
+ self.console.print(
99
+ f"🔍 Detected installation method: [bold]{friendly_name}[/bold]",
100
+ style="blue",
101
+ )
102
+ self.console.print(f"📦 Running: [yellow]{command}[/yellow]")
103
+
104
+ def show_success(self, version: str) -> None:
105
+ """Show successful upgrade message.
106
+
107
+ Args:
108
+ version: Version that was successfully installed
109
+ """
110
+ self.console.print()
111
+ self.console.print("✅ Upgrade completed successfully!", style="green bold")
112
+ self.console.print(f" Now running: [green]v{version}[/green]")
113
+ self.console.print()
114
+ self.console.print(" Run [blue]'rxiv --version'[/blue] to verify the installation", style="dim")
115
+
116
+ def show_error(self, error: str | None) -> None:
117
+ """Show upgrade error message.
118
+
119
+ Args:
120
+ error: Error message or None
121
+ """
122
+ self.console.print()
123
+ self.console.print("❌ Upgrade failed", style="red bold")
124
+ if error:
125
+ self.console.print(f" {error}", style="red")
126
+
127
+ def show_manual_instructions(self, install_method: str) -> None:
128
+ """Show manual upgrade instructions.
129
+
130
+ Args:
131
+ install_method: The detected installation method
132
+ """
133
+ self.console.print("\n💡 Try running manually:", style="yellow bold")
134
+
135
+ if install_method == "homebrew":
136
+ self.console.print(" [cyan]brew update && brew upgrade rxiv-maker[/cyan]")
137
+ elif install_method == "pipx":
138
+ self.console.print(" [cyan]pipx upgrade rxiv-maker[/cyan]")
139
+ elif install_method == "uv":
140
+ self.console.print(" [cyan]uv tool upgrade rxiv-maker[/cyan]")
141
+ elif install_method == "dev":
142
+ self.console.print(" [cyan]cd <repo> && git pull && uv sync[/cyan]", style="dim")
143
+ else:
144
+ self.console.print(" [cyan]pip install --upgrade rxiv-maker[/cyan]")
145
+ self.console.print(" [dim]# Or with --user flag:[/dim]")
146
+ self.console.print(" [cyan]pip install --upgrade --user rxiv-maker[/cyan]")
147
+
148
+ def confirm_upgrade(self, version: str) -> bool:
149
+ """Prompt user for confirmation using click.
150
+
151
+ Args:
152
+ version: Version to upgrade to
153
+
154
+ Returns:
155
+ True if user confirms, False otherwise
156
+ """
157
+ try:
158
+ self.console.print()
159
+ return click.confirm(f"Upgrade rxiv-maker to v{version}?", default=True)
160
+ except (KeyboardInterrupt, EOFError):
161
+ self.console.print("\n⚠️ Upgrade cancelled.", style="yellow")
162
+ return False
@@ -854,8 +854,17 @@ class SyntaxValidator(BaseValidator):
854
854
  heading_texts = {}
855
855
  previous_level = 0
856
856
 
857
- for line_num, line in enumerate(lines, 1):
858
- match = heading_pattern.match(line)
857
+ # Protect code blocks (including {{py:exec}}) from being treated as headings
858
+ content = "\n".join(lines)
859
+ protected_content = self._protect_validation_sensitive_content(content)
860
+ protected_lines = protected_content.split("\n")
861
+
862
+ for line_num, (original_line, protected_line) in enumerate(zip(lines, protected_lines, strict=False), 1):
863
+ # Skip protected code blocks (they might contain # comments)
864
+ if "XXPROTECTEDCODEXX" in protected_line:
865
+ continue
866
+
867
+ match = heading_pattern.match(protected_line)
859
868
  if match:
860
869
  hashes = match.group(1)
861
870
  heading_text = match.group(2).strip()
@@ -904,7 +913,7 @@ class SyntaxValidator(BaseValidator):
904
913
  f"Heading hierarchy skips levels: {previous_level} → {level}",
905
914
  file_path=file_path,
906
915
  line_number=line_num,
907
- context=line.strip(),
916
+ context=original_line.strip(),
908
917
  suggestion=(
909
918
  f"Consider using {'#' * (previous_level + 1)} instead of {'#' * level}.\n"
910
919
  f" Skipping heading levels (e.g., ## to ####) makes document structure unclear."
@@ -927,7 +936,7 @@ class SyntaxValidator(BaseValidator):
927
936
  f"Standard section '{heading_text}' using level 1 heading (#)",
928
937
  file_path=file_path,
929
938
  line_number=line_num,
930
- context=line.strip(),
939
+ context=original_line.strip(),
931
940
  suggestion=(
932
941
  f"Change to level 2 heading: ## {heading_text}\n"
933
942
  f" Level 1 (#) should only be used for the document title.\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxiv-maker
3
- Version: 1.16.5
3
+ Version: 1.16.7
4
4
  Summary: Write scientific preprints in Markdown. Generate publication-ready PDFs efficiently.
5
5
  Project-URL: Homepage, https://github.com/HenriquesLab/rxiv-maker
6
6
  Project-URL: Documentation, https://github.com/HenriquesLab/rxiv-maker#readme
@@ -27,7 +27,7 @@ Requires-Python: >=3.10
27
27
  Requires-Dist: click>=8.0.0
28
28
  Requires-Dist: crossref-commons>=0.0.7
29
29
  Requires-Dist: gitpython>=3.1.0
30
- Requires-Dist: henriqueslab-updater>=1.1.3
30
+ Requires-Dist: henriqueslab-updater>=1.2.0
31
31
  Requires-Dist: latex2mathml>=3.78.0
32
32
  Requires-Dist: matplotlib>=3.7.0
33
33
  Requires-Dist: numpy>=1.24.0
@@ -358,9 +358,14 @@ rxiv config --non-interactive # Show current settings
358
358
 
359
359
  ### Maintenance
360
360
  ```bash
361
- rxiv upgrade # Upgrade to latest version
362
- rxiv changelog # View changelog and release notes
363
- rxiv changelog --recent 5 # View last 5 versions
361
+ # Upgrade commands (auto-detects Homebrew, pip, uv, pipx)
362
+ rxiv upgrade # Interactive upgrade with confirmation
363
+ rxiv upgrade --yes # Upgrade without confirmation
364
+ rxiv upgrade --check-only # Check for updates only
365
+
366
+ # Changelog and version information
367
+ rxiv changelog # View changelog and release notes
368
+ rxiv changelog --recent 5 # View last 5 versions
364
369
  ```
365
370
 
366
371
  > **💡 CI/Automation Note:** All interactive commands support non-interactive mode or configuration files for use in CI/CD pipelines and automated workflows. Use `--non-interactive` flag or configure via `~/.rxiv-maker/config` for non-TTY environments.
@@ -1,5 +1,5 @@
1
1
  rxiv_maker/__init__.py,sha256=p04JYC5ZhP6dLXkoWVlKNyiRvsDE1a4C88f9q4xO3tA,3268
2
- rxiv_maker/__version__.py,sha256=scH9t8S4Yy0pZ0rKdWKP1B1m1lQmPo9FwrixAZxLlR0,51
2
+ rxiv_maker/__version__.py,sha256=--L7TPRaASOQ9_Gb1AzwOD0jIisWKuGJB7fbHOHsoFs,51
3
3
  rxiv_maker/rxiv_maker_cli.py,sha256=9Lu_mhFPXwx5jzAR6StCNxwCm_fkmP5qiOYdNuh_AwI,120
4
4
  rxiv_maker/validate.py,sha256=AIzgP59KbCQJqC9WIGfUdVv0xI6ud9g1fFznQkaGz5Q,9373
5
5
  rxiv_maker/cli/__init__.py,sha256=Jw0DTFUSofN-02xpVrt1UUzRcgH5NNd-GPNidhmNwpU,77
@@ -29,7 +29,7 @@ rxiv_maker/cli/commands/repos.py,sha256=SQ9nuhSkyHKFPYn_TrOxyQoGdDRI-OBOgUSGJpRu
29
29
  rxiv_maker/cli/commands/repos_search.py,sha256=6sUMvyHlHEX1p88hPtu0_Hf8z6JpOinJ53l9ZI-rirc,7743
30
30
  rxiv_maker/cli/commands/setup.py,sha256=9ue4bDETpSPGVkWFDfpuTDsysax5-QKGxmt42Gb7oeU,2294
31
31
  rxiv_maker/cli/commands/track_changes.py,sha256=omf_77A7htRSa8naUEPTTtUTxrSwMpzHFOuU0j1xAJw,1163
32
- rxiv_maker/cli/commands/upgrade.py,sha256=VJ1snYyLGrb5-snF4olfsMwfQsuKkAbXEr8aQ88WYYE,6349
32
+ rxiv_maker/cli/commands/upgrade.py,sha256=UpdqEQwbNYmDMbSrYGv_pVd-7u8PPT3US5RVENhKK4w,1852
33
33
  rxiv_maker/cli/commands/validate.py,sha256=3JghFQevJvQDQII4p_QWbQXMEUyDpM-t9-WxtaT4edo,1629
34
34
  rxiv_maker/cli/commands/version.py,sha256=VMlfSxxsrZH02d24MXLUDZfHBW39yZRpWxUpMhQ-X0Y,2737
35
35
  rxiv_maker/cli/framework/__init__.py,sha256=4FPXdP8J6v4eeEn46mwY0VtnwxjR1jnW_kTrXykQlQs,2704
@@ -38,7 +38,7 @@ rxiv_maker/cli/framework/cache_commands.py,sha256=J91UYLTsLTRoNdzuhAbNL2bJJovYYf
38
38
  rxiv_maker/cli/framework/config_commands.py,sha256=a1uOQkCCw3d4qlro3OwHIorcoNg03T_R4-HbfVb-hmQ,19336
39
39
  rxiv_maker/cli/framework/content_commands.py,sha256=RilxKeG2c1m2fu0CtWAvP3cGh11DGx9P-nh2kIewAg4,22596
40
40
  rxiv_maker/cli/framework/decorators.py,sha256=fh085e3k1CaLSMoZevt8hvgnEuejrf-mcNS-dwXoY_A,10365
41
- rxiv_maker/cli/framework/utility_commands.py,sha256=_P_KwjlyiNS-vVboU-DqkHynGPqzXyoZRWGmLMLTnOs,26214
41
+ rxiv_maker/cli/framework/utility_commands.py,sha256=drIAc1TAYpne76gj7SZeZhPozVAY5uL9GFPVT_Ez0-E,26437
42
42
  rxiv_maker/cli/framework/workflow_commands.py,sha256=CFa3c5oJMmy9cUZxTAU97eKC6YrOljzerSAMrywjbKw,29684
43
43
  rxiv_maker/config/defaults.py,sha256=vHyLGVxe5-z9TLxu5f6NhquPvqQkER_KZv_j1I4_dHQ,3055
44
44
  rxiv_maker/config/validator.py,sha256=9XDPfo_YgasGt6NLkl6HIhaGh1fr6XsFNiXU2DSsivw,38299
@@ -96,7 +96,7 @@ rxiv_maker/engines/operations/build_manager.py,sha256=TAX4-r8HjraAzzvQuIt0CNlvWL
96
96
  rxiv_maker/engines/operations/cleanup.py,sha256=RfbXif0neEVMlprFIHWyvQh6kwghalcesY3t-69Iwsw,18095
97
97
  rxiv_maker/engines/operations/fix_bibliography.py,sha256=ZD8uO4YCxDCMWH4WtBSDc4TOMgM383fcLgsCCW0yK60,22428
98
98
  rxiv_maker/engines/operations/generate_docs.py,sha256=8d_oVYUuRRqTuYN1KnJKqM5Ydp4_yt52ntBv8gUoRVk,11223
99
- rxiv_maker/engines/operations/generate_figures.py,sha256=3oIuS0wryO9WpPZ3UD2qm0YscNidTOEiO4_Jd6r3SmY,32842
99
+ rxiv_maker/engines/operations/generate_figures.py,sha256=YeKzH6qVsuPGjtCsvWugLJoys6y73xTyO7Y5g30KM20,38730
100
100
  rxiv_maker/engines/operations/generate_preprint.py,sha256=wpKDAu2RLJ4amSdhX5GZ7hU-iTsTRt4etcEA7AZYp04,2662
101
101
  rxiv_maker/engines/operations/prepare_arxiv.py,sha256=cd0JN5IO-Wy9T8ab75eibyaA8_K8Gpwrz2F-95OMnx4,21551
102
102
  rxiv_maker/engines/operations/setup_environment.py,sha256=gERuThHTldH0YqgXn85995deHBP6csY1ZhCNgU6-vFg,12691
@@ -165,6 +165,7 @@ rxiv_maker/utils/performance.py,sha256=EBDVNshSaeG7Nu-GCZtRAzTunGn4z_Bb2jEck045b
165
165
  rxiv_maker/utils/platform.py,sha256=DCD3gvm7_DBcT67gGIXhTDV5mPrBjWrL7R2JdsmIgng,17773
166
166
  rxiv_maker/utils/python_execution_reporter.py,sha256=l3hqLXtGAg_wKUlkikK1oaHPeBpoCkj3iR6i1fc11ys,10606
167
167
  rxiv_maker/utils/retry.py,sha256=aNsuc7HuxMwG4yubgX0GxIEZ0iF6_4lACusVMc0ZSXA,11026
168
+ rxiv_maker/utils/rich_upgrade_notifier.py,sha256=aMqkx9l_2KtEiwVFdhOU4oqsAgdWQ86LuqWN4k_cTA0,6365
168
169
  rxiv_maker/utils/text_utils.py,sha256=ntovIx7qXB0LWO9OmCu7-rqTSfyEvthrTjYEJTbmZ_U,1972
169
170
  rxiv_maker/utils/tips.py,sha256=XHgbJkyFjYt5Pz-Rdz_yCjnRLguxIsHwdFmNSg323NA,8078
170
171
  rxiv_maker/utils/title_sync.py,sha256=C7NT80DgBJrS70mil6b7ghcZyKR2n5_MNlC6Yli_dXM,15094
@@ -180,15 +181,15 @@ rxiv_maker/validators/latex_error_parser.py,sha256=crk3NAniLBp2iABP4lxts7gvCEg6K
180
181
  rxiv_maker/validators/math_validator.py,sha256=LcRIGAv47OsPfOg4E48l2vKN1Q7lHAeduNN5MFMLQGE,27669
181
182
  rxiv_maker/validators/pdf_validator.py,sha256=YU4WRPeTEOtvBlquFEtZrG9p_WjlN5nnCByTSRAvWyw,21530
182
183
  rxiv_maker/validators/reference_validator.py,sha256=UqvsEa3kVOBkbGMo24fXpFVUtpN1feIf9MfDQIraQZs,15868
183
- rxiv_maker/validators/syntax_validator.py,sha256=H7JK3H116nI9f_3uzAS_RlIsbcbgajhpoU5T2aGh46w,43098
184
+ rxiv_maker/validators/syntax_validator.py,sha256=hHpKVKky3UiA1ZylA6jJVP3DN47LgaSSJCK2PPA-8BA,43599
184
185
  rxiv_maker/validators/doi/__init__.py,sha256=NqATXseuS0zVNns56RvFe8TdqgvueY0Rbw2Pjozlajc,494
185
186
  rxiv_maker/validators/doi/api_clients.py,sha256=tqdYUq8LFgRIO0tWfcenwmy2uO-IB1-GMvBfF3lP7-0,21763
186
187
  rxiv_maker/validators/doi/metadata_comparator.py,sha256=euqHhKP5sHQAdZbdoAahUn6YqJqOfXIOobNgAqFHlN8,11533
187
188
  rxiv_maker/tex/template.tex,sha256=zrJ3aFfu8j9zkg1l375eE9w-j42P3rz16wMD3dSgi1I,1354
188
189
  rxiv_maker/tex/style/rxiv_maker_style.bst,sha256=jbVqrJgAm6F88cow5vtZuPBwwmlcYykclTm8RvZIo6Y,24281
189
190
  rxiv_maker/tex/style/rxiv_maker_style.cls,sha256=F2qtnS9mI6SwOIaVH76egXZkB2_GzbH4gCTG_ZcfCDQ,24253
190
- rxiv_maker-1.16.5.dist-info/METADATA,sha256=2U4BcFzbnYaEhzXTOgtUCk13H5w_J_RyoCZ5lQQjaGE,17950
191
- rxiv_maker-1.16.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
192
- rxiv_maker-1.16.5.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
193
- rxiv_maker-1.16.5.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
194
- rxiv_maker-1.16.5.dist-info/RECORD,,
191
+ rxiv_maker-1.16.7.dist-info/METADATA,sha256=6O761FLD317XmFqsnlCerTogrcC1Kq4wUJW9xnz4psM,18177
192
+ rxiv_maker-1.16.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
193
+ rxiv_maker-1.16.7.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
194
+ rxiv_maker-1.16.7.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
195
+ rxiv_maker-1.16.7.dist-info/RECORD,,