New Google Python Docstring formatter (#630)
Co-authored-by: UltralyticsAssistant <[email protected]>
This commit is contained in:
co-authored by
UltralyticsAssistant
parent
04d6506b12
commit
52f1435ad7
@@ -26,7 +26,8 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets._GITHUB_TOKEN || github.token }} # Auto-generated fallback
|
||||
labels: true # Auto-label issues/PRs using AI
|
||||
python: true # Format Python with Ruff and docformatter
|
||||
python: true # Format Python with Ruff
|
||||
python_docstrings: true # Format Python docstrings
|
||||
prettier: true # Format YAML, JSON, Markdown, CSS
|
||||
spelling: true # Check spelling with codespell
|
||||
links: false # Check broken links with Lychee
|
||||
|
||||
@@ -158,3 +158,6 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# Extras
|
||||
test_docstring_formatter.py
|
||||
|
||||
@@ -30,8 +30,8 @@ AI-powered formatting, labeling, and PR summaries for Python, Swift, and Markdow
|
||||
### 📄 Features
|
||||
|
||||
- **Python Code:** Formatted using [Ruff](https://github.com/astral-sh/ruff), an extremely fast Python linter and formatter
|
||||
- **Python Docstrings:** Google-style formatting enforced with Ultralytics Python docstring formatter (optional)
|
||||
- **Markdown Files:** Styled with [Prettier](https://github.com/prettier/prettier) to ensure consistent documentation appearance
|
||||
- **Docstrings:** Cleaned and standardized using [docformatter](https://github.com/PyCQA/docformatter)
|
||||
- **Swift Code:** Formatted with [`swift-format`](https://github.com/swiftlang/swift-format) _(requires `macos-latest` runner)_
|
||||
- **Spell Check:** Common misspellings caught using [codespell](https://github.com/codespell-project/codespell)
|
||||
- **Broken Links Check:** Broken links identified using [Lychee](https://github.com/lycheeverse/lychee)
|
||||
@@ -80,7 +80,8 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Auto-generated token
|
||||
labels: true # Auto-label issues/PRs using AI
|
||||
python: true # Format Python with Ruff and docformatter
|
||||
python: true # Format Python with Ruff
|
||||
python_docstrings: false # Format Python docstrings (default: false)
|
||||
prettier: true # Format YAML, JSON, Markdown, CSS
|
||||
swift: false # Format Swift (requires macos-latest)
|
||||
dart: false # Format Dart/Flutter
|
||||
|
||||
+7
-8
@@ -18,6 +18,10 @@ inputs:
|
||||
description: "Run Python formatting"
|
||||
required: false
|
||||
default: "false"
|
||||
python_docstrings:
|
||||
description: "Run Python docstring formatting"
|
||||
required: false
|
||||
default: "false"
|
||||
markdown:
|
||||
description: "Run Markdown formatting (deprecated in favor of prettier)"
|
||||
required: false
|
||||
@@ -161,14 +165,9 @@ runs:
|
||||
ruff format \
|
||||
--line-length 120 \
|
||||
. || true
|
||||
docformatter \
|
||||
--wrap-summaries 120 \
|
||||
--wrap-descriptions 120 \
|
||||
--pre-summary-newline \
|
||||
--close-quotes-on-newline \
|
||||
--in-place \
|
||||
--recursive \
|
||||
.
|
||||
if [ "${{ inputs.python_docstrings }}" = "true" ]; then
|
||||
ultralytics-actions-format-python-docstrings . || true
|
||||
fi
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# │ ├── scan_prs.py
|
||||
# │ ├── summarize_pr.py
|
||||
# │ ├── summarize_release.py
|
||||
# │ ├── format_python_docstrings.py
|
||||
# │ ├── update_file_headers.py
|
||||
# │ └── update_markdown_code_blocks.py
|
||||
# └── tests/
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SECTIONS = ("Args", "Attributes", "Methods", "Returns", "Yields", "Raises", "Example", "Notes", "References")
|
||||
LIST_RX = re.compile(r"""^(\s*)(?:[-*•]\s+|(?:\d+|[A-Za-z]+)[\.\)]\s+)""")
|
||||
TABLE_RX = re.compile(r"^\s*\|.*\|\s*$")
|
||||
TABLE_RULE_RX = re.compile(r"^\s*[:\-\|\s]{3,}$")
|
||||
TREE_CHARS = ("└", "├", "│", "─")
|
||||
|
||||
|
||||
def wrap_words(words: list[str], width: int, indent: int, min_words_per_line: int = 1) -> list[str]:
|
||||
"""Wrap words to width with indent; optionally avoid very short orphan lines."""
|
||||
pad = " " * indent
|
||||
if not words:
|
||||
return []
|
||||
lines: list[list[str]] = []
|
||||
cur: list[str] = []
|
||||
cur_len = indent
|
||||
for w in words:
|
||||
need = len(w) + (1 if cur else 0)
|
||||
if cur and cur_len + need > width:
|
||||
lines.append(cur)
|
||||
cur, cur_len = [w], indent + len(w)
|
||||
else:
|
||||
cur.append(w)
|
||||
cur_len += need
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
|
||||
# Rebalance to avoid too-short continuation lines when requested
|
||||
if min_words_per_line > 1:
|
||||
i = 1
|
||||
while i < len(lines):
|
||||
if len(lines[i]) < min_words_per_line and len(lines[i - 1]) > 1:
|
||||
donor = lines[i - 1][-1]
|
||||
this_len = len(pad) + sum(len(w) for w in lines[i]) + (len(lines[i]) - 1)
|
||||
if this_len + (1 if lines[i] else 0) + len(donor) <= width:
|
||||
lines[i - 1].pop()
|
||||
lines[i].insert(0, donor)
|
||||
if i - 1 > 0 and len(lines[i - 1]) == 1:
|
||||
i -= 1
|
||||
continue
|
||||
i += 1
|
||||
|
||||
return [pad + " ".join(line) for line in lines]
|
||||
|
||||
|
||||
def wrap_para(text: str, width: int, indent: int, min_words_per_line: int = 1) -> list[str]:
|
||||
"""Wrap a paragraph string; orphan control via min_words_per_line."""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return []
|
||||
return wrap_words(text.split(), width, indent, min_words_per_line)
|
||||
|
||||
|
||||
def wrap_hanging(head: str, desc: str, width: int, cont_indent: int) -> list[str]:
|
||||
"""Wrap 'head + desc' with hanging indent; ensure first continuation has ≥2 words."""
|
||||
room = width - len(head)
|
||||
words = desc.split()
|
||||
if not words:
|
||||
return [head.rstrip()]
|
||||
|
||||
take, used = [], 0
|
||||
for w in words:
|
||||
need = len(w) + (1 if take else 0)
|
||||
if used + need <= room:
|
||||
take.append(w)
|
||||
used += need
|
||||
else:
|
||||
break
|
||||
|
||||
out: list[str] = []
|
||||
if take:
|
||||
out.append(head + " ".join(take))
|
||||
rest = words[len(take) :]
|
||||
else:
|
||||
out.append(head.rstrip())
|
||||
rest = words
|
||||
|
||||
out.extend(wrap_words(rest, width, cont_indent, min_words_per_line=2))
|
||||
return out
|
||||
|
||||
|
||||
def is_list_item(s: str) -> bool:
|
||||
"""True if s looks like a bullet/numbered list item."""
|
||||
return bool(LIST_RX.match(s.lstrip()))
|
||||
|
||||
|
||||
def is_fence_line(s: str) -> bool:
|
||||
t = s.lstrip()
|
||||
return t.startswith("```")
|
||||
|
||||
|
||||
def is_table_like(s: str) -> bool:
|
||||
return bool(TABLE_RX.match(s)) or bool(TABLE_RULE_RX.match(s))
|
||||
|
||||
|
||||
def is_tree_like(s: str) -> bool:
|
||||
return any(ch in s for ch in TREE_CHARS)
|
||||
|
||||
|
||||
def is_indented_block_line(s: str) -> bool:
|
||||
# Treat lines with >=8 leading spaces or any tab as preformatted
|
||||
return bool(s.startswith(" ")) or s.startswith("\t")
|
||||
|
||||
|
||||
def header_name(line: str) -> str | None:
|
||||
"""Return canonical section header or None."""
|
||||
s = line.strip()
|
||||
if not s.endswith(":") or len(s) <= 1:
|
||||
return None
|
||||
name = s[:-1].strip()
|
||||
if name == "Examples":
|
||||
name = "Example"
|
||||
if name == "Note":
|
||||
name = "Notes"
|
||||
return name if name in SECTIONS else None
|
||||
|
||||
|
||||
def add_header(lines: list[str], indent: int, title: str, opener_line: str) -> None:
|
||||
"""Append a section header; no blank before first header, exactly one before subsequent ones."""
|
||||
while lines and lines[-1] == "":
|
||||
lines.pop()
|
||||
if lines and lines[-1] != opener_line:
|
||||
lines.append("")
|
||||
lines.append(" " * indent + f"{title}:")
|
||||
|
||||
|
||||
def emit_paragraphs(
|
||||
src: list[str], width: int, indent: int, list_indent: int | None = None, orphan_min: int = 1
|
||||
) -> list[str]:
|
||||
"""Wrap normal text; preserve lists, fenced code, tables, ASCII trees, and deeply-indented blocks."""
|
||||
out: list[str] = []
|
||||
buf: list[str] = []
|
||||
in_fence = False
|
||||
|
||||
def flush():
|
||||
nonlocal buf
|
||||
if buf:
|
||||
out.extend(wrap_para(" ".join(x.strip() for x in buf), width, indent, min_words_per_line=orphan_min))
|
||||
buf = []
|
||||
|
||||
for raw in src:
|
||||
s = raw.rstrip("\n")
|
||||
stripped = s.strip()
|
||||
|
||||
# blank line
|
||||
if not stripped:
|
||||
flush()
|
||||
out.append("")
|
||||
continue
|
||||
|
||||
# fence start/stop
|
||||
if is_fence_line(s):
|
||||
flush()
|
||||
out.append(s.rstrip())
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
|
||||
if in_fence or is_table_like(s) or is_tree_like(s) or is_indented_block_line(s):
|
||||
flush()
|
||||
out.append(s.rstrip())
|
||||
continue
|
||||
|
||||
if is_list_item(s):
|
||||
flush()
|
||||
out.append((" " * list_indent + stripped) if list_indent is not None else s.rstrip())
|
||||
continue
|
||||
|
||||
buf.append(s)
|
||||
flush()
|
||||
while out and out[-1] == "":
|
||||
out.pop()
|
||||
return out
|
||||
|
||||
|
||||
def parse_sections(text: str) -> dict[str, list[str]]:
|
||||
"""Parse Google-style docstring into sections."""
|
||||
parts = {k: [] for k in ("summary", "description", *SECTIONS)}
|
||||
cur = "summary"
|
||||
for raw in text.splitlines():
|
||||
line = raw.rstrip("\n")
|
||||
h = header_name(line)
|
||||
if h:
|
||||
cur = h
|
||||
continue
|
||||
if not line.strip():
|
||||
if cur == "summary" and parts["summary"]:
|
||||
cur = "description"
|
||||
if parts[cur]:
|
||||
parts[cur].append("")
|
||||
continue
|
||||
parts[cur].append(line)
|
||||
return parts
|
||||
|
||||
|
||||
def looks_like_param(s: str) -> bool:
|
||||
"""Heuristic: 'name:' without being a list item."""
|
||||
if is_list_item(s) or ":" not in s:
|
||||
return False
|
||||
head = s.split(":", 1)[0].strip()
|
||||
return bool(head)
|
||||
|
||||
|
||||
def iter_items(lines: list[str]) -> list[list[str]]:
|
||||
"""Group lines into logical items separated by next param."""
|
||||
items, i, n = [], 0, len(lines)
|
||||
while i < n:
|
||||
while i < n and not lines[i].strip():
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
item = [lines[i]]
|
||||
i += 1
|
||||
while i < n:
|
||||
st = lines[i].strip()
|
||||
if st and looks_like_param(st):
|
||||
break
|
||||
item.append(lines[i])
|
||||
i += 1
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def format_structured_block(lines: list[str], width: int, base: int) -> list[str]:
|
||||
"""Format Args/Returns/etc.; continuation at base+4, lists at base+8. Preserve missing colons."""
|
||||
out: list[str] = []
|
||||
cont, lst = base + 4, base + 8
|
||||
for item in iter_items(lines):
|
||||
first = item[0].strip()
|
||||
name, desc = ([*first.split(":", 1), ""])[:2]
|
||||
name, desc = name.strip(), desc.strip()
|
||||
had_colon = ":" in first
|
||||
|
||||
if not name or (" " in name and "(" not in name and ")" not in name):
|
||||
out.extend(emit_paragraphs(item, width, cont, lst, orphan_min=2))
|
||||
continue
|
||||
|
||||
head = " " * cont + (f"{name}: " if (desc or had_colon) else name)
|
||||
out.extend(wrap_hanging(head, desc, width, cont + 4))
|
||||
|
||||
tail = item[1:]
|
||||
if tail:
|
||||
body = emit_paragraphs(tail, width, cont + 4, lst, orphan_min=2)
|
||||
if body:
|
||||
out.extend(body)
|
||||
return out
|
||||
|
||||
|
||||
def detect_opener(original_literal: str) -> tuple[str, str, bool]:
|
||||
"""Return (prefix, quotes, inline_hint) from the original string token safely."""
|
||||
s = original_literal.lstrip()
|
||||
i = 0
|
||||
while i < len(s) and s[i] in "rRuUbBfF":
|
||||
i += 1
|
||||
quotes = '"""'
|
||||
if i + 3 <= len(s) and s[i : i + 3] in ('"""', "'''"):
|
||||
quotes = s[i : i + 3]
|
||||
keep = "".join(ch for ch in s[:i] if ch in "rRuU")
|
||||
j = i + len(quotes)
|
||||
inline_hint = j < len(s) and s[j : j + 1] not in {"", "\n", "\r"}
|
||||
return keep, quotes, inline_hint
|
||||
|
||||
|
||||
def format_google(text: str, indent: int, width: int, quotes: str, prefix: str, inline_first_line: bool) -> str:
|
||||
"""Format multi-line Google-style docstring with given quotes/prefix."""
|
||||
p = parse_sections(text)
|
||||
opener = prefix + quotes
|
||||
out = [opener]
|
||||
if p["summary"]:
|
||||
lines = emit_paragraphs(p["summary"], width, indent, list_indent=indent, orphan_min=1)
|
||||
if inline_first_line and lines:
|
||||
pad = " " * indent
|
||||
first = lines[0][len(pad) :] if lines[0].startswith(pad) else lines[0].lstrip()
|
||||
out[0] = opener + first
|
||||
out.extend(lines[1:])
|
||||
else:
|
||||
out.extend(lines)
|
||||
if any(x.strip() for x in p["description"]):
|
||||
out.append("")
|
||||
out.extend(emit_paragraphs(p["description"], width, indent, list_indent=indent, orphan_min=1))
|
||||
for sec in ("Args", "Attributes", "Methods", "Returns", "Yields", "Raises"):
|
||||
if any(x.strip() for x in p[sec]):
|
||||
add_header(out, indent, sec, opener)
|
||||
out.extend(format_structured_block(p[sec], width, indent))
|
||||
for sec in ("Example", "Notes", "References"):
|
||||
if any(x.strip() for x in p[sec]):
|
||||
title = "Examples" if sec == "Example" else sec
|
||||
add_header(out, indent, title, opener)
|
||||
out.extend(x.rstrip() for x in p[sec])
|
||||
while out and out[-1] == "":
|
||||
out.pop()
|
||||
out.append(" " * indent + quotes)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def format_docstring(
|
||||
content: str,
|
||||
indent: int,
|
||||
width: int,
|
||||
quotes: str,
|
||||
prefix: str,
|
||||
inline_hint: bool,
|
||||
preserve_inline: bool = True,
|
||||
) -> str:
|
||||
"""Single-line if short/sectionless/no-lists; else Google-style. Preserve quotes/prefix."""
|
||||
if not content or not content.strip():
|
||||
return f"{prefix}{quotes}{quotes}"
|
||||
text = content.strip()
|
||||
has_section = any(f"{s}:" in text for s in (*SECTIONS, "Examples"))
|
||||
has_list = any(is_list_item(l) for l in text.splitlines())
|
||||
single_ok = (
|
||||
("\n" not in text)
|
||||
and not has_section
|
||||
and not has_list
|
||||
and (indent + len(prefix) + len(quotes) * 2 + len(text) <= width)
|
||||
)
|
||||
if single_ok:
|
||||
words = text.split()
|
||||
if words and not (words[0].startswith(("http://", "https://")) or words[0][0].isupper()):
|
||||
words[0] = words[0][0].upper() + words[0][1:]
|
||||
out = " ".join(words)
|
||||
if out and out[-1] not in ".!?":
|
||||
out += "."
|
||||
return f"{prefix}{quotes}{out}{quotes}"
|
||||
return format_google(text, indent, width, quotes, prefix, inline_first_line=(preserve_inline and inline_hint))
|
||||
|
||||
|
||||
class Visitor(ast.NodeVisitor):
|
||||
"""Collect docstring replacements for classes and functions."""
|
||||
|
||||
def __init__(self, src: list[str], width: int = 120, preserve_inline: bool = True):
|
||||
"""Init with source lines, target width, and inline preservation flag."""
|
||||
self.src, self.width, self.repl, self.preserve_inline = src, width, [], preserve_inline
|
||||
|
||||
def visit_Module(self, node):
|
||||
"""Skip module docstring; visit children."""
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self._handle(node)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self._handle(node)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self._handle(node)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _handle(self, node):
|
||||
"""If first stmt is a string expr, schedule replacement."""
|
||||
try:
|
||||
doc = ast.get_docstring(node, clean=False)
|
||||
if not doc or not node.body or not isinstance(node.body[0], ast.Expr):
|
||||
return
|
||||
s = node.body[0].value
|
||||
if not (isinstance(s, ast.Constant) and isinstance(s.value, str)):
|
||||
return
|
||||
sl, el = node.body[0].lineno - 1, node.body[0].end_lineno - 1
|
||||
sc, ec = node.body[0].col_offset, node.body[0].end_col_offset
|
||||
if sl < 0 or el >= len(self.src):
|
||||
return
|
||||
original = (
|
||||
self.src[sl][sc:ec]
|
||||
if sl == el
|
||||
else "\n".join([self.src[sl][sc:], *self.src[sl + 1 : el], self.src[el][:ec]])
|
||||
)
|
||||
prefix, quotes, inline_hint = detect_opener(original)
|
||||
formatted = format_docstring(doc, sc, self.width, quotes, prefix, inline_hint, self.preserve_inline)
|
||||
if formatted.strip() != original.strip():
|
||||
self.repl.append((sl, el, sc, ec, formatted))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def format_python_file(text: str, width: int = 120, preserve_inline: bool = True) -> str:
|
||||
"""Return source with reformatted docstrings; on failure, return original."""
|
||||
if not text.strip():
|
||||
return text
|
||||
try:
|
||||
tree = ast.parse(text)
|
||||
except SyntaxError:
|
||||
return text
|
||||
src = text.splitlines()
|
||||
v = Visitor(src, width, preserve_inline=preserve_inline)
|
||||
try:
|
||||
v.visit(tree)
|
||||
except Exception:
|
||||
return text
|
||||
for sl, el, sc, ec, rep in reversed(v.repl):
|
||||
try:
|
||||
if sl == el:
|
||||
src[sl] = src[sl][:sc] + rep + src[sl][ec:]
|
||||
else:
|
||||
nl = rep.splitlines()
|
||||
nl[0] = src[sl][:sc] + nl[0]
|
||||
nl[-1] += src[el][ec:]
|
||||
src[sl : el + 1] = nl
|
||||
except Exception:
|
||||
continue
|
||||
return "\n".join(src)
|
||||
|
||||
|
||||
def preserve_trailing_newlines(original: str, formatted: str) -> str:
|
||||
"""Preserve the original trailing newline count."""
|
||||
o = len(original) - len(original.rstrip("\n"))
|
||||
f = len(formatted) - len(formatted.rstrip("\n"))
|
||||
return formatted if o == f else formatted.rstrip("\n") + ("\n" * o)
|
||||
|
||||
|
||||
def iter_py_files(paths: list[Path]) -> list[Path]:
|
||||
"""Expand input paths to sorted unique *.py files."""
|
||||
out: list[Path] = []
|
||||
for p in paths:
|
||||
if p.is_dir():
|
||||
out.extend(sorted(p.rglob("*.py")))
|
||||
elif p.is_file() and p.suffix == ".py":
|
||||
out.append(p)
|
||||
seen, uniq = set(), []
|
||||
for f in out:
|
||||
if f not in seen:
|
||||
seen.add(f)
|
||||
uniq.append(f)
|
||||
return uniq
|
||||
|
||||
|
||||
def process_file(path: Path, width: int = 120, check: bool = False, preserve_inline: bool = True) -> bool:
|
||||
"""Process one file; True if unchanged/success, False if changed."""
|
||||
if path.suffix != ".py":
|
||||
return True
|
||||
try:
|
||||
orig = path.read_text(encoding="utf-8")
|
||||
fmt = preserve_trailing_newlines(orig, format_python_file(orig, width, preserve_inline=preserve_inline))
|
||||
if check:
|
||||
if orig != fmt:
|
||||
print(f" {path}")
|
||||
return False
|
||||
return True
|
||||
if orig != fmt:
|
||||
path.write_text(fmt, encoding="utf-8")
|
||||
print(f" {path}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" Error: {path}: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def parse_cli(argv: list[str]) -> tuple[list[Path], int, bool, bool]:
|
||||
"""Minimal argv parser: (paths, width, check, preserve_inline)."""
|
||||
width, check, paths, preserve_inline = 120, False, [], True
|
||||
for a in argv:
|
||||
if a == "--check":
|
||||
check = True
|
||||
elif a == "--no-preserve-inline":
|
||||
preserve_inline = False
|
||||
elif a.startswith("--line-width="):
|
||||
try:
|
||||
width = int(a.split("=", 1)[1])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
paths.append(Path(a))
|
||||
return paths, width, check, preserve_inline
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print(
|
||||
"Usage: format_python_docstrings.py [--check] [--no-preserve-inline] [--line-width=120] <files_or_dirs...>"
|
||||
)
|
||||
return
|
||||
paths, width, check, preserve_inline = parse_cli(args)
|
||||
files = iter_py_files(paths)
|
||||
if not files:
|
||||
print("No Python files found")
|
||||
return
|
||||
|
||||
t0 = time.time()
|
||||
print(f"{'Checking' if check else 'Formatting'} {len(files)} file{'s' if len(files) != 1 else ''}")
|
||||
changed = sum(not process_file(f, width, check, preserve_inline=preserve_inline) for f in files)
|
||||
|
||||
dur = time.time() - t0
|
||||
if changed:
|
||||
verb = "would be reformatted" if check else "reformatted"
|
||||
unchanged = len(files) - changed
|
||||
parts = []
|
||||
if changed:
|
||||
parts.append(f"{changed} file{'s' if changed != 1 else ''} {verb}")
|
||||
if unchanged:
|
||||
parts.append(f"{unchanged} file{'s' if unchanged != 1 else ''} left unchanged")
|
||||
print(f"{', '.join(parts)} ({dur:.1f}s)")
|
||||
if check:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"{len(files)} file{'s' if len(files) != 1 else ''} left unchanged ({dur:.1f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -38,7 +38,7 @@ def add_indentation(code_block, num_spaces):
|
||||
|
||||
|
||||
def format_code_with_ruff(temp_dir):
|
||||
"""Formats Python code files in the specified directory using ruff linter and docformatter tools."""
|
||||
"""Formats Python code files in the specified directory using ruff and Python docstring formatter."""
|
||||
if not next(Path(temp_dir).rglob("*.py"), None):
|
||||
return
|
||||
|
||||
@@ -81,23 +81,17 @@ def format_code_with_ruff(temp_dir):
|
||||
print(f"ERROR running ruff check ❌ {e}")
|
||||
|
||||
try:
|
||||
# Run docformatter
|
||||
# Run Ultralytics Python docstring formatter
|
||||
subprocess.run(
|
||||
[
|
||||
"docformatter",
|
||||
"--wrap-summaries=120",
|
||||
"--wrap-descriptions=120",
|
||||
"--pre-summary-newline",
|
||||
"--close-quotes-on-newline",
|
||||
"--in-place",
|
||||
"--recursive",
|
||||
"ultralytics-actions-format-python-docstrings",
|
||||
str(temp_dir),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
print("Completed docformatter ✅")
|
||||
print("Completed Python docstring formatting ✅")
|
||||
except Exception as e:
|
||||
print(f"ERROR running docformatter ❌ {e}")
|
||||
print(f"ERROR running Python docstring formatter ❌ {e}")
|
||||
|
||||
|
||||
def format_bash_with_prettier(temp_dir):
|
||||
|
||||
+1
-8
@@ -67,7 +67,6 @@ classifiers = [
|
||||
dependencies = [
|
||||
"requests>=2.28.1",
|
||||
"ruff>=0.9.1",
|
||||
"docformatter>=1.7.5",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -90,6 +89,7 @@ ultralytics-actions-summarize-pr = "actions.summarize_pr:main"
|
||||
ultralytics-actions-summarize-release = "actions.summarize_release:main"
|
||||
ultralytics-actions-update-markdown-code-blocks = "actions.update_markdown_code_blocks:main"
|
||||
ultralytics-actions-headers = "actions.update_file_headers:main"
|
||||
ultralytics-actions-format-python-docstrings = "actions.format_python_docstrings:main"
|
||||
ultralytics-actions-info = "actions.utils:ultralytics_actions_info"
|
||||
|
||||
[tool.setuptools]
|
||||
@@ -107,10 +107,3 @@ line-length = 120
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.docformatter]
|
||||
wrap-summaries = 120
|
||||
wrap-descriptions = 120
|
||||
pre-summary-newline = true
|
||||
close-quotes-on-newline = true
|
||||
in-place = true
|
||||
|
||||
Reference in New Issue
Block a user