Repository: openlawlibrary/pygls
Pull request #352 (2023-07-25 14:55:13)

Fix detection of `LanguageServer` type annotation when future annotations are used
## Description

The following example fails on `main` because the annotation is a string rather than a concrete type due to the use of `from __future__ import annotations`. `typing.get_type_hints` can be used to resolve the annotations for the signature to support detection in this case. 

```python
from __future__ import annotations

from pygls import server

async def example(
    foo: server.LanguageServer,
    params: None,
) -> None:
    pass


from pygls.feature_manager import has_ls_param_or_annotation

assert has_ls_param_or_annotation(example, server.LanguageServer)
```

This can also be reproduced by wrapping the annotation type in a string manually (as I do in the test in this pull request).

## Code review checklist (for code reviewer to complete)

- [x] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR)
- [x] Title summarizes what is changing
- [x] Commit messages are meaningful (see [this][commit messages] for details)
- [x] Tests have been included and/or updated, as appropriate
- ~[ ] Docstrings have been included and/or updated, as appropriate~
- ~[ ] Standalone docs have been updated accordingly~
- [x] [CONTRIBUTORS.md][contributors] was updated, as appropriate
- [x] Changelog has been updated, as needed (see [CHANGELOG.md][changelog])

[changelog]: https://github.com/openlawlibrary/pygls/blob/master/CHANGELOG.md
[commit messages]: https://chris.beams.io/posts/git-commit/
[contributors]: https://github.com/openlawlibrary/pygls/blob/master/CONTRIBUTORS.md


tombh: Looks good ❤️

I'm happy to merge. But maybe @alcarney wants to give it a thumbs up too?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f27811..95ed61e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning][semver].
 
 - `pygls` no longer overrides the event loop for the current thread when given an explicit loop to use. ([#334])
 - Fixed `MethodTypeNotRegisteredError` when registering a `TEXT_DOCUMENT_DID_SAVE` feature with options. ([#338])
+- Fixed detection of `LanguageServer` type annotations when using string-based annotations. ([#352])
 
 [#328]: https://github.com/openlawlibrary/pygls/issues/328
 [#334]: https://github.com/openlawlibrary/pygls/issues/334
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ad09e26..d0941ae 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -21,3 +21,4 @@
 - [Tomoya Tanjo](https://github.com/tom-tan)
 - [bollwyvl](https://github.com/bollwyvl)
 - [yorodm](https://github.com/yorodm)
+- [Zanie Blue](https://github.com/zanieb)
diff --git a/pygls/feature_manager.py b/pygls/feature_manager.py
index dc02ae4..9e6dcc6 100644
--- a/pygls/feature_manager.py
+++ b/pygls/feature_manager.py
@@ -19,7 +19,7 @@ import functools
 import inspect
 import itertools
 import logging
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional, get_type_hints
 
 from pygls.constants import (ATTR_COMMAND_TYPE, ATTR_EXECUTE_IN_THREAD, ATTR_FEATURE_TYPE,
                              ATTR_REGISTERED_NAME, ATTR_REGISTERED_TYPE, PARAM_LS)
@@ -49,7 +49,7 @@ def has_ls_param_or_annotation(f, annotation):
     try:
         sig = inspect.signature(f)
         first_p = next(itertools.islice(sig.parameters.values(), 0, 1))
-        return first_p.name == PARAM_LS or first_p.annotation is annotation
+        return first_p.name == PARAM_LS or get_type_hints(f)[first_p.name] == annotation
     except Exception:
         return False
 


Test patch:
diff --git a/tests/test_feature_manager.py b/tests/test_feature_manager.py
index 954c80e..c566538 100644
--- a/tests/test_feature_manager.py
+++ b/tests/test_feature_manager.py
@@ -33,19 +33,23 @@ from lsprotocol import types as lsp
 from typeguard import TypeCheckError
 from typeguard._utils import qualified_name
 
+class Temp:
+    pass
 
-def test_has_ls_param_or_annotation():
-    class Temp:
-        pass
 
+def test_has_ls_param_or_annotation():
     def f1(ls, a, b, c):
         pass
 
     def f2(temp: Temp, a, b, c):
         pass
 
+    def f3(temp: "Temp", a, b, c):
+        pass
+
     assert has_ls_param_or_annotation(f1, None)
     assert has_ls_param_or_annotation(f2, Temp)
+    assert has_ls_param_or_annotation(f3, Temp)
 
 
 def test_register_command_validation_error(feature_manager):