Skip to content

Commit 87143b0

Browse files
committed
Migrate pydantic to cattrs
1 parent 4fe227c commit 87143b0

File tree

6 files changed

+122
-231
lines changed

6 files changed

+122
-231
lines changed

jedi_language_server/initialization_options.py

Lines changed: 97 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,115 +4,149 @@
44
initialization options.
55
"""
66

7-
from typing import List, Optional, Pattern, Set
7+
from dataclasses import dataclass, field
8+
from typing import Any, List, Optional, Pattern, Set
89

10+
from attrs import fields, has
11+
from cattrs import Converter
12+
from cattrs.gen import make_dict_structure_fn, override
913
from lsprotocol.types import MarkupKind
10-
from pydantic import BaseModel, ConfigDict, Field
1114

1215
# pylint: disable=missing-class-docstring
1316
# pylint: disable=too-few-public-methods
1417

18+
light_dataclass = dataclass(kw_only=True, eq=False, match_args=False)
1519

16-
def snake_to_camel(string: str) -> str:
17-
"""Convert from snake_case to camelCase."""
18-
return "".join(
19-
word.capitalize() if idx > 0 else word
20-
for idx, word in enumerate(string.split("_"))
21-
)
2220

23-
24-
class Model(BaseModel):
25-
model_config = ConfigDict(alias_generator=snake_to_camel)
26-
27-
28-
class CodeAction(Model):
21+
@light_dataclass
22+
class CodeAction:
2923
name_extract_variable: str = "jls_extract_var"
3024
name_extract_function: str = "jls_extract_def"
3125

3226

33-
class Completion(Model):
27+
@light_dataclass
28+
class Completion:
3429
disable_snippets: bool = False
3530
resolve_eagerly: bool = False
36-
ignore_patterns: List[Pattern[str]] = []
31+
ignore_patterns: List[Pattern[str]] = field(default_factory=list)
3732

3833

39-
class Diagnostics(Model):
34+
@light_dataclass
35+
class Diagnostics:
4036
enable: bool = True
4137
did_open: bool = True
4238
did_save: bool = True
4339
did_change: bool = True
4440

4541

46-
class HoverDisableOptions(Model):
42+
@light_dataclass
43+
class HoverDisableOptions:
4744
all: bool = False
48-
names: Set[str] = set()
49-
full_names: Set[str] = set()
45+
names: Set[str] = field(default_factory=set)
46+
full_names: Set[str] = field(default_factory=set)
5047

5148

52-
class HoverDisable(Model):
49+
@light_dataclass
50+
class HoverDisable:
5351
"""All Attributes have _ appended to avoid syntax conflicts.
5452
5553
For example, the keyword class would have required a special case.
5654
To get around this, I decided it's simpler to always assume an
5755
underscore at the end.
5856
"""
5957

60-
keyword_: HoverDisableOptions = Field(
61-
default=HoverDisableOptions(), alias="keyword"
62-
)
63-
module_: HoverDisableOptions = Field(
64-
default=HoverDisableOptions(), alias="module"
65-
)
66-
class_: HoverDisableOptions = Field(
67-
default=HoverDisableOptions(), alias="class"
68-
)
69-
instance_: HoverDisableOptions = Field(
70-
default=HoverDisableOptions(), alias="instance"
71-
)
72-
function_: HoverDisableOptions = Field(
73-
default=HoverDisableOptions(), alias="function"
74-
)
75-
param_: HoverDisableOptions = Field(
76-
default=HoverDisableOptions(), alias="param"
77-
)
78-
path_: HoverDisableOptions = Field(
79-
default=HoverDisableOptions(), alias="path"
80-
)
81-
property_: HoverDisableOptions = Field(
82-
default=HoverDisableOptions(), alias="property"
83-
)
84-
statement_: HoverDisableOptions = Field(
85-
default=HoverDisableOptions(), alias="statement"
58+
keyword_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
59+
module_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
60+
class_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
61+
instance_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
62+
function_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
63+
param_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
64+
path_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
65+
property_: HoverDisableOptions = field(default_factory=HoverDisableOptions)
66+
statement_: HoverDisableOptions = field(
67+
default_factory=HoverDisableOptions
8668
)
8769

8870

89-
class Hover(Model):
71+
@light_dataclass
72+
class Hover:
9073
enable: bool = True
91-
disable: HoverDisable = HoverDisable()
74+
disable: HoverDisable = field(default_factory=HoverDisable)
9275

9376

94-
class JediSettings(Model):
95-
auto_import_modules: List[str] = []
77+
@light_dataclass
78+
class JediSettings:
79+
auto_import_modules: List[str] = field(default_factory=list)
9680
case_insensitive_completion: bool = True
9781
debug: bool = False
9882

9983

100-
class Symbols(Model):
101-
ignore_folders: List[str] = [".nox", ".tox", ".venv", "__pycache__"]
84+
@light_dataclass
85+
class Symbols:
86+
ignore_folders: List[str] = field(
87+
default_factory=lambda: [".nox", ".tox", ".venv", "__pycache__"]
88+
)
10289
max_symbols: int = 20
10390

10491

105-
class Workspace(Model):
92+
@light_dataclass
93+
class Workspace:
10694
environment_path: Optional[str] = None
107-
extra_paths: List[str] = []
108-
symbols: Symbols = Symbols()
95+
extra_paths: List[str] = field(default_factory=list)
96+
symbols: Symbols = field(default_factory=Symbols)
10997

11098

111-
class InitializationOptions(Model):
112-
code_action: CodeAction = CodeAction()
113-
completion: Completion = Completion()
114-
diagnostics: Diagnostics = Diagnostics()
115-
hover: Hover = Hover()
116-
jedi_settings: JediSettings = JediSettings()
99+
@light_dataclass
100+
class InitializationOptions:
101+
code_action: CodeAction = field(default_factory=CodeAction)
102+
completion: Completion = field(default_factory=Completion)
103+
diagnostics: Diagnostics = field(default_factory=Diagnostics)
104+
hover: Hover = field(default_factory=Hover)
105+
jedi_settings: JediSettings = field(default_factory=JediSettings)
117106
markup_kind_preferred: Optional[MarkupKind] = None
118-
workspace: Workspace = Workspace()
107+
workspace: Workspace = field(default_factory=Workspace)
108+
109+
110+
initialization_options_converter = Converter()
111+
112+
WEIRD_NAMES = {
113+
"keyword_": "keyword",
114+
"module_": "module",
115+
"class_": "class",
116+
"instance_": "instance",
117+
"function_": "function",
118+
"param_": "param",
119+
"path_": "path",
120+
"property_": "property",
121+
"statement_ ": "statement",
122+
}
123+
124+
125+
def convert_class_keys(string: str) -> str:
126+
"""Convert from snake_case to camelCase.
127+
128+
Also handles random special cases for keywords.
129+
"""
130+
if string in WEIRD_NAMES:
131+
return WEIRD_NAMES[string]
132+
return "".join(
133+
word.capitalize() if idx > 0 else word
134+
for idx, word in enumerate(string.split("_"))
135+
)
136+
137+
138+
def structure(cls: type) -> Any:
139+
"""Hook to convert names when marshalling initialization_options."""
140+
return make_dict_structure_fn(
141+
cls,
142+
initialization_options_converter,
143+
**{
144+
a.name: override(rename=convert_class_keys(a.name))
145+
for a in fields(cls)
146+
}
147+
)
148+
149+
150+
initialization_options_converter.register_structure_hook_factory(
151+
has, structure
152+
)

jedi_language_server/server.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import itertools
1010
from typing import Any, List, Optional, Union
1111

12+
import cattrs
1213
from jedi import Project, __version__
1314
from jedi.api.refactoring import RefactoringError
1415
from lsprotocol.types import (
@@ -63,13 +64,15 @@
6364
WorkspaceEdit,
6465
WorkspaceSymbolParams,
6566
)
66-
from pydantic import ValidationError
6767
from pygls.capabilities import get_capability
6868
from pygls.protocol import LanguageServerProtocol, lsp_method
6969
from pygls.server import LanguageServer
7070

7171
from . import jedi_utils, pygls_utils, text_edit_utils
72-
from .initialization_options import InitializationOptions
72+
from .initialization_options import (
73+
InitializationOptions,
74+
initialization_options_converter,
75+
)
7376

7477

7578
class JediLanguageServerProtocol(LanguageServerProtocol):
@@ -84,13 +87,19 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
8487
"""
8588
server: "JediLanguageServer" = self._server
8689
try:
87-
server.initialization_options = InitializationOptions.parse_obj(
88-
{}
89-
if params.initialization_options is None
90-
else params.initialization_options
90+
server.initialization_options = (
91+
initialization_options_converter.structure(
92+
{}
93+
if params.initialization_options is None
94+
else params.initialization_options,
95+
InitializationOptions,
96+
)
97+
)
98+
except cattrs.BaseValidationError as error:
99+
msg = (
100+
"Invalid InitializationOptions, using defaults:"
101+
f" {cattrs.transform_error(error)}"
91102
)
92-
except ValidationError as error:
93-
msg = f"Invalid InitializationOptions, using defaults: {error}"
94103
server.show_message(msg, msg_type=MessageType.Error)
95104
server.show_message_log(msg, msg_type=MessageType.Error)
96105
server.initialization_options = InitializationOptions()

mypy.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[mypy]
2-
plugins = pydantic.mypy
32
strict = True
43
enable_error_code = ignore-without-code,redundant-expr,truthy-bool
54

0 commit comments

Comments
 (0)