avrae-ls 0.6.3__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/__main__.py CHANGED
@@ -42,6 +42,13 @@ def main(argv: list[str] | None = None) -> None:
42
42
  const=".",
43
43
  help="Run alias tests in PATH (defaults to current directory)",
44
44
  )
45
+ parser.add_argument(
46
+ "--silent-gvar-fetch",
47
+ action="store_true",
48
+ help="Silently ignore gvar fetch failures and treat them as None",
49
+ )
50
+ parser.add_argument("--token", help="Avrae API token (overrides config)")
51
+ parser.add_argument("--base-url", help="Avrae API base URL (overrides config)")
45
52
  parser.add_argument("--version", action="store_true", help="Print version and exit")
46
53
  args = parser.parse_args(argv)
47
54
 
@@ -56,12 +63,26 @@ def main(argv: list[str] | None = None) -> None:
56
63
  parser.error("--run-tests cannot be combined with --tcp")
57
64
  if args.analyze:
58
65
  parser.error("--run-tests cannot be combined with --analyze")
59
- sys.exit(_run_alias_tests(Path(args.run_tests)))
66
+ sys.exit(
67
+ _run_alias_tests(
68
+ Path(args.run_tests),
69
+ token_override=args.token,
70
+ base_url_override=args.base_url,
71
+ silent_gvar_fetch=args.silent_gvar_fetch,
72
+ )
73
+ )
60
74
 
61
75
  if args.analyze:
62
76
  if args.tcp:
63
77
  parser.error("--analyze cannot be combined with --tcp")
64
- sys.exit(_run_analysis(Path(args.analyze)))
78
+ sys.exit(
79
+ _run_analysis(
80
+ Path(args.analyze),
81
+ token_override=args.token,
82
+ base_url_override=args.base_url,
83
+ silent_gvar_fetch=args.silent_gvar_fetch,
84
+ )
85
+ )
65
86
 
66
87
  server = create_server()
67
88
  if args.tcp:
@@ -80,7 +101,13 @@ def _configure_logging(level: str) -> None:
80
101
  )
81
102
 
82
103
 
83
- def _run_analysis(path: Path) -> int:
104
+ def _run_analysis(
105
+ path: Path,
106
+ *,
107
+ token_override: str | None = None,
108
+ base_url_override: str | None = None,
109
+ silent_gvar_fetch: bool = False,
110
+ ) -> int:
84
111
  if not path.exists():
85
112
  print(f"File not found: {path}", file=sys.stderr)
86
113
  return 2
@@ -90,6 +117,12 @@ def _run_analysis(path: Path) -> int:
90
117
  log.info("Analyzing %s (workspace root: %s)", path, workspace_root)
91
118
 
92
119
  config, warnings = load_config(workspace_root, default_enable_gvar_fetch=True)
120
+ if token_override:
121
+ config.service.token = token_override
122
+ if base_url_override:
123
+ config.service.base_url = base_url_override
124
+ if silent_gvar_fetch:
125
+ config.silent_gvar_fetch = True
93
126
  for warning in warnings:
94
127
  log.warning(warning)
95
128
 
@@ -104,7 +137,13 @@ def _run_analysis(path: Path) -> int:
104
137
  return 1 if results else 0
105
138
 
106
139
 
107
- def _run_alias_tests(target: Path) -> int:
140
+ def _run_alias_tests(
141
+ target: Path,
142
+ *,
143
+ token_override: str | None = None,
144
+ base_url_override: str | None = None,
145
+ silent_gvar_fetch: bool = False,
146
+ ) -> int:
108
147
  if not target.exists():
109
148
  print(f"Test path not found: {target}", file=sys.stderr)
110
149
  return 2
@@ -114,6 +153,12 @@ def _run_alias_tests(target: Path) -> int:
114
153
  log.info("Running alias tests in %s (workspace root: %s)", target, workspace_root)
115
154
 
116
155
  config, warnings = load_config(workspace_root, default_enable_gvar_fetch=True)
156
+ if token_override:
157
+ config.service.token = token_override
158
+ if base_url_override:
159
+ config.service.base_url = base_url_override
160
+ if silent_gvar_fetch:
161
+ config.silent_gvar_fetch = True
117
162
  for warning in warnings:
118
163
  log.warning(warning)
119
164
 
@@ -157,7 +202,11 @@ def _print_test_results(results: Iterable[AliasTestResult], workspace_root: Path
157
202
  passed += 1
158
203
  continue
159
204
  if res.error:
160
- print(f" Error: {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}")
208
+ else:
209
+ print(f" Error: {res.error}")
161
210
  if res.details:
162
211
  print(f" {res.details}")
163
212
  expected_val, actual_val = _summarize_mismatch(res.case.expected, res.actual)
avrae_ls/alias_preview.py CHANGED
@@ -17,6 +17,7 @@ class RenderedAlias:
17
17
  stdout: str
18
18
  error: Optional[BaseException]
19
19
  last_value: Any | None = None
20
+ error_line: int | None = None
20
21
 
21
22
 
22
23
  @dataclass
@@ -58,7 +59,7 @@ class SimulatedCommand:
58
59
  embed: EmbedPreview | None = None
59
60
 
60
61
 
61
- def _strip_alias_header(text: str) -> str:
62
+ def _strip_alias_header_with_offset(text: str) -> tuple[str, int]:
62
63
  lines = text.splitlines()
63
64
  if lines and lines[0].lstrip().startswith("!alias"):
64
65
  first = lines[0].lstrip()
@@ -66,9 +67,32 @@ def _strip_alias_header(text: str) -> str:
66
67
  remainder = parts[2] if len(parts) > 2 else ""
67
68
  body = "\n".join(lines[1:])
68
69
  if remainder:
69
- return remainder + ("\n" + body if body else "")
70
- return body
71
- return text
70
+ return remainder + ("\n" + body if body else ""), 0
71
+ return body, 1
72
+ return text, 0
73
+
74
+
75
+ def _strip_alias_header(text: str) -> str:
76
+ body, _ = _strip_alias_header_with_offset(text)
77
+ return body
78
+
79
+
80
+ def _line_index_for_offset(text: str, offset: int) -> int:
81
+ return text.count("\n", 0, offset)
82
+
83
+
84
+ def _error_line_for_match(
85
+ body: str, match: re.Match[str], error: BaseException, line_offset: int
86
+ ) -> int | None:
87
+ 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)
91
+ else:
92
+ code = match.group(1)
93
+ leading_newlines = len(code) - len(code.lstrip("\n"))
94
+ line_index = base_line + leading_newlines
95
+ return line_index + line_offset + 1
72
96
 
73
97
 
74
98
  async def render_alias_command(
@@ -79,12 +103,13 @@ async def render_alias_command(
79
103
  args: list[str] | None = None,
80
104
  ) -> RenderedAlias:
81
105
  """Replace <drac2> blocks with their evaluated values and return final command."""
82
- body = _strip_alias_header(text)
106
+ body, line_offset = _strip_alias_header_with_offset(text)
83
107
  body = apply_argument_parsing(body, args)
84
108
  stdout_parts: list[str] = []
85
109
  parts: list[str] = []
86
110
  last_value = None
87
111
  error: BaseException | None = None
112
+ error_line: int | None = None
88
113
 
89
114
  pos = 0
90
115
  matches: list[tuple[str, re.Match[str]]] = []
@@ -109,6 +134,7 @@ async def render_alias_command(
109
134
  stdout_parts.append(result.stdout)
110
135
  if result.error:
111
136
  error = result.error
137
+ error_line = _error_line_for_match(body, match, result.error, line_offset)
112
138
  break
113
139
  last_value = result.value
114
140
  if result.value is not None:
@@ -124,7 +150,13 @@ async def render_alias_command(
124
150
  parts.append(body[pos:])
125
151
 
126
152
  final_command = "".join(parts)
127
- return RenderedAlias(command=final_command, stdout="".join(stdout_parts), error=error, last_value=last_value)
153
+ return RenderedAlias(
154
+ command=final_command,
155
+ stdout="".join(stdout_parts),
156
+ error=error,
157
+ last_value=last_value,
158
+ error_line=error_line,
159
+ )
128
160
 
129
161
 
130
162
  def validate_embed_payload(payload: str) -> Tuple[bool, str | None]:
avrae_ls/alias_tests.py CHANGED
@@ -42,6 +42,7 @@ class AliasTestResult:
42
42
  embed: dict[str, Any] | None = None
43
43
  error: str | None = None
44
44
  details: str | None = None
45
+ error_line: int | None = None
45
46
 
46
47
 
47
48
  def discover_test_files(
@@ -159,6 +160,7 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
159
160
  actual=None,
160
161
  stdout=rendered.stdout,
161
162
  error=str(rendered.error),
163
+ error_line=rendered.error_line,
162
164
  )
163
165
 
164
166
  preview = simulate_command(rendered.command)
avrae_ls/ast_utils.py ADDED
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import Iterable, List
5
+
6
+
7
+ def collect_target_names(targets: Iterable[ast.AST]) -> List[str]:
8
+ names: list[str] = []
9
+ for target in targets:
10
+ if isinstance(target, ast.Name):
11
+ names.append(target.id)
12
+ elif isinstance(target, (ast.Tuple, ast.List)):
13
+ names.extend(collect_target_names(target.elts))
14
+ return names
avrae_ls/code_actions.py CHANGED
@@ -10,7 +10,8 @@ from typing import Iterable, List, Sequence
10
10
  from lsprotocol import types
11
11
 
12
12
  from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
13
- from .parser import DraconicBlock, find_draconic_blocks
13
+ from .parser import DraconicBlock
14
+ from .source_context import build_source_context
14
15
 
15
16
  log = logging.getLogger(__name__)
16
17
 
@@ -42,10 +43,13 @@ def code_actions_for_document(
42
43
  source: str,
43
44
  params: types.CodeActionParams,
44
45
  workspace_root: Path,
46
+ *,
47
+ treat_as_module: bool = False,
45
48
  ) -> List[types.CodeAction]:
46
49
  """Collect code actions for a document without requiring a running server."""
47
50
  actions: list[types.CodeAction] = []
48
- blocks = find_draconic_blocks(source)
51
+ source_ctx = build_source_context(source, treat_as_module, apply_args=False)
52
+ blocks = source_ctx.blocks
49
53
  snippets = _load_snippets(workspace_root)
50
54
  only_kinds = list(params.context.only or [])
51
55