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

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 

8 

9"""Implementation of 'apio format' command""" 

10 

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 

31 

32# -------------- apio format 

33 

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. 

40 

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] 

45 

46[NOTE] The file arguments are relative to the project directory, even if \ 

47the --project-dir option is used. 

48 

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: 

52 

53 

54[code]format-verible-options = 

55 --column_limit=80 

56 --indentation_spaces=4 

57 --line_terminator LF[/code] 

58 

59If needed, sections of source code can be protected from formatting using \ 

60Verible formatter directives: 

61 

62[code]// verilog_format: off 

63... untouched code ... 

64// verilog_format: on[/code] 

65 

66Another useful option provides a workaround for the Verible formatter \ 

67error 'Some token partitions failed to complete within the search limit': 

68 

69[code]format-verible-options = 

70 --max_search_states=2000000[/code] 

71 

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""" 

75 

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"] 

79 

80 

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 """ 

102 

103 # pylint: disable=too-many-locals 

104 

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 ) 

113 

114 # -- Get the optional formatter options from apio.ini 

115 cmd_options = apio_ctx.project.get_list_option( 

116 "format-verible-options", default=[] 

117 ) 

118 

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") 

122 

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 ) 

128 

129 # -- Convert the tuple with file names into a list. 

130 _files: list[str] = list(files) 

131 

132 # -- Change to the project's folder. 

133 os.chdir(apio_ctx.project_dir) 

134 

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)) 

140 

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 ] 

145 

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}") 

149 

150 # -- Sort files, case insensitive. 

151 _files = sort_files(_files) 

152 

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) 

156 

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) 

163 

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 ) 

171 

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.") 

175 

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)) 

184 

185 # -- Remember bytes before 

186 bytes_before = path.read_bytes() 

187 

188 # -- Execute the formatter command line. 

189 

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") 

198 

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.") 

205 

206 # -- All done ok. 

207 cout(f"Processed {util.plurality(_files, 'file')}.", style=SUCCESS)