Skip to content

Commit

Permalink
docs: add guide on handling invalid data
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Jan 10, 2024
1 parent 5c919d1 commit c295be2
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 0 deletions.
155 changes: 155 additions & 0 deletions docs/source/howto/handle-invalid-data.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
How To Handle Invalid Data
==========================

.. highlight:: python

By default, servers written with *pygls* are quite pedantic and will complain loudly when given data they consider to be invalid

.. dropdown:: Example Data
:open:

.. code-block:: json
{
"jsonrpc": "2.0",
"id": 10,
"method": "textDocument/codeAction",
"params": {
"textDocument": {
"uri": "file:///path/to/file.txt"
},
"range": {
"start": { "line": 7, "character": 14 },
"end": { "line": 7, "character": 14 }
},
"context": {
"diagnostics": [
{
"range": {
"start": {
"line": null, // Invalid!
"character": 0
},
"end":{
"line": 1,
"character": 65535
}
},
"message": "an example message",
"severity": 2,
"source": "example"
}
]
}
}
}
.. dropdown:: Example Error

::

ERROR:pygls.protocol:Error receiving data
+ Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring TextDocumentCodeActionRequest (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring CodeActionParams (1 sub-exception)
| Structuring class TextDocumentCodeActionRequest @ attribute params
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring CodeActionContext (1 sub-exception)
| Structuring class CodeActionParams @ attribute context
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.IterableValidationError: While structuring typing.List[lsprotocol.types.Diagnostic] (2 sub-exceptions)
| Structuring class CodeActionContext @ attribute diagnostics
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring Diagnostic (1 sub-exception)
| Structuring typing.List[lsprotocol.types.Diagnostic] @ index 0
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring Range (2 sub-exceptions)
| Structuring class Diagnostic @ attribute range
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| ...
| cattrs.errors.ClassValidationError: While structuring Position (1 sub-exception)
| Structuring class Range @ attribute start
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| ...
| TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
| Structuring class Position @ attribute line
+------------------------------------

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
pygls.exceptions.JsonRpcInvalidParams: Invalid Params

This is due to the fact ``pygls`` relies on `lsprotocol <https://github.com/microsoft/lsprotocol/>`__ for all of its type definitions.
``lsprotocol`` in turn builds on `attrs <https://www.attrs.org/en/stable/>`__ and `cattrs <https://catt.rs/en/stable/>`__ to provide the serialisation and deserialisation of types to/from JSON.

This is done through a ``converter`` object::

>>> from lsprotocol.types import Position
>>> from pygls.protocol import default_converter

>>> converter = default_converter()
>>> p = converter.structure({"line": 1, "character": 2}, Position)
>>> p.line
1
>>> p.character
2

Each language server receives a :func:`~pygls.protocol.default_converter` which is derived from the converter provided by ``lsprotocol``.
This means it will follow the Language Server Protocol exactly.

Structure Hooks
---------------

By registering your own `structure hooks <https://catt.rs/en/stable/structuring.html#registering-custom-structuring-hooks>`__ you can take control over how malformed types should be handled.

Using the example data above, let's define a custom converter which includes a hook to silently ignore any diagnostics that are rejected when parsing the ``context`` field of a :lsp:`textDocument/codeAction` request.

.. code-block:: python
from lsprotocol import types
from pygls.protocol import default_converter
def my_converter_factory():
converter = default_converter()
def code_action_context_hook(obj, type_):
diagnostics = []
raw_diagnostics = obj.get("diagnostics", []) or []
for d in raw_diagnostics:
try:
diagnostics.append(converter.structure(d, Diagnostic))
except Exception:
pass
return CodeActionContext(diagnostics=diagnostics)
converter.register_structure_hook(CodeActionContext, code_action_context_hook)
return converter
To use this custom converter with a language server set ``my_converter_factory`` as the server's ``converter_factory``.

.. code-block:: python
server = LanguageServer(
name="my-language-server",
version="v1.0",
converter_factory=my_converter_factory,
)
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ User Guide
:hidden:
:caption: How To

Handle Invalid Data <howto/handle-invalid-data>
Migrate to v1 <howto/migrate-to-v1>

.. _Language Server Protocol: https://microsoft.github.io/language-server-protocol/specification
Expand Down

0 comments on commit c295be2

Please sign in to comment.