Coverage for apio/commands/apio_format.py: 81%
53 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Implementation of 'apio format' command"""
11import os
12import subprocess
13from pathlib import Path
14from glob import glob
15import click
16from apio.common.apio_console import cout, cstyle, fatal_error
17from apio.common.apio_styles import EMPH3, SUCCESS
18from apio.common.common_util import (
19 PROJECT_BUILD_PATH,
20 sort_files,
21 cmd_in_env,
22)
23from apio.apio_context import (
24 ApioContext,
25 PackagesPolicy,
26 ProjectPolicy,
27 RemoteConfigPolicy,
28)
29from apio.commands import options
30from apio.utils import util, cmd_util
32# -------------- apio format
34# -- Text in the rich-text format of the python rich library.
35APIO_FORMAT_HELP = """
36The command 'apio format' formats the project's source files to ensure \
37consistency and style without altering their semantics. The command accepts \
38the names of specific source files to format or formats all project source \
39files by default.
41Examples:[code]
42 apio format # Format all source files.
43 apio format -v # Same but with verbose output.
44 apio format main.v main_tb.v # Format the two files.[/code]
46[NOTE] The file arguments are relative to the project directory, even if \
47the --project-dir option is used.
49The format command utilizes the format tool from the Verible project, which \
50can be configured by setting its flags in the apio.ini project file \
51For example:
54[code]format-verible-options =
55 --column_limit=80
56 --indentation_spaces=4
57 --line_terminator LF[/code]
59If needed, sections of source code can be protected from formatting using \
60Verible formatter directives:
62[code]// verilog_format: off
63... untouched code ...
64// verilog_format: on[/code]
66Another useful option provides a workaround for the Verible formatter \
67error 'Some token partitions failed to complete within the search limit':
69[code]format-verible-options =
70 --max_search_states=2000000[/code]
72For a full list of Verible formatter flags, refer to the documentation page \
73online or use the command 'apio raw -- verible-verilog-format --helpfull'.
74"""
76# -- File types that the format support. 'sv' indicates System Verilog
77# -- and 'h' indicates an includes file.
78_FILE_TYPES = [".v", ".sv", ".vh", ".svh"]
81@click.command(
82 name="format",
83 cls=cmd_util.ApioCommand,
84 short_help="Format verilog source files.",
85 help=APIO_FORMAT_HELP,
86)
87@click.argument("files", nargs=-1, required=False)
88@options.env_option_gen()
89@options.project_dir_option
90@options.verbose_option
91def cli(
92 *,
93 # Arguments
94 files: tuple[str],
95 env: str | None,
96 project_dir: Path | None,
97 verbose: bool,
98):
99 """Implements the format command which formats given or all source
100 files to format.
101 """
103 # pylint: disable=too-many-locals
105 # -- Create an apio context with a project object.
106 apio_ctx = ApioContext(
107 project_policy=ProjectPolicy.PROJECT_REQUIRED,
108 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
109 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
110 project_dir_arg=project_dir,
111 env_arg=env,
112 )
114 # -- Get the optional formatter options from apio.ini
115 cmd_options = apio_ctx.project.get_list_option(
116 "format-verible-options", default=[]
117 )
119 # -- Add verbose option if needed.
120 if verbose and "--verbose" not in cmd_options: 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 cmd_options.append("--verbose")
123 # -- Prepare the packages for use.
124 tools_env = apio_ctx.get_env_for_subprocess(
125 include_apio_packages=True,
126 quiet=not verbose,
127 )
129 # -- Convert the tuple with file names into a list.
130 _files: list[str] = list(files)
132 # -- Change to the project's folder.
133 os.chdir(apio_ctx.project_dir)
135 # -- If user didn't specify files to format, all all source files to
136 # -- the list.
137 if not _files:
138 for ext in _FILE_TYPES:
139 _files.extend(glob("**/*" + ext, recursive=True))
141 # -- Filter out files that are under the _build directory.
142 _files = [
143 f for f in _files if PROJECT_BUILD_PATH not in Path(f).parents
144 ]
146 # -- Error if no file to format.
147 if not _files: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 fatal_error(f"No files of types {_FILE_TYPES}")
150 # -- Sort files, case insensitive.
151 _files = sort_files(_files)
153 # -- Find length of longest file name. We use it to align the
154 # -- status of each file.
155 width = max((len(str(f)) for f in _files), default=0)
157 # -- Iterate the files and format one at a time. We could format
158 # -- all of them at once but this way we can make the output more
159 # -- user friendly.
160 for f in _files:
161 # -- Convert to a Path object.
162 path = Path(f)
164 # -- Check the file extension.
165 _, ext = os.path.splitext(path)
166 if ext not in _FILE_TYPES: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 fatal_error(
168 f"'{f}' has an unexpected extension.",
169 info=f"Should be one of {_FILE_TYPES}",
170 )
172 # -- Check that the file exists and is a file.
173 if not path.is_file(): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 fatal_error(f"'{f}' is not a file.")
176 # -- Construct the formatter command line.
177 command = (
178 ["verible-verilog-format", "--nofailsafe_success", "--inplace"]
179 + cmd_options
180 + [f]
181 )
182 if verbose: 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 cout(str(command))
185 # -- Remember bytes before
186 bytes_before = path.read_bytes()
188 # -- Execute the formatter command line.
190 result = subprocess.run(
191 cmd_in_env(command, tools_env),
192 shell=False,
193 check=False,
194 env=tools_env,
195 )
196 if result.returncode != 0: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true
197 fatal_error(f"Formatting of '{f}' failed")
199 # -- Report
200 styled_fname = cstyle(f"{f:{width}}", style=EMPH3)
201 if path.read_bytes() != bytes_before: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true
202 cout(f"{styled_fname} formatted.")
203 else:
204 cout(f"{styled_fname} already formatted.")
206 # -- All done ok.
207 cout(f"Processed {util.plurality(_files, 'file')}.", style=SUCCESS)