avrae-ls 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- avrae_ls/api.py +229 -229
- avrae_ls/argparser.py +16 -3
- avrae_ls/completions.py +177 -27
- avrae_ls/config.py +61 -2
- avrae_ls/context.py +62 -1
- avrae_ls/diagnostics.py +207 -2
- avrae_ls/parser.py +7 -2
- avrae_ls/runtime.py +94 -18
- avrae_ls/server.py +38 -1
- avrae_ls/symbols.py +149 -33
- avrae_ls-0.4.0.dist-info/METADATA +86 -0
- {avrae_ls-0.3.0.dist-info → avrae_ls-0.4.0.dist-info}/RECORD +16 -16
- avrae_ls-0.3.0.dist-info/METADATA +0 -47
- {avrae_ls-0.3.0.dist-info → avrae_ls-0.4.0.dist-info}/WHEEL +0 -0
- {avrae_ls-0.3.0.dist-info → avrae_ls-0.4.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.3.0.dist-info → avrae_ls-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.3.0.dist-info → avrae_ls-0.4.0.dist-info}/top_level.txt +0 -0
avrae_ls/argparser.py
CHANGED
|
@@ -2,7 +2,7 @@ import collections
|
|
|
2
2
|
import itertools
|
|
3
3
|
import re
|
|
4
4
|
import string
|
|
5
|
-
from typing import Iterator
|
|
5
|
+
from typing import ClassVar, Iterator
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class BadArgument(Exception):
|
|
@@ -169,6 +169,19 @@ def argparse(args, character=None, splitter=argsplit, parse_ephem=True) -> "Pars
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
class ParsedArguments:
|
|
172
|
+
ATTRS: ClassVar[list[str]] = []
|
|
173
|
+
METHODS: ClassVar[list[str]] = [
|
|
174
|
+
"get",
|
|
175
|
+
"last",
|
|
176
|
+
"adv",
|
|
177
|
+
"join",
|
|
178
|
+
"ignore",
|
|
179
|
+
"update",
|
|
180
|
+
"update_nx",
|
|
181
|
+
"set_context",
|
|
182
|
+
"add_context",
|
|
183
|
+
]
|
|
184
|
+
|
|
172
185
|
def __init__(self, args: list[Argument]):
|
|
173
186
|
self._parsed = collections.defaultdict(lambda: [])
|
|
174
187
|
for arg in args:
|
|
@@ -299,7 +312,7 @@ class ParsedArguments:
|
|
|
299
312
|
def __contains__(self, item):
|
|
300
313
|
return item in self._parsed and self._parsed[item]
|
|
301
314
|
|
|
302
|
-
def __len__(self):
|
|
315
|
+
def __len__(self) -> int:
|
|
303
316
|
return len(self._parsed)
|
|
304
317
|
|
|
305
318
|
def __setitem__(self, key, value):
|
|
@@ -317,7 +330,7 @@ class ParsedArguments:
|
|
|
317
330
|
if arg in self._parsed:
|
|
318
331
|
del self._parsed[arg]
|
|
319
332
|
|
|
320
|
-
def __iter__(self):
|
|
333
|
+
def __iter__(self) -> Iterator[str]:
|
|
321
334
|
return iter(self._parsed.keys())
|
|
322
335
|
|
|
323
336
|
def __repr__(self):
|
avrae_ls/completions.py
CHANGED
|
@@ -6,11 +6,12 @@ import re
|
|
|
6
6
|
import typing
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from functools import lru_cache
|
|
9
|
-
from typing import Any, Dict, Iterable, List, Optional
|
|
9
|
+
from typing import Any, ClassVar, Dict, Iterable, List, Optional
|
|
10
10
|
|
|
11
11
|
from lsprotocol import types
|
|
12
12
|
|
|
13
13
|
from .context import ContextData, GVarResolver
|
|
14
|
+
from .argparser import ParsedArguments
|
|
14
15
|
from .runtime import _default_builtins
|
|
15
16
|
from .api import (
|
|
16
17
|
AliasAction,
|
|
@@ -42,6 +43,76 @@ from .api import (
|
|
|
42
43
|
)
|
|
43
44
|
from .signature_help import FunctionSig
|
|
44
45
|
|
|
46
|
+
|
|
47
|
+
class _BuiltinList:
|
|
48
|
+
ATTRS: ClassVar[list[str]] = []
|
|
49
|
+
METHODS: ClassVar[list[str]] = ["append", "extend", "insert", "remove", "pop", "clear", "index", "count", "sort", "reverse", "copy"]
|
|
50
|
+
|
|
51
|
+
def __iter__(self) -> Iterable[Any]:
|
|
52
|
+
return iter([])
|
|
53
|
+
|
|
54
|
+
def append(self, value: Any) -> None: ...
|
|
55
|
+
def extend(self, iterable: Iterable[Any]) -> None: ...
|
|
56
|
+
def insert(self, index: int, value: Any) -> None: ...
|
|
57
|
+
def remove(self, value: Any) -> None: ...
|
|
58
|
+
def pop(self, index: int = -1) -> Any: ...
|
|
59
|
+
def clear(self) -> None: ...
|
|
60
|
+
def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
|
|
61
|
+
def count(self, value: Any) -> int: ...
|
|
62
|
+
def sort(self, *, key=None, reverse: bool = False) -> None: ...
|
|
63
|
+
def reverse(self) -> None: ...
|
|
64
|
+
def copy(self) -> list[Any]: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _BuiltinDict:
|
|
68
|
+
ATTRS: ClassVar[list[str]] = []
|
|
69
|
+
METHODS: ClassVar[list[str]] = ["get", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "clear", "copy"]
|
|
70
|
+
|
|
71
|
+
def __iter__(self) -> Iterable[Any]:
|
|
72
|
+
return iter({})
|
|
73
|
+
|
|
74
|
+
def get(self, key: Any, default: Any = None) -> Any: ...
|
|
75
|
+
def keys(self) -> Any: ...
|
|
76
|
+
def values(self) -> Any: ...
|
|
77
|
+
def items(self) -> Any: ...
|
|
78
|
+
def pop(self, key: Any, default: Any = None) -> Any: ...
|
|
79
|
+
def popitem(self) -> tuple[Any, Any]: ...
|
|
80
|
+
def update(self, *args, **kwargs) -> None: ...
|
|
81
|
+
def setdefault(self, key: Any, default: Any = None) -> Any: ...
|
|
82
|
+
def clear(self) -> None: ...
|
|
83
|
+
def copy(self) -> dict[Any, Any]: ...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _BuiltinStr:
|
|
87
|
+
ATTRS: ClassVar[list[str]] = []
|
|
88
|
+
METHODS: ClassVar[list[str]] = [
|
|
89
|
+
"lower",
|
|
90
|
+
"upper",
|
|
91
|
+
"title",
|
|
92
|
+
"split",
|
|
93
|
+
"join",
|
|
94
|
+
"replace",
|
|
95
|
+
"strip",
|
|
96
|
+
"startswith",
|
|
97
|
+
"endswith",
|
|
98
|
+
"format",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def __iter__(self) -> Iterable[str]:
|
|
102
|
+
return iter("")
|
|
103
|
+
|
|
104
|
+
def lower(self) -> str: ...
|
|
105
|
+
def upper(self) -> str: ...
|
|
106
|
+
def title(self) -> str: ...
|
|
107
|
+
def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
|
|
108
|
+
def join(self, iterable: Iterable[str]) -> str: ...
|
|
109
|
+
def replace(self, old: str, new: str, count: int = -1) -> str: ...
|
|
110
|
+
def strip(self, chars: str | None = None) -> str: ...
|
|
111
|
+
def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
|
|
112
|
+
def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
|
|
113
|
+
def format(self, *args, **kwargs) -> str: ...
|
|
114
|
+
|
|
115
|
+
|
|
45
116
|
TYPE_MAP: Dict[str, object] = {
|
|
46
117
|
"character": CharacterAPI,
|
|
47
118
|
"combat": SimpleCombat,
|
|
@@ -74,6 +145,10 @@ TYPE_MAP: Dict[str, object] = {
|
|
|
74
145
|
"SimpleGroup": SimpleGroup,
|
|
75
146
|
"effect": SimpleEffect,
|
|
76
147
|
"SimpleEffect": SimpleEffect,
|
|
148
|
+
"list": _BuiltinList,
|
|
149
|
+
"dict": _BuiltinDict,
|
|
150
|
+
"str": _BuiltinStr,
|
|
151
|
+
"ParsedArguments": ParsedArguments,
|
|
77
152
|
}
|
|
78
153
|
|
|
79
154
|
|
|
@@ -108,6 +183,7 @@ class MethodMeta:
|
|
|
108
183
|
class TypeMeta:
|
|
109
184
|
attrs: Dict[str, AttrMeta]
|
|
110
185
|
methods: Dict[str, MethodMeta]
|
|
186
|
+
element_type: str = ""
|
|
111
187
|
|
|
112
188
|
|
|
113
189
|
def gather_suggestions(
|
|
@@ -304,9 +380,16 @@ def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture
|
|
|
304
380
|
dot = line_text.rfind(".")
|
|
305
381
|
if dot == -1:
|
|
306
382
|
return None
|
|
307
|
-
|
|
383
|
+
tail = line_text[dot + 1 :]
|
|
384
|
+
prefix_match = re.match(r"\s*([A-Za-z_]\w*)?", tail)
|
|
385
|
+
prefix = prefix_match.group(1) or "" if prefix_match else ""
|
|
386
|
+
suffix = tail[prefix_match.end() if prefix_match else 0 :]
|
|
308
387
|
placeholder = "__COMPLETE__"
|
|
309
|
-
new_line = f"{line_text[:dot]}.{placeholder}"
|
|
388
|
+
new_line = f"{line_text[:dot]}.{placeholder}{suffix}"
|
|
389
|
+
# Close unmatched parentheses so the temporary code parses.
|
|
390
|
+
paren_balance = new_line.count("(") - new_line.count(")")
|
|
391
|
+
if paren_balance > 0:
|
|
392
|
+
new_line = new_line + (")" * paren_balance)
|
|
310
393
|
mod_lines = list(lines)
|
|
311
394
|
mod_lines[line] = new_line
|
|
312
395
|
mod_code = "\n".join(mod_lines)
|
|
@@ -330,17 +413,29 @@ def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture
|
|
|
330
413
|
Finder().visit(tree)
|
|
331
414
|
if receiver_src is None:
|
|
332
415
|
return None
|
|
333
|
-
|
|
334
|
-
return cleaned, prefix
|
|
416
|
+
return receiver_src, prefix
|
|
335
417
|
|
|
336
418
|
|
|
337
419
|
def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
|
|
338
420
|
lines = code.splitlines()
|
|
339
421
|
if 0 <= line < len(lines):
|
|
340
|
-
prefix = lines[line][:character]
|
|
341
|
-
|
|
342
|
-
|
|
422
|
+
prefix = lines[line][:character]
|
|
423
|
+
trimmed = prefix.rstrip()
|
|
424
|
+
if trimmed.endswith("."):
|
|
425
|
+
prefix = trimmed[:-1]
|
|
426
|
+
else:
|
|
427
|
+
dot = prefix.rfind(".")
|
|
428
|
+
if dot != -1:
|
|
429
|
+
after = prefix[dot + 1 :]
|
|
430
|
+
if not re.match(r"\s*[A-Za-z_]", after):
|
|
431
|
+
prefix = prefix[:dot] + after
|
|
343
432
|
lines[line] = prefix
|
|
433
|
+
candidate = "\n".join(lines)
|
|
434
|
+
try:
|
|
435
|
+
ast.parse(candidate)
|
|
436
|
+
except SyntaxError:
|
|
437
|
+
indent = re.match(r"[ \t]*", lines[line]).group(0)
|
|
438
|
+
lines[line] = indent + "pass"
|
|
344
439
|
return "\n".join(lines)
|
|
345
440
|
|
|
346
441
|
|
|
@@ -370,54 +465,91 @@ def _infer_type_map(code: str) -> Dict[str, str]:
|
|
|
370
465
|
|
|
371
466
|
class Visitor(ast.NodeVisitor):
|
|
372
467
|
def visit_Assign(self, node: ast.Assign):
|
|
373
|
-
val_type = self._value_type(node.value)
|
|
468
|
+
val_type, elem_type = self._value_type(node.value)
|
|
374
469
|
for target in node.targets:
|
|
375
470
|
if not isinstance(target, ast.Name):
|
|
376
471
|
continue
|
|
377
472
|
if val_type:
|
|
378
473
|
type_map[target.id] = val_type
|
|
474
|
+
if elem_type:
|
|
475
|
+
type_map[f"{target.id}.__element__"] = elem_type
|
|
379
476
|
self._record_dict_key_types(target.id, node.value)
|
|
380
477
|
self.generic_visit(node)
|
|
381
478
|
|
|
479
|
+
def visit_For(self, node: ast.For):
|
|
480
|
+
iter_type, elem_type = self._value_type(node.iter)
|
|
481
|
+
if not elem_type and isinstance(node.iter, ast.Name):
|
|
482
|
+
elem_type = type_map.get(f"{node.iter.id}.__element__")
|
|
483
|
+
if elem_type and isinstance(node.target, ast.Name):
|
|
484
|
+
type_map[node.target.id] = elem_type
|
|
485
|
+
self.generic_visit(node)
|
|
486
|
+
|
|
382
487
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
383
|
-
val_type = self._value_type(node.value) if node.value else None
|
|
488
|
+
val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
|
|
384
489
|
if isinstance(node.target, ast.Name):
|
|
385
490
|
if val_type:
|
|
386
491
|
type_map[node.target.id] = val_type
|
|
492
|
+
if elem_type:
|
|
493
|
+
type_map[f"{node.target.id}.__element__"] = elem_type
|
|
387
494
|
self._record_dict_key_types(node.target.id, node.value)
|
|
388
495
|
self.generic_visit(node)
|
|
389
496
|
|
|
390
|
-
def _value_type(self, value: ast.AST | None) -> Optional[str]:
|
|
497
|
+
def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
|
|
391
498
|
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
|
|
392
499
|
if value.func.id in {"character", "combat"}:
|
|
393
|
-
return value.func.id
|
|
500
|
+
return value.func.id, None
|
|
394
501
|
if value.func.id == "vroll":
|
|
395
|
-
return "SimpleRollResult"
|
|
502
|
+
return "SimpleRollResult", None
|
|
503
|
+
if value.func.id == "argparse":
|
|
504
|
+
return "ParsedArguments", None
|
|
505
|
+
if value.func.id in {"list", "dict", "str"}:
|
|
506
|
+
return value.func.id, None
|
|
507
|
+
if isinstance(value, ast.List):
|
|
508
|
+
return "list", None
|
|
509
|
+
if isinstance(value, ast.Dict):
|
|
510
|
+
return "dict", None
|
|
511
|
+
if isinstance(value, ast.Constant):
|
|
512
|
+
if isinstance(value.value, str):
|
|
513
|
+
return "str", None
|
|
396
514
|
if isinstance(value, ast.Name):
|
|
397
515
|
if value.id in type_map:
|
|
398
|
-
return type_map[value.id]
|
|
516
|
+
return type_map[value.id], type_map.get(f"{value.id}.__element__")
|
|
399
517
|
if value.id in {"character", "combat", "ctx"}:
|
|
400
|
-
return value.id
|
|
518
|
+
return value.id, None
|
|
401
519
|
if isinstance(value, ast.Attribute):
|
|
402
520
|
attr_name = value.attr
|
|
403
521
|
base_type = None
|
|
522
|
+
base_elem = None
|
|
404
523
|
if isinstance(value.value, ast.Name):
|
|
405
524
|
base_type = type_map.get(value.value.id)
|
|
525
|
+
base_elem = type_map.get(f"{value.value.id}.__element__")
|
|
406
526
|
if base_type is None:
|
|
407
|
-
base_type = self._value_type(value.value)
|
|
408
|
-
if base_type
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
527
|
+
base_type, base_elem = self._value_type(value.value)
|
|
528
|
+
if base_type:
|
|
529
|
+
meta = _type_meta(base_type)
|
|
530
|
+
attr_meta = meta.attrs.get(attr_name)
|
|
531
|
+
if attr_meta:
|
|
532
|
+
if attr_meta.type_name:
|
|
533
|
+
return attr_meta.type_name, attr_meta.element_type or None
|
|
534
|
+
if attr_meta.element_type:
|
|
535
|
+
return base_type, attr_meta.element_type
|
|
536
|
+
if base_elem:
|
|
537
|
+
return base_elem, None
|
|
538
|
+
if attr_name in TYPE_MAP:
|
|
539
|
+
return attr_name, None
|
|
540
|
+
return None, None
|
|
541
|
+
return None, None
|
|
412
542
|
|
|
413
543
|
def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
|
|
414
544
|
if not isinstance(value, ast.Dict):
|
|
415
545
|
return
|
|
416
546
|
for key_node, val_node in zip(value.keys or [], value.values or []):
|
|
417
547
|
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
|
418
|
-
val_type = self._value_type(val_node)
|
|
548
|
+
val_type, elem_type = self._value_type(val_node)
|
|
419
549
|
if val_type:
|
|
420
550
|
type_map[f"{var_name}.{key_node.value}"] = val_type
|
|
551
|
+
if elem_type:
|
|
552
|
+
type_map[f"{var_name}.{key_node.value}.__element__"] = elem_type
|
|
421
553
|
|
|
422
554
|
Visitor().visit(tree)
|
|
423
555
|
return type_map
|
|
@@ -431,13 +563,26 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
|
|
|
431
563
|
dict_key = f"{base}.{key}"
|
|
432
564
|
if dict_key in mapping:
|
|
433
565
|
return mapping[dict_key]
|
|
566
|
+
bracket = receiver.rfind("[")
|
|
567
|
+
if bracket != -1 and receiver.endswith("]"):
|
|
568
|
+
base_expr = receiver[:bracket]
|
|
569
|
+
elem_hint = mapping.get(f"{base_expr}.__element__")
|
|
570
|
+
if elem_hint:
|
|
571
|
+
return elem_hint
|
|
572
|
+
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
573
|
+
if base_type:
|
|
574
|
+
base_meta = _type_meta(base_type)
|
|
575
|
+
if base_meta.element_type:
|
|
576
|
+
return base_meta.element_type
|
|
577
|
+
return base_type
|
|
434
578
|
receiver = receiver.rstrip("()")
|
|
435
579
|
if "." in receiver:
|
|
436
580
|
base_expr, attr_name = receiver.rsplit(".", 1)
|
|
437
581
|
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
438
582
|
if base_type:
|
|
439
583
|
meta = _type_meta(base_type)
|
|
440
|
-
|
|
584
|
+
attr_key = attr_name.split("[", 1)[0]
|
|
585
|
+
attr_meta = meta.attrs.get(attr_key)
|
|
441
586
|
if attr_meta:
|
|
442
587
|
if attr_meta.element_type:
|
|
443
588
|
return attr_meta.element_type
|
|
@@ -446,16 +591,19 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
|
|
|
446
591
|
|
|
447
592
|
if receiver in mapping:
|
|
448
593
|
return mapping[receiver]
|
|
594
|
+
elem_key = f"{receiver}.__element__"
|
|
595
|
+
if elem_key in mapping:
|
|
596
|
+
return mapping[elem_key]
|
|
449
597
|
if receiver in TYPE_MAP:
|
|
450
598
|
return receiver
|
|
451
|
-
tail = receiver.split(".")[-1]
|
|
599
|
+
tail = receiver.split(".")[-1].split("[", 1)[0]
|
|
452
600
|
if tail in TYPE_MAP:
|
|
453
601
|
return tail
|
|
454
602
|
return receiver
|
|
455
603
|
|
|
456
604
|
|
|
457
605
|
def _type_meta(type_name: str) -> TypeMeta:
|
|
458
|
-
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}))
|
|
606
|
+
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
|
|
459
607
|
|
|
460
608
|
|
|
461
609
|
@lru_cache()
|
|
@@ -474,6 +622,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
|
474
622
|
for type_name, cls in TYPE_MAP.items():
|
|
475
623
|
attrs: dict[str, AttrMeta] = {}
|
|
476
624
|
methods: dict[str, MethodMeta] = {}
|
|
625
|
+
iterable_element = _iter_element_for_type_name(type_name)
|
|
477
626
|
|
|
478
627
|
for attr in getattr(cls, "ATTRS", []):
|
|
479
628
|
doc = ""
|
|
@@ -508,7 +657,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
|
508
657
|
doc = (meth_obj.__doc__ or "").strip()
|
|
509
658
|
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
510
659
|
|
|
511
|
-
meta[type_name] = TypeMeta(attrs=attrs, methods=methods)
|
|
660
|
+
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=iterable_element)
|
|
512
661
|
return meta
|
|
513
662
|
|
|
514
663
|
|
|
@@ -571,8 +720,9 @@ def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) ->
|
|
|
571
720
|
if args:
|
|
572
721
|
elem = args[0]
|
|
573
722
|
elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
|
|
574
|
-
|
|
575
|
-
|
|
723
|
+
container_name = reverse_type_map.get(origin) or "list"
|
|
724
|
+
return container_name, elem_name
|
|
725
|
+
return reverse_type_map.get(origin) or "list", ""
|
|
576
726
|
|
|
577
727
|
if isinstance(ann, type) and ann in reverse_type_map:
|
|
578
728
|
return reverse_type_map[ann], ""
|
avrae_ls/config.py
CHANGED
|
@@ -3,12 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
6
8
|
from dataclasses import dataclass, field
|
|
7
9
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Dict, Iterable, Tuple
|
|
10
|
+
from typing import Any, Dict, Iterable, Mapping, Tuple
|
|
9
11
|
|
|
10
12
|
CONFIG_FILENAME = ".avraels.json"
|
|
11
13
|
log = logging.getLogger(__name__)
|
|
14
|
+
_ENV_VAR_PATTERN = re.compile(r"\$(\w+)|\$\{([^}]+)\}")
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class ConfigError(Exception):
|
|
@@ -25,6 +28,8 @@ class DiagnosticSettings:
|
|
|
25
28
|
class AvraeServiceConfig:
|
|
26
29
|
base_url: str = "https://api.avrae.io"
|
|
27
30
|
token: str | None = None
|
|
31
|
+
verify_timeout: float = 5.0
|
|
32
|
+
verify_retries: int = 0
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
@dataclass
|
|
@@ -360,6 +365,30 @@ class AvraeLSConfig:
|
|
|
360
365
|
)
|
|
361
366
|
|
|
362
367
|
|
|
368
|
+
def _expand_env_vars(data: Any, env: Mapping[str, str], missing_vars: set[str]) -> Any:
|
|
369
|
+
if isinstance(data, dict):
|
|
370
|
+
return {key: _expand_env_vars(value, env, missing_vars) for key, value in data.items()}
|
|
371
|
+
if isinstance(data, list):
|
|
372
|
+
return [_expand_env_vars(value, env, missing_vars) for value in data]
|
|
373
|
+
if isinstance(data, str):
|
|
374
|
+
def _replace(match: re.Match[str]) -> str:
|
|
375
|
+
var = match.group(1) or match.group(2) or ""
|
|
376
|
+
if var in env:
|
|
377
|
+
return env[var]
|
|
378
|
+
missing_vars.add(var)
|
|
379
|
+
return ""
|
|
380
|
+
|
|
381
|
+
return _ENV_VAR_PATTERN.sub(_replace, data)
|
|
382
|
+
return data
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _coerce_optional_str(value: Any) -> str | None:
|
|
386
|
+
if value is None:
|
|
387
|
+
return None
|
|
388
|
+
value_str = str(value)
|
|
389
|
+
return value_str if value_str.strip() else None
|
|
390
|
+
|
|
391
|
+
|
|
363
392
|
def load_config(workspace_root: Path) -> Tuple[AvraeLSConfig, Iterable[str]]:
|
|
364
393
|
"""Load `.avraels.json` from the workspace root, returning config and warnings."""
|
|
365
394
|
path = workspace_root / CONFIG_FILENAME
|
|
@@ -374,12 +403,42 @@ def load_config(workspace_root: Path) -> Tuple[AvraeLSConfig, Iterable[str]]:
|
|
|
374
403
|
return AvraeLSConfig.default(workspace_root), [warning]
|
|
375
404
|
|
|
376
405
|
warnings: list[str] = []
|
|
406
|
+
env_missing: set[str] = set()
|
|
407
|
+
env = dict(os.environ)
|
|
408
|
+
env.setdefault("workspaceRoot", str(workspace_root))
|
|
409
|
+
env.setdefault("workspaceFolder", str(workspace_root))
|
|
410
|
+
raw = _expand_env_vars(raw, env, env_missing)
|
|
411
|
+
for var in sorted(env_missing):
|
|
412
|
+
warning = f"{CONFIG_FILENAME}: environment variable '{var}' is not set; substituting an empty string."
|
|
413
|
+
warnings.append(warning)
|
|
414
|
+
log.warning(warning)
|
|
415
|
+
|
|
377
416
|
enable_gvar_fetch = bool(raw.get("enableGvarFetch", False))
|
|
378
417
|
|
|
379
418
|
service_cfg = raw.get("avraeService") or {}
|
|
419
|
+
def _get_service_timeout(raw_timeout) -> float:
|
|
420
|
+
try:
|
|
421
|
+
timeout = float(raw_timeout)
|
|
422
|
+
if timeout > 0:
|
|
423
|
+
return timeout
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
return AvraeServiceConfig.verify_timeout
|
|
427
|
+
|
|
428
|
+
def _get_service_retries(raw_retries) -> int:
|
|
429
|
+
try:
|
|
430
|
+
retries = int(raw_retries)
|
|
431
|
+
if retries >= 0:
|
|
432
|
+
return retries
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
return AvraeServiceConfig.verify_retries
|
|
436
|
+
|
|
380
437
|
service = AvraeServiceConfig(
|
|
381
438
|
base_url=str(service_cfg.get("baseUrl") or AvraeServiceConfig.base_url),
|
|
382
|
-
token=service_cfg.get("token"),
|
|
439
|
+
token=_coerce_optional_str(service_cfg.get("token")),
|
|
440
|
+
verify_timeout=_get_service_timeout(service_cfg.get("verifySignatureTimeout")),
|
|
441
|
+
verify_retries=_get_service_retries(service_cfg.get("verifySignatureRetries")),
|
|
383
442
|
)
|
|
384
443
|
|
|
385
444
|
diag_cfg = raw.get("diagnostics") or {}
|
avrae_ls/context.py
CHANGED
|
@@ -33,11 +33,12 @@ class ContextBuilder:
|
|
|
33
33
|
|
|
34
34
|
def build(self, profile_name: str | None = None) -> ContextData:
|
|
35
35
|
profile = self._select_profile(profile_name)
|
|
36
|
+
combat = self._ensure_me_combatant(profile)
|
|
36
37
|
merged_vars = self._merge_character_cvars(profile.character, self._load_var_files().merge(profile.vars))
|
|
37
38
|
self._gvar_resolver.seed(merged_vars.gvars)
|
|
38
39
|
return ContextData(
|
|
39
40
|
ctx=dict(profile.ctx),
|
|
40
|
-
combat=
|
|
41
|
+
combat=combat,
|
|
41
42
|
character=dict(profile.character),
|
|
42
43
|
vars=merged_vars,
|
|
43
44
|
)
|
|
@@ -69,6 +70,66 @@ class ContextBuilder:
|
|
|
69
70
|
merged = merged.merge(VarSources(cvars=builtin_cvars))
|
|
70
71
|
return merged
|
|
71
72
|
|
|
73
|
+
def _ensure_me_combatant(self, profile: ContextProfile) -> Dict[str, Any]:
|
|
74
|
+
combat = dict(profile.combat or {})
|
|
75
|
+
combatants = list(combat.get("combatants") or [])
|
|
76
|
+
me = combat.get("me")
|
|
77
|
+
author_id = (profile.ctx.get("author") or {}).get("id")
|
|
78
|
+
|
|
79
|
+
def _matches_author(combatant: Dict[str, Any]) -> bool:
|
|
80
|
+
try:
|
|
81
|
+
return author_id is not None and str(combatant.get("controller")) == str(author_id)
|
|
82
|
+
except Exception:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# Use an existing combatant controlled by the author if me is missing.
|
|
86
|
+
if me is None:
|
|
87
|
+
for existing in combatants:
|
|
88
|
+
if _matches_author(existing):
|
|
89
|
+
me = existing
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
# If still missing, synthesize a combatant from the character sheet.
|
|
93
|
+
if me is None and profile.character:
|
|
94
|
+
me = {
|
|
95
|
+
"name": profile.character.get("name", "Player"),
|
|
96
|
+
"id": "cmb_player",
|
|
97
|
+
"controller": author_id,
|
|
98
|
+
"group": None,
|
|
99
|
+
"race": profile.character.get("race"),
|
|
100
|
+
"monster_name": None,
|
|
101
|
+
"is_hidden": False,
|
|
102
|
+
"init": profile.character.get("stats", {}).get("dexterity", 10),
|
|
103
|
+
"initmod": 0,
|
|
104
|
+
"type": "combatant",
|
|
105
|
+
"note": "Mock combatant for preview",
|
|
106
|
+
"effects": [],
|
|
107
|
+
"stats": profile.character.get("stats") or {},
|
|
108
|
+
"levels": profile.character.get("levels") or profile.character.get("class_levels") or {},
|
|
109
|
+
"skills": profile.character.get("skills") or {},
|
|
110
|
+
"saves": profile.character.get("saves") or {},
|
|
111
|
+
"resistances": profile.character.get("resistances") or {},
|
|
112
|
+
"spellbook": profile.character.get("spellbook") or {},
|
|
113
|
+
"attacks": profile.character.get("attacks") or [],
|
|
114
|
+
"max_hp": profile.character.get("max_hp"),
|
|
115
|
+
"hp": profile.character.get("hp"),
|
|
116
|
+
"temp_hp": profile.character.get("temp_hp"),
|
|
117
|
+
"ac": profile.character.get("ac"),
|
|
118
|
+
"creature_type": profile.character.get("creature_type"),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if me is not None:
|
|
122
|
+
combat["me"] = me
|
|
123
|
+
if not any(c is me for c in combatants) and not any(_matches_author(c) for c in combatants):
|
|
124
|
+
combatants.insert(0, me)
|
|
125
|
+
combat["combatants"] = combatants
|
|
126
|
+
if "current" not in combat or combat.get("current") is None:
|
|
127
|
+
combat["current"] = me
|
|
128
|
+
else:
|
|
129
|
+
combat["combatants"] = combatants
|
|
130
|
+
|
|
131
|
+
return combat
|
|
132
|
+
|
|
72
133
|
|
|
73
134
|
class GVarResolver:
|
|
74
135
|
def __init__(self, config: AvraeLSConfig):
|