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

84 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-24 16:38 +0000

1import asyncio 

2import random 

3import textwrap 

4from argparse import BooleanOptionalAction 

5 

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

7from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES 

8from debputy.util import _error 

9 

10_EDITOR_SNIPPETS = { 

11 "emacs": "emacs+eglot", 

12 "emacs+eglot": textwrap.dedent( 

13 """\ 

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

15 ;; 

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

17 ;; 

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

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

20 

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

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

23 ;; Inform eglot about the debputy LSP 

24 (with-eval-after-load 'eglot 

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

26 '( 

27 ( 

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

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

30 ;; Requires elpa-dpkg-dev-el 

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

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

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

34 ;; No language id for these atm. 

35 makefile-gmake-mode 

36 ;; Requires elpa-yaml-mode 

37 yaml-mode 

38 ) 

39 . ("debputy" "lsp" "server") 

40 ))) 

41 

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

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

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

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

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

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

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

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

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

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

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

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

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

55 """ 

56 ), 

57 "vim": "vim+youcompleteme", 

58 "vim+youcompleteme": textwrap.dedent( 

59 """\ 

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

61 # 

62 # Requires: apt install vim-youcompleteme 

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

64 

65 # Make vim recognize debputy.manifest as YAML file 

66 au BufNewFile,BufRead debputy.manifest setf yaml 

67 # Inform vim/ycm about the debputy LSP 

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

69 # Feel free to provide one :) 

70 let g:ycm_language_server = [ 

71 \\ { 'name': 'debputy', 

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

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

74 \\ }, 

75 \\ ] 

76 

77 packadd! youcompleteme 

78 # Add relevant ycm keybinding such as: 

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

80 """ 

81 ), 

82 "vim+vim9lsp": textwrap.dedent( 

83 """\ 

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

85 # 

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

87 

88 vim9script 

89 

90 # Make vim recognize debputy.manifest as YAML file 

91 autocmd BufNewFile,BufRead debputy.manifest setfiletype yaml 

92 

93 packadd! lsp 

94 

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

96 

97 if executable('debputy') 

98 lspServers->add({ 

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

100 path: 'debputy', 

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

102 }) 

103 endif 

104 

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

106 autocmd User LspSetup g:LspAddServer(lspServers) 

107 """ 

108 ), 

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

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

111 """\ 

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

113 # 

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

115 

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

117 

118 # Make vim recognize debputy.manifest as YAML file 

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

120 """ 

121 ), 

122} 

123 

124 

125lsp_command = ROOT_COMMAND.add_dispatching_subcommand( 

126 "lsp", 

127 dest="lsp_command", 

128 help_description="Language server related subcommands", 

129) 

130 

131 

132@lsp_command.register_subcommand( 

133 "server", 

134 log_only_to_stderr=True, 

135 help_description="Start the language server", 

136 argparser=[ 

137 add_arg( 

138 "--tcp", 

139 action="store_true", 

140 help="Use TCP server", 

141 ), 

142 add_arg( 

143 "--ws", 

144 action="store_true", 

145 help="Use WebSocket server", 

146 ), 

147 add_arg( 

148 "--host", 

149 default="127.0.0.1", 

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

151 ), 

152 add_arg( 

153 "--port", 

154 type=int, 

155 default=2087, 

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

157 ), 

158 add_arg( 

159 "--ignore-language-ids", 

160 dest="trust_language_ids", 

161 default=True, 

162 action="store_false", 

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

164 ), 

165 add_arg( 

166 "--force-locale", 

167 dest="forced_locale", 

168 default=None, 

169 action="store", 

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

171 ), 

172 ], 

173) 

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

175 parsed_args = context.parsed_args 

176 

177 feature_set = context.load_plugins() 

178 

179 from debputy.lsp.lsp_self_check import assert_can_start_lsp 

180 

181 assert_can_start_lsp() 

182 

183 from debputy.lsp.lsp_features import ( 

184 ensure_lsp_features_are_loaded, 

185 ) 

186 from debputy.lsp.lsp_dispatch import DEBPUTY_LANGUAGE_SERVER 

187 

188 ensure_lsp_features_are_loaded() 

189 debputy_language_server = DEBPUTY_LANGUAGE_SERVER 

190 debputy_language_server.plugin_feature_set = feature_set 

191 debputy_language_server.dctrl_parser = context.dctrl_parser 

192 debputy_language_server.trust_language_ids = parsed_args.trust_language_ids 

193 debputy_language_server.forced_locale = parsed_args.forced_locale 

194 

195 debputy_language_server.finish_startup_initialization() 

196 

197 if parsed_args.tcp and parsed_args.ws: 

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

199 

200 if parsed_args.tcp: 

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

202 elif parsed_args.ws: 

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

204 else: 

205 debputy_language_server.start_io() 

206 

207 

208@lsp_command.register_subcommand( 

209 "editor-config", 

210 help_description="Provide editor configuration snippets", 

211 argparser=[ 

212 add_arg( 

213 "editor_name", 

214 metavar="editor", 

215 choices=_EDITOR_SNIPPETS, 

216 default=None, 

217 nargs="?", 

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

219 ), 

220 ], 

221) 

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

223 editor_name = context.parsed_args.editor_name 

224 

225 if editor_name is None: 

226 content = [] 

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

228 alias_of = "" 

229 if payload in _EDITOR_SNIPPETS: 

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

231 content.append((editor_name, alias_of)) 

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

233 print( 

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

235 ) 

236 print() 

237 for editor_name, alias_of in content: 

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

239 print() 

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

241 print( 

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

243 ) 

244 return 

245 result = _EDITOR_SNIPPETS[editor_name] 

246 while result in _EDITOR_SNIPPETS: 

247 result = _EDITOR_SNIPPETS[result] 

248 print(result) 

249 

250 

251@lsp_command.register_subcommand( 

252 "features", 

253 help_description="Describe language ids and features", 

254) 

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

256 

257 from debputy.lsp.lsp_self_check import assert_can_start_lsp 

258 

259 try: 

260 from debputy.lsp.lsp_features import describe_lsp_features 

261 except ImportError: 

262 assert_can_start_lsp() 

263 raise AssertionError( 

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

265 ) 

266 

267 describe_lsp_features(context) 

268 

269 

270@ROOT_COMMAND.register_subcommand( 

271 "lint", 

272 log_only_to_stderr=True, 

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

274 argparser=[ 

275 add_arg( 

276 "--spellcheck", 

277 dest="spellcheck", 

278 action="store_true", 

279 help="Enable spellchecking", 

280 ), 

281 add_arg( 

282 "--auto-fix", 

283 dest="auto_fix", 

284 action="store_true", 

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

286 ), 

287 add_arg( 

288 "--linter-exit-code", 

289 dest="linter_exit_code", 

290 default=True, 

291 action=BooleanOptionalAction, 

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

293 ), 

294 add_arg( 

295 "--lint-report-format", 

296 dest="lint_report_format", 

297 default="term", 

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

299 help="The report output format", 

300 ), 

301 add_arg( 

302 "--report-output", 

303 dest="report_output", 

304 default=None, 

305 action="store", 

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

307 ), 

308 add_arg( 

309 "--warn-about-check-manifest", 

310 dest="warn_about_check_manifest", 

311 default=True, 

312 action=BooleanOptionalAction, 

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

314 ), 

315 ], 

316) 

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

318 try: 

319 import lsprotocol 

320 except ImportError: 

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

322 

323 from debputy.linting.lint_impl import perform_linting 

324 

325 context.must_be_called_in_source_root() 

326 asyncio.run(perform_linting(context)) 

327 

328 

329@ROOT_COMMAND.register_subcommand( 

330 "reformat", 

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

332 argparser=[ 

333 add_arg( 

334 "--style", 

335 dest="named_style", 

336 choices=ALL_PUBLIC_NAMED_STYLES, 

337 default=None, 

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

339 ), 

340 add_arg( 

341 "--auto-fix", 

342 dest="auto_fix", 

343 default=True, 

344 action=BooleanOptionalAction, 

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

346 ), 

347 add_arg( 

348 "--linter-exit-code", 

349 dest="linter_exit_code", 

350 default=True, 

351 action=BooleanOptionalAction, 

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

353 ), 

354 add_arg( 

355 "--supported-style-is-required", 

356 dest="supported_style_required", 

357 default=True, 

358 action="store_true", 

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

360 ), 

361 add_arg( 

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

363 dest="supported_style_required", 

364 action="store_false", 

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

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

367 ), 

368 add_arg( 

369 "--missing-style-is-ok", 

370 dest="supported_style_required", 

371 action="store_false", 

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

373 ), 

374 ], 

375) 

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

377 try: 

378 import lsprotocol 

379 except ImportError: 

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

381 

382 from debputy.linting.lint_impl import perform_reformat 

383 

384 context.must_be_called_in_source_root() 

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

386 

387 

388def ensure_lint_and_lsp_commands_are_loaded() -> None: 

389 # Loading the module does the heavy lifting 

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

391 # gets tempted to remove 

392 assert ROOT_COMMAND.has_command("lsp") 

393 assert ROOT_COMMAND.has_command("lint") 

394 assert ROOT_COMMAND.has_command("reformat")