Coverage for apio/commands/apio_format.py: 83%

52 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03:53 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2024 FPGAwars 

4# -- Authors 

5# -- * Jesús Arroyo (2016-2019) 

6# -- * Juan Gonzalez (obijuan) (2019-2024) 

7# -- License GPLv2 

8"""Implementation of 'apio format' command""" 

9 

10import os 

11from pathlib import Path 

12from glob import glob 

13import click 

14from apio.common.apio_console import cout, cstyle, fatal_error 

15from apio.common.apio_styles import EMPH3, SUCCESS 

16from apio.common.common_util import PROJECT_BUILD_PATH, sort_files 

17from apio.apio_context import ( 

18 ApioContext, 

19 PackagesPolicy, 

20 ProjectPolicy, 

21 RemoteConfigPolicy, 

22) 

23from apio.commands import options 

24from apio.utils import util, cmd_util 

25 

26# -------------- apio format 

27 

28# -- Text in the rich-text format of the python rich library. 

29APIO_FORMAT_HELP = """ 

30The command 'apio format' formats the project's source files to ensure \ 

31consistency and style without altering their semantics. The command accepts \ 

32the names of specific source files to format or formats all project source \ 

33files by default. 

34 

35Examples:[code] 

36 apio format # Format all source files. 

37 apio format -v # Same but with verbose output. 

38 apio format main.v main_tb.v # Format the two files.[/code] 

39 

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

41the --project-dir option is used. 

42 

43The format command utilizes the format tool from the Verible project, which \ 

44can be configured by setting its flags in the apio.ini project file \ 

45For example: 

46 

47 

48[code]format-verible-options = 

49 --column_limit=80 

50 --indentation_spaces=4 

51 --line_terminator LF[/code] 

52 

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

54Verible formatter directives: 

55 

56[code]// verilog_format: off 

57... untouched code ... 

58// verilog_format: on[/code] 

59 

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

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

62 

63[code]format-verible-options = 

64 --max_search_states=2000000[/code] 

65 

66For a full list of Verible formatter flags, refer to the documentation page \ 

67online or use the command 'apio raw -- verible-verilog-format --helpfull'. 

68""" 

69 

70# -- File types that the format support. 'sv' indicates System Verilog 

71# -- and 'h' indicates an includes file. 

72_FILE_TYPES = [".v", ".sv", ".vh", ".svh"] 

73 

74 

75@click.command( 

76 name="format", 

77 cls=cmd_util.ApioCommand, 

78 short_help="Format verilog source files.", 

79 help=APIO_FORMAT_HELP, 

80) 

81@click.argument("files", nargs=-1, required=False) 

82@options.env_option_gen() 

83@options.project_dir_option 

84@options.verbose_option 

85def cli( 

86 *, 

87 # Arguments 

88 files: tuple[str], 

89 env: str | None, 

90 project_dir: Path | None, 

91 verbose: bool, 

92): 

93 """Implements the format command which formats given or all source 

94 files to format. 

95 """ 

96 

97 # -- Create an apio context with a project object. 

98 apio_ctx = ApioContext( 

99 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

100 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

101 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

102 project_dir_arg=project_dir, 

103 env_arg=env, 

104 ) 

105 

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

107 cmd_options = apio_ctx.project.get_list_option( 

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

109 ) 

110 

111 # -- Add verbose option if needed. 

112 if verbose and "--verbose" not in cmd_options: 112 ↛ 113line 112 didn't jump to line 113 because the condition on line 112 was never true

113 cmd_options.append("--verbose") 

114 

115 # -- Prepare the packages for use. 

116 apio_ctx.set_env_for_packages(quiet=not verbose) 

117 

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

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

120 

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

122 os.chdir(apio_ctx.project_dir) 

123 

124 # -- If user didn't specify files to format, all all source files to 

125 # -- the list. 

126 if not _files: 

127 for ext in _FILE_TYPES: 

128 _files.extend(glob("**/*" + ext, recursive=True)) 

129 

130 # -- Filter out files that are under the _build directory. 

131 _files = [ 

132 f for f in _files if PROJECT_BUILD_PATH not in Path(f).parents 

133 ] 

134 

135 # -- Error if no file to format. 

136 if not _files: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

137 fatal_error(f"No files of types {_FILE_TYPES}") 

138 

139 # -- Sort files, case insensitive. 

140 _files = sort_files(_files) 

141 

142 # -- Find length of longest file name. We use it to align the 

143 # -- status of each file. 

144 width = max((len(str(f)) for f in _files), default=0) 

145 

146 # -- Iterate the files and format one at a time. We could format 

147 # -- all of them at once but this way we can make the output more 

148 # -- user friendly. 

149 for f in _files: 

150 # -- Convert to a Path object. 

151 path = Path(f) 

152 

153 # -- Check the file extension. 

154 _, ext = os.path.splitext(path) 

155 if ext not in _FILE_TYPES: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true

156 fatal_error( 

157 f"'{f}' has an unexpected extension.", 

158 info=f"Should be one of {_FILE_TYPES}", 

159 ) 

160 

161 # -- Check that the file exists and is a file. 

162 if not path.is_file(): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 fatal_error(f"'{f}' is not a file.") 

164 

165 # -- Construct the formatter command line. 

166 command = ( 

167 "verible-verilog-format --nofailsafe_success --inplace " 

168 f' {" ".join(cmd_options)} "{f}"' 

169 ) 

170 if verbose: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true

171 cout(command) 

172 

173 # -- Remember bytes before 

174 bytes_before = path.read_bytes() 

175 

176 # -- Execute the formatter command line. 

177 exit_code = os.system(command) 

178 if exit_code != 0: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true

179 fatal_error(f"Formatting of '{f}' failed") 

180 

181 # -- Report 

182 styled_fname = cstyle(f"{f:{width}}", style=EMPH3) 

183 if path.read_bytes() != bytes_before: 

184 cout(f"{styled_fname} formatted.") 

185 else: 

186 cout(f"{styled_fname} already formatted.") 

187 

188 # -- All done ok. 

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