Skip to content

Commit b40f38f

Browse files
committed
Ensure signature help label contains parameter substrings per LSP spec
1 parent a362006 commit b40f38f

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎pylsp/plugins/signature.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,23 @@ def pylsp_signature_help(config, document, position):
3737

3838
s = signatures[0]
3939

40+
try:
41+
function_sig = s.to_string()
42+
except Exception:
43+
function_sig = ""
44+
4045
docstring = s.docstring()
4146

42-
# Docstring contains one or more lines of signature, followed by empty line, followed by docstring
43-
function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
44-
function_sig = " ".join([line.strip() for line in function_sig_lines])
47+
if not function_sig:
48+
# Docstring contains one or more lines of signature, followed by empty line, followed by docstring
49+
function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
50+
function_sig = " ".join([line.strip() for line in function_sig_lines])
51+
52+
# Ensure LSP 3.17 compliance: parameter labels must be substrings of the signature label.
53+
if s.params and not all(p.name in function_sig for p in s.params):
54+
name = s.name or "callable"
55+
param_list = ", ".join(p.name for p in s.params)
56+
function_sig = f"{name}({param_list})"
4557

4658
signature_docstring = s.docstring(raw=True)
4759
if not signature_config.get("include_docstring", True):
@@ -64,6 +76,7 @@ def pylsp_signature_help(config, document, position):
6476
),
6577
}
6678
for p in s.params
79+
if p.name in function_sig
6780
]
6881

6982
# We only return a single signature because Python doesn't allow overloading

‎test/plugins/test_signature.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,59 @@ def test_signature_without_docstring(
118118
sigs = sig_info["signatures"]
119119
assert len(sigs) == 1
120120
assert sigs[0]["documentation"] == {"kind": "markdown", "value": ""}
121+
122+
123+
CALLABLE_CLASS_DOC = """class MyCallable:
124+
\"\"\"Docstring for MyCallable\"\"\"
125+
def __call__(self, a, b=1):
126+
\"\"\"Call docstring\"\"\"
127+
pass
128+
129+
c = MyCallable()
130+
c(
131+
"""
132+
133+
134+
def test_callable_instance_signature(workspace) -> None:
135+
# Over '( ' in c(
136+
sig_position = {"line": 8, "character": 2}
137+
doc = Document(DOC_URI, workspace, CALLABLE_CLASS_DOC)
138+
139+
sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position)
140+
141+
sigs = sig_info["signatures"]
142+
assert len(sigs) == 1
143+
# Parameter labels must be substrings of the signature label per LSP 3.17
144+
assert sigs[0]["label"] == "MyCallable(a, b=1)"
145+
param_labels = [p["label"] for p in sigs[0]["parameters"]]
146+
assert param_labels == ["a", "b"]
147+
assert all(label in sigs[0]["label"] for label in param_labels)
148+
149+
150+
def test_signature_fallback_when_label_missing_params(workspace, monkeypatch) -> None:
151+
# Test fallback when s.to_string() does not contain all parameter names
152+
sig_position = {"line": 10, "character": 5}
153+
doc = Document(DOC_URI, workspace, DOC)
154+
155+
orig_jedi_script = doc.jedi_script
156+
157+
class MockScript:
158+
def get_signatures(self, **kwargs):
159+
signatures = orig_jedi_script().get_signatures(**kwargs)
160+
if not signatures:
161+
return signatures
162+
s = signatures[0]
163+
# Mock to_string to return a label missing the parameters (like ufunc())
164+
monkeypatch.setattr(s, "to_string", lambda: "mock_func()")
165+
return [s]
166+
167+
monkeypatch.setattr(doc, "jedi_script", lambda: MockScript())
168+
169+
sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position)
170+
sigs = sig_info["signatures"]
171+
assert len(sigs) == 1
172+
# Fallback should ensure parameter labels are substrings of signature label
173+
assert sigs[0]["label"] == "main(param1, param2)"
174+
param_labels = [p["label"] for p in sigs[0]["parameters"]]
175+
assert param_labels == ["param1", "param2"]
176+
assert all(label in sigs[0]["label"] for label in param_labels)

0 commit comments

Comments
 (0)