avrae-ls 0.7.0__py3-none-any.whl → 0.8.1__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/__main__.py CHANGED
@@ -202,11 +202,17 @@ def _print_test_results(results: Iterable[AliasTestResult], workspace_root: Path
202
202
  passed += 1
203
203
  continue
204
204
  if res.error:
205
- if res.error_line is not None:
206
- alias_name = res.case.alias_path.name
207
- print(f" Error (line {res.error_line} {alias_name}): {res.error}")
205
+ if res.error_line is not None and res.error_col is not None:
206
+ label = f" Execution Error (line {res.error_line} col {res.error_col})"
207
+ elif res.error_line is not None:
208
+ label = f" Execution Error (line {res.error_line})"
208
209
  else:
209
- print(f" Error: {res.error}")
210
+ label = " Execution Error"
211
+ print(_colorize_error_line(label))
212
+ print(f" {res.error}")
213
+ if res.stdout:
214
+ print(f" Stdout: {res.stdout.strip()}")
215
+ continue
210
216
  if res.details:
211
217
  print(f" {res.details}")
212
218
  expected_val, actual_val = _summarize_mismatch(res.case.expected, res.actual)
@@ -262,6 +268,12 @@ def _colorize_diff_line(line: str) -> str:
262
268
  return line
263
269
 
264
270
 
271
+ def _colorize_error_line(line: str) -> str:
272
+ if not sys.stdout.isatty():
273
+ return line
274
+ return f"\x1b[31m{line}\x1b[0m"
275
+
276
+
265
277
  def _print_labeled_value(label: str, value: str) -> None:
266
278
  lines = value.splitlines() or [""]
267
279
  if len(lines) == 1:
avrae_ls/alias_preview.py CHANGED
@@ -18,6 +18,7 @@ class RenderedAlias:
18
18
  error: Optional[BaseException]
19
19
  last_value: Any | None = None
20
20
  error_line: int | None = None
21
+ error_col: int | None = None
21
22
 
22
23
 
23
24
  @dataclass
@@ -81,18 +82,43 @@ def _line_index_for_offset(text: str, offset: int) -> int:
81
82
  return text.count("\n", 0, offset)
82
83
 
83
84
 
84
- def _error_line_for_match(
85
+ def _error_position_for_match(
85
86
  body: str, match: re.Match[str], error: BaseException, line_offset: int
86
- ) -> int | None:
87
+ ) -> tuple[int | None, int | None]:
87
88
  base_line = _line_index_for_offset(body, match.start(1))
88
- lineno = getattr(error, "lineno", None)
89
- if isinstance(lineno, int) and lineno > 0 and getattr(error, "text", None) is not None:
90
- line_index = base_line + (lineno - 1)
89
+ base_line_start = body.rfind("\n", 0, match.start(1))
90
+ base_col = match.start(1) - (base_line_start + 1 if base_line_start != -1 else 0)
91
+ line_in_code: int | None = None
92
+ col_in_code: int | None = None
93
+ node = getattr(error, "node", None)
94
+ if node is not None:
95
+ node_line = getattr(node, "lineno", None)
96
+ if isinstance(node_line, int) and node_line > 0:
97
+ line_in_code = node_line
98
+ node_col = getattr(node, "col_offset", None)
99
+ if isinstance(node_col, int) and node_col >= 0:
100
+ col_in_code = node_col
101
+ if line_in_code is None:
102
+ lineno = getattr(error, "lineno", None)
103
+ if isinstance(lineno, int) and lineno > 0:
104
+ line_in_code = lineno
105
+ offset = getattr(error, "offset", None)
106
+ if isinstance(offset, int) and offset > 0:
107
+ col_in_code = offset - 1
108
+ if line_in_code is not None:
109
+ line_index = base_line + (line_in_code - 1)
91
110
  else:
92
111
  code = match.group(1)
93
112
  leading_newlines = len(code) - len(code.lstrip("\n"))
94
113
  line_index = base_line + leading_newlines
95
- return line_index + line_offset + 1
114
+ col_in_code = 0 if col_in_code is None else col_in_code
115
+ if col_in_code is None:
116
+ col_in_code = 0
117
+ if line_in_code is None or line_in_code == 1:
118
+ col_index = base_col + col_in_code
119
+ else:
120
+ col_index = col_in_code
121
+ return line_index + line_offset + 1, col_index + 1
96
122
 
97
123
 
98
124
  async def render_alias_command(
@@ -110,6 +136,7 @@ async def render_alias_command(
110
136
  last_value = None
111
137
  error: BaseException | None = None
112
138
  error_line: int | None = None
139
+ error_col: int | None = None
113
140
 
114
141
  pos = 0
115
142
  matches: list[tuple[str, re.Match[str]]] = []
@@ -134,7 +161,7 @@ async def render_alias_command(
134
161
  stdout_parts.append(result.stdout)
135
162
  if result.error:
136
163
  error = result.error
137
- error_line = _error_line_for_match(body, match, result.error, line_offset)
164
+ error_line, error_col = _error_position_for_match(body, match, result.error, line_offset)
138
165
  break
139
166
  last_value = result.value
140
167
  if result.value is not None:
@@ -156,6 +183,7 @@ async def render_alias_command(
156
183
  error=error,
157
184
  last_value=last_value,
158
185
  error_line=error_line,
186
+ error_col=error_col,
159
187
  )
160
188
 
161
189
 
avrae_ls/alias_tests.py CHANGED
@@ -43,6 +43,7 @@ class AliasTestResult:
43
43
  error: str | None = None
44
44
  details: str | None = None
45
45
  error_line: int | None = None
46
+ error_col: int | None = None
46
47
 
47
48
 
48
49
  def discover_test_files(
@@ -161,6 +162,7 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
161
162
  stdout=rendered.stdout,
162
163
  error=str(rendered.error),
163
164
  error_line=rendered.error_line,
165
+ error_col=rendered.error_col,
164
166
  )
165
167
 
166
168
  preview = simulate_command(rendered.command)
@@ -173,7 +175,15 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
173
175
  error=preview.validation_error,
174
176
  )
175
177
 
176
- actual = preview.preview if preview.preview is not None else rendered.last_value
178
+ if preview.preview is not None:
179
+ actual = preview.preview
180
+ else:
181
+ if rendered.command.strip() == "" and rendered.last_value is None:
182
+ actual = None
183
+ elif rendered.last_value is not None and rendered.command.strip() == str(rendered.last_value):
184
+ actual = rendered.last_value
185
+ else:
186
+ actual = rendered.command
177
187
  embed_dict = preview.embed.to_dict() if preview.embed else None
178
188
 
179
189
  if embed_dict is not None and isinstance(case.expected, dict):
avrae_ls/context.py CHANGED
@@ -14,6 +14,7 @@ from .config import AvraeLSConfig, ContextProfile, VarSources
14
14
  from .cvars import derive_character_cvars
15
15
 
16
16
  log = logging.getLogger(__name__)
17
+ _SKIP_GVAR = object()
17
18
 
18
19
 
19
20
  @dataclass
@@ -63,7 +64,7 @@ class ContextBuilder:
63
64
  data = _read_json_file(path)
64
65
  if data is None:
65
66
  continue
66
- merged = merged.merge(VarSources.from_data(data))
67
+ merged = merged.merge(_var_sources_from_file(path, data))
67
68
  return merged
68
69
 
69
70
  def _merge_character_cvars(self, character: Dict[str, Any], vars: VarSources) -> VarSources:
@@ -366,3 +367,49 @@ def _read_json_file(path: Path) -> Dict[str, Any] | None:
366
367
  except json.JSONDecodeError as exc:
367
368
  log.warning("Failed to parse var file %s: %s", path, exc)
368
369
  return None
370
+
371
+
372
+ def _var_sources_from_file(path: Path, data: Dict[str, Any]) -> VarSources:
373
+ parsed = VarSources.from_data(data)
374
+ return VarSources(
375
+ cvars=parsed.cvars,
376
+ uvars=parsed.uvars,
377
+ svars=parsed.svars,
378
+ gvars=_resolve_gvar_file_refs(path, parsed.gvars),
379
+ )
380
+
381
+
382
+ def _resolve_gvar_file_refs(var_file: Path, gvars: Dict[str, Any]) -> Dict[str, Any]:
383
+ resolved: dict[str, Any] = {}
384
+ for key, value in gvars.items():
385
+ parsed = _parse_gvar_value(var_file, key, value)
386
+ if parsed is _SKIP_GVAR:
387
+ continue
388
+ resolved[str(key)] = parsed
389
+ return resolved
390
+
391
+
392
+ def _parse_gvar_value(var_file: Path, key: Any, value: Any) -> Any:
393
+ if not isinstance(value, dict):
394
+ return value
395
+
396
+ file_path = value.get("filePath")
397
+ if file_path is None:
398
+ file_path = value.get("path")
399
+ if file_path is None:
400
+ return value
401
+ if not isinstance(file_path, str) or not file_path.strip():
402
+ log.warning("Invalid gvar file path for '%s' in %s; expected a non-empty string.", key, var_file)
403
+ return _SKIP_GVAR
404
+
405
+ gvar_path = Path(file_path)
406
+ if not gvar_path.is_absolute():
407
+ gvar_path = var_file.parent / gvar_path
408
+ try:
409
+ return gvar_path.read_text()
410
+ except FileNotFoundError:
411
+ log.warning("Gvar content file not found for '%s': %s", key, gvar_path)
412
+ return _SKIP_GVAR
413
+ except OSError as exc:
414
+ log.warning("Failed to read gvar content file for '%s' (%s): %s", key, gvar_path, exc)
415
+ return _SKIP_GVAR
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avrae-ls
3
- Version: 0.7.0
3
+ Version: 0.8.1
4
4
  Summary: Language server for Avrae draconic aliases
5
5
  Author: 1drturtle
6
6
  License: MIT License
@@ -40,6 +40,8 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  # Avrae Draconic Alias Language Server
42
42
 
43
+ [![Tests](https://github.com/1drturtle/avrae-ls/actions/workflows/ci.yml/badge.svg)](https://github.com/1drturtle/avrae-ls/actions/workflows/ci.yml)
44
+
43
45
  Language Server Protocol (LSP) implementation targeting Avrae-style draconic aliases. It provides syntax/semantic diagnostics, a mocked execution command, and a thin configuration layer driven by a workspace `.avraels.json` file. Credit to Avrae team for all code yoinked!
44
46
 
45
47
  ## Install (released package)
@@ -105,12 +107,12 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
105
107
 
106
108
  - Mock execution never writes back to Avrae: cvar/uvar/gvar mutations only live for the current run and reset before the next.
107
109
  - Network is limited to gvar fetches (when `enableGvarFetch` is true) and `verify_signature`; other Avrae/Discord calls are replaced with mocked context data from `.avraels.json`.
108
- - `get_gvar`/`using` values are pulled from local var files first; remote fetches go to `https://api.avrae.io/customizations/gvars/<id>` (or your `avraeService.baseUrl`) using `avraeService.token` and are cached for the session.
110
+ - `get_gvar`/`using` values are pulled from local var files first; remote fetches go to `https://api.avrae.io/customizations/gvars/<id>` (or your `avraeService.baseUrl`) using `avraeService.token` and are cached for the session. In var files, a gvar can be a direct value or a `{ "filePath": "relative/or/absolute/path" }` object (also supports `"path"`) that loads file contents as the gvar value.
109
111
  - `signature()` returns a mock string (`mock-signature:<int>`). `verify_signature()` POSTs to `/bot/signature/verify`, reuses the last successful response per signature, and includes `avraeService.token` if present.
110
112
 
111
113
  ## Troubleshooting gvar fetch / verify_signature
112
114
 
113
- - `get_gvar` returns `None` or `using(...)` raises `ModuleNotFoundError`: ensure the workspace `.avraels.json` sets `enableGvarFetch: true`, includes a valid `avraeService.token`, or seed the gvar in a var file referenced by `varFiles`.
115
+ - `get_gvar` returns `None` or `using(...)` raises `ModuleNotFoundError`: ensure the workspace `.avraels.json` sets `enableGvarFetch: true`, includes a valid `avraeService.token`, or seed the gvar in a var file referenced by `varFiles` (including `filePath` gvar entries).
114
116
  - HTTP 401/403/404 from fetch/verify calls: check the token (401/403) and the gvar/signature id (404). Override `avraeService.baseUrl` if you mirror the API.
115
117
  - Slow or flaky calls: disable remote fetches by flipping `enableGvarFetch` off to rely purely on local vars.
116
118
 
@@ -1,7 +1,7 @@
1
1
  avrae_ls/__init__.py,sha256=BmjrnksGkbG7TPqwbyQvgYj9uei8pFSFpfkRpaGVdJU,63
2
- avrae_ls/__main__.py,sha256=Ulr1H2dP3-1aBhfbo9f1UesohLazza-W-5-VPXw8h-0,10327
3
- avrae_ls/alias_preview.py,sha256=LxoUuF_OFTNPnFDjbpIKtE_nQKwlDZDcqJrlRzhVGFE,13433
4
- avrae_ls/alias_tests.py,sha256=_Aw-IUN11FojviPRUMzWqQXU8xZu3FAa4MlwgnmARfA,11995
2
+ avrae_ls/__main__.py,sha256=lx_Sncc4uiAPeoLtdspnZIdexgNEZNkD2hh-vJgWt30,10743
3
+ avrae_ls/alias_preview.py,sha256=GAYc-lURawY3cO_ykfGAk_tdnjuy1CtIXnX6gYACftM,14601
4
+ avrae_ls/alias_tests.py,sha256=lhb7pZFd_XYmu2BggVXn7sLNAoxLtT_V4mzy871xV4I,12361
5
5
  avrae_ls/api.py,sha256=7QVJAmqgKkiS22qj39_aSVsDuO_kUkyl2Ho1IsD58yE,65110
6
6
  avrae_ls/argparser.py,sha256=DRptXGyK4f0r7NsuV1Sg4apG-qihIClOX408jgk4HH4,13762
7
7
  avrae_ls/argument_parsing.py,sha256=ezKl65VwuNEDxt6KlYwVQcpy1110UDvf4BqZqgZTcqk,2122
@@ -10,7 +10,7 @@ avrae_ls/code_actions.py,sha256=MLZ5euETh2G4lRO33QkHnIwWPt-XlXMCqy-BlRXz0pk,9562
10
10
  avrae_ls/codes.py,sha256=iPRPQ6i9DZheae4_ra1y29vCw3Y4SEu6Udf5WiZj_RY,136
11
11
  avrae_ls/completions.py,sha256=YJNK85MkFRJAf7IzltdcWnoKv3jMtFEoIsJwt-hnXJM,25062
12
12
  avrae_ls/config.py,sha256=PJakf5JF7dbq1NbSFkoobgjKLzazyyMCyfveoqKzoLU,16810
13
- avrae_ls/context.py,sha256=0IrGNqZH1hJF7o4f8TfJNzpWURtgYB7y_0UvZhWRxeo,13732
13
+ avrae_ls/context.py,sha256=gsSi2ExDVRItzuWoMgRhUf2FqqlkxATFDXgPOF-VcoE,15298
14
14
  avrae_ls/cvars.py,sha256=0tcVbUHx_CKJ6aou3kEsKX37LRWAjkUWlqqIuSRFlXk,3197
15
15
  avrae_ls/diagnostics.py,sha256=7B5rL_zu3UByPD1GrGFK5cr6dOQYXz2b362x9MZgUEk,26990
16
16
  avrae_ls/dice.py,sha256=DY7V7L-EwAXaCgddgVe9xU1s9lVtiw5Zc2reipNgdTk,874
@@ -32,8 +32,8 @@ draconic/string.py,sha256=kGrRc6wNHRq1y5xw8Os-fBhfINDtIY2nBWQWkyLSfQI,2858
32
32
  draconic/types.py,sha256=1Lsr6z8bW5agglGI4hLt_nPtYuZOIf_ueSpPDB4WDrs,13686
33
33
  draconic/utils.py,sha256=D4vJ-txqS2-rlqsEpXAC46_j1sZX4UjY-9zIgElo96k,3122
34
34
  draconic/versions.py,sha256=CUEsgUWjAmjez0432WwiBwZlIzWPIObwZUf8Yld18EE,84
35
- avrae_ls-0.7.0.dist-info/METADATA,sha256=JXBrCBS-f5pC4sPnZm4RIz-_5kgfr6YyqvjuBHJy3KE,7624
36
- avrae_ls-0.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
- avrae_ls-0.7.0.dist-info/entry_points.txt,sha256=OtYXipMQzqmxpMoApgo0MeJYFmMbkbFN51Ibhpb8hF4,52
38
- avrae_ls-0.7.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
39
- avrae_ls-0.7.0.dist-info/RECORD,,
35
+ avrae_ls-0.8.1.dist-info/METADATA,sha256=HmuearSQW6CJEYhxN-qz-MSGvDhh_4Q_KhAVwtd7hiw,7980
36
+ avrae_ls-0.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
+ avrae_ls-0.8.1.dist-info/entry_points.txt,sha256=OtYXipMQzqmxpMoApgo0MeJYFmMbkbFN51Ibhpb8hF4,52
38
+ avrae_ls-0.8.1.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
39
+ avrae_ls-0.8.1.dist-info/RECORD,,