Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions pylsp/plugins/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ def pylsp_signature_help(config, document, position):

s = signatures[0]

try:
function_sig = s.to_string()
except Exception:
function_sig = ""

docstring = s.docstring()

# Docstring contains one or more lines of signature, followed by empty line, followed by docstring
function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
function_sig = " ".join([line.strip() for line in function_sig_lines])
if not function_sig:
# Docstring contains one or more lines of signature, followed by empty line, followed by docstring
function_sig_lines = (docstring.split("\n\n") or [""])[0].splitlines()
function_sig = " ".join([line.strip() for line in function_sig_lines])

# Ensure LSP 3.17 compliance: parameter labels must be substrings of the signature label.
if s.params and not all(p.name in function_sig for p in s.params):
name = s.name or "callable"
param_list = ", ".join(p.name for p in s.params)
function_sig = f"{name}({param_list})"

signature_docstring = s.docstring(raw=True)
if not signature_config.get("include_docstring", True):
Expand All @@ -64,6 +76,7 @@ def pylsp_signature_help(config, document, position):
),
}
for p in s.params
if p.name in function_sig
]

# We only return a single signature because Python doesn't allow overloading
Expand Down
56 changes: 56 additions & 0 deletions test/plugins/test_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,59 @@ def test_signature_without_docstring(
sigs = sig_info["signatures"]
assert len(sigs) == 1
assert sigs[0]["documentation"] == {"kind": "markdown", "value": ""}


CALLABLE_CLASS_DOC = """class MyCallable:
\"\"\"Docstring for MyCallable\"\"\"
def __call__(self, a, b=1):
\"\"\"Call docstring\"\"\"
pass

c = MyCallable()
c(
"""


def test_callable_instance_signature(workspace) -> None:
# Over '( ' in c(
sig_position = {"line": 8, "character": 2}
doc = Document(DOC_URI, workspace, CALLABLE_CLASS_DOC)

sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position)

sigs = sig_info["signatures"]
assert len(sigs) == 1
# Parameter labels must be substrings of the signature label per LSP 3.17
assert sigs[0]["label"] == "MyCallable(a, b=1)"
param_labels = [p["label"] for p in sigs[0]["parameters"]]
assert param_labels == ["a", "b"]
assert all(label in sigs[0]["label"] for label in param_labels)


def test_signature_fallback_when_label_missing_params(workspace, monkeypatch) -> None:
# Test fallback when s.to_string() does not contain all parameter names
sig_position = {"line": 10, "character": 5}
doc = Document(DOC_URI, workspace, DOC)

orig_jedi_script = doc.jedi_script

class MockScript:
def get_signatures(self, **kwargs):
signatures = orig_jedi_script().get_signatures(**kwargs)
if not signatures:
return signatures
s = signatures[0]
# Mock to_string to return a label missing the parameters (like ufunc())
monkeypatch.setattr(s, "to_string", lambda: "mock_func()")
return [s]

monkeypatch.setattr(doc, "jedi_script", lambda: MockScript())

sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position)
sigs = sig_info["signatures"]
assert len(sigs) == 1
# Fallback should ensure parameter labels are substrings of signature label
assert sigs[0]["label"] == "main(param1, param2)"
param_labels = [p["label"] for p in sigs[0]["parameters"]]
assert param_labels == ["param1", "param2"]
assert all(label in sigs[0]["label"] for label in param_labels)