diff options
Diffstat (limited to '')
27 files changed, 649 insertions, 360 deletions
@@ -1,11 +1,18 @@ #!/bin/sh -DEBPUTY_PATH="$(dirname "$(readlink -f "$0")")/src" +DEBPUTY_ROOT="$(dirname "$(readlink -f "$0")")" +DEBPUTY_PATH="${DEBPUTY_ROOT}/src" +DEBPUTY_DH_LIB="${DEBPUTY_ROOT}/lib" if [ -z "${PYTHONPATH}" ]; then PYTHONPATH="${DEBPUTY_PATH}" else PYTHONPATH="${DEBPUTY_PATH}:${PYTHONPATH}" fi +if [ -z "${PERL5LIB}" ]; then + PERL5LIB="${DEBPUTY_DH_LIB}" +else + PERL5LIB="${DEBPUTY_DH_LIB}:${PERL5LIB}" +fi -export PYTHONPATH +export PYTHONPATH PERL5LIB python3 -m debputy.commands.debputy_cmd "$@" @@ -90,6 +90,11 @@ if (! defined $dh{DESTDIR}) { } my $debputy_cmd = $ENV{'DEBPUTY_CMD'} // 'debputy'; +# `debputy` does not know about any -v/--verbose passed directly. +# But it does listen to `DH_VERBOSE` in these integration modes, so we just +# use that for now. At some point, this might be replaced by a proper +# command line option. +$ENV{'DH_VERBOSE'} = '1' if $dh{VERBOSE}; my @debputy_cmdline = ($debputy_cmd); for my $plugin (@plugins) { diff --git a/src/debputy/commands/deb_materialization.py b/src/debputy/commands/deb_materialization.py index 58764d0..6695a26 100644 --- a/src/debputy/commands/deb_materialization.py +++ b/src/debputy/commands/deb_materialization.py @@ -3,6 +3,7 @@ import argparse import collections import contextlib import json +import logging import os import subprocess import sys @@ -28,6 +29,7 @@ from debputy.util import ( detect_fakeroot, print_command, program_name, + escape_shell, ) from debputy.version import __version__ @@ -50,6 +52,13 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument("--version", action="version", version=__version__) + parser.add_argument( + "--verbose", + default=False, + action="store_true", + dest="verbose", + help="Make command verbose", + ) subparsers = parser.add_subparsers(dest="command", required=True) @@ -177,13 +186,20 @@ def parse_args() -> argparse.Namespace: upstream_args = [] parsed_args = parser.parse_args(argv[1:]) setattr(parsed_args, "upstream_args", upstream_args) + if parsed_args.verbose: + logging.getLogger().setLevel(logging.INFO) return parsed_args def _run(cmd: List[str]) -> None: print_command(*cmd) - subprocess.check_call(cmd) + try: + subprocess.check_call(cmd) + except FileNotFoundError: + _error(f" {escape_shell(*cmd)} failed! Command was not available in PATH") + except subprocess.CalledProcessError: + _error(f" {escape_shell(*cmd)} had a non-zero exit code.") def strip_path_prefix(member_path: str) -> str: diff --git a/src/debputy/commands/debputy_cmd/__main__.py b/src/debputy/commands/debputy_cmd/__main__.py index 2d6519f..37f89cd 100644 --- a/src/debputy/commands/debputy_cmd/__main__.py +++ b/src/debputy/commands/debputy_cmd/__main__.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 -B import argparse import json +import logging import os import shutil import stat @@ -661,6 +662,7 @@ def _run_tests_for_plugin(context: CommandContext) -> None: "dh-integration-generate-debs", help_description="[Internal command] Generate .deb/.udebs packages from debian/<pkg> (Not stable API)", requested_plugins_only=True, + default_log_level=logging.WARN, argparser=[ _add_packages_args, add_arg( @@ -696,10 +698,14 @@ def _dh_integration_generate_debs(context: CommandContext) -> None: _error( f"Plugins are not supported in the zz-debputy-rrr sequence. Detected plugins: {plugin_names}" ) + debug_materialization = ( + os.environ.get("DH_VERBOSE", "") != "" or parsed_args.debug_mode + ) plugins = context.load_plugins().plugin_data for plugin in plugins.values(): - _info(f"Loaded plugin {plugin.plugin_name}") + if not plugin.is_bundled: + _info(f"Loaded plugin {plugin.plugin_name}") manifest = context.parse_manifest() package_data_table = manifest.perform_installations( @@ -763,6 +769,7 @@ def _dh_integration_generate_debs(context: CommandContext) -> None: manifest, package_data_table, is_dh_rrr_only_mode, + debug_materialization=debug_materialization, ) diff --git a/src/debputy/commands/debputy_cmd/context.py b/src/debputy/commands/debputy_cmd/context.py index 29bc573..e3cf501 100644 --- a/src/debputy/commands/debputy_cmd/context.py +++ b/src/debputy/commands/debputy_cmd/context.py @@ -1,6 +1,7 @@ import argparse import dataclasses import errno +import logging import os from typing import ( Optional, @@ -385,6 +386,7 @@ class GenericSubCommand(SubcommandBase): "_require_substitution", "_requested_plugins_only", "_log_only_to_stderr", + "_default_log_level", ) def __init__( @@ -398,6 +400,7 @@ class GenericSubCommand(SubcommandBase): require_substitution: bool = True, requested_plugins_only: bool = False, log_only_to_stderr: bool = False, + default_log_level: int = logging.INFO, ) -> None: super().__init__(name, aliases=aliases, help_description=help_description) self._handler = handler @@ -405,6 +408,7 @@ class GenericSubCommand(SubcommandBase): self._require_substitution = require_substitution self._requested_plugins_only = requested_plugins_only self._log_only_to_stderr = log_only_to_stderr + self._default_log_level = default_log_level def configure_handler( self, @@ -428,6 +432,7 @@ class GenericSubCommand(SubcommandBase): ) if self._log_only_to_stderr: setup_logging(reconfigure_logging=True, log_only_to_stderr=True) + logging.getLogger().setLevel(self._default_log_level) return self._handler(context) @@ -469,6 +474,7 @@ class DispatchingCommandMixin(CommandBase): require_substitution: bool = True, requested_plugins_only: bool = False, log_only_to_stderr: bool = False, + default_log_level: int = logging.INFO, ) -> Callable[[CommandHandler], GenericSubCommand]: if isinstance(name, str): cmd_name = name @@ -495,6 +501,7 @@ class DispatchingCommandMixin(CommandBase): require_substitution=require_substitution, requested_plugins_only=requested_plugins_only, log_only_to_stderr=log_only_to_stderr, + default_log_level=default_log_level, ) self.add_subcommand(subcommand) if argparser is not None: diff --git a/src/debputy/highlevel_manifest.py b/src/debputy/highlevel_manifest.py index 1fea1a2..d3898ad 100644 --- a/src/debputy/highlevel_manifest.py +++ b/src/debputy/highlevel_manifest.py @@ -1194,7 +1194,10 @@ class HighLevelManifest: for s in search_dirs if s.search_dir.fs_path != source_root_dir.fs_path ) - _present_installation_dirs(search_dirs, check_for_uninstalled_dirs, into) + if enable_manifest_installation_feature: + _present_installation_dirs( + search_dirs, check_for_uninstalled_dirs, into + ) else: dtmp_dir = None search_dirs = install_request_context.search_dirs @@ -1404,7 +1407,8 @@ class HighLevelManifest: dbgsym_info, ) - _list_automatic_discard_rules(path_matcher) + if enable_manifest_installation_feature: + _list_automatic_discard_rules(path_matcher) return package_data_table diff --git a/src/debputy/lsp/debputy_ls.py b/src/debputy/lsp/debputy_ls.py index 7365491..290997f 100644 --- a/src/debputy/lsp/debputy_ls.py +++ b/src/debputy/lsp/debputy_ls.py @@ -437,14 +437,17 @@ class DebputyLanguageServer(LanguageServer): def determine_language_id( self, doc: "TextDocument", - ) -> Tuple[Literal["editor-provided", "filename"], str]: + ) -> Tuple[Literal["editor-provided", "filename"], str, str]: lang_id = doc.language_id - if self.trust_language_ids and lang_id and not lang_id.isspace(): - return "editor-provided", lang_id path = doc.path try: last_idx = path.rindex("debian/") except ValueError: - return "filename", os.path.basename(path) - guess_language_id = path[last_idx:] - return "filename", guess_language_id + cleaned_filename = os.path.basename(path) + else: + cleaned_filename = path[last_idx:] + + if self.trust_language_ids and lang_id and not lang_id.isspace(): + return "editor-provided", lang_id, cleaned_filename + + return "filename", cleaned_filename, cleaned_filename diff --git a/src/debputy/lsp/lsp_debian_changelog.py b/src/debputy/lsp/lsp_debian_changelog.py index 277b06e..824bc87 100644 --- a/src/debputy/lsp/lsp_debian_changelog.py +++ b/src/debputy/lsp/lsp_debian_changelog.py @@ -22,7 +22,11 @@ from lsprotocol.types import ( from debputy.linting.lint_util import LintState from debputy.lsp.diagnostics import DiagnosticData -from debputy.lsp.lsp_features import lsp_diagnostics, lsp_standard_handler +from debputy.lsp.lsp_features import ( + lsp_diagnostics, + lsp_standard_handler, + LanguageDispatch, +) from debputy.lsp.quickfixes import ( propose_correct_text_quick_fix, ) @@ -45,11 +49,11 @@ except ImportError: _MAXIMUM_WIDTH: int = 82 _HEADER_LINE = re.compile(r"^(\S+)\s*[(]([^)]+)[)]") # TODO: Add reset _LANGUAGE_IDS = [ - "debian/changelog", + LanguageDispatch.from_language_id("debian/changelog"), # emacs's name - "debian-changelog", + LanguageDispatch.from_language_id("debian-changelog"), # vim's name - "debchangelog", + LanguageDispatch.from_language_id("debchangelog"), ] _WEEKDAYS_BY_IDX = [ diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py index fd8598e..42f6a65 100644 --- a/src/debputy/lsp/lsp_debian_control.py +++ b/src/debputy/lsp/lsp_debian_control.py @@ -9,7 +9,6 @@ from typing import ( Mapping, List, Dict, - Any, ) from lsprotocol.types import ( @@ -55,6 +54,7 @@ from debputy.lsp.lsp_features import ( lsp_semantic_tokens_full, lsp_will_save_wait_until, lsp_format_document, + LanguageDispatch, ) from debputy.lsp.lsp_generic_deb822 import ( deb822_completer, @@ -85,8 +85,6 @@ from debputy.lsp.vendoring._deb822_repro import ( from debputy.lsp.vendoring._deb822_repro.parsing import ( Deb822KeyValuePairElement, LIST_SPACE_SEPARATED_INTERPRETATION, - Interpretation, - Deb822ParsedTokenList, ) try: @@ -103,11 +101,11 @@ except ImportError: _LANGUAGE_IDS = [ - "debian/control", + LanguageDispatch.from_language_id("debian/control"), # emacs's name - "debian-control", + LanguageDispatch.from_language_id("debian-control"), # vim's name - "debcontrol", + LanguageDispatch.from_language_id("debcontrol"), ] diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py index 7a0fbdb..42af500 100644 --- a/src/debputy/lsp/lsp_debian_control_reference_data.py +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -755,6 +755,10 @@ def _each_value_match_regex_validation( if m is not None: continue + if "${" in v: + # Ignore substvars + continue + section_value_loc = value_ref.locatable value_range_te = section_value_loc.range_in_parent().relative_to( value_element_pos @@ -975,7 +979,10 @@ _PKGNAME_VS_SECTION_RULES = [ ), _package_name_section_rule("embedded", lambda n: n.startswith("moblin-")), _package_name_section_rule("javascript", lambda n: n.startswith("node-")), - _package_name_section_rule("zope", lambda n: n.startswith(("python-zope", "zope"))), + _package_name_section_rule( + "zope", + lambda n: n.startswith(("python-zope", "python3-zope", "zope")), + ), _package_name_section_rule( "python", lambda n: n.startswith(("python-", "python3-")), @@ -2818,7 +2825,7 @@ BINARY_FIELDS = _fields( Add `Breaks: foo (<< X~)` + `Replaces: foo (<< X~)` to **bar** - * Upgrading **bar** while **foo** is version X or less causes problems **foo** or **bar** to break. + * Upgrading **bar** while **foo** is version X or less causes **foo** or **bar** to break. How do I solve this? Add `Breaks: foo (<< X~)` to **bar** diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py index ad454ba..54fb75e 100644 --- a/src/debputy/lsp/lsp_debian_copyright.py +++ b/src/debputy/lsp/lsp_debian_copyright.py @@ -50,6 +50,7 @@ from debputy.lsp.lsp_features import ( lsp_semantic_tokens_full, lsp_will_save_wait_until, lsp_format_document, + LanguageDispatch, ) from debputy.lsp.lsp_generic_deb822 import ( deb822_completer, @@ -93,11 +94,11 @@ except ImportError: _CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") _LANGUAGE_IDS = [ - "debian/copyright", + LanguageDispatch.from_language_id("debian/copyright"), # emacs's name - "debian-copyright", + LanguageDispatch.from_language_id("debian-copyright"), # vim's name - "debcopyright", + LanguageDispatch.from_language_id("debcopyright"), ] _DEP5_FILE_METADATA = Dep5FileMetadata() diff --git a/src/debputy/lsp/lsp_debian_debputy_manifest.py b/src/debputy/lsp/lsp_debian_debputy_manifest.py index e75534b..bd3c746 100644 --- a/src/debputy/lsp/lsp_debian_debputy_manifest.py +++ b/src/debputy/lsp/lsp_debian_debputy_manifest.py @@ -19,8 +19,6 @@ from lsprotocol.types import ( DiagnosticSeverity, HoverParams, Hover, - MarkupKind, - MarkupContent, TEXT_DOCUMENT_CODE_ACTION, CompletionParams, CompletionList, @@ -29,43 +27,34 @@ from lsprotocol.types import ( Location, ) +from debputy.highlevel_manifest import MANIFEST_YAML from debputy.linting.lint_util import LintState from debputy.lsp.diagnostics import DiagnosticData -from debputy.lsp.quickfixes import propose_correct_text_quick_fix -from debputy.manifest_parser.base_types import DebputyDispatchableType -from debputy.plugin.api.feature_set import PluginProvidedFeatureSet -from debputy.yaml.compat import ( - Node, - CommentedMap, - LineCol, - CommentedSeq, - CommentedBase, - MarkedYAMLError, - YAMLError, -) - -from debputy.highlevel_manifest import MANIFEST_YAML from debputy.lsp.lsp_features import ( lint_diagnostics, lsp_standard_handler, lsp_hover, lsp_completer, + LanguageDispatch, +) +from debputy.lsp.lsp_generic_yaml import ( + resolve_hover_text, + as_hover_doc, + is_before, + word_range_at_position, ) +from debputy.lsp.quickfixes import propose_correct_text_quick_fix from debputy.lsp.text_util import ( LintCapablePositionCodec, detect_possible_typo, ) +from debputy.manifest_parser.base_types import DebputyDispatchableType from debputy.manifest_parser.declarative_parser import ( AttributeDescription, ParserGenerator, DeclarativeNonMappingInputParser, ) from debputy.manifest_parser.declarative_parser import DeclarativeMappingInputParser -from debputy.manifest_parser.parser_doc import ( - render_rule, - render_attribute_doc, - doc_args_for_parser_doc, -) from debputy.manifest_parser.util import AttributePath from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin from debputy.plugin.api.impl_types import ( @@ -77,8 +66,16 @@ from debputy.plugin.api.impl_types import ( InPackageContextParser, DeclarativeValuelessKeywordInputParser, ) -from debputy.util import _info, _warn - +from debputy.util import _info +from debputy.yaml.compat import ( + Node, + CommentedMap, + LineCol, + CommentedSeq, + CommentedBase, + MarkedYAMLError, + YAMLError, +) try: from pygls.server import LanguageServer @@ -88,10 +85,12 @@ except ImportError: _LANGUAGE_IDS = [ - "debian/debputy.manifest", - "debputy.manifest", + LanguageDispatch.from_language_id("debian/debputy.manifest"), + LanguageDispatch.from_language_id("debputy.manifest"), # LSP's official language ID for YAML files - "yaml", + LanguageDispatch.from_language_id( + "yaml", filename_selector="debian/debputy.manifest" + ), ] @@ -99,42 +98,12 @@ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) -def is_valid_file(path: str) -> bool: - # For debian/debputy.manifest, the language ID is often set to makefile meaning we get random - # "non-debian/debputy.manifest" YAML files here. Skip those. - return path.endswith("debian/debputy.manifest") - - -def _word_range_at_position( - lines: List[str], - line_no: int, - char_offset: int, -) -> Range: - line = lines[line_no] - line_len = len(line) - start_idx = char_offset - end_idx = char_offset - while end_idx + 1 < line_len and not line[end_idx + 1].isspace(): - end_idx += 1 - - while start_idx - 1 >= 0 and not line[start_idx - 1].isspace(): - start_idx -= 1 - - return Range( - Position(line_no, start_idx), - Position(line_no, end_idx), - ) - - @lint_diagnostics(_LANGUAGE_IDS) def _lint_debian_debputy_manifest( lint_state: LintState, ) -> Optional[List[Diagnostic]]: lines = lint_state.lines position_codec = lint_state.position_codec - path = lint_state.path - if not is_valid_file(path): - return None diagnostics: List[Diagnostic] = [] try: content = MANIFEST_YAML.load("".join(lines)) @@ -147,7 +116,7 @@ def _lint_debian_debputy_manifest( column = e.problem_mark.column + 1 error_range = position_codec.range_to_client_units( lines, - _word_range_at_position( + word_range_at_position( lines, line, column, @@ -191,7 +160,7 @@ def _lint_debian_debputy_manifest( def _unknown_key( - key: str, + key: Optional[str], expected_keys: Iterable[str], line: int, col: int, @@ -200,6 +169,7 @@ def _unknown_key( *, message_format: str = 'Unknown or unsupported key "{key}".', ) -> Tuple["Diagnostic", Optional[str]]: + key_len = len(key) if key else 1 key_range = position_codec.range_to_client_units( lines, Range( @@ -209,12 +179,12 @@ def _unknown_key( ), Position( line, - col + len(key), + col + key_len, ), ), ) - candidates = detect_possible_typo(key, expected_keys) + candidates = detect_possible_typo(key, expected_keys) if key is not None else () extra = "" corrected_key = None if candidates: @@ -223,6 +193,8 @@ def _unknown_key( # That would enable this to work in more cases. corrected_key = candidates[0] if len(candidates) == 1 else None + if key is None: + message_format = "Missing key" diagnostic = Diagnostic( key_range, message_format.format(key=key) + extra, @@ -312,6 +284,9 @@ def _lint_attr_value( value: Any, ) -> Iterable["Diagnostic"]: attr_type = attr.attribute_type + type_mapping = pg.get_mapped_type_from_target_type(attr_type) + if type_mapping is not None: + attr_type = type_mapping.source_type orig = get_origin(attr_type) valid_values: Sequence[Any] = tuple() if orig == Literal: @@ -449,7 +424,6 @@ def _lint_content( elif isinstance(parser, InPackageContextParser): if not isinstance(content, CommentedMap): return - print(lint_state) known_packages = lint_state.binary_packages lc = content.lc for k, v in content.items(): @@ -475,28 +449,6 @@ def _lint_content( ) -def is_at(position: Position, lc_pos: Tuple[int, int]) -> bool: - return position.line == lc_pos[0] and position.character == lc_pos[1] - - -def is_before(position: Position, lc_pos: Tuple[int, int]) -> bool: - line, column = lc_pos - if position.line < line: - return True - if position.line == line and position.character < column: - return True - return False - - -def is_after(position: Position, lc_pos: Tuple[int, int]) -> bool: - line, column = lc_pos - if position.line > line: - return True - if position.line == line and position.character > column: - return True - return False - - def _trace_cursor( content: Any, attribute_path: AttributePath, @@ -629,59 +581,9 @@ def resolve_keyword( return None -def _render_param_doc( - rule_name: str, - declarative_parser: DeclarativeMappingInputParser, - plugin_metadata: DebputyPluginMetadata, - attribute: str, -) -> Optional[str]: - attr = declarative_parser.source_attributes.get(attribute) - if attr is None: - return None - - doc_args, parser_doc = doc_args_for_parser_doc( - rule_name, - declarative_parser, - plugin_metadata, - ) - rendered_docs = render_attribute_doc( - declarative_parser, - declarative_parser.source_attributes, - declarative_parser.input_time_required_parameters, - declarative_parser.at_least_one_of, - parser_doc, - doc_args, - is_interactive=True, - rule_name=rule_name, - ) - - for attributes, rendered_doc in rendered_docs: - if attribute in attributes: - full_doc = [ - f"# Attribute `{attribute}`", - "", - ] - full_doc.extend(rendered_doc) - - return "\n".join(full_doc) - return None - - DEBPUTY_PLUGIN_METADATA = plugin_metadata_for_debputys_own_plugin() -def _guess_rule_name(segments: List[Union[str, int]], idx: int) -> str: - orig_idx = idx - idx -= 1 - while idx >= 0: - segment = segments[idx] - if isinstance(segment, str): - return segment - idx -= 1 - _warn(f"Unable to derive rule name from {segments} [{orig_idx}]") - return "<Bug: unknown rule name>" - - def _escape(v: str) -> str: return '"' + v.replace("\n", "\\n") + '"' @@ -698,23 +600,27 @@ def _insert_snippet(lines: List[str], server_position: Position) -> bool: lhs = lhs_ws.strip() if lhs.endswith(":"): _info("Insertion of value (key seen)") - new_line = line[: server_position.character] + _COMPLETION_HINT_VALUE + new_line = line[: server_position.character] + _COMPLETION_HINT_VALUE + "\n" elif lhs.startswith("-"): _info("Insertion of key or value (list item)") # Respect the provided indentation snippet = _COMPLETION_HINT_KEY if ":" not in lhs else _COMPLETION_HINT_VALUE - new_line = line[: server_position.character] + snippet + new_line = line[: server_position.character] + snippet + "\n" elif not lhs or (lhs_ws and not lhs_ws[0].isspace()): _info(f"Insertion of key or value: {_escape(line[server_position.character:])}") # Respect the provided indentation snippet = _COMPLETION_HINT_KEY if ":" not in lhs else _COMPLETION_HINT_VALUE - new_line = line[: server_position.character] + snippet + new_line = line[: server_position.character] + snippet + "\n" elif lhs.isalpha() and ":" not in lhs: _info(f"Expanding value to a key: {_escape(line[server_position.character:])}") # Respect the provided indentation - new_line = line[: server_position.character] + _COMPLETION_HINT_KEY + new_line = line[: server_position.character] + _COMPLETION_HINT_KEY + "\n" else: - c = line[server_position.character] + c = ( + line[server_position.character] + if server_position.character < len(line) + else "(OOB)" + ) _info(f"Not touching line: {_escape(line)} -- {_escape(c)}") return False _info(f'Evaluating complete on synthetic line: "{new_line}"') @@ -728,13 +634,13 @@ def debputy_manifest_completer( params: CompletionParams, ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: doc = ls.workspace.get_text_document(params.text_document.uri) - if not is_valid_file(doc.path): - return None lines = doc.lines server_position = doc.position_codec.position_from_client_units( lines, params.position ) attribute_root_path = AttributePath.root_path() + orig_line = lines[server_position.line].rstrip() + has_colon = ":" in orig_line added_key = _insert_snippet(lines, server_position) attempts = 1 if added_key else 2 content = None @@ -800,7 +706,7 @@ def debputy_manifest_completer( if isinstance(parser, DispatchingParserBase): if matched_key: items = [ - CompletionItem(f"{k}:") + CompletionItem(k if has_colon else f"{k}:") for k in parser.registered_keywords() if k not in parent and not isinstance( @@ -822,7 +728,9 @@ def debputy_manifest_completer( binary_packages = ls.lint_state(doc).binary_packages if binary_packages is not None: items = [ - CompletionItem(f"{p}:") for p in binary_packages if p not in parent + CompletionItem(p if has_colon else f"{p}:") + for p in binary_packages + if p not in parent ] elif isinstance(parser, DeclarativeMappingInputParser): if matched_key: @@ -836,7 +744,7 @@ def debputy_manifest_completer( locked.add(attr_name) break items = [ - CompletionItem(f"{k}:") + CompletionItem(k if has_colon else f"{k}:") for k in parser.manifest_attributes if k not in locked ] @@ -870,10 +778,17 @@ def _completion_from_attr( pg: ParserGenerator, matched: Any, ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: - orig = get_origin(attr.attribute_type) + type_mapping = pg.get_mapped_type_from_target_type(attr.attribute_type) + if type_mapping is not None: + attr_type = type_mapping.source_type + else: + attr_type = attr.attribute_type + + orig = get_origin(attr_type) valid_values: Sequence[Any] = tuple() + if orig == Literal: - valid_values = get_args(attr.attribute_type) + valid_values = get_args(attr_type) elif orig == bool or attr.attribute_type == bool: valid_values = ("true", "false") elif isinstance(orig, type) and issubclass(orig, DebputyDispatchableType): @@ -894,8 +809,6 @@ def debputy_manifest_hover( params: HoverParams, ) -> Optional[Hover]: doc = ls.workspace.get_text_document(params.text_document.uri) - if not is_valid_file(doc.path): - return None lines = doc.lines position_codec = doc.position_codec attribute_root_path = AttributePath.root_path() @@ -937,100 +850,4 @@ def debputy_manifest_hover( matched, matched_key, ) - return _hover_doc(ls, hover_doc_text) - - -def resolve_hover_text_for_value( - feature_set: PluginProvidedFeatureSet, - parser: DeclarativeMappingInputParser, - plugin_metadata: DebputyPluginMetadata, - segment: Union[str, int], - matched: Any, -) -> Optional[str]: - - hover_doc_text: Optional[str] = None - attr = parser.manifest_attributes.get(segment) - attr_type = attr.attribute_type if attr is not None else None - if attr_type is None: - _info(f"Matched value for {segment} -- No attr or type") - return None - if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType): - parser_generator = feature_set.manifest_parser_generator - parser = parser_generator.dispatch_parser_table_for(attr_type) - if parser is None or not isinstance(matched, str): - _info( - f"Unknown parser for {segment} or matched is not a str -- {attr_type} {type(matched)=}" - ) - return None - subparser = parser.parser_for(matched) - if subparser is None: - _info(f"Unknown parser for {matched} (subparser)") - return None - hover_doc_text = render_rule( - matched, - subparser.parser, - plugin_metadata, - ) - else: - _info(f"Unknown value: {matched} -- {segment}") - return hover_doc_text - - -def resolve_hover_text( - feature_set: PluginProvidedFeatureSet, - parser: Optional[Union[DeclarativeInputParser[Any], DispatchingParserBase]], - plugin_metadata: DebputyPluginMetadata, - segments: List[Union[str, int]], - at_depth_idx: int, - matched: Any, - matched_key: bool, -) -> Optional[str]: - hover_doc_text: Optional[str] = None - if at_depth_idx == len(segments): - segment = segments[at_depth_idx - 1] - _info(f"Matched {segment} at ==, {matched_key=} ") - hover_doc_text = render_rule( - segment, - parser, - plugin_metadata, - is_root_rule=False, - ) - elif at_depth_idx + 1 == len(segments) and isinstance( - parser, DeclarativeMappingInputParser - ): - segment = segments[at_depth_idx] - _info(f"Matched {segment} at -1, {matched_key=} ") - if isinstance(segment, str): - if not matched_key: - hover_doc_text = resolve_hover_text_for_value( - feature_set, - parser, - plugin_metadata, - segment, - matched, - ) - if matched_key or hover_doc_text is None: - rule_name = _guess_rule_name(segments, at_depth_idx) - hover_doc_text = _render_param_doc( - rule_name, - parser, - plugin_metadata, - segment, - ) - else: - _info(f"No doc: {at_depth_idx=} {len(segments)=}") - - return hover_doc_text - - -def _hover_doc( - ls: "DebputyLanguageServer", hover_doc_text: Optional[str] -) -> Optional[Hover]: - if hover_doc_text is None: - return None - return Hover( - contents=MarkupContent( - kind=ls.hover_markup_format(MarkupKind.Markdown, MarkupKind.PlainText), - value=hover_doc_text, - ), - ) + return as_hover_doc(ls, hover_doc_text) diff --git a/src/debputy/lsp/lsp_debian_patches_series.py b/src/debputy/lsp/lsp_debian_patches_series.py index c703e37..81ae32f 100644 --- a/src/debputy/lsp/lsp_debian_patches_series.py +++ b/src/debputy/lsp/lsp_debian_patches_series.py @@ -19,6 +19,7 @@ from debputy.lsp.lsp_features import ( lsp_completer, lsp_semantic_tokens_full, SEMANTIC_TOKEN_TYPES_IDS, + LanguageDispatch, ) from debputy.lsp.quickfixes import ( propose_remove_range_quick_fix, @@ -57,9 +58,9 @@ except ImportError: _LANGUAGE_IDS = [ - "debian/patches/series", + LanguageDispatch.from_language_id("debian/patches/series"), # quilt path name - "patches/series", + LanguageDispatch.from_language_id("patches/series"), ] diff --git a/src/debputy/lsp/lsp_debian_rules.py b/src/debputy/lsp/lsp_debian_rules.py index 390ddfa..c2bf56d 100644 --- a/src/debputy/lsp/lsp_debian_rules.py +++ b/src/debputy/lsp/lsp_debian_rules.py @@ -34,6 +34,7 @@ from debputy.lsp.lsp_features import ( lint_diagnostics, lsp_standard_handler, lsp_completer, + LanguageDispatch, ) from debputy.lsp.quickfixes import propose_correct_text_quick_fix from debputy.lsp.spellchecking import spellcheck_line @@ -111,13 +112,15 @@ _COMMAND_WORDS = frozenset( ) _LANGUAGE_IDS = [ - "debian/rules", + LanguageDispatch.from_language_id("debian/rules"), # LSP's official language ID for Makefile - "makefile", + LanguageDispatch.from_language_id("makefile", filename_selector="debian/rules"), # emacs's name (there is no debian-rules mode) - "makefile-gmake", + LanguageDispatch.from_language_id( + "makefile-gmake", filename_selector="debian/rules" + ), # vim's name (there is no debrules) - "make", + LanguageDispatch.from_language_id("make", filename_selector="debian/rules"), ] @@ -133,16 +136,8 @@ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) -def is_valid_file(path: str) -> bool: - # For debian/rules, the language ID is often set to makefile meaning we get random "non-debian/rules" - # makefiles here. Skip those. - return path.endswith("debian/rules") - - @lint_diagnostics(_LANGUAGE_IDS) def _lint_debian_rules(lint_state: LintState) -> Optional[List[Diagnostic]]: - if not is_valid_file(lint_state.path): - return None return _lint_debian_rules_impl(lint_state) @@ -356,8 +351,6 @@ def _debian_rules_completions( params: CompletionParams, ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: doc = ls.workspace.get_text_document(params.text_document.uri) - if not is_valid_file(doc.path): - return None lines = doc.lines server_position = doc.position_codec.position_from_client_units( lines, params.position diff --git a/src/debputy/lsp/lsp_debian_tests_control.py b/src/debputy/lsp/lsp_debian_tests_control.py index 5517b52..111aae8 100644 --- a/src/debputy/lsp/lsp_debian_tests_control.py +++ b/src/debputy/lsp/lsp_debian_tests_control.py @@ -48,6 +48,7 @@ from debputy.lsp.lsp_features import ( lsp_semantic_tokens_full, lsp_will_save_wait_until, lsp_format_document, + LanguageDispatch, ) from debputy.lsp.lsp_generic_deb822 import ( deb822_completer, @@ -91,11 +92,11 @@ except ImportError: _CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") _LANGUAGE_IDS = [ - "debian/tests/control", + LanguageDispatch.from_language_id("debian/tests/control"), # emacs's name - expected in elpa-dpkg-dev-el (>> 37.11) - "debian-autopkgtest-control-mode", + LanguageDispatch.from_language_id("debian-autopkgtest-control-mode"), # Likely to be vim's name if it had support - "debtestscontrol", + LanguageDispatch.from_language_id("debtestscontrol"), ] _DTESTS_CTRL_FILE_METADATA = DTestsCtrlFileMetadata() diff --git a/src/debputy/lsp/lsp_dispatch.py b/src/debputy/lsp/lsp_dispatch.py index 9a54c2b..8f370ef 100644 --- a/src/debputy/lsp/lsp_dispatch.py +++ b/src/debputy/lsp/lsp_dispatch.py @@ -5,12 +5,25 @@ from typing import ( Union, Optional, TypeVar, - Callable, Mapping, List, TYPE_CHECKING, ) +from debputy import __version__ +from debputy.lsp.lsp_features import ( + DIAGNOSTIC_HANDLERS, + COMPLETER_HANDLERS, + HOVER_HANDLERS, + SEMANTIC_TOKENS_FULL_HANDLERS, + CODE_ACTION_HANDLERS, + SEMANTIC_TOKENS_LEGEND, + WILL_SAVE_WAIT_UNTIL_HANDLERS, + FORMAT_FILE_HANDLERS, + _DispatchRule, + C, +) +from debputy.util import _info from lsprotocol.types import ( DidOpenTextDocumentParams, DidChangeTextDocumentParams, @@ -39,19 +52,6 @@ from lsprotocol.types import ( TEXT_DOCUMENT_FORMATTING, ) -from debputy import __version__ -from debputy.lsp.lsp_features import ( - DIAGNOSTIC_HANDLERS, - COMPLETER_HANDLERS, - HOVER_HANDLERS, - SEMANTIC_TOKENS_FULL_HANDLERS, - CODE_ACTION_HANDLERS, - SEMANTIC_TOKENS_LEGEND, - WILL_SAVE_WAIT_UNTIL_HANDLERS, - FORMAT_FILE_HANDLERS, -) -from debputy.util import _info - _DOCUMENT_VERSION_TABLE: Dict[str, int] = {} @@ -115,15 +115,17 @@ async def _open_or_changed_document( doc = ls.workspace.get_text_document(doc_uri) _DOCUMENT_VERSION_TABLE[doc_uri] = version - id_source, language_id = ls.determine_language_id(doc) - handler = DIAGNOSTIC_HANDLERS.get(language_id) + id_source, language_id, normalized_filename = ls.determine_language_id(doc) + handler = _resolve_handler(DIAGNOSTIC_HANDLERS, language_id, normalized_filename) if handler is None: _info( - f"Opened/Changed document: {doc.path} ({language_id}, {id_source}) - no diagnostics handler" + f"Opened/Changed document: {doc.path} ({language_id}, {id_source}," + f" normalized filename: {normalized_filename}) - no diagnostics handler" ) return _info( - f"Opened/Changed document: {doc.path} ({language_id}, {id_source}) - running diagnostics for doc version {version}" + f"Opened/Changed document: {doc.path} ({language_id}, {id_source}, normalized filename: {normalized_filename})" + f" - running diagnostics for doc version {version}" ) last_publish_count = -1 @@ -253,23 +255,39 @@ def _dispatch_standard_handler( ls: "DebputyLanguageServer", doc_uri: str, params: P, - handler_table: Mapping[str, Callable[[L, P], R]], + handler_table: Mapping[str, List[_DispatchRule[C]]], request_type: str, ) -> Optional[R]: doc = ls.workspace.get_text_document(doc_uri) - id_source, language_id = ls.determine_language_id(doc) - handler = handler_table.get(language_id) + id_source, language_id, normalized_filename = ls.determine_language_id(doc) + handler = _resolve_handler(handler_table, language_id, normalized_filename) if handler is None: _info( - f"{request_type} for document: {doc.path} ({language_id}, {id_source}) - no handler" + f"{request_type} for document: {doc.path} ({language_id}, {id_source}," + f" normalized filename: {normalized_filename}) - no handler" ) return None _info( - f"{request_type} for document: {doc.path} ({language_id}, {id_source}) - delegating to handler" + f"{request_type} for document: {doc.path} ({language_id}, {id_source}," + f" normalized filename: {normalized_filename}) - delegating to handler" ) return handler( ls, params, ) + + +def _resolve_handler( + handler_table: Mapping[str, List[_DispatchRule[C]]], + language_id: str, + normalized_filename: str, +) -> Optional[C]: + dispatch_rules = handler_table.get(language_id) + if not dispatch_rules: + return None + for dispatch_rule in dispatch_rules: + if dispatch_rule.language_dispatch.filename_match(normalized_filename): + return dispatch_rule.handler + return None diff --git a/src/debputy/lsp/lsp_features.py b/src/debputy/lsp/lsp_features.py index 41313f3..16e9d4d 100644 --- a/src/debputy/lsp/lsp_features.py +++ b/src/debputy/lsp/lsp_features.py @@ -1,4 +1,5 @@ import collections +import dataclasses import inspect import sys from typing import ( @@ -10,8 +11,13 @@ from typing import ( List, Optional, AsyncIterator, + Self, + Generic, ) +from debputy.commands.debputy_cmd.context import CommandContext +from debputy.commands.debputy_cmd.output import _output_styling +from debputy.lsp.lsp_self_check import LSP_CHECKS from lsprotocol.types import ( TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL, TEXT_DOCUMENT_CODE_ACTION, @@ -23,10 +29,6 @@ from lsprotocol.types import ( SemanticTokenTypes, ) -from debputy.commands.debputy_cmd.context import CommandContext -from debputy.commands.debputy_cmd.output import _output_styling -from debputy.lsp.lsp_self_check import LSP_CHECKS - try: from pygls.server import LanguageServer from debputy.lsp.debputy_ls import DebputyLanguageServer @@ -52,15 +54,17 @@ SEMANTIC_TOKEN_TYPES_IDS = { t: idx for idx, t in enumerate(SEMANTIC_TOKENS_LEGEND.token_types) } +DiagnosticHandler = Callable[ + [ + "DebputyLanguageServer", + Union["DidOpenTextDocumentParams", "DidChangeTextDocumentParams"], + ], + AsyncIterator[Optional[List[Diagnostic]]], +] + DIAGNOSTIC_HANDLERS: Dict[ str, - Callable[ - [ - "DebputyLanguageServer", - Union["DidOpenTextDocumentParams", "DidChangeTextDocumentParams"], - ], - AsyncIterator[Optional[List[Diagnostic]]], - ], + List["_DispatchRule[DiagnosticHandler]"], ] = {} COMPLETER_HANDLERS = {} HOVER_HANDLERS = {} @@ -71,6 +75,35 @@ WILL_SAVE_WAIT_UNTIL_HANDLERS = {} FORMAT_FILE_HANDLERS = {} _ALIAS_OF = {} + +@dataclasses.dataclass(slots=True, frozen=True) +class LanguageDispatch: + language_id: Optional[str] + filename_selector: Optional[Union[str, Callable[[str], bool]]] = None + + @classmethod + def from_language_id( + cls, + language_id: str, + filename_selector: Optional[Union[str, Callable[[str], bool]]] = None, + ) -> Self: + return cls(language_id, filename_selector=filename_selector) + + def filename_match(self, filename: str) -> bool: + selector = self.filename_selector + if selector is None: + return True + if isinstance(selector, str): + return filename == selector + return selector(filename) + + +@dataclasses.dataclass(slots=True, frozen=True) +class _DispatchRule(Generic[C]): + language_dispatch: LanguageDispatch + handler: C + + _STANDARD_HANDLERS = { TEXT_DOCUMENT_FORMATTING: ( FORMAT_FILE_HANDLERS, @@ -88,7 +121,7 @@ _STANDARD_HANDLERS = { def lint_diagnostics( - file_formats: Union[str, Sequence[str]] + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] ) -> Callable[[LinterImpl], LinterImpl]: def _wrapper(func: C) -> C: @@ -109,18 +142,25 @@ def lint_diagnostics( raise ValueError("Linters are all non-async at the moment") for file_format in file_formats: - if file_format in DIAGNOSTIC_HANDLERS: + if file_format.language_id in DIAGNOSTIC_HANDLERS: raise AssertionError( "There is already a diagnostics handler for " + file_format ) - DIAGNOSTIC_HANDLERS[file_format] = _lint_wrapper + handler_metadata = _DispatchRule(file_format, _lint_wrapper) + handlers = DIAGNOSTIC_HANDLERS.get(file_format.language_id) + if handlers is None: + DIAGNOSTIC_HANDLERS[file_format.language_id] = [handler_metadata] + else: + handlers.append(handler_metadata) return func return _wrapper -def lsp_diagnostics(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: +def lsp_diagnostics( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] +) -> Callable[[C], C]: def _wrapper(func: C) -> C: @@ -145,35 +185,45 @@ def lsp_diagnostics(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C] return _wrapper -def lsp_completer(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: +def lsp_completer( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] +) -> Callable[[C], C]: return _registering_wrapper(file_formats, COMPLETER_HANDLERS) -def lsp_hover(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: +def lsp_hover( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] +) -> Callable[[C], C]: return _registering_wrapper(file_formats, HOVER_HANDLERS) -def lsp_folding_ranges(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: +def lsp_folding_ranges( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] +) -> Callable[[C], C]: return _registering_wrapper(file_formats, FOLDING_RANGE_HANDLERS) def lsp_will_save_wait_until( - file_formats: Union[str, Sequence[str]] + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] ) -> Callable[[C], C]: return _registering_wrapper(file_formats, WILL_SAVE_WAIT_UNTIL_HANDLERS) -def lsp_format_document(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: +def lsp_format_document( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] +) -> Callable[[C], C]: return _registering_wrapper(file_formats, FORMAT_FILE_HANDLERS) def lsp_semantic_tokens_full( - file_formats: Union[str, Sequence[str]] + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]] ) -> Callable[[C], C]: return _registering_wrapper(file_formats, SEMANTIC_TOKENS_FULL_HANDLERS) -def lsp_standard_handler(file_formats: Union[str, Sequence[str]], topic: str) -> None: +def lsp_standard_handler( + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]], topic: str +) -> None: res = _STANDARD_HANDLERS.get(topic) if res is None: raise ValueError(f"No standard handler for {topic}") @@ -184,7 +234,8 @@ def lsp_standard_handler(file_formats: Union[str, Sequence[str]], topic: str) -> def _registering_wrapper( - file_formats: Union[str, Sequence[str]], handler_dict: Dict[str, C] + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]], + handler_dict: Dict[str, C], ) -> Callable[[C], C]: def _wrapper(func: C) -> C: _register_handler(file_formats, handler_dict, func) @@ -194,11 +245,11 @@ def _registering_wrapper( def _register_handler( - file_formats: Union[str, Sequence[str]], - handler_dict: Dict[str, C], + file_formats: Union[LanguageDispatch, Sequence[LanguageDispatch]], + handler_dict: Dict[str, List[_DispatchRule[C]]], handler: C, ) -> None: - if isinstance(file_formats, str): + if isinstance(file_formats, LanguageDispatch): file_formats = [file_formats] else: if not file_formats: @@ -206,13 +257,18 @@ def _register_handler( main = file_formats[0] for alias in file_formats[1:]: if alias not in _ALIAS_OF: - _ALIAS_OF[alias] = main + _ALIAS_OF[alias.language_id] = main.language_id for file_format in file_formats: if file_format in handler_dict: raise AssertionError(f"There is already a handler for {file_format}") - handler_dict[file_format] = handler + handler_metadata = _DispatchRule(file_format, handler) + handlers = handler_dict.get(file_format.language_id) + if handlers is None: + handler_dict[file_format.language_id] = [handler_metadata] + else: + handlers.append(handler_metadata) def ensure_lsp_features_are_loaded() -> None: diff --git a/src/debputy/lsp/lsp_generic_yaml.py b/src/debputy/lsp/lsp_generic_yaml.py new file mode 100644 index 0000000..e464bdc --- /dev/null +++ b/src/debputy/lsp/lsp_generic_yaml.py @@ -0,0 +1,213 @@ +from typing import Union, Any, Optional, List, Tuple + +from debputy.manifest_parser.base_types import DebputyDispatchableType +from debputy.manifest_parser.declarative_parser import DeclarativeMappingInputParser +from debputy.manifest_parser.parser_doc import ( + render_rule, + render_attribute_doc, + doc_args_for_parser_doc, +) +from debputy.plugin.api.feature_set import PluginProvidedFeatureSet +from debputy.plugin.api.impl_types import ( + DebputyPluginMetadata, + DeclarativeInputParser, + DispatchingParserBase, +) +from debputy.util import _info, _warn +from lsprotocol.types import MarkupContent, MarkupKind, Hover, Position, Range + +try: + from pygls.server import LanguageServer + from debputy.lsp.debputy_ls import DebputyLanguageServer +except ImportError: + pass + + +def resolve_hover_text_for_value( + feature_set: PluginProvidedFeatureSet, + parser: DeclarativeMappingInputParser, + plugin_metadata: DebputyPluginMetadata, + segment: Union[str, int], + matched: Any, +) -> Optional[str]: + + hover_doc_text: Optional[str] = None + attr = parser.manifest_attributes.get(segment) + attr_type = attr.attribute_type if attr is not None else None + if attr_type is None: + _info(f"Matched value for {segment} -- No attr or type") + return None + if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType): + parser_generator = feature_set.manifest_parser_generator + parser = parser_generator.dispatch_parser_table_for(attr_type) + if parser is None or not isinstance(matched, str): + _info( + f"Unknown parser for {segment} or matched is not a str -- {attr_type} {type(matched)=}" + ) + return None + subparser = parser.parser_for(matched) + if subparser is None: + _info(f"Unknown parser for {matched} (subparser)") + return None + hover_doc_text = render_rule( + matched, + subparser.parser, + plugin_metadata, + ) + else: + _info(f"Unknown value: {matched} -- {segment}") + return hover_doc_text + + +def resolve_hover_text( + feature_set: PluginProvidedFeatureSet, + parser: Optional[Union[DeclarativeInputParser[Any], DispatchingParserBase]], + plugin_metadata: DebputyPluginMetadata, + segments: List[Union[str, int]], + at_depth_idx: int, + matched: Any, + matched_key: bool, +) -> Optional[str]: + hover_doc_text: Optional[str] = None + if at_depth_idx == len(segments): + segment = segments[at_depth_idx - 1] + _info(f"Matched {segment} at ==, {matched_key=} ") + hover_doc_text = render_rule( + segment, + parser, + plugin_metadata, + is_root_rule=False, + ) + elif at_depth_idx + 1 == len(segments) and isinstance( + parser, DeclarativeMappingInputParser + ): + segment = segments[at_depth_idx] + _info(f"Matched {segment} at -1, {matched_key=} ") + if isinstance(segment, str): + if not matched_key: + hover_doc_text = resolve_hover_text_for_value( + feature_set, + parser, + plugin_metadata, + segment, + matched, + ) + if matched_key or hover_doc_text is None: + rule_name = _guess_rule_name(segments, at_depth_idx) + hover_doc_text = _render_param_doc( + rule_name, + parser, + plugin_metadata, + segment, + ) + else: + _info(f"No doc: {at_depth_idx=} {len(segments)=}") + + return hover_doc_text + + +def as_hover_doc( + ls: "DebputyLanguageServer", + hover_doc_text: Optional[str], +) -> Optional[Hover]: + if hover_doc_text is None: + return None + return Hover( + contents=MarkupContent( + kind=ls.hover_markup_format(MarkupKind.Markdown, MarkupKind.PlainText), + value=hover_doc_text, + ), + ) + + +def _render_param_doc( + rule_name: str, + declarative_parser: DeclarativeMappingInputParser, + plugin_metadata: DebputyPluginMetadata, + attribute: str, +) -> Optional[str]: + attr = declarative_parser.source_attributes.get(attribute) + if attr is None: + return None + + doc_args, parser_doc = doc_args_for_parser_doc( + rule_name, + declarative_parser, + plugin_metadata, + ) + rendered_docs = render_attribute_doc( + declarative_parser, + declarative_parser.source_attributes, + declarative_parser.input_time_required_parameters, + declarative_parser.at_least_one_of, + parser_doc, + doc_args, + is_interactive=True, + rule_name=rule_name, + ) + + for attributes, rendered_doc in rendered_docs: + if attribute in attributes: + full_doc = [ + f"# Attribute `{attribute}`", + "", + ] + full_doc.extend(rendered_doc) + + return "\n".join(full_doc) + return None + + +def _guess_rule_name(segments: List[Union[str, int]], idx: int) -> str: + orig_idx = idx + idx -= 1 + while idx >= 0: + segment = segments[idx] + if isinstance(segment, str): + return segment + idx -= 1 + _warn(f"Unable to derive rule name from {segments} [{orig_idx}]") + return "<Bug: unknown rule name>" + + +def is_at(position: Position, lc_pos: Tuple[int, int]) -> bool: + return position.line == lc_pos[0] and position.character == lc_pos[1] + + +def is_before(position: Position, lc_pos: Tuple[int, int]) -> bool: + line, column = lc_pos + if position.line < line: + return True + if position.line == line and position.character < column: + return True + return False + + +def is_after(position: Position, lc_pos: Tuple[int, int]) -> bool: + line, column = lc_pos + if position.line > line: + return True + if position.line == line and position.character > column: + return True + return False + + +def word_range_at_position( + lines: List[str], + line_no: int, + char_offset: int, +) -> Range: + line = lines[line_no] + line_len = len(line) + start_idx = char_offset + end_idx = char_offset + while end_idx + 1 < line_len and not line[end_idx + 1].isspace(): + end_idx += 1 + + while start_idx - 1 >= 0 and not line[start_idx - 1].isspace(): + start_idx -= 1 + + return Range( + Position(line_no, start_idx), + Position(line_no, end_idx), + ) diff --git a/src/debputy/manifest_parser/declarative_parser.py b/src/debputy/manifest_parser/declarative_parser.py index 850cfa8..72beec3 100644 --- a/src/debputy/manifest_parser/declarative_parser.py +++ b/src/debputy/manifest_parser/declarative_parser.py @@ -714,12 +714,18 @@ class ParserGenerator: ] = {} self._in_package_context_parser: Dict[str, Any] = {} - def register_mapped_type(self, mapped_type: TypeMapping) -> None: + def register_mapped_type(self, mapped_type: TypeMapping[Any, Any]) -> None: existing = self._registered_types.get(mapped_type.target_type) if existing is not None: raise ValueError(f"The type {existing} is already registered") self._registered_types[mapped_type.target_type] = mapped_type + def get_mapped_type_from_target_type( + self, + mapped_type: Type[T], + ) -> Optional[TypeMapping[Any, T]]: + return self._registered_types.get(mapped_type) + def discard_mapped_type(self, mapped_type: Type[T]) -> None: del self._registered_types[mapped_type] diff --git a/src/debputy/package_build/assemble_deb.py b/src/debputy/package_build/assemble_deb.py index bed60e6..6f0d873 100644 --- a/src/debputy/package_build/assemble_deb.py +++ b/src/debputy/package_build/assemble_deb.py @@ -19,11 +19,12 @@ from debputy.util import ( ensure_dir, _warn, assume_not_none, + _info, ) _RRR_DEB_ASSEMBLY_KEYWORD = "debputy/deb-assembly" -_WARNED_ABOUT_FALLBACK_ASSEMBLY = False +_NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = False def _serialize_intermediate_manifest(members: IntermediateManifest) -> str: @@ -51,13 +52,13 @@ def determine_assembly_method( ) return True, False, gain_root_cmd.split() if rrr == "no": - global _WARNED_ABOUT_FALLBACK_ASSEMBLY - if not _WARNED_ABOUT_FALLBACK_ASSEMBLY: - _warn( + global _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY + if not _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY: + _info( 'Using internal assembly method due to "Rules-Requires-Root" being "no" and dpkg-deb assembly would' " require (fake)root for binary packages that needs it." ) - _WARNED_ABOUT_FALLBACK_ASSEMBLY = True + _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = True return True, True, [] _error( @@ -77,6 +78,8 @@ def assemble_debs( manifest: HighLevelManifest, package_data_table: PackageDataTable, is_dh_rrr_only_mode: bool, + *, + debug_materialization: bool = False, ) -> None: parsed_args = context.parsed_args output_path = parsed_args.output @@ -145,6 +148,7 @@ def assemble_debs( is_udeb=dctrl_bin.is_udeb, # Review this if we ever do dbgsyms for udebs use_fallback_assembly=False, needs_root=False, + debug_materialization=debug_materialization, ) _assemble_deb( @@ -159,6 +163,7 @@ def assemble_debs( use_fallback_assembly=use_fallback_assembly, needs_root=needs_root, gain_root_cmd=gain_root_cmd, + debug_materialization=debug_materialization, ) @@ -174,6 +179,8 @@ def _assemble_deb( use_fallback_assembly: bool = False, needs_root: bool = False, gain_root_cmd: Optional[Sequence[str]] = None, + *, + debug_materialization: bool = False, ) -> None: scratch_root_dir = scratch_dir() materialization_dir = os.path.join( @@ -189,9 +196,11 @@ def _assemble_deb( # conditions than the package needing root. (R³: binary-targets implies `needs_root=True` # without a gain_root_cmd) materialize_cmd.extend(gain_root_cmd) + materialize_cmd.append(deb_materialize_cmd) + if debug_materialization: + materialize_cmd.append("--verbose") materialize_cmd.extend( [ - deb_materialize_cmd, "materialize-deb", "--intermediate-package-manifest", "-", @@ -223,11 +232,11 @@ def _assemble_deb( materialize_cmd.extend(upstream_args) if combined_materialization_and_assembly: - print( + _info( f"Materializing and assembling {package} via: {escape_shell(*materialize_cmd)}" ) else: - print(f"Materializing {package} via: {escape_shell(*materialize_cmd)}") + _info(f"Materializing {package} via: {escape_shell(*materialize_cmd)}") proc = subprocess.Popen(materialize_cmd, stdin=subprocess.PIPE) proc.communicate( _serialize_intermediate_manifest(intermediate_manifest).encode("utf-8") @@ -244,7 +253,7 @@ def _assemble_deb( "--output", output, ] - print(f"Assembling {package} via: {escape_shell(*build_materialization)}") + _info(f"Assembling {package} via: {escape_shell(*build_materialization)}") try: subprocess.check_call(build_materialization) except subprocess.CalledProcessError as e: diff --git a/src/debputy/plugin/api/impl.py b/src/debputy/plugin/api/impl.py index 64a1ca8..c951c2f 100644 --- a/src/debputy/plugin/api/impl.py +++ b/src/debputy/plugin/api/impl.py @@ -1466,8 +1466,7 @@ def find_json_plugin( def find_related_implementation_files_for_plugin( plugin_metadata: DebputyPluginMetadata, ) -> List[str]: - plugin_path = plugin_metadata.plugin_path - if not os.path.isfile(plugin_path): + if plugin_metadata.is_bundled: plugin_name = plugin_metadata.plugin_name _error( f"Cannot run find related files for {plugin_name}: The plugin seems to be bundled" @@ -1500,7 +1499,7 @@ def find_tests_for_plugin( plugin_name = plugin_metadata.plugin_name plugin_path = plugin_metadata.plugin_path - if not os.path.isfile(plugin_path): + if plugin_metadata.is_bundled: _error( f"Cannot run tests for {plugin_name}: The plugin seems to be bundled or loaded via a" " mechanism that does not support detecting its tests." diff --git a/src/debputy/plugin/api/impl_types.py b/src/debputy/plugin/api/impl_types.py index 9075ac6..99f589c 100644 --- a/src/debputy/plugin/api/impl_types.py +++ b/src/debputy/plugin/api/impl_types.py @@ -117,6 +117,10 @@ class DebputyPluginMetadata: _is_initialized: bool = False @property + def is_bundled(self) -> bool: + return self.plugin_path == "<bundled>" + + @property def is_loaded(self) -> bool: return self.plugin_initializer is not None diff --git a/src/debputy/transformation_rules.py b/src/debputy/transformation_rules.py index fdf9528..c7f8a2a 100644 --- a/src/debputy/transformation_rules.py +++ b/src/debputy/transformation_rules.py @@ -482,9 +482,9 @@ class PathMetadataTransformationRule(TransformationRule): capability_mode = self._capability_mode definition_source = self._definition_source d: Optional[List[FSPath]] = [] if self._recursive else None - needs_file_match = False + needs_file_match = True if self._owner is not None or self._group is not None or self._mode is not None: - needs_file_match = True + needs_file_match = False for match_rule in self._match_rules: match_ok = False diff --git a/src/debputy/util.py b/src/debputy/util.py index 11f6ccd..01ffaa0 100644 --- a/src/debputy/util.py +++ b/src/debputy/util.py @@ -192,8 +192,9 @@ def escape_shell(*args: str) -> str: return " ".join(_escape_shell_word(w) for w in args) -def print_command(*args: str) -> None: - print(f" {escape_shell(*args)}") +def print_command(*args: str, print_at_log_level: int = logging.INFO) -> None: + if logging.getLogger().isEnabledFor(print_at_log_level): + print(f" {escape_shell(*args)}") def debian_policy_normalize_symlink_target( @@ -695,7 +696,9 @@ def package_cross_check_precheck( def setup_logging( - *, log_only_to_stderr: bool = False, reconfigure_logging: bool = False + *, + log_only_to_stderr: bool = False, + reconfigure_logging: bool = False, ) -> None: global _LOGGING_SET_UP, _DEFAULT_LOGGER, _STDOUT_HANDLER, _STDERR_HANDLER if _LOGGING_SET_UP and not reconfigure_logging: @@ -792,7 +795,7 @@ def setup_logging( logging.setLogRecordFactory(record_factory) - logging.getLogger().setLevel(logging.INFO) + logging.getLogger().setLevel(logging.WARN) _DEFAULT_LOGGER = logging.getLogger(name) if bad_request: diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py index c91d43d..7478461 100644 --- a/tests/lint_tests/test_lint_dctrl.py +++ b/tests/lint_tests/test_lint_dctrl.py @@ -497,6 +497,44 @@ def test_dctrl_lint_sv_udeb_only(line_linter: LintWrapper) -> None: assert not diagnostics +def test_dctrl_lint_udeb_menu_iten(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + Source: foo + Section: devel + Priority: optional + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo-udeb + Architecture: all + Package-Type: udeb + Section: debian-installer + XB-Installer-Menu-Item: 12345 + Description: Some very interesting synopsis + A very interesting description + that spans multiple lines + . + Just so be clear, this is for a test. + + Package: bar-udeb + Architecture: all + Package-Type: udeb + Section: debian-installer + XB-Installer-Menu-Item: ${foo} + Description: Some very interesting synopsis + A very interesting description + that spans multiple lines + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert not diagnostics + + def test_dctrl_lint_multiple_vcs(line_linter: LintWrapper) -> None: lines = textwrap.dedent( f"""\ diff --git a/tests/lint_tests/test_lint_debputy.py b/tests/lint_tests/test_lint_debputy.py index 8e405f8..28dab00 100644 --- a/tests/lint_tests/test_lint_debputy.py +++ b/tests/lint_tests/test_lint_debputy.py @@ -85,6 +85,29 @@ def test_debputy_lint_unknown_keys(line_linter: LintWrapper) -> None: assert f"{fourth_error.range}" == "16:4-16:8" +def test_debputy_lint_null_keys(line_linter: LintWrapper) -> None: + lines = textwrap.dedent( + """\ + manifest-version: '0.1' + installations: + - install-docs: + : + - GETTING-STARTED-WITH-dh-debputy.md + - MANIFEST-FORMAT.md + - MIGRATING-A-DH-PLUGIN.md + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + assert len(diagnostics) == 1 + issue = diagnostics[0] + + msg = "Missing key" + assert issue.message == msg + assert f"{issue.range}" == "3:4-3:5" + assert issue.severity == DiagnosticSeverity.Error + + @requires_levenshtein def test_debputy_lint_unknown_keys_spelling(line_linter: LintWrapper) -> None: lines = textwrap.dedent( diff --git a/tests/lsp_tests/test_lsp_debputy_manifest_completer.py b/tests/lsp_tests/test_lsp_debputy_manifest_completer.py index 196df2e..dab26d3 100644 --- a/tests/lsp_tests/test_lsp_debputy_manifest_completer.py +++ b/tests/lsp_tests/test_lsp_debputy_manifest_completer.py @@ -597,3 +597,52 @@ def test_basic_debputy_completer_manifest_conditions( assert "not:" in keywords # str-only forms are not applicable here assert "cross-compiling" not in keywords + + +def test_basic_debputy_completer_mid_doc(ls: "DebputyLanguageServer") -> None: + debputy_manifest_uri = "file:///nowhere/debian/debputy.manifest" + cursor_pos = put_doc_with_cursor( + ls, + debputy_manifest_uri, + "debian/debputy.manifest", + textwrap.dedent( + """\ + manifest-version: 0.1 + installations: + - install-docs: + s<CURSOR> + - foo +""" + ), + ) + + completions = debputy_manifest_completer( + ls, + CompletionParams(TextDocumentIdentifier(debputy_manifest_uri), cursor_pos), + ) + assert isinstance(completions, list) + keywords = {m.label for m in completions} + assert "sources:" in keywords + + cursor_pos = put_doc_with_cursor( + ls, + debputy_manifest_uri, + "debian/debputy.manifest", + textwrap.dedent( + """\ + manifest-version: 0.1 + installations: + - install-docs: + s<CURSOR>: + - foo +""" + ), + ) + + completions = debputy_manifest_completer( + ls, + CompletionParams(TextDocumentIdentifier(debputy_manifest_uri), cursor_pos), + ) + assert isinstance(completions, list) + keywords = {m.label for m in completions} + assert "sources" in keywords |