rxiv-maker 1.16.6__py3-none-any.whl → 1.16.8__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.6"
3
+ __version__ = "1.16.8"
@@ -1,15 +1,86 @@
1
1
  """DOCX export command for rxiv-maker CLI."""
2
2
 
3
+ import platform
4
+ import shutil
5
+ import subprocess
6
+
3
7
  import rich_click as click
4
8
  from rich.console import Console
5
9
 
6
10
  from ...core.logging_config import get_logger
11
+ from ...core.managers.dependency_manager import DependencyStatus, get_dependency_manager
7
12
  from ...exporters.docx_exporter import DocxExporter
8
13
 
9
14
  logger = get_logger()
10
15
  console = Console()
11
16
 
12
17
 
18
+ def _check_and_offer_poppler_installation(console: Console, quiet: bool, verbose: bool) -> None:
19
+ """Check poppler availability and offer automatic installation via brew.
20
+
21
+ Args:
22
+ console: Rich console for output
23
+ quiet: Whether to suppress output
24
+ verbose: Whether verbose mode is enabled
25
+ """
26
+ # Check if poppler is installed
27
+ manager = get_dependency_manager()
28
+ result = manager.check_dependency("pdftoppm")
29
+
30
+ if result.status == DependencyStatus.AVAILABLE:
31
+ if verbose:
32
+ console.print("[dim]✓ Poppler utilities available[/dim]")
33
+ return
34
+
35
+ # Poppler is missing - offer to install
36
+ system = platform.system()
37
+
38
+ if system == "Darwin" and shutil.which("brew"):
39
+ # macOS with Homebrew
40
+ if not quiet:
41
+ console.print("[yellow]⚠️ Poppler not found[/yellow]")
42
+ console.print(" Poppler is needed to embed PDF figures in DOCX files.")
43
+ console.print(" Without it, PDF figures will appear as placeholders.")
44
+ console.print()
45
+
46
+ if click.confirm(" Would you like to install poppler now via Homebrew?", default=True):
47
+ console.print("[cyan]Installing poppler...[/cyan]")
48
+ try:
49
+ result = subprocess.run(
50
+ ["brew", "install", "poppler"],
51
+ capture_output=True,
52
+ text=True,
53
+ timeout=300,
54
+ )
55
+
56
+ if result.returncode == 0:
57
+ console.print("[green]✅ Poppler installed successfully![/green]")
58
+ # Clear dependency cache so it gets re-checked
59
+ manager.clear_cache()
60
+ else:
61
+ console.print(f"[red]❌ Installation failed:[/red] {result.stderr}")
62
+ console.print(" You can install manually with: brew install poppler")
63
+ except subprocess.TimeoutExpired:
64
+ console.print("[red]❌ Installation timed out[/red]")
65
+ except Exception as e:
66
+ console.print(f"[red]❌ Installation error:[/red] {e}")
67
+ else:
68
+ console.print(" [dim]Skipping poppler installation. PDF figures will show as placeholders.[/dim]")
69
+
70
+ elif system == "Linux":
71
+ # Linux
72
+ if not quiet:
73
+ console.print("[yellow]⚠️ Poppler not found[/yellow]")
74
+ console.print(" Install with: sudo apt install poppler-utils")
75
+ console.print()
76
+ else:
77
+ # Other platforms or brew not available
78
+ if not quiet:
79
+ console.print("[yellow]⚠️ Poppler not found[/yellow]")
80
+ console.print(f" Install instructions: {result.resolution_hint}")
81
+ console.print()
82
+
83
+
13
84
  @click.command(context_settings={"help_option_names": ["-h", "--help"]})
14
85
  @click.argument(
15
86
  "manuscript_path",
@@ -86,6 +157,9 @@ def docx(
86
157
  include_footnotes=not no_footnotes,
87
158
  )
88
159
 
160
+ # Pre-flight check for poppler (if manuscript contains PDF figures)
161
+ _check_and_offer_poppler_installation(console, quiet, verbose)
162
+
89
163
  # Perform export
90
164
  docx_path = exporter.export()
91
165
 
@@ -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)
@@ -487,6 +487,18 @@ class DependencyManager:
487
487
  )
488
488
  )
489
489
 
490
+ # Poppler utilities for PDF to image conversion (DOCX export)
491
+ self.register_dependency(
492
+ DependencySpec(
493
+ name="pdftoppm",
494
+ type=DependencyType.SYSTEM_BINARY,
495
+ required=False, # Only needed for DOCX export with PDF figures
496
+ alternatives=["pdfinfo"],
497
+ contexts={"docx", "export"},
498
+ install_hint="macOS: brew install poppler | Linux: sudo apt install poppler-utils",
499
+ )
500
+ )
501
+
490
502
  def register_dependency(self, spec: DependencySpec) -> None:
491
503
  """Register a dependency specification.
492
504
 
@@ -318,9 +318,13 @@ class DocxExporter:
318
318
  # Get DOI from entry
319
319
  doi = entry.fields.get("doi")
320
320
 
321
- # TODO: Implement DOI resolution if requested and DOI missing
322
- # if self.resolve_dois and not doi:
323
- # doi = self._resolve_doi_from_metadata(entry)
321
+ # Attempt DOI resolution if requested and DOI missing
322
+ if self.resolve_dois and not doi:
323
+ doi = self._resolve_doi_from_metadata(entry)
324
+ if doi:
325
+ # Store in entry for this export
326
+ entry.fields["doi"] = doi
327
+ logger.info(f"Resolved DOI for {key}: {doi}")
324
328
 
325
329
  # Format entry (full format for DOCX bibliography)
326
330
  formatted = format_bibliography_entry(entry, doi, slim=False, author_format=self.author_format)
@@ -332,6 +336,99 @@ class DocxExporter:
332
336
 
333
337
  return bibliography
334
338
 
339
+ def _resolve_doi_from_metadata(self, entry) -> str | None:
340
+ """Resolve DOI from entry metadata using CrossRef API.
341
+
342
+ Args:
343
+ entry: Bibliography entry to resolve DOI for
344
+
345
+ Returns:
346
+ Resolved DOI if found, None otherwise
347
+ """
348
+ import requests
349
+
350
+ # Try to construct a search query from available fields
351
+ title = entry.fields.get("title", "").strip()
352
+ year = entry.fields.get("year", "").strip()
353
+
354
+ if not title:
355
+ logger.debug(f"Cannot resolve DOI for {entry.key}: no title")
356
+ return None
357
+
358
+ # Clean title for search (remove LaTeX commands, braces, etc.)
359
+ search_title = self._clean_title_for_search(title)
360
+
361
+ # Try CrossRef search API
362
+ try:
363
+ url = "https://api.crossref.org/works"
364
+ params = {
365
+ "query.title": search_title,
366
+ "rows": 5, # Get top 5 results
367
+ }
368
+
369
+ response = requests.get(url, params=params, timeout=10)
370
+
371
+ if response.status_code == 200:
372
+ data = response.json()
373
+ items = data.get("message", {}).get("items", [])
374
+
375
+ # Find best match
376
+ for item in items:
377
+ item_title = item.get("title", [""])[0].lower()
378
+ search_title_lower = search_title.lower()
379
+
380
+ # Simple similarity check - titles should be very similar
381
+ if item_title and (search_title_lower in item_title or item_title in search_title_lower):
382
+ # Verify year matches if available
383
+ if year:
384
+ item_year = item.get("published", {}).get("date-parts", [[None]])[0][0]
385
+ if item_year and str(item_year) != year:
386
+ continue
387
+
388
+ doi = item.get("DOI")
389
+ if doi:
390
+ logger.info(f"Resolved DOI for {entry.key}: {doi}")
391
+ return doi
392
+
393
+ logger.debug(f"Could not resolve DOI for {entry.key} via CrossRef")
394
+ return None
395
+
396
+ except requests.exceptions.Timeout:
397
+ logger.debug(f"CrossRef API timeout resolving DOI for {entry.key}")
398
+ return None
399
+ except requests.exceptions.ConnectionError:
400
+ logger.debug(f"CrossRef API connection error for {entry.key}")
401
+ return None
402
+ except Exception as e:
403
+ logger.debug(f"Error resolving DOI for {entry.key}: {e}")
404
+ return None
405
+
406
+ def _clean_title_for_search(self, title: str) -> str:
407
+ """Clean title for CrossRef search by removing LaTeX commands.
408
+
409
+ Args:
410
+ title: Raw title from BibTeX entry
411
+
412
+ Returns:
413
+ Cleaned title suitable for search
414
+ """
415
+ import re
416
+
417
+ # Remove LaTeX commands
418
+ title = re.sub(r"\\[a-zA-Z]+\{([^}]*)\}", r"\1", title) # \textit{foo} -> foo
419
+ title = re.sub(r"\\[a-zA-Z]+", "", title) # \LaTeX -> LaTeX
420
+
421
+ # Remove braces
422
+ title = title.replace("{", "").replace("}", "")
423
+
424
+ # Remove special characters
425
+ title = re.sub(r"[^a-zA-Z0-9\s\-]", " ", title)
426
+
427
+ # Normalize whitespace
428
+ title = " ".join(title.split())
429
+
430
+ return title.strip()
431
+
335
432
  def _get_metadata(self) -> Dict[str, Any]:
336
433
  """Extract metadata for title page.
337
434
 
@@ -404,6 +404,19 @@ class DocxWriter:
404
404
  paragraph_format = paragraph.paragraph_format
405
405
  paragraph_format.left_indent = Pt(36) # Indent code blocks
406
406
 
407
+ def _check_poppler_availability(self) -> bool:
408
+ """Check if poppler is available for PDF conversion.
409
+
410
+ Returns:
411
+ True if poppler is available, False otherwise
412
+ """
413
+ from ..core.managers.dependency_manager import DependencyStatus, get_dependency_manager
414
+
415
+ manager = get_dependency_manager()
416
+ result = manager.check_dependency("pdftoppm")
417
+
418
+ return result.status == DependencyStatus.AVAILABLE
419
+
407
420
  def _add_figure(self, doc: Document, section: Dict[str, Any], figure_number: int = None):
408
421
  """Add figure to document with caption.
409
422
 
@@ -425,9 +438,31 @@ class DocxWriter:
425
438
  if not figure_path.exists():
426
439
  logger.warning(f"Figure file not found: {figure_path}")
427
440
  elif figure_path.suffix.lower() == ".pdf":
428
- # Convert PDF to image
429
- img_source = convert_pdf_to_image(figure_path)
430
- logger.debug(f" PDF converted: {img_source is not None}")
441
+ # Check poppler availability first (cached after first check)
442
+ if not hasattr(self, "_poppler_checked"):
443
+ self._poppler_available = self._check_poppler_availability()
444
+ self._poppler_checked = True
445
+
446
+ if not self._poppler_available:
447
+ logger.warning(
448
+ "Poppler not installed - PDF figures will be shown as placeholders. "
449
+ "Install with: brew install poppler (macOS) or sudo apt install poppler-utils (Linux)"
450
+ )
451
+
452
+ if self._poppler_available:
453
+ # Convert PDF to image
454
+ try:
455
+ from pdf2image.exceptions import PDFInfoNotInstalledError, PopplerNotInstalledError
456
+
457
+ img_source = convert_pdf_to_image(figure_path)
458
+ logger.debug(f" PDF converted: {img_source is not None}")
459
+ except (PopplerNotInstalledError, PDFInfoNotInstalledError) as e:
460
+ logger.error(f"Poppler utilities not found: {e}")
461
+ img_source = None
462
+ # Update our cached status
463
+ self._poppler_available = False
464
+ else:
465
+ img_source = None
431
466
  elif figure_path.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".bmp"]:
432
467
  # Use image file directly
433
468
  img_source = str(figure_path)
@@ -81,21 +81,41 @@ def generate_bst_file(format_type: str, output_dir: Path) -> Path:
81
81
  except IOError as e:
82
82
  raise IOError(f"Failed to read template .bst file: {e}") from e
83
83
 
84
- # Replace the format string on line 222
84
+ # Replace the format string in the format.names function (line ~222)
85
85
  # The line looks like: s nameptr "{ff~}{vv~}{ll}{, jj}" format.name$ 't :=
86
- # We need to replace the format string in quotes
87
- pattern = r'(s\s+nameptr\s+")([^"]+)("\s+format\.name\$)'
86
+ # We need to replace ONLY in format.names, NOT in format.full.names
87
+ #
88
+ # Strategy: Match the FUNCTION {format.names} block specifically
89
+ # This ensures we don't modify format.full.names which is used for citation labels
90
+ #
91
+ # Pattern explanation:
92
+ # 1. Match "FUNCTION {format.names}" to find the start of the function
93
+ # 2. Use non-greedy match (.*?) to capture until we find our target line
94
+ # 3. Match and capture the format string in quotes
95
+ # 4. This avoids matching format.full.names which appears later in the file
96
+ pattern = r'(FUNCTION\s+\{format\.names\}.*?s\s+nameptr\s+")([^"]+)("\s+format\.name\$)'
88
97
  replacement = rf"\1{format_string}\3"
89
98
 
90
- modified_content, num_subs = re.subn(pattern, replacement, bst_content)
99
+ modified_content, num_subs = re.subn(pattern, replacement, bst_content, flags=re.DOTALL)
91
100
 
92
101
  if num_subs == 0:
93
- logger.warning("No format string pattern found in .bst file. The .bst file may have been modified.")
102
+ logger.warning(
103
+ "No format string pattern found in format.names function. "
104
+ "The .bst file structure may have changed. "
105
+ "Citation formatting may not work as expected."
106
+ )
94
107
  # Still write the file but log a warning
95
108
  elif num_subs > 1:
96
- logger.warning(
97
- f"Found {num_subs} format string patterns in .bst file. Expected only 1. All have been replaced."
109
+ # This should never happen with the new pattern, but keep the check
110
+ logger.error(
111
+ f"Found {num_subs} matches in .bst file. Expected exactly 1. "
112
+ "This indicates an unexpected .bst file structure. "
113
+ "Please report this issue."
98
114
  )
115
+ raise ValueError(f"Unexpected .bst file structure: found {num_subs} matches for format.names, expected 1")
116
+ else:
117
+ # Success - exactly one match
118
+ logger.debug(f"Successfully updated format.names function with format '{format_type}'")
99
119
 
100
120
  # Create output directory if it doesn't exist
101
121
  output_dir = Path(output_dir)
@@ -8,6 +8,7 @@ This module provides utility functions for DOCX generation including:
8
8
  """
9
9
 
10
10
  import io
11
+ import logging
11
12
  import re
12
13
  from pathlib import Path
13
14
  from typing import Optional
@@ -18,6 +19,8 @@ from rxiv_maker.utils.author_name_formatter import format_author_list
18
19
 
19
20
  from ..utils.bibliography_parser import BibEntry
20
21
 
22
+ logger = logging.getLogger(__name__)
23
+
21
24
 
22
25
  def remove_yaml_header(content: str) -> str:
23
26
  r"""Remove YAML frontmatter from markdown content.
@@ -395,6 +398,12 @@ def convert_pdf_to_image(pdf_path: Path, dpi: int = 150, max_width: int = 6) ->
395
398
  """
396
399
  try:
397
400
  from pdf2image import convert_from_path
401
+ from pdf2image.exceptions import (
402
+ PDFInfoNotInstalledError,
403
+ PDFPageCountError,
404
+ PDFSyntaxError,
405
+ PopplerNotInstalledError,
406
+ )
398
407
 
399
408
  # Convert first page of PDF to image
400
409
  images = convert_from_path(
@@ -427,7 +436,19 @@ def convert_pdf_to_image(pdf_path: Path, dpi: int = 150, max_width: int = 6) ->
427
436
 
428
437
  return img_bytes
429
438
 
439
+ except (PopplerNotInstalledError, PDFInfoNotInstalledError):
440
+ # Re-raise poppler errors so caller can handle them appropriately
441
+ # (e.g., CLI can offer to install poppler)
442
+ raise
443
+ except PDFSyntaxError as e:
444
+ # PDF file is malformed/corrupted
445
+ logger.warning(f"PDF file appears to be corrupted: {pdf_path.name} - {e}")
446
+ return None
447
+ except PDFPageCountError as e:
448
+ # Issue getting page count
449
+ logger.warning(f"Could not determine PDF page count for {pdf_path.name}: {e}")
450
+ return None
430
451
  except Exception as e:
431
- # Log error but don't crash - return None to indicate failure
432
- print(f"Warning: Failed to convert PDF to image: {e}")
452
+ # Other unexpected errors
453
+ logger.warning(f"Failed to convert {pdf_path.name} to image: {e}")
433
454
  return None
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rxiv-maker
3
- Version: 1.16.6
3
+ Version: 1.16.8
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=qf0GMhAIH5L_wI59eHcfpHtgoZxaqziVuvj0M1daj0g,51
2
+ rxiv_maker/__version__.py,sha256=FDIQKkG-uf-zpYYZrGmwOD83CED6k3EcIvKFr382p-g,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
@@ -19,7 +19,7 @@ rxiv_maker/cli/commands/clean.py,sha256=8yQVbhHLQX0xIM7MD-0sG_agX7xVxzl1AkqPzZRR
19
19
  rxiv_maker/cli/commands/completion.py,sha256=TKc6QPEFVN8qQsu2LbPEZvsSZr_CBvyekkfTItTDI6A,789
20
20
  rxiv_maker/cli/commands/config.py,sha256=adN4ci_EB-NZsNXNY9FlFxE6CLVxblunX1dobrjNef0,33761
21
21
  rxiv_maker/cli/commands/create_repo.py,sha256=jWPwT1Ai7XBhFAi9JmSAobdFYXPlSgQCD-gx_ZUNwEo,9947
22
- rxiv_maker/cli/commands/docx.py,sha256=yFJxwDyQ8F6M9n4R6AUx0lq03iMQevuI_9eY2t6ke24,2849
22
+ rxiv_maker/cli/commands/docx.py,sha256=memJ5SeSQ8rD_wwOH9gUs1TgJUpP25_gIKEu8lKn5nQ,5896
23
23
  rxiv_maker/cli/commands/figures.py,sha256=WraC0BjwxKImzCwt1A3vWs8_T4w2misP1eeW6aznpCw,3250
24
24
  rxiv_maker/cli/commands/get_rxiv_preprint.py,sha256=nSPuFpS5xZ9VaUrmwnFZ5RCAAjj0ORAj8x_EJmH0Xtg,8543
25
25
  rxiv_maker/cli/commands/init.py,sha256=2UiXtk7a5bklqwevWwhbL9lU1ASBxUwzm7JdrINHcGA,1797
@@ -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
@@ -80,7 +80,7 @@ rxiv_maker/core/cache/secure_cache_utils.py,sha256=EejPWvxw_mUPqO0TRBHYYTsLXWZEU
80
80
  rxiv_maker/core/managers/__init__.py,sha256=sh4ZuZH4YrAu4XTiN9ky1-tQQASKiSTY0udJJAzDRcU,950
81
81
  rxiv_maker/core/managers/cache_manager.py,sha256=8btUaRDYPOrUynHWBMz7RVeS9xcpUoyhemFNUi8NqpQ,21893
82
82
  rxiv_maker/core/managers/config_manager.py,sha256=DdM-3_mfrjHFkPQ2vOmpQnXaoSJU-ianVwUvCEIwZss,18829
83
- rxiv_maker/core/managers/dependency_manager.py,sha256=DwVRcvk4JcZGzYc-K6ue7aGfw8fSnne50pPI_8Arlzw,25800
83
+ rxiv_maker/core/managers/dependency_manager.py,sha256=qtUR4sQD2x1zv4Fi77d6ThwPx9eHZ9_ZyNy0hRpIKKQ,26308
84
84
  rxiv_maker/core/managers/execution_manager.py,sha256=cEDS0KyWBHf_N74Fc8MC4VRXxmEcaz_DJtyWO5-o628,29585
85
85
  rxiv_maker/core/managers/file_manager.py,sha256=SVRnP1JQoGCAms3E7iSpOp_RG60P36Qk9HGAmJDaFvE,18641
86
86
  rxiv_maker/core/managers/install_manager.py,sha256=8HChOfbm5-uXsksKMDqmgrYNMJJtyDoL2RDNpbFwhwc,15533
@@ -106,8 +106,8 @@ rxiv_maker/engines/operations/validate_pdf.py,sha256=qyrtL752Uap3i6ntQheY570soVj
106
106
  rxiv_maker/exporters/__init__.py,sha256=NcTD1SDb8tTgsHhCS1A7TVEZncyWbDRTa6sJIdLqcsE,350
107
107
  rxiv_maker/exporters/docx_citation_mapper.py,sha256=Qp3IEqrR6lQGQPQ4JeGdOCWeg97XBbVCKdX1gtX9XfY,4584
108
108
  rxiv_maker/exporters/docx_content_processor.py,sha256=3srXfq4lC_FJpTAfu-WB930WheSm6rCy-vNUtFlsflY,23878
109
- rxiv_maker/exporters/docx_exporter.py,sha256=i2G_AF-Co4AOD6ZdRzDZ7VgcJLdCmxu-Fn3SykvBywQ,15152
110
- rxiv_maker/exporters/docx_writer.py,sha256=qI_0JklqAxF_TUHxO3wPopZpJ44O2qYjMPOptrnU0XM,41915
109
+ rxiv_maker/exporters/docx_exporter.py,sha256=Q18TuZCbx6fzAs1f9G2A9h5N2Gw5qhCrL7a2NKXizg8,18672
110
+ rxiv_maker/exporters/docx_writer.py,sha256=CW2Xu9-vpuEct9344bM8ofGkCiWlAYijoXVzZ7MQZ6g,43460
111
111
  rxiv_maker/install/__init__.py,sha256=kAB6P-12IKg_K1MQ-uzeC5IR11O2cNxj0t_2JMhooZs,590
112
112
  rxiv_maker/install/dependency_handlers/__init__.py,sha256=NN9dP1usXpYgLpSw0uEnJ6ugX2zefihVjdyDdm1k-cE,231
113
113
  rxiv_maker/install/dependency_handlers/latex.py,sha256=xopSJxYkg3D63rH7RoVLN-Ykl87AZqhlUrrG3m6LoWo,3304
@@ -147,11 +147,11 @@ rxiv_maker/utils/__init__.py,sha256=4ya5VR8jqRqUChlnUeMeeetOuWV-gIvjPwcE1u_1OnI,
147
147
  rxiv_maker/utils/author_name_formatter.py,sha256=UjvarbyQm89EUIYqckygx3g37o-EcNyvipBtY8GJDxs,10222
148
148
  rxiv_maker/utils/bibliography_checksum.py,sha256=Jh4VILSpGQ5KJ9UBCUb7oFy6lZ9_ncXD87vEXxw5jbY,10270
149
149
  rxiv_maker/utils/bibliography_parser.py,sha256=WZIQoEpVwdbLmbkw9FdkVgoLE5GX7itqnzPnEEb_fFU,6846
150
- rxiv_maker/utils/bst_generator.py,sha256=kS4k_cc2GNm3l-bwaVU7uffWbH3VDzHwgCXHn7Ro9xI,5002
150
+ rxiv_maker/utils/bst_generator.py,sha256=m69JWMIvf9eRiHcaWB-8D3DQCDO8flVIYbOBMuzV-F0,6097
151
151
  rxiv_maker/utils/changelog_parser.py,sha256=WCDp9Iy6H6_3nC6FB7RLt6i00zuCyvU17sCU4e3pqCY,11954
152
152
  rxiv_maker/utils/citation_utils.py,sha256=spIgVxPAN6jPvoG-eOE00rVX_buUGKnUjP1Fhz31sl4,5134
153
153
  rxiv_maker/utils/dependency_checker.py,sha256=EdyIvk-W_bhC1DJCpFw5ePhjEU74C9j7RYMm06unBMA,14366
154
- rxiv_maker/utils/docx_helpers.py,sha256=jhxkrU80JjDJXDE3gNfsJt9PqLyByYDfymY_Jm9ZqoI,12860
154
+ rxiv_maker/utils/docx_helpers.py,sha256=rNNnz3mhXymrTgQR4bZDnGwpUFylWVpfpJnY0bWnl30,13635
155
155
  rxiv_maker/utils/doi_resolver.py,sha256=8_oy5cTtklm1GCKXpn509yqYsu4P5gYbMjtfQ8dRgFA,10253
156
156
  rxiv_maker/utils/email_encoder.py,sha256=QMD5JbGNu68gD8SBdGHfNY8uCgbMzEcmzE1TCYDMgWY,5139
157
157
  rxiv_maker/utils/figure_checksum.py,sha256=PWgh2QAErNnnQCV-t-COACQXKICUaggAAIxhgHLCGNM,10748
@@ -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
@@ -187,8 +188,8 @@ rxiv_maker/validators/doi/metadata_comparator.py,sha256=euqHhKP5sHQAdZbdoAahUn6Y
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.6.dist-info/METADATA,sha256=7_r-ZEzlmKCVaCu_a-Z8tg8pr4kzfI3tDbvfCbc1dCw,17950
191
- rxiv_maker-1.16.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
192
- rxiv_maker-1.16.6.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
193
- rxiv_maker-1.16.6.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
194
- rxiv_maker-1.16.6.dist-info/RECORD,,
191
+ rxiv_maker-1.16.8.dist-info/METADATA,sha256=3GydBaQKqwL3nUaxEPdNRRobVMhmb7hRvJW7e5qQ_gc,18177
192
+ rxiv_maker-1.16.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
193
+ rxiv_maker-1.16.8.dist-info/entry_points.txt,sha256=ghCN0hI9A1GlG7QY5F6E-xYPflA8CyS4B6bTQ1YLop0,97
194
+ rxiv_maker-1.16.8.dist-info/licenses/LICENSE,sha256=GSZFoPIhWDNJEtSHTQ5dnELN38zFwRiQO2antBezGQk,1093
195
+ rxiv_maker-1.16.8.dist-info/RECORD,,