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
« 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
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
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
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 )))
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.)
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 \\ ]
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
88 vim9script
90 # Make vim recognize debputy.manifest as YAML file
91 autocmd BufNewFile,BufRead debputy.manifest setfiletype yaml
93 packadd! lsp
95 final lspServers: list<dict<any>> = []
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
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
116 require("lspconfig").debputy.setup {capabilities = capabilities}
118 # Make vim recognize debputy.manifest as YAML file
119 vim.filetype.add({ filename = {["debputy.manifest"] = "yaml"} })
120 """
121 ),
122}
125lsp_command = ROOT_COMMAND.add_dispatching_subcommand(
126 "lsp",
127 dest="lsp_command",
128 help_description="Language server related subcommands",
129)
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
177 feature_set = context.load_plugins()
179 from debputy.lsp.lsp_self_check import assert_can_start_lsp
181 assert_can_start_lsp()
183 from debputy.lsp.lsp_features import (
184 ensure_lsp_features_are_loaded,
185 )
186 from debputy.lsp.lsp_dispatch import DEBPUTY_LANGUAGE_SERVER
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
195 debputy_language_server.finish_startup_initialization()
197 if parsed_args.tcp and parsed_args.ws:
198 _error("Sorry, --tcp and --ws are mutually exclusive")
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()
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
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)
251@lsp_command.register_subcommand(
252 "features",
253 help_description="Describe language ids and features",
254)
255def lsp_describe_features(context: CommandContext) -> None:
257 from debputy.lsp.lsp_self_check import assert_can_start_lsp
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 )
267 describe_lsp_features(context)
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)")
323 from debputy.linting.lint_impl import perform_linting
325 context.must_be_called_in_source_root()
326 asyncio.run(perform_linting(context))
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)")
382 from debputy.linting.lint_impl import perform_reformat
384 context.must_be_called_in_source_root()
385 perform_reformat(context, named_style=context.parsed_args.named_style)
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")