avrae-ls 0.6.4__py3-none-any.whl → 0.7.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/config.py CHANGED
@@ -82,6 +82,7 @@ class ContextProfile:
82
82
  class AvraeLSConfig:
83
83
  workspace_root: Path
84
84
  enable_gvar_fetch: bool = False
85
+ silent_gvar_fetch: bool = False
85
86
  service: AvraeServiceConfig = field(default_factory=AvraeServiceConfig)
86
87
  var_files: Tuple[Path, ...] = field(default_factory=tuple)
87
88
  default_profile: str = "default"
avrae_ls/context.py CHANGED
@@ -145,6 +145,19 @@ class GVarResolver:
145
145
  self._config = config
146
146
  self._cache: Dict[str, Any] = {}
147
147
 
148
+ def _silent_failure(self, key: str) -> bool:
149
+ if not self._config.silent_gvar_fetch:
150
+ return False
151
+ self._cache[str(key)] = None
152
+ return True
153
+
154
+ def _silent_failure_many(self, keys: Iterable[str]) -> bool:
155
+ if not self._config.silent_gvar_fetch:
156
+ return False
157
+ for key in keys:
158
+ self._cache[str(key)] = None
159
+ return True
160
+
148
161
  def reset(self, gvars: Dict[str, Any] | None = None) -> None:
149
162
  self._cache = {}
150
163
  if gvars:
@@ -176,10 +189,18 @@ class GVarResolver:
176
189
  if not missing:
177
190
  return results
178
191
  if not self._config.enable_gvar_fetch:
179
- log.warning("GVAR fetch disabled; skipping %s", missing)
192
+ if not self._config.silent_gvar_fetch:
193
+ log.warning("GVAR fetch disabled; skipping %s", missing)
194
+ if self._silent_failure_many(missing):
195
+ for key in missing:
196
+ results[key] = True
180
197
  return results
181
198
  if not self._config.service.token:
182
- log.debug("GVAR fetch skipped for %s: no token configured", missing)
199
+ if not self._config.silent_gvar_fetch:
200
+ log.debug("GVAR fetch skipped for %s: no token configured", missing)
201
+ if self._silent_failure_many(missing):
202
+ for key in missing:
203
+ results[key] = True
183
204
  return results
184
205
 
185
206
  sem = asyncio.Semaphore(self._CONCURRENCY)
@@ -191,8 +212,9 @@ class GVarResolver:
191
212
  try:
192
213
  ensured = await self._fetch_remote(key, client=client, sem=sem)
193
214
  except Exception as exc: # pragma: no cover - defensive
194
- log.error("GVAR fetch failed for %s: %s", key, exc)
195
- ensured = False
215
+ if not self._config.silent_gvar_fetch:
216
+ log.error("GVAR fetch failed for %s: %s", key, exc)
217
+ ensured = self._silent_failure(key)
196
218
  results[key] = ensured
197
219
 
198
220
  async with httpx.AsyncClient(timeout=5) as client:
@@ -205,11 +227,13 @@ class GVarResolver:
205
227
  log.debug("GVAR ensure_blocking cache hit for %s", key)
206
228
  return True
207
229
  if not self._config.enable_gvar_fetch:
208
- log.warning("GVAR fetch disabled; skipping %s", key)
209
- return False
230
+ if not self._config.silent_gvar_fetch:
231
+ log.warning("GVAR fetch disabled; skipping %s", key)
232
+ return self._silent_failure(key)
210
233
  if not self._config.service.token:
211
- log.debug("GVAR fetch skipped for %s: no token configured", key)
212
- return False
234
+ if not self._config.silent_gvar_fetch:
235
+ log.debug("GVAR fetch skipped for %s: no token configured", key)
236
+ return self._silent_failure(key)
213
237
 
214
238
  base_url = self._config.service.base_url.rstrip("/")
215
239
  url = f"{base_url}/customizations/gvars/{key}"
@@ -219,17 +243,19 @@ class GVarResolver:
219
243
  with httpx.Client(timeout=5) as client:
220
244
  resp = client.get(url, headers=headers)
221
245
  except Exception as exc:
222
- log.error("GVAR blocking fetch failed for %s: %s", key, exc)
223
- return False
246
+ if not self._config.silent_gvar_fetch:
247
+ log.error("GVAR blocking fetch failed for %s: %s", key, exc)
248
+ return self._silent_failure(key)
224
249
 
225
250
  if resp.status_code != 200:
226
- log.warning(
227
- "GVAR blocking fetch returned %s for %s (body: %s)",
228
- resp.status_code,
229
- key,
230
- (resp.text or "").strip(),
231
- )
232
- return False
251
+ if not self._config.silent_gvar_fetch:
252
+ log.warning(
253
+ "GVAR blocking fetch returned %s for %s (body: %s)",
254
+ resp.status_code,
255
+ key,
256
+ (resp.text or "").strip(),
257
+ )
258
+ return self._silent_failure(key)
233
259
 
234
260
  value: Any = None
235
261
  try:
@@ -241,8 +267,9 @@ class GVarResolver:
241
267
  value = payload["value"]
242
268
 
243
269
  if value is None:
244
- log.error("GVAR %s payload missing value", key)
245
- return False
270
+ if not self._config.silent_gvar_fetch:
271
+ log.error("GVAR %s payload missing value", key)
272
+ return self._silent_failure(key)
246
273
  self._cache[key] = value
247
274
  return True
248
275
 
@@ -262,9 +289,9 @@ class GVarResolver:
262
289
  if key in self._cache:
263
290
  return True
264
291
  if not self._config.enable_gvar_fetch:
265
- return False
292
+ return self._silent_failure(key)
266
293
  if not self._config.service.token:
267
- return False
294
+ return self._silent_failure(key)
268
295
 
269
296
  base_url = self._config.service.base_url.rstrip("/")
270
297
  url = f"{base_url}/customizations/gvars/{key}"
@@ -286,21 +313,23 @@ class GVarResolver:
286
313
  log.debug("GVAR fetching %s from %s", key, url)
287
314
  resp = await _do_request(session)
288
315
  except Exception as exc:
289
- log.error("GVAR fetch failed for %s: %s", key, exc)
316
+ if not self._config.silent_gvar_fetch:
317
+ log.error("GVAR fetch failed for %s: %s", key, exc)
290
318
  if close_client:
291
319
  await session.aclose()
292
- return False
320
+ return self._silent_failure(key)
293
321
  if close_client:
294
322
  await session.aclose()
295
323
 
296
324
  if resp.status_code != 200:
297
- log.warning(
298
- "GVAR fetch returned %s for %s (body: %s)",
299
- resp.status_code,
300
- key,
301
- (resp.text or "").strip(),
302
- )
303
- return False
325
+ if not self._config.silent_gvar_fetch:
326
+ log.warning(
327
+ "GVAR fetch returned %s for %s (body: %s)",
328
+ resp.status_code,
329
+ key,
330
+ (resp.text or "").strip(),
331
+ )
332
+ return self._silent_failure(key)
304
333
 
305
334
  value: Any = None
306
335
  try:
@@ -314,8 +343,9 @@ class GVarResolver:
314
343
  log.debug("GVAR fetch parsed value for %s (type=%s)", key, type(value).__name__)
315
344
 
316
345
  if value is None:
317
- log.error("GVAR %s payload missing value", key)
318
- return False
346
+ if not self._config.silent_gvar_fetch:
347
+ log.error("GVAR %s payload missing value", key)
348
+ return self._silent_failure(key)
319
349
  self._cache[key] = value
320
350
  return True
321
351
 
avrae_ls/diagnostics.py CHANGED
@@ -10,11 +10,14 @@ from lsprotocol import types
10
10
 
11
11
  from .alias_preview import simulate_command
12
12
  from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
13
- from .argument_parsing import apply_argument_parsing
14
- from .completions import _infer_type_map, _resolve_type_name, _type_meta
13
+ from .source_context import build_source_context
14
+ from .type_inference import infer_type_map, resolve_type_name
15
+ from .type_system import type_meta
15
16
  from .config import DiagnosticSettings
16
17
  from .context import ContextData, GVarResolver
17
- from .parser import find_draconic_blocks
18
+ from .parser import wrap_draconic
19
+ from .lsp_utils import range_from_positions, shift_range
20
+ from .ast_utils import collect_target_names
18
21
  from .runtime import MockExecutor, _default_builtins
19
22
 
20
23
  log = logging.getLogger(__name__)
@@ -37,17 +40,19 @@ class DiagnosticProvider:
37
40
  source: str,
38
41
  ctx_data: ContextData,
39
42
  gvar_resolver: GVarResolver,
43
+ *,
44
+ treat_as_module: bool = False,
40
45
  ) -> List[types.Diagnostic]:
41
46
  diagnostics: list[types.Diagnostic] = []
42
47
 
43
- source = apply_argument_parsing(source)
44
- blocks = find_draconic_blocks(source)
48
+ source_ctx = build_source_context(source, treat_as_module)
49
+ blocks = source_ctx.blocks
45
50
  if not blocks:
46
- plain = _plain_command_diagnostics(source)
51
+ plain = _plain_command_diagnostics(source_ctx.prepared)
47
52
  if plain is not None:
48
53
  diagnostics.extend(plain)
49
54
  return diagnostics
50
- diagnostics.extend(await self._analyze_code(source, ctx_data, gvar_resolver))
55
+ diagnostics.extend(await self._analyze_code(source_ctx.prepared, ctx_data, gvar_resolver))
51
56
  return diagnostics
52
57
 
53
58
  for block in blocks:
@@ -67,7 +72,7 @@ class DiagnosticProvider:
67
72
  try:
68
73
  body = parser.parse(code)
69
74
  except draconic.DraconicSyntaxError as exc:
70
- wrapped, added = _wrap_draconic(code)
75
+ wrapped, added = wrap_draconic(code)
71
76
  try:
72
77
  body = parser.parse(wrapped)
73
78
  line_shift = -added
@@ -252,11 +257,12 @@ class DiagnosticProvider:
252
257
 
253
258
 
254
259
  def _syntax_diagnostic(exc: draconic.DraconicSyntaxError) -> types.Diagnostic:
255
- rng = _range_from_positions(
260
+ rng = range_from_positions(
256
261
  exc.lineno,
257
262
  exc.offset,
258
263
  exc.end_lineno,
259
264
  exc.end_offset,
265
+ one_based=True,
260
266
  )
261
267
  return types.Diagnostic(
262
268
  message=exc.msg,
@@ -268,7 +274,13 @@ def _syntax_diagnostic(exc: draconic.DraconicSyntaxError) -> types.Diagnostic:
268
274
 
269
275
  def _syntax_from_std(exc: SyntaxError) -> types.Diagnostic:
270
276
  lineno, offset = exc.lineno, exc.offset
271
- rng = _range_from_positions(lineno, offset, getattr(exc, "end_lineno", None), getattr(exc, "end_offset", None))
277
+ rng = range_from_positions(
278
+ lineno,
279
+ offset,
280
+ getattr(exc, "end_lineno", None),
281
+ getattr(exc, "end_offset", None),
282
+ one_based=True,
283
+ )
272
284
  return types.Diagnostic(
273
285
  message=exc.msg,
274
286
  range=rng,
@@ -278,16 +290,7 @@ def _syntax_from_std(exc: SyntaxError) -> types.Diagnostic:
278
290
 
279
291
 
280
292
  def _names_in_target(target: ast.AST) -> Set[str]:
281
- names: set[str] = set()
282
- if isinstance(target, ast.Name):
283
- names.add(target.id)
284
- elif isinstance(target, ast.Tuple):
285
- for elt in target.elts:
286
- names.update(_names_in_target(elt))
287
- elif isinstance(target, ast.List):
288
- for elt in target.elts:
289
- names.update(_names_in_target(elt))
290
- return names
293
+ return set(collect_target_names([target]))
291
294
 
292
295
 
293
296
  async def _check_gvars(
@@ -437,7 +440,7 @@ def _property_call_diagnostics(
437
440
  base_type = _resolve_expr_type(node.func.value, type_map, code)
438
441
  if not base_type:
439
442
  return []
440
- meta = _type_meta(base_type)
443
+ meta = type_meta(base_type)
441
444
  attr = node.func.attr
442
445
  if attr in meta.methods or attr not in meta.attrs:
443
446
  return []
@@ -484,7 +487,7 @@ def _iterable_attr_diagnostics(
484
487
  base_type = _resolve_expr_type(node.value, type_map, code)
485
488
  if not base_type:
486
489
  return []
487
- meta = _type_meta(base_type)
490
+ meta = type_meta(base_type)
488
491
  attr_meta = meta.attrs.get(node.attr)
489
492
  if not attr_meta:
490
493
  return []
@@ -510,18 +513,18 @@ def _iterable_attr_diagnostics(
510
513
 
511
514
 
512
515
  def _diagnostic_type_map(code: str) -> Dict[str, str]:
513
- mapping = _infer_type_map(code)
516
+ mapping = infer_type_map(code)
514
517
  if mapping:
515
518
  return mapping
516
- wrapped, _ = _wrap_draconic(code)
517
- return _infer_type_map(wrapped)
519
+ wrapped, _ = wrap_draconic(code)
520
+ return infer_type_map(wrapped)
518
521
 
519
522
 
520
523
  def _resolve_expr_type(expr: ast.AST, type_map: Dict[str, str], code: str) -> str:
521
524
  expr_text = _expr_to_str(expr)
522
525
  if not expr_text:
523
526
  return ""
524
- return _resolve_type_name(expr_text, code, type_map)
527
+ return resolve_type_name(expr_text, code, type_map)
525
528
 
526
529
 
527
530
  def _expr_to_str(expr: ast.AST) -> str:
@@ -584,11 +587,12 @@ def _make_diagnostic(
584
587
  ) -> types.Diagnostic:
585
588
  severity = SEVERITY.get(level, types.DiagnosticSeverity.Warning)
586
589
  if hasattr(node, "lineno"):
587
- rng = _range_from_positions(
590
+ rng = range_from_positions(
588
591
  getattr(node, "lineno", 1),
589
592
  getattr(node, "col_offset", 0) + 1,
590
593
  getattr(node, "end_lineno", None),
591
594
  getattr(node, "end_col_offset", None),
595
+ one_based=True,
592
596
  )
593
597
  else:
594
598
  rng = types.Range(
@@ -611,7 +615,7 @@ def _shift_diagnostics(diags: List[types.Diagnostic], line_offset: int, char_off
611
615
  shifted.append(
612
616
  types.Diagnostic(
613
617
  message=diag.message,
614
- range=_shift_range(diag.range, line_offset, char_offset),
618
+ range=shift_range(diag.range, line_offset, char_offset),
615
619
  severity=diag.severity,
616
620
  source=diag.source,
617
621
  code=diag.code,
@@ -624,20 +628,6 @@ def _shift_diagnostics(diags: List[types.Diagnostic], line_offset: int, char_off
624
628
  return shifted
625
629
 
626
630
 
627
- def _shift_range(rng: types.Range, line_offset: int, char_offset: int) -> types.Range:
628
- def _shift_pos(pos: types.Position) -> types.Position:
629
- return types.Position(
630
- line=max(pos.line + line_offset, 0),
631
- character=max(pos.character + (char_offset if pos.line == 0 else 0), 0),
632
- )
633
-
634
- return types.Range(start=_shift_pos(rng.start), end=_shift_pos(rng.end))
635
-
636
-
637
- def _wrap_draconic(code: str) -> tuple[str, int]:
638
- indented = "\n".join(f" {line}" for line in code.splitlines())
639
- wrapped = f"def __alias_main__():\n{indented}\n__alias_main__()"
640
- return wrapped, 1
641
631
 
642
632
 
643
633
  def _build_builtin_signatures() -> dict[str, inspect.Signature]:
@@ -788,23 +778,6 @@ def _check_imports(body: Sequence[ast.AST], severity_level: str) -> List[types.D
788
778
  return diagnostics
789
779
 
790
780
 
791
- def _range_from_positions(
792
- lineno: int | None,
793
- col_offset: int | None,
794
- end_lineno: int | None,
795
- end_col_offset: int | None,
796
- ) -> types.Range:
797
- start = types.Position(
798
- line=max((lineno or 1) - 1, 0),
799
- character=max((col_offset or 1) - 1, 0),
800
- )
801
- end = types.Position(
802
- line=max(((end_lineno or lineno or 1) - 1), 0),
803
- character=max(((end_col_offset or col_offset or 1) - 1), 0),
804
- )
805
- return types.Range(start=start, end=end)
806
-
807
-
808
781
  def _plain_command_diagnostics(source: str) -> list[types.Diagnostic] | None:
809
782
  """Handle simple commands (embed/echo) without draconic blocks."""
810
783
  simulated = simulate_command(source)
@@ -815,7 +788,7 @@ def _plain_command_diagnostics(source: str) -> list[types.Diagnostic] | None:
815
788
  return [
816
789
  types.Diagnostic(
817
790
  message=simulated.validation_error,
818
- range=_range_from_positions(1, 1, 1, 1),
791
+ range=range_from_positions(1, 1, 1, 1, one_based=True),
819
792
  severity=SEVERITY["warning"],
820
793
  source="avrae-ls",
821
794
  )
avrae_ls/lsp_utils.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from lsprotocol import types
4
+
5
+
6
+ def range_from_positions(
7
+ lineno: int | None,
8
+ col_offset: int | None,
9
+ end_lineno: int | None,
10
+ end_col_offset: int | None,
11
+ *,
12
+ one_based: bool = False,
13
+ ensure_nonempty: bool = False,
14
+ ) -> types.Range:
15
+ start_line = max((lineno or 1) - 1, 0)
16
+ if one_based:
17
+ start_char = max((col_offset or 1) - 1, 0)
18
+ else:
19
+ start_char = max(col_offset or 0, 0)
20
+ end_line = max(((end_lineno or lineno or 1) - 1), 0)
21
+ if one_based:
22
+ end_char = max(((end_col_offset or col_offset or 1) - 1), 0)
23
+ else:
24
+ raw_end_char = end_col_offset if end_col_offset is not None else col_offset
25
+ end_char = max(raw_end_char if raw_end_char is not None else start_char, start_char)
26
+ if ensure_nonempty and end_char <= start_char:
27
+ end_char = start_char + 1
28
+ return types.Range(
29
+ start=types.Position(line=start_line, character=start_char),
30
+ end=types.Position(line=end_line, character=end_char),
31
+ )
32
+
33
+
34
+ def shift_range(rng: types.Range, line_offset: int, char_offset: int) -> types.Range:
35
+ def _shift_pos(pos: types.Position) -> types.Position:
36
+ return types.Position(
37
+ line=max(pos.line + line_offset, 0),
38
+ character=max(pos.character + (char_offset if pos.line == 0 else 0), 0),
39
+ )
40
+
41
+ return types.Range(start=_shift_pos(rng.start), end=_shift_pos(rng.end))
avrae_ls/parser.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from dataclasses import dataclass
5
+ from pathlib import Path
5
6
  from typing import List
6
7
 
7
8
 
@@ -17,9 +18,29 @@ class DraconicBlock:
17
18
  DRACONIC_RE = re.compile(r"<drac2>([\s\S]*?)</drac2>", re.IGNORECASE)
18
19
  INLINE_DRACONIC_RE = re.compile(r"\{\{([\s\S]*?)\}\}", re.DOTALL)
19
20
  INLINE_ROLL_RE = re.compile(r"(?<!\{)\{(?!\{)([\s\S]*?)(?<!\})\}(?!\})", re.DOTALL)
21
+ ALIAS_MODULE_SUFFIX = ".alias-module"
20
22
 
21
23
 
22
- def find_draconic_blocks(source: str) -> List[DraconicBlock]:
24
+ def is_alias_module_path(path: str | Path | None) -> bool:
25
+ if path is None:
26
+ return False
27
+ return str(path).endswith(ALIAS_MODULE_SUFFIX)
28
+
29
+
30
+ def _full_source_block(source: str) -> DraconicBlock:
31
+ line_count = source.count("\n") + 1 if source else 1
32
+ return DraconicBlock(
33
+ code=source,
34
+ line_offset=0,
35
+ char_offset=0,
36
+ line_count=line_count,
37
+ inline=False,
38
+ )
39
+
40
+
41
+ def find_draconic_blocks(source: str, *, treat_as_module: bool = False) -> List[DraconicBlock]:
42
+ if treat_as_module:
43
+ return [_full_source_block(source)]
23
44
  matches: list[tuple[int, DraconicBlock]] = []
24
45
 
25
46
  def _block_from_match(match: re.Match[str], inline: bool = False) -> tuple[int, int, DraconicBlock]:
@@ -60,9 +81,15 @@ def find_draconic_blocks(source: str) -> List[DraconicBlock]:
60
81
  return blocks
61
82
 
62
83
 
63
- def primary_block_or_source(source: str) -> tuple[str, int, int]:
64
- blocks = find_draconic_blocks(source)
84
+ def primary_block_or_source(source: str, *, treat_as_module: bool = False) -> tuple[str, int, int]:
85
+ blocks = find_draconic_blocks(source, treat_as_module=treat_as_module)
65
86
  if not blocks:
66
87
  return source, 0, 0
67
88
  block = blocks[0]
68
89
  return block.code, block.line_offset, block.char_offset
90
+
91
+
92
+ def wrap_draconic(code: str) -> tuple[str, int]:
93
+ indented = "\n".join(f" {line}" for line in code.splitlines())
94
+ wrapped = f"def __alias_main__():\n{indented}\n__alias_main__()"
95
+ return wrapped, 1