Coverage for src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py: 26%

83 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import random 

2import textwrap 

3from argparse import BooleanOptionalAction 

4 

5from debputy.commands.debputy_cmd.context import ROOT_COMMAND, CommandContext, add_arg 

6from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES 

7from debputy.util import _error 

8 

9_EDITOR_SNIPPETS = { 

10 "emacs": "emacs+eglot", 

11 "emacs+eglot": textwrap.dedent( 

12 """\ 

13 ;; `deputy lsp server` glue for emacs eglot (eglot is built-in these days) 

14 ;; 

15 ;; Add to ~/.emacs or ~/.emacs.d/init.el and then activate via `M-x eglot`. 

16 ;; 

17 ;; Requires: apt install elpa-dpkg-dev-el elpa-yaml-mode 

18 ;; Recommends: apt install elpa-markdown-mode 

19 

20 ;; Make emacs recognize debian/debputy.manifest as a YAML file 

21 (add-to-list 'auto-mode-alist '("/debian/debputy.manifest\\'" . yaml-mode)) 

22 ;; Inform eglot about the debputy LSP 

23 (with-eval-after-load 'eglot 

24 (add-to-list 'eglot-server-programs 

25 '( 

26 ( 

27 ;; Requires elpa-dpkg-dev-el (>= 37.12) 

28 (debian-autopkgtest-control-mode :language-id "debian/tests/control") 

29 ;; Requires elpa-dpkg-dev-el 

30 (debian-control-mode :language-id "debian/control") 

31 (debian-changelog-mode :language-id "debian/changelog") 

32 (debian-copyright-mode :language-id "debian/copyright") 

33 ;; No language id for these atm. 

34 makefile-gmake-mode 

35 ;; Requires elpa-yaml-mode 

36 yaml-mode 

37 ) 

38 . ("debputy" "lsp" "server") 

39 ))) 

40 

41 ;; Auto-start eglot for the relevant modes. 

42 (add-hook 'debian-control-mode-hook 'eglot-ensure) 

43 ;; Requires elpa-dpkg-dev-el (>= 37.12) 

44 ;; Technically, the `eglot-ensure` works before then, but it causes a 

45 ;; visible and very annoying long delay on opening the first changelog. 

46 ;; It still has a minor delay in 37.12, which may still be too long for 

47 ;; for your preference. In that case, comment it out. 

48 (add-hook 'debian-changelog-mode-hook 'eglot-ensure) 

49 (add-hook 'debian-copyright-mode-hook 'eglot-ensure) 

50 ;; Requires elpa-dpkg-dev-el (>= 37.12) 

51 (add-hook 'debian-autopkgtest-control-mode-hook 'eglot-ensure) 

52 (add-hook 'makefile-gmake-mode-hook 'eglot-ensure) 

53 (add-hook 'yaml-mode-hook 'eglot-ensure) 

54 """ 

55 ), 

56 "vim": "vim+youcompleteme", 

57 "vim+youcompleteme": textwrap.dedent( 

58 """\ 

59 # debputy lsp server glue for vim with vim-youcompleteme. Add to ~/.vimrc 

60 # 

61 # Requires: apt install vim-youcompleteme 

62 # - Your vim **MUST** be a provider of `vim-python3` (`vim-nox`/`vim-gtk`, etc.) 

63 

64 # Make vim recognize debputy.manifest as YAML file 

65 au BufNewFile,BufRead debputy.manifest setf yaml 

66 # Inform vim/ycm about the debputy LSP 

67 # - NB: No known support for debian/tests/control that we can hook into. 

68 # Feel free to provide one :) 

69 let g:ycm_language_server = [ 

70 \\ { 'name': 'debputy', 

71 \\ 'filetypes': [ 'debcontrol', 'debcopyright', 'debchangelog', 'make', 'yaml'], 

72 \\ 'cmdline': [ 'debputy', 'lsp', 'server', '--ignore-language-ids' ] 

73 \\ }, 

74 \\ ] 

75 

76 packadd! youcompleteme 

77 # Add relevant ycm keybinding such as: 

78 # nmap <leader>d <plug>(YCMHover) 

79 """ 

80 ), 

81 "vim+vim9lsp": textwrap.dedent( 

82 """\ 

83 # debputy lsp server glue for vim with vim9 lsp. Add to ~/.vimrc 

84 # 

85 # Requires https://github.com/yegappan/lsp to be in your packages path 

86 

87 vim9script 

88 

89 # Make vim recognize debputy.manifest as YAML file 

90 autocmd BufNewFile,BufRead debputy.manifest setfiletype yaml 

91 

92 packadd! lsp 

93 

94 final lspServers: list<dict<any>> = [] 

95 

96 if executable('debputy') 

97 lspServers->add({ 

98 filetype: ['debcontrol', 'debcopyright', 'debchangelog', 'make', 'yaml'], 

99 path: 'debputy', 

100 args: ['lsp', 'server', '--ignore-language-ids'] 

101 }) 

102 endif 

103 

104 autocmd User LspSetup g:LspOptionsSet({semanticHighlight: true}) 

105 autocmd User LspSetup g:LspAddServer(lspServers) 

106 """ 

107 ), 

108 "neovim": "neovim+nvim-lspconfig", 

109 "neovim+nvim-lspconfig": textwrap.dedent( 

110 """\ 

111 # debputy lsp server glue for neovim with nvim-lspconfig. Add to ~/.config/nvim/init.lua 

112 # 

113 # Requires https://github.com/neovim/nvim-lspconfig to be in your packages path 

114 

115 require("lspconfig").debputy.setup {capabilities = capabilities} 

116 

117 # Make vim recognize debputy.manifest as YAML file 

118 vim.filetype.add({filename = {["debputy.manifest"] = "yaml"}) 

119 """ 

120 ), 

121} 

122 

123 

124lsp_command = ROOT_COMMAND.add_dispatching_subcommand( 

125 "lsp", 

126 dest="lsp_command", 

127 help_description="Language server related subcommands", 

128) 

129 

130 

131@lsp_command.register_subcommand( 

132 "server", 

133 log_only_to_stderr=True, 

134 help_description="Start the language server", 

135 argparser=[ 

136 add_arg( 

137 "--tcp", 

138 action="store_true", 

139 help="Use TCP server", 

140 ), 

141 add_arg( 

142 "--ws", 

143 action="store_true", 

144 help="Use WebSocket server", 

145 ), 

146 add_arg( 

147 "--host", 

148 default="127.0.0.1", 

149 help="Bind to this address (Use with --tcp / --ws)", 

150 ), 

151 add_arg( 

152 "--port", 

153 type=int, 

154 default=2087, 

155 help="Bind to this port (Use with --tcp / --ws)", 

156 ), 

157 add_arg( 

158 "--ignore-language-ids", 

159 dest="trust_language_ids", 

160 default=True, 

161 action="store_false", 

162 help="Disregard language IDs from the editor (rely solely on filename instead)", 

163 ), 

164 add_arg( 

165 "--force-locale", 

166 dest="forced_locale", 

167 default=None, 

168 action="store", 

169 help="Disregard locale from editor and always use provided locale code (translations)", 

170 ), 

171 ], 

172) 

173def lsp_server_cmd(context: CommandContext) -> None: 

174 parsed_args = context.parsed_args 

175 

176 feature_set = context.load_plugins() 

177 

178 from debputy.lsp.lsp_self_check import assert_can_start_lsp 

179 

180 assert_can_start_lsp() 

181 

182 from debputy.lsp.lsp_features import ( 

183 ensure_lsp_features_are_loaded, 

184 ) 

185 from debputy.lsp.lsp_dispatch import DEBPUTY_LANGUAGE_SERVER 

186 

187 ensure_lsp_features_are_loaded() 

188 debputy_language_server = DEBPUTY_LANGUAGE_SERVER 

189 debputy_language_server.plugin_feature_set = feature_set 

190 debputy_language_server.dctrl_parser = context.dctrl_parser 

191 debputy_language_server.trust_language_ids = parsed_args.trust_language_ids 

192 debputy_language_server.forced_locale = parsed_args.forced_locale 

193 

194 debputy_language_server.finish_startup_initialization() 

195 

196 if parsed_args.tcp and parsed_args.ws: 

197 _error("Sorry, --tcp and --ws are mutually exclusive") 

198 

199 if parsed_args.tcp: 

200 debputy_language_server.start_tcp(parsed_args.host, parsed_args.port) 

201 elif parsed_args.ws: 

202 debputy_language_server.start_ws(parsed_args.host, parsed_args.port) 

203 else: 

204 debputy_language_server.start_io() 

205 

206 

207@lsp_command.register_subcommand( 

208 "editor-config", 

209 help_description="Provide editor configuration snippets", 

210 argparser=[ 

211 add_arg( 

212 "editor_name", 

213 metavar="editor", 

214 choices=_EDITOR_SNIPPETS, 

215 default=None, 

216 nargs="?", 

217 help="The editor to provide a snippet for", 

218 ), 

219 ], 

220) 

221def lsp_editor_glue(context: CommandContext) -> None: 

222 editor_name = context.parsed_args.editor_name 

223 

224 if editor_name is None: 

225 content = [] 

226 for editor_name, payload in _EDITOR_SNIPPETS.items(): 

227 alias_of = "" 

228 if payload in _EDITOR_SNIPPETS: 

229 alias_of = f" (short for: {payload})" 

230 content.append((editor_name, alias_of)) 

231 max_name = max(len(c[0]) for c in content) 

232 print( 

233 "This version of debputy has instructions or editor config snippets for the following editors: " 

234 ) 

235 print() 

236 for editor_name, alias_of in content: 

237 print(f" * {editor_name:<{max_name}}{alias_of}") 

238 print() 

239 choice = random.Random().choice(list(_EDITOR_SNIPPETS)) 

240 print( 

241 f"Use `debputy editor-config {choice}` (as an example) to see the instructions for a concrete editor." 

242 ) 

243 return 

244 result = _EDITOR_SNIPPETS[editor_name] 

245 while result in _EDITOR_SNIPPETS: 

246 result = _EDITOR_SNIPPETS[result] 

247 print(result) 

248 

249 

250@lsp_command.register_subcommand( 

251 "features", 

252 help_description="Describe language ids and features", 

253) 

254def lsp_describe_features(context: CommandContext) -> None: 

255 

256 from debputy.lsp.lsp_self_check import assert_can_start_lsp 

257 

258 try: 

259 from debputy.lsp.lsp_features import describe_lsp_features 

260 except ImportError: 

261 assert_can_start_lsp() 

262 raise AssertionError( 

263 "Cannot load the language server features but `assert_can_start_lsp` did not fail" 

264 ) 

265 

266 describe_lsp_features(context) 

267 

268 

269@ROOT_COMMAND.register_subcommand( 

270 "lint", 

271 log_only_to_stderr=True, 

272 help_description="Provide diagnostics for the packaging (like `lsp server` except no editor is needed)", 

273 argparser=[ 

274 add_arg( 

275 "--spellcheck", 

276 dest="spellcheck", 

277 action="store_true", 

278 help="Enable spellchecking", 

279 ), 

280 add_arg( 

281 "--auto-fix", 

282 dest="auto_fix", 

283 action="store_true", 

284 help="Automatically fix problems with trivial or obvious corrections.", 

285 ), 

286 add_arg( 

287 "--linter-exit-code", 

288 dest="linter_exit_code", 

289 default=True, 

290 action=BooleanOptionalAction, 

291 help='Enable or disable the "linter" convention of exiting with an error if severe issues were found', 

292 ), 

293 add_arg( 

294 "--lint-report-format", 

295 dest="lint_report_format", 

296 default="term", 

297 choices=["term", "junit4-xml"], 

298 help="The report output format", 

299 ), 

300 add_arg( 

301 "--report-output", 

302 dest="report_output", 

303 default=None, 

304 action="store", 

305 help="Where to place the report (for report formats that generate files/directory reports)", 

306 ), 

307 add_arg( 

308 "--warn-about-check-manifest", 

309 dest="warn_about_check_manifest", 

310 default=True, 

311 action=BooleanOptionalAction, 

312 help="Warn about limitations that check-manifest would cover if d/debputy.manifest is present", 

313 ), 

314 ], 

315) 

316def lint_cmd(context: CommandContext) -> None: 

317 try: 

318 import lsprotocol 

319 except ImportError: 

320 _error("This feature requires lsprotocol (apt-get install python3-lsprotocol)") 

321 

322 from debputy.linting.lint_impl import perform_linting 

323 

324 context.must_be_called_in_source_root() 

325 perform_linting(context) 

326 

327 

328@ROOT_COMMAND.register_subcommand( 

329 "reformat", 

330 help_description="Reformat the packaging files based on the packaging/maintainer rules", 

331 argparser=[ 

332 add_arg( 

333 "--style", 

334 dest="named_style", 

335 choices=ALL_PUBLIC_NAMED_STYLES, 

336 default=None, 

337 help="The formatting style to use (overrides packaging style).", 

338 ), 

339 add_arg( 

340 "--auto-fix", 

341 dest="auto_fix", 

342 default=True, 

343 action=BooleanOptionalAction, 

344 help="Whether to automatically apply any style changes.", 

345 ), 

346 add_arg( 

347 "--linter-exit-code", 

348 dest="linter_exit_code", 

349 default=True, 

350 action=BooleanOptionalAction, 

351 help='Enable or disable the "linter" convention of exiting with an error if issues were found', 

352 ), 

353 add_arg( 

354 "--supported-style-is-required", 

355 dest="supported_style_required", 

356 default=True, 

357 action="store_true", 

358 help="Fail with an error if a supported style cannot be identified.", 

359 ), 

360 add_arg( 

361 "--unknown-or-unsupported-style-is-ok", 

362 dest="supported_style_required", 

363 action="store_false", 

364 help="Do not exit with an error if no supported style can be identified. Useful for general" 

365 ' pipelines to implement "reformat if possible"', 

366 ), 

367 add_arg( 

368 "--missing-style-is-ok", 

369 dest="supported_style_required", 

370 action="store_false", 

371 help="[Deprecated] Use --unknown-or-unsupported-style-is-ok instead", 

372 ), 

373 ], 

374) 

375def reformat_cmd(context: CommandContext) -> None: 

376 try: 

377 import lsprotocol 

378 except ImportError: 

379 _error("This feature requires lsprotocol (apt-get install python3-lsprotocol)") 

380 

381 from debputy.linting.lint_impl import perform_reformat 

382 

383 context.must_be_called_in_source_root() 

384 perform_reformat(context, named_style=context.parsed_args.named_style) 

385 

386 

387def ensure_lint_and_lsp_commands_are_loaded() -> None: 

388 # Loading the module does the heavy lifting 

389 # However, having this function means that we do not have an "unused" import that some tool 

390 # gets tempted to remove 

391 assert ROOT_COMMAND.has_command("lsp") 

392 assert ROOT_COMMAND.has_command("lint") 

393 assert ROOT_COMMAND.has_command("reformat")