aegis-core 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.3
2
+ Name: aegis_core
3
+ Version: 1.5.2
4
+ Summary: A library to manipulate Mecha for language server usage
5
+ License: MIT
6
+ Author: TheNuclearNexus
7
+ Author-email: 39636175+TheNuclearNexus@users.noreply.github.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: bolt (>=0.49.1,<0.50.0)
16
+ Requires-Dist: lsprotocol (>=2023.0.1,<2024.0.0)
17
+ Requires-Dist: mecha (>=0.96.0,<0.97.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Aegis Core
@@ -0,0 +1 @@
1
+ # Aegis Core
@@ -0,0 +1 @@
1
+ __version__ = "0.0.0"
File without changes
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from beet import Context
4
+ from bolt import AstAttribute, AstIdentifier, AstImportedItem, AstTargetIdentifier
5
+ from mecha import AstNode, AstResourceLocation
6
+
7
+ from .provider import *
8
+ from .resource_location import *
9
+ from .variable import *
10
+
11
+
12
+ def get_default_providers() -> dict[type[AstNode], type[BaseFeatureProvider]]:
13
+ return {
14
+ AstIdentifier: VariableFeatureProvider,
15
+ AstAttribute: VariableFeatureProvider,
16
+ AstTargetIdentifier: VariableFeatureProvider,
17
+ AstImportedItem: VariableFeatureProvider,
18
+ AstResourceLocation: ResourceLocationFeatureProvider,
19
+ }
20
+
21
+
22
+ @dataclass
23
+ class AegisFeatureProviders:
24
+ ctx: Context
25
+ _providers: dict[type[AstNode], type[BaseFeatureProvider]] = field(
26
+ init=False, default_factory=get_default_providers
27
+ )
28
+
29
+ def attach(self, node_type: type[AstNode], provider: type[BaseFeatureProvider]):
30
+ self._providers[node_type] = provider
31
+
32
+ def retrieve(
33
+ self,
34
+ node_type: type[AstNode] | AstNode,
35
+ ) -> type[BaseFeatureProvider]:
36
+ if not isinstance(node_type, type):
37
+ node_type = type(node_type)
38
+
39
+ return self._providers.get(node_type, BaseFeatureProvider)
@@ -0,0 +1,85 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Generic, TypeVar
3
+
4
+ import lsprotocol.types as lsp
5
+ from beet import Context
6
+ from mecha import AstNode
7
+
8
+ from ...semantics import TokenModifier, TokenType
9
+
10
+ __all__ = [
11
+ "BaseFeatureProvider",
12
+ "BaseParams",
13
+ "CompletionParams",
14
+ "HoverParams",
15
+ "DefinitionParams",
16
+ "ReferencesParams",
17
+ "RenameParams",
18
+ "SemanticsParams",
19
+ ]
20
+
21
+ Node = TypeVar("Node", bound=AstNode)
22
+
23
+
24
+ @dataclass
25
+ class BaseParams(Generic[Node]):
26
+ ctx: Context
27
+ node: Node
28
+
29
+
30
+ @dataclass
31
+ class CompletionParams(BaseParams[Node]): ...
32
+
33
+
34
+ @dataclass
35
+ class HoverParams(BaseParams[Node]):
36
+ text_range: lsp.Range
37
+
38
+
39
+ @dataclass
40
+ class DefinitionParams(BaseParams[Node]): ...
41
+
42
+
43
+ @dataclass
44
+ class ReferencesParams(BaseParams[Node]): ...
45
+
46
+
47
+ @dataclass
48
+ class RenameParams(BaseParams[Node]): ...
49
+
50
+
51
+ @dataclass
52
+ class SemanticsParams(BaseParams[Node]): ...
53
+
54
+
55
+ class BaseFeatureProvider(Generic[Node]):
56
+
57
+ @classmethod
58
+ def completion(
59
+ cls, params: CompletionParams[Node]
60
+ ) -> list[lsp.CompletionItem] | None:
61
+ return None
62
+
63
+ @classmethod
64
+ def hover(cls, params: HoverParams[Node]) -> lsp.Hover | None:
65
+ return None
66
+
67
+ @classmethod
68
+ def definition(
69
+ cls, params: DefinitionParams[Node]
70
+ ) -> list[lsp.Location | lsp.LocationLink] | lsp.Location | lsp.LocationLink | None:
71
+ return None
72
+
73
+ @classmethod
74
+ def references(cls, params: ReferencesParams[Node]) -> list[lsp.Location] | None:
75
+ return None
76
+
77
+ @classmethod
78
+ def rename(cls, params: RenameParams[Node]) -> lsp.WorkspaceEdit | None:
79
+ return None
80
+
81
+ @classmethod
82
+ def semantics(
83
+ cls, params: SemanticsParams[Node]
84
+ ) -> list[tuple[AstNode, TokenType, TokenModifier]] | None:
85
+ return None
@@ -0,0 +1,199 @@
1
+ __all__ = ["ResourceLocationFeatureProvider"]
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ import lsprotocol.types as lsp
8
+ from beet import File, NamespaceFile
9
+ from mecha import AstResourceLocation
10
+
11
+ from aegis_core.registry import AegisGameRegistries
12
+
13
+ from ...ast.helpers import node_location_to_range
14
+ from ...ast.metadata import ResourceLocationMetadata, retrieve_metadata
15
+ from ...indexing.project_index import AegisProjectIndex
16
+ from .provider import BaseFeatureProvider
17
+
18
+
19
+ def add_registry_items(
20
+ registries: AegisGameRegistries,
21
+ represents: str,
22
+ prefix: str = "",
23
+ kind: lsp.CompletionItemKind = lsp.CompletionItemKind.Value,
24
+ ):
25
+ if represents in registries:
26
+ registry_items = registries[represents]
27
+
28
+ return [
29
+ lsp.CompletionItem(prefix + "minecraft:" + k, kind=kind, sort_text=k)
30
+ for k in registry_items
31
+ ]
32
+
33
+ return []
34
+
35
+
36
+ def get_path(path: str) -> tuple[str | None, Path]:
37
+ segments = path.split(":")
38
+ if len(segments) == 1:
39
+ return (None, Path(segments[0]))
40
+ else:
41
+ return (segments[0], Path(segments[1]))
42
+
43
+
44
+ class ResourceLocationFeatureProvider(BaseFeatureProvider[AstResourceLocation]):
45
+ @classmethod
46
+ def hover(cls, params) -> lsp.Hover | None:
47
+ node = params.node
48
+ text_range = params.text_range
49
+
50
+ metadata = retrieve_metadata(node, ResourceLocationMetadata)
51
+
52
+ if metadata is None or metadata.represents is None:
53
+ path_type = None
54
+ elif isinstance(metadata.represents, str):
55
+ path_type = metadata.represents
56
+ elif issubclass(metadata.represents, File):
57
+ path_type = metadata.represents.snake_name
58
+
59
+ type_line = f"**{path_type}**\n" if path_type else ""
60
+
61
+ return lsp.Hover(
62
+ lsp.MarkupContent(
63
+ lsp.MarkupKind.Markdown,
64
+ f"{type_line}```yaml\n{node.get_canonical_value()}\n```",
65
+ ),
66
+ text_range,
67
+ )
68
+
69
+ @classmethod
70
+ def definition(cls, params):
71
+ project_index = params.ctx.inject(AegisProjectIndex)
72
+ node = params.node
73
+
74
+ metadata = retrieve_metadata(node, ResourceLocationMetadata)
75
+
76
+ if not metadata or not metadata.represents:
77
+ return
78
+
79
+ if isinstance(metadata.represents, str):
80
+ return
81
+
82
+ path = node.get_canonical_value()
83
+ definitions = project_index[metadata.represents].get_definitions(path)
84
+
85
+ return [
86
+ lsp.LocationLink(
87
+ target_uri=Path(path).as_uri(),
88
+ target_range=node_location_to_range(location),
89
+ target_selection_range=node_location_to_range(location),
90
+ origin_selection_range=node_location_to_range(node),
91
+ )
92
+ for path, *location in definitions
93
+ ]
94
+
95
+ @classmethod
96
+ def references(cls, params) -> list[lsp.Location] | None:
97
+ project_index = params.ctx.inject(AegisProjectIndex)
98
+ node = params.node
99
+
100
+ metadata = retrieve_metadata(node, ResourceLocationMetadata)
101
+
102
+ if not metadata or not metadata.represents:
103
+ return
104
+
105
+ if isinstance(metadata.represents, str):
106
+ return
107
+
108
+ path = node.get_canonical_value()
109
+ references = project_index[metadata.represents].get_references(path)
110
+
111
+ return [
112
+ lsp.Location(Path(path).as_uri(), node_location_to_range(location))
113
+ for path, *location in references
114
+ ]
115
+
116
+ @classmethod
117
+ def completion(cls, params):
118
+ node = params.node
119
+ project_index = params.ctx.inject(AegisProjectIndex)
120
+
121
+ metadata = retrieve_metadata(node, ResourceLocationMetadata)
122
+
123
+ if not metadata or metadata.represents is not type[NamespaceFile]:
124
+ return
125
+
126
+ represents = metadata.represents
127
+
128
+ if represents is type[NamespaceFile]:
129
+ file_type = cast(type[NamespaceFile], represents)
130
+
131
+ path = node.get_canonical_value()
132
+
133
+ if node.is_tag:
134
+ path = path[1:]
135
+
136
+ resolved = get_path(path)
137
+
138
+ unresolved = get_path(metadata.unresolved_path or path)
139
+
140
+ if unresolved[1].name == "~":
141
+ resolved_parent = resolved[1]
142
+ unresolved_parent = unresolved[1]
143
+ else:
144
+ resolved_parent = resolved[1].parent
145
+ unresolved_parent = unresolved[1].parent
146
+
147
+ items = []
148
+
149
+ for file in project_index[file_type]:
150
+ file_path = get_path(file)
151
+
152
+ if not (
153
+ file_path[0] == resolved[0]
154
+ and file_path[1].is_relative_to(resolved_parent)
155
+ ):
156
+ continue
157
+
158
+ relative = file_path[1].relative_to(resolved_parent)
159
+
160
+ if unresolved[0] is None and unresolved[1].name == "":
161
+ new_path = "./" + str(relative)
162
+ else:
163
+ new_path = str(unresolved_parent / relative)
164
+
165
+ insert_text = (
166
+ f"{unresolved[0] + ':' if unresolved[0] else ''}{new_path}"
167
+ )
168
+ if node.is_tag:
169
+ insert_text = "#" + insert_text
170
+
171
+ items.append(
172
+ lsp.CompletionItem(
173
+ label=insert_text,
174
+ documentation=file,
175
+ text_edit=lsp.InsertReplaceEdit(
176
+ insert_text,
177
+ node_location_to_range(node),
178
+ node_location_to_range(node),
179
+ ),
180
+ )
181
+ )
182
+
183
+ return items
184
+
185
+ elif isinstance(represents, str):
186
+ registries = params.ctx.inject(AegisGameRegistries)
187
+ items = []
188
+
189
+ items.extend(add_registry_items(registries, represents))
190
+ items.extend(
191
+ add_registry_items(
192
+ registries,
193
+ "tag/" + represents,
194
+ "#",
195
+ lsp.CompletionItemKind.Constant,
196
+ )
197
+ )
198
+
199
+ return items
@@ -0,0 +1,44 @@
1
+ import lsprotocol.types as lsp
2
+ from bolt import AstAttribute, AstIdentifier, AstImportedItem, AstTargetIdentifier
3
+
4
+ from ...reflection import get_annotation_description
5
+ from ..metadata import VariableMetadata, retrieve_metadata
6
+ from . import BaseFeatureProvider
7
+
8
+ __all__ = ["VariableFeatureProvider"]
9
+
10
+
11
+ class VariableFeatureProvider(
12
+ BaseFeatureProvider[
13
+ AstIdentifier | AstAttribute | AstTargetIdentifier | AstImportedItem
14
+ ]
15
+ ):
16
+ @classmethod
17
+ def hover(cls, params) -> lsp.Hover | None:
18
+ node = params.node
19
+ text_range = params.text_range
20
+
21
+ metadata = retrieve_metadata(node, VariableMetadata)
22
+ name = (
23
+ node.value
24
+ if not isinstance(node, (AstAttribute, AstImportedItem))
25
+ else node.name
26
+ )
27
+
28
+ if metadata and metadata.type_annotation:
29
+
30
+ type_annotation = metadata.type_annotation
31
+
32
+ description = get_annotation_description(name, type_annotation)
33
+
34
+ return lsp.Hover(
35
+ lsp.MarkupContent(lsp.MarkupKind.Markdown, description), text_range
36
+ )
37
+
38
+ return lsp.Hover(
39
+ lsp.MarkupContent(
40
+ lsp.MarkupKind.Markdown,
41
+ f"```python\n(variable) {name}\n```",
42
+ ),
43
+ text_range,
44
+ )
@@ -0,0 +1,31 @@
1
+ from typing import Iterable
2
+
3
+ import lsprotocol.types as lsp
4
+ from mecha import AstNode
5
+ from tokenstream import SourceLocation
6
+
7
+
8
+ def node_location_to_range(node: AstNode | Iterable[SourceLocation]):
9
+ if isinstance(node, AstNode):
10
+ location = node.location
11
+ end_location = node.end_location
12
+ else:
13
+ location, end_location = node
14
+
15
+ return lsp.Range(
16
+ start=location_to_position(location), end=location_to_position(end_location)
17
+ )
18
+
19
+
20
+ def node_start_to_range(node: AstNode):
21
+ start = location_to_position(node.location)
22
+ end = lsp.Position(line=start.line, character=start.character + 1)
23
+
24
+ return lsp.Range(start=start, end=end)
25
+
26
+
27
+ def location_to_position(location: SourceLocation) -> lsp.Position:
28
+ return lsp.Position(
29
+ line=max(location.lineno - 1, 0),
30
+ character=max(location.colno - 1, 0),
31
+ )
@@ -0,0 +1,107 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, TypeVar
3
+
4
+ from beet import NamespaceFile
5
+ from mecha import AstNode
6
+
7
+ from ..reflection import UNKNOWN_TYPE
8
+
9
+ METADATA_KEY = "aegis_metadata"
10
+
11
+ __all__ = [
12
+ "BaseMetadata",
13
+ "VariableMetadata",
14
+ "ResourceLocationMetadata",
15
+ "attach_metadata",
16
+ "retrieve_metadata",
17
+ ]
18
+
19
+
20
+ @dataclass
21
+ class BaseMetadata:
22
+ """
23
+ BaseMetadata provides information to aegis_server about the AstNode.
24
+ """
25
+
26
+
27
+ @dataclass
28
+ class VariableMetadata(BaseMetadata):
29
+ """
30
+ VariableMetadata provides information to aegis_server about a node representing a Bolt variable
31
+
32
+ Attributes
33
+ ----------
34
+ type_annotation : Any
35
+ The python type that the node represents
36
+
37
+ documentation : str
38
+ The documentation string for the node
39
+ """
40
+
41
+ type_annotation: Any = field(default=UNKNOWN_TYPE)
42
+
43
+ documentation: str | None = field(default=None)
44
+
45
+
46
+ @dataclass
47
+ class ResourceLocationMetadata(BaseMetadata):
48
+ """
49
+ ResourceLocationMetadata provides information to aegis_server about a node representing a resource location node
50
+
51
+ Attributes
52
+ ----------
53
+ respresents : str | type[NamespaceFile] | None
54
+ The registry or type of File the resource location represents
55
+
56
+ unresolved_path : str | None
57
+ The unresolved path of the string, ex. ~/foo
58
+ """
59
+
60
+ represents: str | type[NamespaceFile] | None = field(default=None)
61
+
62
+ unresolved_path: str | None = field(default=None)
63
+
64
+
65
+ def attach_metadata(node: AstNode, metadata: BaseMetadata):
66
+ """
67
+ Attaches the provided metadata instance to the node
68
+
69
+ Parameters
70
+ ----------
71
+ node : AstNode
72
+ The node to attach the metadata too
73
+ metadata : BaseMetadata
74
+ The metadata to be attached
75
+ """
76
+ node.__dict__[METADATA_KEY] = metadata
77
+
78
+
79
+ T = TypeVar("T")
80
+
81
+
82
+ def retrieve_metadata(
83
+ node: AstNode, type: tuple[type[T]] | type[T] = BaseMetadata
84
+ ) -> T | None:
85
+ """
86
+ Retrieves the metadata attached to a node
87
+
88
+ Parameters
89
+ ----------
90
+ node : AstNode
91
+ The node to retrieve from
92
+ type : tuple[type] | type
93
+ The type to check the metadata for
94
+
95
+ Returns
96
+ -------
97
+ BaseMetadata
98
+ The metadata attached to the node
99
+ None
100
+ If not metadata is present on the node
101
+ """
102
+ metadata = node.__dict__.get(METADATA_KEY)
103
+
104
+ if isinstance(metadata, type):
105
+ return metadata
106
+
107
+ return None
@@ -0,0 +1 @@
1
+ from .project_index import *
@@ -0,0 +1,170 @@
1
+ import logging
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from threading import Lock
6
+ from typing import ClassVar
7
+
8
+ from beet import Context, File, NamespaceFile
9
+ from beet.core.utils import extra_field, required_field
10
+ from tokenstream import SourceLocation
11
+
12
+ __all__ = ["FilePointer", "ResourceIndex", "AegisProjectIndex"]
13
+
14
+ FilePointer = tuple[SourceLocation, SourceLocation]
15
+
16
+
17
+ @dataclass
18
+ class ResourceIndice:
19
+ definitions: dict[str, set[FilePointer]] = extra_field(default_factory=dict)
20
+ references: dict[str, set[FilePointer]] = extra_field(default_factory=dict)
21
+
22
+ def _dump(self) -> str:
23
+ dump = ""
24
+
25
+ dump += "definitions:\n"
26
+ for path, pointers in self.definitions.items():
27
+ for pointer in pointers:
28
+ dump += f"\t- {path} {pointer[0].lineno}:{pointer[0].colno} -> {pointer[1].lineno}:{pointer[1].colno}\n"
29
+ dump += "references:\n"
30
+ for path, pointers in self.references.items():
31
+ for pointer in pointers:
32
+ dump += f"\t- {path} {pointer[0].lineno}:{pointer[0].colno} -> {pointer[1].lineno}:{pointer[1].colno}\n"
33
+
34
+ return dump
35
+
36
+
37
+ def valid_resource_location(path: str):
38
+ return bool(re.match(r"^[a-z0-9_\.]+:[a-z0-9_\.]+(\/?[a-z0-9_\.]+)*$", path))
39
+
40
+
41
+ @dataclass
42
+ class ResourceIndex:
43
+ _files: dict[str, ResourceIndice] = extra_field(default_factory=dict)
44
+ _lock: Lock = extra_field(default_factory=Lock)
45
+
46
+ def remove_associated(self, path: str | File):
47
+ self._lock.acquire()
48
+
49
+ if isinstance(path, File):
50
+ path = str(Path(path.ensure_source_path()).absolute())
51
+
52
+ for file, indice in list(self._files.items()):
53
+ if path in indice.definitions:
54
+ del indice.definitions[path]
55
+ if path in indice.references:
56
+ del indice.references[path]
57
+
58
+ if len(indice.definitions) == 0:
59
+ del self._files[file]
60
+
61
+ self._lock.release()
62
+
63
+ def add_definition(
64
+ self,
65
+ resource_path: str,
66
+ source_path: str,
67
+ source_location: FilePointer = (
68
+ SourceLocation(0, 0, 0),
69
+ SourceLocation(0, 0, 0),
70
+ ),
71
+ ):
72
+ if not valid_resource_location(resource_path):
73
+ raise Exception(f"Invalid resource location {resource_path}")
74
+
75
+ self._lock.acquire()
76
+
77
+ indice = self._files.setdefault(resource_path, ResourceIndice())
78
+ locations = indice.definitions.setdefault(source_path, set())
79
+ locations.add(source_location)
80
+
81
+ self._lock.release()
82
+
83
+ def get_definitions(
84
+ self, resource_path: str
85
+ ) -> list[tuple[str, SourceLocation, SourceLocation]]:
86
+ if not (file := self._files.get(resource_path)):
87
+ return []
88
+
89
+ definitions = []
90
+ for path, locations in file.definitions.items():
91
+ for location in locations:
92
+ definitions.append((path, *location))
93
+
94
+ return definitions
95
+
96
+ def get_references(
97
+ self, resource_path: str
98
+ ) -> list[tuple[str, SourceLocation, SourceLocation]]:
99
+ if not (file := self._files.get(resource_path)):
100
+ return []
101
+
102
+ references = []
103
+ for path, locations in file.references.items():
104
+ for location in locations:
105
+ references.append((path, *location))
106
+
107
+ return references
108
+
109
+ def add_reference(
110
+ self,
111
+ resource_path: str,
112
+ source_path: str,
113
+ source_location: FilePointer = (
114
+ SourceLocation(0, 0, 0),
115
+ SourceLocation(0, 0, 0),
116
+ ),
117
+ ):
118
+ if not valid_resource_location(resource_path):
119
+ raise Exception(f"Invalid resource location {resource_path}")
120
+
121
+ self._lock.acquire()
122
+
123
+ indice = self._files.setdefault(resource_path, ResourceIndice())
124
+ locations = indice.references.setdefault(source_path, set())
125
+ locations.add(source_location)
126
+
127
+ self._lock.release()
128
+
129
+ def __iter__(self):
130
+ items = self._files.keys()
131
+
132
+ for item in items:
133
+ yield item
134
+
135
+ def _dump(self) -> str:
136
+ dump = ""
137
+
138
+ for file, indice in self._files.items():
139
+ dump += f"\n- '{file}':\n"
140
+ dump += "\t" + "\n\t".join(indice._dump().splitlines())
141
+
142
+ return dump
143
+
144
+
145
+ @dataclass
146
+ class AegisProjectIndex:
147
+ _ctx: Context
148
+ _resources: dict[type[NamespaceFile], ResourceIndex] = field(default_factory=dict)
149
+
150
+ resource_name_to_type: dict[str, type[NamespaceFile]] = field(default_factory=dict)
151
+
152
+ def __post_init__(self):
153
+ self.resource_name_to_type = {
154
+ t.snake_name: t for t in self._ctx.get_file_types()
155
+ }
156
+
157
+ def __getitem__(self, key: type[NamespaceFile]):
158
+ return self._resources.setdefault(key, ResourceIndex())
159
+
160
+ def remove_associated(self, path: str):
161
+ for resource in self._resources.values():
162
+ resource.remove_associated(path)
163
+
164
+ def dump(self) -> str:
165
+ dump = ""
166
+ for resource, index in self._resources.items():
167
+ dump += f"\nResource {resource.__name__}:"
168
+ dump += "\t" + "\n\t".join(index._dump().splitlines())
169
+
170
+ return dump
@@ -0,0 +1,258 @@
1
+ import inspect
2
+ import types
3
+ import typing
4
+ from copy import copy
5
+ from dataclasses import dataclass, field, fields, is_dataclass
6
+ from typing import Any, get_args, get_origin
7
+
8
+ UNKNOWN_TYPE = object()
9
+ TYPE_TO_INFO: dict[type, "TypeInfo"] = {}
10
+
11
+
12
+ @dataclass
13
+ class ParameterInfo:
14
+ annotation: Any
15
+ default: Any
16
+
17
+
18
+ @dataclass
19
+ class FunctionInfo:
20
+ parameters: list[tuple[str, ParameterInfo]]
21
+ return_annotation: Any
22
+ doc: str | None
23
+
24
+ @staticmethod
25
+ def from_signature(
26
+ signature: inspect.Signature, doc: str | None, hints: dict[str, Any]
27
+ ) -> "FunctionInfo":
28
+ return FunctionInfo(
29
+ parameters=[
30
+ (
31
+ name,
32
+ ParameterInfo(
33
+ annotation=parameter.annotation, default=parameter.default
34
+ ),
35
+ )
36
+ for name, parameter in signature.parameters.items()
37
+ ],
38
+ return_annotation=hints.get("return") or signature.return_annotation,
39
+ doc=doc,
40
+ )
41
+
42
+ @staticmethod
43
+ def extract(field_value: Any):
44
+ try:
45
+ hints = typing.get_type_hints(field_value)
46
+ signature = inspect.signature(field_value)
47
+ return FunctionInfo.from_signature(signature, field_value.__doc__, hints)
48
+ except Exception as e:
49
+ return FunctionInfo(
50
+ parameters=[
51
+ (
52
+ "???",
53
+ ParameterInfo(inspect.Parameter.empty, inspect.Parameter.empty),
54
+ )
55
+ ],
56
+ return_annotation=UNKNOWN_TYPE,
57
+ doc="Error while extracting signature: " + str(e),
58
+ )
59
+
60
+ def __hash__(self) -> int:
61
+ return hash(self.__repr__())
62
+
63
+
64
+ @dataclass
65
+ class TypeInfo:
66
+ doc: str | None
67
+ fields: dict[str, Any] = field(default_factory=dict)
68
+ functions: dict[str, FunctionInfo] = field(default_factory=dict)
69
+
70
+ def get_member(self, field_name: str) -> Any | FunctionInfo:
71
+ return self.fields.get(field_name) or self.functions.get(field_name)
72
+
73
+ def add_member(
74
+ self,
75
+ field_annotations: dict[str, Any],
76
+ field_name: str,
77
+ field_value: Any,
78
+ skip_fields=False,
79
+ ):
80
+ if (
81
+ inspect.isfunction(field_value)
82
+ or inspect.ismethod(field_value)
83
+ or inspect.ismethoddescriptor(field_value)
84
+ or inspect.isbuiltin(field_value)
85
+ ):
86
+ self.functions[field_name] = FunctionInfo.extract(field_value)
87
+
88
+ elif skip_fields:
89
+ return
90
+ elif field_name in field_annotations:
91
+ self.fields[field_name] = field_annotations[field_name]
92
+ elif _ := get_origin(field_value):
93
+ self.fields[field_name] = field_value
94
+ else:
95
+ self.fields[field_name] = type(field_value)
96
+
97
+ def __hash__(self) -> int:
98
+ return hash(self.__repr__())
99
+
100
+
101
+ def get_type_info(_type: type) -> TypeInfo:
102
+
103
+ if _type in TYPE_TO_INFO:
104
+ return TYPE_TO_INFO[type]
105
+
106
+ info = TypeInfo(_type.__doc__)
107
+
108
+ # logging.debug("\n\n")
109
+ # logging.debug("-" * 50)
110
+ # logging.debug("Indexing type " + _type.__name__)
111
+
112
+ field_annotations = (
113
+ inspect.get_annotations(_type)
114
+ if inspect.isclass(_type) or callable(_type) or inspect.ismodule(_type)
115
+ else {}
116
+ )
117
+
118
+ if is_dataclass(_type):
119
+ handled_fields = set()
120
+ for field in fields(_type):
121
+ info.add_member({}, field.name, field.type)
122
+ handled_fields.add(field.name)
123
+ for field_name, field_value in inspect.getmembers(_type):
124
+ if field_name in handled_fields:
125
+ continue
126
+
127
+ # field_value = getattr(_type, field_name)
128
+ info.add_member(field_annotations, field_name, field_value)
129
+ else:
130
+ for field_name, field_value in inspect.getmembers(_type):
131
+ # field_value = getattr(_type, field_name)
132
+ # logging.debug(f"{field_name}, {field_value}")
133
+ info.add_member(field_annotations, field_name, field_value)
134
+
135
+ # logging.debug("-" * 50)
136
+ # logging.debug("\n\n")
137
+ # logging.debug(info)
138
+ return info
139
+
140
+
141
+ def get_name_of_type(annotation):
142
+ if annotation is inspect.Parameter.empty:
143
+ return None
144
+
145
+ if annotation is types.NoneType:
146
+ return "None"
147
+
148
+ if annotation is UNKNOWN_TYPE:
149
+ return "???"
150
+
151
+ origin = typing.get_origin(annotation)
152
+ args = list(map(str, map(get_name_of_type, typing.get_args(annotation))))
153
+
154
+ if origin is typing.Union or origin is types.UnionType:
155
+ return " | ".join(args)
156
+ elif origin:
157
+ return f"{origin.__name__}[{', '.join(args)}]"
158
+
159
+ try:
160
+ return annotation.__repr__()
161
+ except:
162
+ return annotation.__name__ if hasattr(annotation, "__name__") else annotation
163
+
164
+
165
+ def format_function_hints(
166
+ name: str,
167
+ signature: FunctionInfo,
168
+ keyword: str = "def",
169
+ show_return_type: bool = True,
170
+ ):
171
+ hint = f"{keyword} {name}("
172
+
173
+ return_type = signature.return_annotation
174
+
175
+ parameters = []
176
+
177
+ for name, parameter in signature.parameters:
178
+ annotation = get_name_of_type(parameter.annotation)
179
+
180
+ if annotation is None and parameter.default is not inspect.Parameter.empty:
181
+ annotation = get_name_of_type(type(parameter.default))
182
+
183
+ annotation_string = ": " + str(annotation) if annotation else ""
184
+ default_string = (
185
+ " = " + parameter.default.__repr__() # type: ignore
186
+ if parameter.default is not inspect.Parameter.empty
187
+ else ""
188
+ )
189
+
190
+ parameters.append(f"\n\t{name}{annotation_string}{default_string}")
191
+
192
+ hint += ",".join(parameters)
193
+
194
+ hint += f"\n)"
195
+
196
+ if show_return_type and signature.return_annotation is not types.NoneType:
197
+ hint += f" -> {get_name_of_type(return_type)}"
198
+
199
+ return hint
200
+
201
+
202
+ def get_doc_string(doc: Any):
203
+ return "\n---\n" + doc if isinstance(doc, str) else ""
204
+
205
+
206
+ def get_variable_description(name: str, value: Any):
207
+ if inspect.isclass(value):
208
+ return f"```python\n(variable) {name}: {get_name_of_type(value)}\n```"
209
+
210
+ doc_string = get_doc_string(value.__doc__)
211
+ return f"```python\n(variable) {name}: {get_name_of_type(type(value))}\n```{doc_string}"
212
+
213
+
214
+ def get_class_description(name: str, value: type | TypeInfo):
215
+ if not isinstance(value, TypeInfo):
216
+ value = get_type_info(value)
217
+
218
+ doc_string = get_doc_string(value.doc)
219
+
220
+ if not (init := value.functions.get("__init__")):
221
+ return f"```python\nclass {name}()\n```{doc_string}"
222
+
223
+ init = copy(init)
224
+
225
+ if len(init.parameters) > 0:
226
+ init.parameters.pop(0)
227
+
228
+ return f"```python\n{format_function_hints(name, init, keyword='class', show_return_type=False)}\n```{doc_string}"
229
+
230
+
231
+ def get_function_description(name: str, function: Any):
232
+ function_info = None
233
+ if isinstance(function, FunctionInfo):
234
+ function_info = function
235
+ else:
236
+ function_info = FunctionInfo.extract(function)
237
+
238
+ doc_string = get_doc_string(function_info.doc)
239
+
240
+ return f"```py\n{format_function_hints(name, function_info)}\n```{doc_string}"
241
+
242
+
243
+ def get_annotation_description(name: str, type_annotation: Any):
244
+ if get_origin(type_annotation) is type:
245
+ args = get_args(type_annotation)
246
+ description = get_class_description(name, args[0])
247
+ elif isinstance(type_annotation, TypeInfo):
248
+ description = get_class_description(name, type_annotation)
249
+ elif (
250
+ inspect.isfunction(type_annotation)
251
+ or inspect.isbuiltin(type_annotation)
252
+ or isinstance(type_annotation, FunctionInfo)
253
+ ):
254
+ description = get_function_description(name, type_annotation)
255
+ else:
256
+ description = get_variable_description(name, type_annotation)
257
+
258
+ return description
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from beet import Context
4
+
5
+
6
+ @dataclass
7
+ class AegisGameRegistries:
8
+ ctx: Context
9
+
10
+ registries: dict[str, list[str]] = field(init=False, default_factory=dict)
11
+
12
+ def __getitem__(self, registry: str) -> list[str]:
13
+ return self.registries.get(registry) or []
14
+
15
+ def __contains__(self, registry: str) -> bool:
16
+ return registry in self.registries
@@ -0,0 +1,38 @@
1
+ from typing import Literal, Union
2
+
3
+ TokenModifier = Union[
4
+ Literal["declaration"],
5
+ Literal["definition"],
6
+ Literal["readonly"],
7
+ Literal["static"],
8
+ Literal["deprecated"],
9
+ Literal["abstract"],
10
+ Literal["async"],
11
+ Literal["modification"],
12
+ Literal["documentation"],
13
+ Literal["defaultLibrary"],
14
+ ]
15
+
16
+ TokenType = Union[
17
+ Literal["comment"],
18
+ Literal["string"],
19
+ Literal["keyword"],
20
+ Literal["number"],
21
+ Literal["regexp"],
22
+ Literal["operator"],
23
+ Literal["namespace"],
24
+ Literal["type"],
25
+ Literal["struct"],
26
+ Literal["class"],
27
+ Literal["interface"],
28
+ Literal["enum"],
29
+ Literal["typeParameter"],
30
+ Literal["function"],
31
+ Literal["method"],
32
+ Literal["decorator"],
33
+ Literal["macro"],
34
+ Literal["variable"],
35
+ Literal["parameter"],
36
+ Literal["property"],
37
+ Literal["label"],
38
+ ]
@@ -0,0 +1,25 @@
1
+ [tool.poetry]
2
+ name = "aegis_core"
3
+ version = "1.5.2"
4
+ description = "A library to manipulate Mecha for language server usage"
5
+ authors = ["TheNuclearNexus <39636175+TheNuclearNexus@users.noreply.github.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ mecha = "^0.96.0"
12
+ bolt = "^0.49.1"
13
+ lsprotocol = "^2023.0.1"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ isort = "^6.0.0"
17
+ black = "^25.1.0"
18
+ pytest = "^8.1.1"
19
+
20
+ [tool.isort]
21
+ profile = "black"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core"]
25
+ build-backend = "poetry.core.masonry.api"