summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xdebputy.sh11
-rwxr-xr-xdh_debputy5
-rw-r--r--src/debputy/commands/deb_materialization.py18
-rw-r--r--src/debputy/commands/debputy_cmd/__main__.py9
-rw-r--r--src/debputy/commands/debputy_cmd/context.py7
-rw-r--r--src/debputy/highlevel_manifest.py8
-rw-r--r--src/debputy/lsp/debputy_ls.py15
-rw-r--r--src/debputy/lsp/lsp_debian_changelog.py12
-rw-r--r--src/debputy/lsp/lsp_debian_control.py10
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py11
-rw-r--r--src/debputy/lsp/lsp_debian_copyright.py7
-rw-r--r--src/debputy/lsp/lsp_debian_debputy_manifest.py305
-rw-r--r--src/debputy/lsp/lsp_debian_patches_series.py5
-rw-r--r--src/debputy/lsp/lsp_debian_rules.py21
-rw-r--r--src/debputy/lsp/lsp_debian_tests_control.py7
-rw-r--r--src/debputy/lsp/lsp_dispatch.py64
-rw-r--r--src/debputy/lsp/lsp_features.py112
-rw-r--r--src/debputy/lsp/lsp_generic_yaml.py213
-rw-r--r--src/debputy/manifest_parser/declarative_parser.py8
-rw-r--r--src/debputy/package_build/assemble_deb.py27
-rw-r--r--src/debputy/plugin/api/impl.py5
-rw-r--r--src/debputy/plugin/api/impl_types.py4
-rw-r--r--src/debputy/transformation_rules.py4
-rw-r--r--src/debputy/util.py11
-rw-r--r--tests/lint_tests/test_lint_dctrl.py38
-rw-r--r--tests/lint_tests/test_lint_debputy.py23
-rw-r--r--tests/lsp_tests/test_lsp_debputy_manifest_completer.py49
27 files changed, 649 insertions, 360 deletions
diff --git a/debputy.sh b/debputy.sh
index 0efc286..9b9abd3 100755
--- a/debputy.sh
+++ b/debputy.sh
@@ -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 "$@"
diff --git a/dh_debputy b/dh_debputy
index 4b9edee..1fe6dd1 100755
--- a/dh_debputy
+++ b/dh_debputy
@@ -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