oct
Loading...
Searching...
No Matches
oct.formatter.oct_format Namespace Reference

Classes

class  FormatterContext

Functions

list[Path] _git_changed_files (Path root)
str _get_relative_path (Path path, Path root)
int _prune_archive (Path archive_dir, str original_name, int max_backups)
int _prune_archive_by_size (Path archive_dir, int max_total_bytes)
None _archive_file (Path path, int max_backups=_DEFAULT_MAX_BACKUPS_PER_FILE, int max_archive_bytes=_DEFAULT_MAX_ARCHIVE_BYTES)
int _leading_comment_block_end (List[str] lines)
str _leading_whitespace (str line)
int _statement_start_index (ast.stmt stmt)
bool _begins_its_own_line (ast.stmt stmt, List[str] lines)
Optional[Tuple[str, int]] _module_docstring_span (str text)
List[str] _detect_missing_docstring_sections (str text)
Tuple[bool, str, List[str]] fix_header_block (Path path, str text, FormatterContext ctx)
Tuple[bool, str, List[str]] fix_docstring (Path path, str text, FormatterContext ctx)
int _dbg_import_insert_index (ast.Module tree, List[str] lines)
Optional[str] _import_block_package ()
Tuple[Optional[str], Optional[str]] _import_block_binding ()
Tuple[set, int] _conflicting_import_lines (ast.Module tree, str module, str symbol)
Optional[str] _owning_top_level_package (Path path)
Tuple[bool, str, List[str]] fix_dbg_import (Path path, str text, FormatterContext ctx)
Optional[Tuple[int, str]] _func_pattern_insert_point (Any node, List[str] lines)
Tuple[bool, str, List[str]] fix_func_pattern (Path path, str text, FormatterContext ctx)
Dict[str, Any] format_file (Path path, FormatterContext ctx)
Dict[str, Any] compute_summary (List[Dict[str, Any]] file_results)
None _print_terminal_report (Dict[str, Any] summary, List[Dict[str, Any]] file_results, bool verbose, bool dry_run)
None _print_json_report (Dict[str, Any] summary, List[Dict[str, Any]] file_results, FormatterContext ctx)
List[str] _formatter_flag_argv (args)
bool _maybe_dispatch_format_by_lens (Path project_root, args)
None run_formatter (Path project_root, Optional[List[str]] argv=None, *, Optional[List[Path]] _files=None, bool _dispatch=True)

Variables

str SHEBANG = "#!/usr/bin/env python3"
str ENCODING = "# -*- coding: utf-8 -*-"
str IMPORT_BLOCK = "from oc_diagnostics import _dbg\n"
str DOCSTRING_SKELETON
dict MISSING_DOCSTRING_SECTIONS
int _DEFAULT_MAX_BACKUPS_PER_FILE = 10
int _DEFAULT_MAX_ARCHIVE_BYTES = 5 * 1024 * 1024
 project_root = find_project_root(Path.cwd())

Detailed Description

Option C auto-formatter implementation.

Purpose
-------
Detect and fix structural Option C compliance violations automatically.
Complements the linter by fixing violations while preserving code semantics.

Responsibilities
----------------
- Detect formatting violations using reused linter checks.
- Fix violations: headers, docstrings, imports, function patterns.
- Support report-only (dry-run) and fix modes.
- Archive originals before modification.
- Provide terminal and JSON output.
- Respect project exclusions and configuration.

Diagnostics
-----------
Domain: OCT.FORMATTER
Levels:
    L2 - formatter lifecycle and command routing
    L3 - file discovery and processing, fix application decisions
    L4 - detailed fixer implementations, archival operations, output formatting

Contracts
---------
- Files are only modified if fix_mode is True.
- Originals are archived before any modification.
- All code below headers is preserved exactly.
- Docstrings are not replaced; missing sections are appended.
- Each fixer is idempotent (running twice is same as once).
- A module is never given an import of a symbol its own package
  provides, so formatting can never make a package import itself.

Dependencies
------------
- oct (package version)
- oct.linter.oct_lint (reused validation checks)
- oct.core.exclusions (directory and wildcard exclusion lists)
- oct.core.project_root (project root detection)
- oct.core.parsing (safe AST parsing helper)
- oct.core.git (lazy; git changed-file discovery)
- oct.core.paths (to_project_relative - the identity header path)
- oct.core.diagnostics (_dbg structured logger)
- oct.core.octrc (lazy; .octrc.json exclusion and archive overrides)
- oct.core.workspace (lazy; subproject lookup for the per-subproject fan-out)

Function Documentation

◆ _archive_file()

None oct.formatter.oct_format._archive_file ( Path path,
int max_backups = _DEFAULT_MAX_BACKUPS_PER_FILE,
int max_archive_bytes = _DEFAULT_MAX_ARCHIVE_BYTES )
protected
Archive original file to ``.formatter_archive/`` with timestamp.

After writing the new backup, prune in two passes:

1. Per-file count cap (OI-424): keep only the N newest backups per
   source file name.
2. Per-directory size cap (OI-516): enforce aggregate ``max_archive_bytes``
   across *all* backups in the archive, evicting oldest first.

Definition at line 285 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _begins_its_own_line()

bool oct.formatter.oct_format._begins_its_own_line ( ast.stmt stmt,
List[str] lines )
protected
Return True when nothing but whitespace precedes ``stmt`` on its line.

Distinguishes a body that occupies its own lines from one written inline
with its signature - ``def f(): return x``, or a multi-line signature
closed as ``) -> int: return x``. An inline body has no line of its own
to insert before (198).

Definition at line 345 of file oct_format.py.

Here is the caller graph for this function:

◆ _conflicting_import_lines()

Tuple[set, int] oct.formatter.oct_format._conflicting_import_lines ( ast.Module tree,
str module,
str symbol )
protected
Return ``(line_indices, statement_count)`` for imports that conflict.

A statement conflicts only when it binds the BARE name ``symbol``, since
that is the sole binding the canonical import can collide with. This is
a structural test over module-level import nodes, replacing a substring
scan that treated any line containing ``"import <symbol>"`` as an import
(198). That scan deleted prose inside docstrings, string literals that
merely spelled an import, and - because ``import _dbg_shim`` contains
``import _dbg`` - the sole body of a ``try:`` block, which does not
parse.

Two conservative limits keep removal from causing the very breakage this
fixer exists to avoid:

- Aliased imports are never removed. They bind a different name, so they
  cannot conflict, and deleting one unbinds a name the module still uses.
- A statement that binds anything else is never removed, so a
  co-imported symbol can never be taken down with it.

Only module-level statements are considered, matching the scope of
:func:`oct.linter.oct_lint.check_dbg_import`.

Definition at line 613 of file oct_format.py.

Here is the caller graph for this function:

◆ _dbg_import_insert_index()

int oct.formatter.oct_format._dbg_import_insert_index ( ast.Module tree,
List[str] lines )
protected
Return the 0-based line index at which the canonical ``_dbg`` import belongs.

The placement is derived from the module structure, never from a fixed
line number (184-D7). The Option C canonical order is: header comments,
module docstring, ``from __future__`` imports, then ordinary imports.

- With a module docstring, start at the line after its closing quotes,
  then move past any ``from __future__`` imports - the language requires
  those to be the first statement after the docstring.
- Without one, start after the whole leading comment/blank block, however
  long it runs, so the import never lands inside it.

In both cases trailing blank lines are skipped, so the import joins the
top of the existing import block instead of hugging the docstring or the
comment header.

Definition at line 529 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _detect_missing_docstring_sections()

List[str] oct.formatter.oct_format._detect_missing_docstring_sections ( str text)
protected
Detect which docstring sections are missing.

Definition at line 397 of file oct_format.py.

Here is the caller graph for this function:

◆ _formatter_flag_argv()

List[str] oct.formatter.oct_format._formatter_flag_argv ( args)
protected
Rebuild the formatter's argv from parsed *args*.

Plan 213 / D-D. ``--changed`` is deliberately NOT reproduced: the
dispatcher has already resolved the changed set at the workspace root
and hands each lens group its own explicit file list.

Definition at line 1103 of file oct_format.py.

Here is the caller graph for this function:

◆ _func_pattern_insert_point()

Optional[Tuple[int, str]] oct.formatter.oct_format._func_pattern_insert_point ( Any node,
List[str] lines )
protected
Return ``(line_index, indent)`` for a function's ``func = "..."`` line.

The position is derived from the function's AST body, never by counting
lines from the ``def`` (198). A multi-line signature puts a PARAMETER on
the line after the ``def``, so "the next line" is not the body - taking
it as the insertion point injects the assignment into the parameter list
and the file stops parsing.

Docstring-first ordering is preserved: when the body opens with a
docstring the assignment lands after it, so the string remains the
function's docstring instead of being demoted to a plain expression.

Returns ``None`` when there is no line to insert before - a body written
inline with its signature (``def f(): return x``). Leaving such a
function unfixed is the correct outcome: the fixer's contract is that it
never corrupts a file, not that it always succeeds.

Definition at line 763 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _get_relative_path()

str oct.formatter.oct_format._get_relative_path ( Path path,
Path root )
protected
Render *path* as a project-relative POSIX string.

200: delegates to :func:`oct.core.paths.to_project_relative`, the helper
OI-533 introduced to standardise path output - and the one the linter's
``expected_path_header`` already uses. This function previously returned
``str(path.relative_to(root))``, which renders with the OS separator, so
on Windows the identity header the formatter WROTE
(``# pkg\\sub\\mod.py``) was never the one the linter EXPECTED
(``# pkg/sub/mod.py``). Every file in a subdirectory therefore had its
header rewritten on every run, forever, each pass archiving the file
again - a direct breach of the module's own per-fixer idempotence
contract.

There is one source of truth for "the project-relative path" and both
tools now call it.

Definition at line 194 of file oct_format.py.

Here is the caller graph for this function:

◆ _git_changed_files()

list[Path] oct.formatter.oct_format._git_changed_files ( Path root)
protected
Return .py files modified in the git working tree (staged + unstaged).

Thin shim over :func:`oct.core.git.git_changed_files` (Phase 4A).
Collapses all ``GitError`` to empty list so the formatter never
crashes due to git unavailability.

Definition at line 85 of file oct_format.py.

Here is the caller graph for this function:

◆ _import_block_binding()

Tuple[Optional[str], Optional[str]] oct.formatter.oct_format._import_block_binding ( )
protected
Return ``(module, symbol)`` named by :data:`IMPORT_BLOCK`.

Derived by parsing the canonical import rather than hardcoded, so the
removal pass keeps following ``IMPORT_BLOCK`` if either the module or
the symbol it provides is ever renamed. Returns ``(None, None)`` if the
constant is not a parseable ``from <module> import <symbol>``.

Definition at line 593 of file oct_format.py.

Here is the caller graph for this function:

◆ _import_block_package()

Optional[str] oct.formatter.oct_format._import_block_package ( )
protected
Return the top-level package named by :data:`IMPORT_BLOCK`.

Derived by parsing the canonical import rather than hardcoded, so the
self-import guard keeps following ``IMPORT_BLOCK`` if the package that
provides the diagnostics symbol is ever renamed. Returns ``None`` if
the constant is not a parseable ``from <package> import ...``.

Definition at line 573 of file oct_format.py.

Here is the caller graph for this function:

◆ _leading_comment_block_end()

int oct.formatter.oct_format._leading_comment_block_end ( List[str] lines)
protected
Return the 0-based index just past the leading comment/blank block.

Scans from the top while every line is blank or a comment, however long
the block runs, so an inserted construct never lands inside it. Shared
by the import and docstring fixers so both follow the same rule (186,
generalised by 198).

Definition at line 309 of file oct_format.py.

Here is the caller graph for this function:

◆ _leading_whitespace()

str oct.formatter.oct_format._leading_whitespace ( str line)
protected
Return the indentation prefix of ``line``, tabs and spaces alike.

Definition at line 326 of file oct_format.py.

Here is the caller graph for this function:

◆ _maybe_dispatch_format_by_lens()

bool oct.formatter.oct_format._maybe_dispatch_format_by_lens ( Path project_root,
args )
protected
Fan ``oct format`` out to one engine run per owning subproject.

Plan 213 / D-D, the formatter half. Returns True when the invocation
fanned out (the caller then returns immediately), False to let the
ordinary single-root pass run.

This matters more here than for the linter: ``run_formatter`` has no
path targets at all, so before 213 a workspace-root invocation walked
the entire tree under ONE ``FormatterContext`` and judged every
subproject's file against the workspace-root lens. In ``--fix`` mode
that did not merely misreport - it REWROTE correct
subproject-relative identity headers into workspace-relative ones.

Only a workspace-root invocation fans out. From inside a subproject,
project-root discovery already resolves to that subproject, so the
lens is correct and behaviour is byte-identical to before.

Definition at line 1122 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _module_docstring_span()

Optional[Tuple[str, int]] oct.formatter.oct_format._module_docstring_span ( str text)
protected
Locate the module docstring, returning ``(docstring, close_index)``.

``close_index`` is the offset in *text* of the docstring's CLOSING
triple quote, which is where :func:`fix_docstring` appends the Option C
sections it finds missing.

200: the docstring itself comes from the linter's shared
:func:`~oct.linter.oct_lint._extract_module_docstring` - the helper this
module already uses at :func:`_detect_missing_docstring_sections` and at
the top of :func:`fix_docstring`. Only the POSITION is computed here.
That is the whole point: ``fix_docstring`` used to find its insertion
point with a raw scan for the first triple-double-quote in the file,
which is a second, independent opinion about which string is the module
docstring. With a single-quoted module docstring the raw scan found a
FUNCTION's docstring and the module sections were appended there. The
output still parsed, so nothing caught it.

204: the position is no longer recovered by searching the source for the
shared helper's answer - the helper now reports the span it found, so the
formatter and the linter agree on WHERE the docstring is structurally,
not by locating the same substring twice.

Returns ``None`` when there is no module docstring, or when its delimiter
is not a triple quote: the Option C sections are appended as new lines,
which a one-line ``'x'`` literal cannot carry without breaking the parse.
Callers must treat ``None`` as "make no change": misplacing (or
duplicating) Option C sections is worse than declining to add them.

Definition at line 359 of file oct_format.py.

Here is the caller graph for this function:

◆ _owning_top_level_package()

Optional[str] oct.formatter.oct_format._owning_top_level_package ( Path path)
protected
Return the name of the top-level package ``path`` belongs to.

Walks up from the file's directory while each level is a real package
(contains an ``__init__.py``); the highest such directory names the
top-level package. A nested submodule therefore reports the package at
the top of its chain, not its immediate parent.

Returns ``None`` when the file is not inside a package at all, which
keeps the test structural: a directory that merely *shares* a
package's name is not that package.

Definition at line 666 of file oct_format.py.

Here is the caller graph for this function:

◆ _print_json_report()

None oct.formatter.oct_format._print_json_report ( Dict[str, Any] summary,
List[Dict[str, Any]] file_results,
FormatterContext ctx )
protected
Print JSON output.

Definition at line 1081 of file oct_format.py.

Here is the caller graph for this function:

◆ _print_terminal_report()

None oct.formatter.oct_format._print_terminal_report ( Dict[str, Any] summary,
List[Dict[str, Any]] file_results,
bool verbose,
bool dry_run )
protected
Print human-readable terminal report.

Definition at line 1040 of file oct_format.py.

Here is the caller graph for this function:

◆ _prune_archive()

int oct.formatter.oct_format._prune_archive ( Path archive_dir,
str original_name,
int max_backups )
protected
Keep only the ``max_backups`` most recent backups of ``original_name``.

Matches files shaped like ``{original_name}.*.bak`` and sorts them
by mtime (newest first). Older entries beyond the cap are removed.
Best-effort: any OS error is swallowed so a failed prune can never
block ``oct format`` (OI-424).

Returns the number of files removed. ``max_backups <= 0`` disables
pruning entirely (useful when a project deliberately wants unbounded
history).

Definition at line 214 of file oct_format.py.

Here is the caller graph for this function:

◆ _prune_archive_by_size()

int oct.formatter.oct_format._prune_archive_by_size ( Path archive_dir,
int max_total_bytes )
protected
OI-516: evict oldest ``.bak`` files until total size fits under ``max_total_bytes``.

Runs *after* per-file count pruning so the archive floor is always
correct under the two coexisting policies. Any OS error on size or
unlink is swallowed - a rotation failure must never block formatting.
Returns the number of files removed.

Definition at line 249 of file oct_format.py.

Here is the caller graph for this function:

◆ _statement_start_index()

int oct.formatter.oct_format._statement_start_index ( ast.stmt stmt)
protected
Return the 0-based index of the first source line owned by ``stmt``.

A decorated statement's own ``lineno`` points at its ``def``/``class``
keyword, not at its first decorator. Inserting a line at that index
would separate a decorator from the statement it decorates, which does
not parse - so decorators are folded in here (198).

Definition at line 331 of file oct_format.py.

Here is the caller graph for this function:

◆ compute_summary()

Dict[str, Any] oct.formatter.oct_format.compute_summary ( List[Dict[str, Any]] file_results)
Aggregate results into summary.

Definition at line 1011 of file oct_format.py.

Here is the caller graph for this function:

◆ fix_dbg_import()

Tuple[bool, str, List[str]] oct.formatter.oct_format.fix_dbg_import ( Path path,
str text,
FormatterContext ctx )
Ensure canonical _dbg import exists at module level.

Skips trivial files (< 20 lines), and skips any module belonging to the
package that *provides* the symbol (195) - see the guard below.

Definition at line 692 of file oct_format.py.

Here is the call graph for this function:

◆ fix_docstring()

Tuple[bool, str, List[str]] oct.formatter.oct_format.fix_docstring ( Path path,
str text,
FormatterContext ctx )
Ensure module has complete docstring with all required sections.

If no docstring: insert skeleton.
If incomplete: add missing sections.
If complete: no change.

Definition at line 465 of file oct_format.py.

Here is the call graph for this function:

◆ fix_func_pattern()

Tuple[bool, str, List[str]] oct.formatter.oct_format.fix_func_pattern ( Path path,
str text,
FormatterContext ctx )
Ensure every function with _dbg() calls has func = "function_name" as first statement.

Delegates violation detection to :func:`oct.linter.oct_lint.find_func_pattern_violations`
so the linter remains the single source of truth for what constitutes a
func-pattern violation.  This function only handles the fix (inserting
the ``func = "..."`` line), and takes every insertion point from the AST
rather than from line arithmetic (198).

Skips trivial files (< 20 lines).

Definition at line 811 of file oct_format.py.

Here is the call graph for this function:

◆ fix_header_block()

Tuple[bool, str, List[str]] oct.formatter.oct_format.fix_header_block ( Path path,
str text,
FormatterContext ctx )
Ensure file has correct 4-line header block.

Returns (changed, new_text, messages). Caller is responsible for
archiving and writing to disk.

Definition at line 418 of file oct_format.py.

Here is the call graph for this function:

◆ format_file()

Dict[str, Any] oct.formatter.oct_format.format_file ( Path path,
FormatterContext ctx )
Process a single file through all formatters.

Plan 277: a fixer named in ``ctx.disabled_fixers`` does not run. Its
entry in ``fixes`` still appears, reporting ``changed`` False and
carrying an extra ``"skipped": "disabled"`` key, so the result stays
a superset of the shape every existing reader expects. With the
default empty set - ``oct format`` run directly, and every caller
that builds its own context - nothing about this function changes.

Definition at line 888 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ run_formatter()

None oct.formatter.oct_format.run_formatter ( Path project_root,
Optional[List[str]] argv = None,
* ,
Optional[List[Path]] _files = None,
bool _dispatch = True )
Run the Option C formatter on the given project root.

Parameters
----------
project_root : Path
    The detected Option C project root directory.
argv : list[str] | None
    Optional list of command-line arguments.
    If None, argparse will use sys.argv[1:].
_files : list[Path] | None
    Plan 213 internal. An explicit file list that overrides scope
    discovery, used by the workspace lens dispatcher to hand a lens
    group exactly the files it owns.
_dispatch : bool
    Plan 213 internal re-entrancy guard. The lens dispatcher re-invokes
    this function once per owning subproject with ``_dispatch=False``
    so the per-group runs cannot fan out again.

Definition at line 1211 of file oct_format.py.

Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ _DEFAULT_MAX_ARCHIVE_BYTES

int oct.formatter.oct_format._DEFAULT_MAX_ARCHIVE_BYTES = 5 * 1024 * 1024
protected

Definition at line 160 of file oct_format.py.

◆ _DEFAULT_MAX_BACKUPS_PER_FILE

int oct.formatter.oct_format._DEFAULT_MAX_BACKUPS_PER_FILE = 10
protected

Definition at line 155 of file oct_format.py.

◆ DOCSTRING_SKELETON

str oct.formatter.oct_format.DOCSTRING_SKELETON
Initial value:
1= '''\
2"""
3Purpose
4-------
5Describe the purpose of this module.
6
7Responsibilities
8----------------
9- List the responsibilities of this module.
10
11Diagnostics
12-----------
13Domain: <DOMAIN>
14Levels:
15 L2 - lifecycle
16 L3 - semantic details
17 L4 - deep tracing
18
19Contracts
20---------
21- State the contracts and guarantees of this module.
22"""
23'''

Definition at line 104 of file oct_format.py.

◆ ENCODING

str oct.formatter.oct_format.ENCODING = "# -*- coding: utf-8 -*-"

Definition at line 100 of file oct_format.py.

◆ IMPORT_BLOCK

str oct.formatter.oct_format.IMPORT_BLOCK = "from oc_diagnostics import _dbg\n"

Definition at line 102 of file oct_format.py.

◆ MISSING_DOCSTRING_SECTIONS

dict oct.formatter.oct_format.MISSING_DOCSTRING_SECTIONS
Initial value:
1= {
2 "Purpose": ,
3 "Responsibilities": ,
4 "Diagnostics": ,
5 "Contracts": ,
6}

Definition at line 128 of file oct_format.py.

◆ project_root

oct.formatter.oct_format.project_root = find_project_root(Path.cwd())

Definition at line 1369 of file oct_format.py.

◆ SHEBANG

str oct.formatter.oct_format.SHEBANG = "#!/usr/bin/env python3"

Definition at line 99 of file oct_format.py.