vibefpmgui.formatter

Formatting Fortran files in the editor with fprettify.

fprettify reads the editor's text on stdin and writes the formatted text to stdout, so unsaved changes are formatted too and the result is one undoable edit. It runs in the folder of the file, so a .fprettify.rc there (or in a parent folder, or ~) applies. Like fortitude, it only understands free-form Fortran.

 1"""Formatting Fortran files in the editor with fprettify.
 2
 3`fprettify <https://github.com/fortran-lang/fprettify>`_ reads the editor's
 4text on stdin and writes the formatted text to stdout, so unsaved changes are
 5formatted too and the result is one undoable edit. It runs in the folder of
 6the file, so a ``.fprettify.rc`` there (or in a parent folder, or ``~``)
 7applies. Like fortitude, it only understands free-form Fortran.
 8"""
 9
10from __future__ import annotations
11
12import os
13import shutil
14import subprocess
15from pathlib import Path
16
17from .console import NO_WINDOW
18from .lint import LINT_EXTS
19
20FORMAT_EXTS = LINT_EXTS
21
22
23def executable() -> str | None:
24    return shutil.which("fprettify")
25
26
27def can_format(path: Path) -> bool:
28    return path.suffix.lower() in FORMAT_EXTS
29
30
31def format_text(text: str, path: Path, timeout: float = 60) -> tuple[str | None, str]:
32    """(formatted text, "") or (None, why it failed)."""
33    program = executable()
34    if program is None:
35        return None, "fprettify was not found on the PATH."
36    # fprettify's own regexes trigger SyntaxWarnings on newer Pythons.
37    env = dict(os.environ, PYTHONWARNINGS="ignore")
38    try:
39        out = subprocess.run([program, "-"], input=text, capture_output=True, text=True,
40                             encoding="utf-8", errors="replace", cwd=path.parent,
41                             timeout=timeout, env=env, check=False, **NO_WINDOW)
42    except (OSError, subprocess.SubprocessError) as exc:
43        return None, str(exc)
44    if out.returncode != 0:
45        lines = [line for line in out.stderr.splitlines() if line.strip()]
46        return None, lines[-1] if lines else f"fprettify exited with code {out.returncode}"
47    if text.strip() and not out.stdout.strip():
48        return None, "fprettify returned no text."
49    return out.stdout, ""
FORMAT_EXTS = {'.f23', '.f08', '.f95', '.f90', '.f03', '.f18'}
def executable() -> str | None:
24def executable() -> str | None:
25    return shutil.which("fprettify")
def can_format(path: pathlib.Path) -> bool:
28def can_format(path: Path) -> bool:
29    return path.suffix.lower() in FORMAT_EXTS
def format_text( text: str, path: pathlib.Path, timeout: float = 60) -> tuple[str | None, str]:
32def format_text(text: str, path: Path, timeout: float = 60) -> tuple[str | None, str]:
33    """(formatted text, "") or (None, why it failed)."""
34    program = executable()
35    if program is None:
36        return None, "fprettify was not found on the PATH."
37    # fprettify's own regexes trigger SyntaxWarnings on newer Pythons.
38    env = dict(os.environ, PYTHONWARNINGS="ignore")
39    try:
40        out = subprocess.run([program, "-"], input=text, capture_output=True, text=True,
41                             encoding="utf-8", errors="replace", cwd=path.parent,
42                             timeout=timeout, env=env, check=False, **NO_WINDOW)
43    except (OSError, subprocess.SubprocessError) as exc:
44        return None, str(exc)
45    if out.returncode != 0:
46        lines = [line for line in out.stderr.splitlines() if line.strip()]
47        return None, lines[-1] if lines else f"fprettify exited with code {out.returncode}"
48    if text.strip() and not out.stdout.strip():
49        return None, "fprettify returned no text."
50    return out.stdout, ""

(formatted text, "") or (None, why it failed).