avrae-ls 0.4.1__py3-none-any.whl → 0.5.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/argparser.py DELETED
@@ -1,430 +0,0 @@
1
- import collections
2
- import itertools
3
- import re
4
- import string
5
- from typing import ClassVar, Iterator
6
-
7
-
8
- class BadArgument(Exception):
9
- pass
10
-
11
-
12
- class ExpectedClosingQuoteError(Exception):
13
- def __init__(self, quote):
14
- super().__init__(quote)
15
- self.quote = quote
16
-
17
- def __str__(self):
18
- return f"Expected closing quote {self.quote}"
19
-
20
-
21
- class InvalidArgument(Exception):
22
- pass
23
-
24
-
25
- class StringView:
26
- """Minimal stand-in for disnake.ext.commands.view.StringView used by argparser."""
27
-
28
- def __init__(self, buffer: str):
29
- self.buffer = buffer
30
- self.index = 0
31
-
32
- @property
33
- def current(self):
34
- if self.eof:
35
- return None
36
- return self.buffer[self.index]
37
-
38
- @property
39
- def eof(self) -> bool:
40
- return self.index >= len(self.buffer)
41
-
42
- def skip_ws(self):
43
- while not self.eof and self.buffer[self.index].isspace():
44
- self.index += 1
45
-
46
- def get(self):
47
- if self.eof:
48
- return None
49
- ch = self.buffer[self.index]
50
- self.index += 1
51
- return ch
52
-
53
- def undo(self):
54
- if self.index > 0:
55
- self.index -= 1
56
-
57
-
58
- EPHEMERAL_ARG_RE = re.compile(r"(\S+)(\d+)")
59
- SINGLE_ARG_RE = re.compile(r"([a-zA-Z]\S*(?<!\d))(\d+)?") # g1: flag name g2: ephem?
60
- FLAG_ARG_RE = re.compile(r"-+([a-zA-Z]\S*(?<!\d))(\d+)?") # g1: flag name g2: ephem?
61
- SINGLE_ARG_EXCEPTIONS = {"-i", "-h", "-v"}
62
-
63
-
64
- def argsplit(args: str):
65
- view = CustomStringView(args.strip())
66
- args_out = []
67
- while not view.eof:
68
- view.skip_ws()
69
- word = view.get_quoted_word()
70
- if word is not None:
71
- args_out.append(word)
72
- return args_out
73
-
74
-
75
- # ==== argparse ====
76
- class Argument:
77
- def __init__(self, name: str, value, pos: int):
78
- self.name = name
79
- self.value = value
80
- self.pos = pos
81
-
82
- def __repr__(self):
83
- return f"<{type(self).__name__} name={self.name!r} value={self.value!r} pos={self.pos}>"
84
-
85
- def __eq__(self, other):
86
- return self.name == other.name and self.value == other.value and self.pos == other.pos
87
-
88
-
89
- class EphemeralArgument(Argument):
90
- def __init__(self, name: str, value, pos: int, uses: int):
91
- super().__init__(name, value, pos)
92
- self.uses = uses
93
- self.used = 0
94
-
95
- def has_remaining_uses(self):
96
- return self.used < self.uses
97
-
98
- def __repr__(self):
99
- return (
100
- f"<{type(self).__name__} name={self.name!r} value={self.value!r} pos={self.pos} uses={self.uses} "
101
- f"used={self.used}>"
102
- )
103
-
104
- def __eq__(self, other):
105
- return (
106
- self.name == other.name and self.value == other.value and self.pos == other.pos and self.uses == other.uses
107
- )
108
-
109
-
110
- def _argparse_arg(name: str, ephem: str | None, value, idx: int, parse_ephem: bool) -> Argument:
111
- if ephem and parse_ephem:
112
- return EphemeralArgument(name=name, value=value, pos=idx, uses=int(ephem))
113
- elif ephem:
114
- return Argument(name=name + ephem, value=value, pos=idx)
115
- else:
116
- return Argument(name=name, value=value, pos=idx)
117
-
118
-
119
- def _argparse_iterator(args: list[str], parse_ephem: bool) -> Iterator[Argument]:
120
- flag_arg_state = None # state: name, ephem?
121
- idx = 0
122
- for idx, arg in enumerate(args):
123
- # prio: single arg exceptions, flag args, values, single args
124
- if arg in SINGLE_ARG_EXCEPTIONS:
125
- if flag_arg_state is not None:
126
- name, ephem = flag_arg_state
127
- yield _argparse_arg(name, ephem, True, idx - 1, parse_ephem)
128
- flag_arg_state = None
129
- yield Argument(name=arg.lstrip("-"), value=True, pos=idx)
130
- elif match := FLAG_ARG_RE.fullmatch(arg):
131
- if flag_arg_state is not None:
132
- name, ephem = flag_arg_state
133
- yield _argparse_arg(name, ephem, True, idx - 1, parse_ephem)
134
- flag_arg_state = match.group(1), match.group(2)
135
- elif flag_arg_state is not None:
136
- name, ephem = flag_arg_state
137
- yield _argparse_arg(name, ephem, arg, idx - 1, parse_ephem)
138
- flag_arg_state = None
139
- elif match := SINGLE_ARG_RE.fullmatch(arg):
140
- name = match.group(1)
141
- ephem = match.group(2)
142
- yield _argparse_arg(name, ephem, True, idx, parse_ephem)
143
- # else: the current element at the head is junk
144
-
145
- if flag_arg_state is not None:
146
- name, ephem = flag_arg_state
147
- yield _argparse_arg(name, ephem, True, idx, parse_ephem)
148
-
149
-
150
- # --- main entrypoint ---
151
- def argparse(args, character=None, splitter=argsplit, parse_ephem=True) -> "ParsedArguments":
152
- """
153
- Given an argument string, returns the parsed arguments using the argument nondeterministic finite automaton.
154
- If *character* is given, evaluates {}-style math inside the string before parsing.
155
- If the argument is a string, uses *splitter* to split the string into args.
156
- If *parse_ephem* is False, arguments like ``-d1`` are saved literally rather than as an ephemeral argument.
157
- """
158
- if isinstance(args, str):
159
- args = splitter(args)
160
-
161
- if character:
162
- from aliasing.evaluators import MathEvaluator
163
-
164
- evaluator = MathEvaluator.with_character(character)
165
- args = [evaluator.transformed_str(a) for a in args]
166
-
167
- parsed_args = list(_argparse_iterator(args, parse_ephem))
168
- return ParsedArguments(parsed_args)
169
-
170
-
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
-
185
- def __init__(self, args: list[Argument]):
186
- self._parsed = collections.defaultdict(lambda: [])
187
- for arg in args:
188
- self._parsed[arg.name].append(arg)
189
- self._current_context = None
190
- self._contexts = {} # type: dict[..., ParsedArguments]
191
-
192
- @classmethod
193
- def from_dict(cls, d):
194
- inst = cls([])
195
- for key, value in d.items():
196
- inst[key] = value
197
- return inst
198
-
199
- @classmethod
200
- def empty_args(cls):
201
- return cls([])
202
-
203
- def get(self, arg, default=None, type_=str, ephem=False):
204
- if default is None:
205
- default = []
206
- parsed = list(self._get_values(arg, ephem=ephem))
207
- if not parsed:
208
- return default
209
- try:
210
- return [type_(v) for v in parsed]
211
- except (ValueError, TypeError):
212
- raise InvalidArgument(f"One or more arguments cannot be cast to {type_.__name__} (in `{arg}`)")
213
-
214
- def last(self, arg, default=None, type_=str, ephem=False):
215
- last_arg = self._get_last(arg, ephem=ephem)
216
- if last_arg is None:
217
- return default
218
- try:
219
- return type_(last_arg)
220
- except (ValueError, TypeError):
221
- raise InvalidArgument(f"{last_arg} cannot be cast to {type_.__name__} (in `{arg}`)")
222
-
223
- def adv(self, eadv=False, boolwise=False, ephem=False, custom: dict = None):
224
- adv_str, dis_str, ea_str = "adv", "dis", "eadv"
225
- if custom is not None:
226
- if "adv" in custom:
227
- adv_str = custom["adv"]
228
- if "dis" in custom:
229
- dis_str = custom["dis"]
230
- if "eadv" in custom:
231
- ea_str = custom["eadv"]
232
-
233
- adv_arg = self.last(adv_str, default=False, type_=bool, ephem=ephem)
234
- dis_arg = self.last(dis_str, default=False, type_=bool, ephem=ephem)
235
- ea_arg = eadv and self.last(ea_str, default=False, type_=bool, ephem=ephem)
236
-
237
- if ea_arg and not dis_arg:
238
- out = 2
239
- elif dis_arg and not (adv_arg or ea_arg):
240
- out = -1
241
- elif adv_arg and not dis_arg:
242
- out = 1
243
- else:
244
- out = 0
245
-
246
- if not boolwise:
247
- return out
248
- else:
249
- return {-1: False, 0: None, 1: True}.get(out)
250
-
251
- def join(self, arg, connector: str, default=None, ephem=False):
252
- return connector.join(self.get(arg, ephem=ephem)) or default
253
-
254
- def ignore(self, arg):
255
- del self[arg]
256
- for context in self._contexts.values():
257
- del context[arg]
258
-
259
- def update(self, new):
260
- for k, v in new.items():
261
- self[k] = v
262
-
263
- def update_nx(self, new):
264
- for k, v in new.items():
265
- if k not in self and v is not None:
266
- self[k] = v
267
-
268
- @staticmethod
269
- def _yield_from_iterable(iterable: Iterator[Argument], ephem: bool):
270
- for value in iterable:
271
- if not ephem and isinstance(value, EphemeralArgument):
272
- continue
273
- elif isinstance(value, EphemeralArgument):
274
- if not value.has_remaining_uses():
275
- continue
276
- value.used += 1
277
- yield value.value
278
-
279
- def _get_values(self, arg, ephem=False):
280
- iterable = self._parsed[arg]
281
- if self._current_context in self._contexts:
282
- iterable = itertools.chain(self._parsed[arg], self._contexts[self._current_context]._parsed[arg])
283
- yield from self._yield_from_iterable(iterable, ephem)
284
-
285
- def _get_last(self, arg, ephem=False):
286
- iterable = reversed(self._parsed[arg])
287
- if self._current_context in self._contexts:
288
- iterable = itertools.chain(
289
- reversed(self._contexts[self._current_context]._parsed[arg]), reversed(self._parsed[arg])
290
- )
291
- return next((self._yield_from_iterable(iterable, ephem)), None)
292
-
293
- def set_context(self, context):
294
- self._current_context = context
295
-
296
- def add_context(self, context, args):
297
- if isinstance(args, dict):
298
- if all(
299
- isinstance(k, (collections.UserString, str))
300
- and isinstance(v, (collections.UserList, list))
301
- and all(isinstance(i, (collections.UserString, str)) for i in v)
302
- for k, v in args.items()
303
- ):
304
- args = ParsedArguments.from_dict(args)
305
- else:
306
- raise InvalidArgument(f"Argument is not in the format dict[str, list[str]] (in {args})")
307
- elif not isinstance(args, ParsedArguments):
308
- raise InvalidArgument(f"Argument is not a dict or ParsedArguments (in {args})")
309
-
310
- self._contexts[context] = args
311
-
312
- def __contains__(self, item):
313
- return item in self._parsed and self._parsed[item]
314
-
315
- def __len__(self) -> int:
316
- return len(self._parsed)
317
-
318
- def __setitem__(self, key, value):
319
- if isinstance(value, (collections.UserList, list)):
320
- true_val = [Argument(key, v, idx) for idx, v in enumerate(value)]
321
- else:
322
- true_val = [Argument(key, value, 0)]
323
- self._parsed[key] = true_val
324
- if match := EPHEMERAL_ARG_RE.fullmatch(key):
325
- arg, num = match.group(1), match.group(2)
326
- ephem_val = [EphemeralArgument(arg, v.value, v.pos, int(num)) for v in true_val]
327
- self._parsed[arg] = ephem_val
328
-
329
- def __delitem__(self, arg):
330
- if arg in self._parsed:
331
- del self._parsed[arg]
332
-
333
- def __iter__(self) -> Iterator[str]:
334
- return iter(self._parsed.keys())
335
-
336
- def __repr__(self):
337
- return f"<ParsedArguments parsed={dict.__repr__(self._parsed)} context={self._current_context}>"
338
-
339
-
340
- # ==== other helpers ====
341
- def argquote(arg: str):
342
- if any(char in arg for char in string.whitespace):
343
- arg = arg.replace('"', '\\"')
344
- arg = f'"{arg}"'
345
- return arg
346
-
347
-
348
- QUOTE_PAIRS = {
349
- '"': '"',
350
- "'": "'",
351
- "‘": "’",
352
- "‚": "‛",
353
- "“": "”",
354
- "„": "‟",
355
- "⹂": "⹂",
356
- "「": "」",
357
- "『": "』",
358
- "〝": "〞",
359
- "﹁": "﹂",
360
- "﹃": "﹄",
361
- """: """,
362
- "「": "」",
363
- "«": "»",
364
- "‹": "›",
365
- "《": "》",
366
- "〈": "〉",
367
- }
368
- ALL_QUOTES = set(QUOTE_PAIRS.keys()) | set(QUOTE_PAIRS.values())
369
-
370
-
371
- class CustomStringView(StringView):
372
- def get_quoted_word(self):
373
- current = self.current
374
- if current is None:
375
- return None
376
-
377
- close_quote = QUOTE_PAIRS.get(current)
378
- is_quoted = bool(close_quote)
379
- if is_quoted:
380
- result = []
381
- _escaped_quotes = (current, close_quote)
382
- else:
383
- result = [current]
384
- _escaped_quotes = ALL_QUOTES
385
-
386
- while not self.eof:
387
- current = self.get()
388
- if not current:
389
- if is_quoted:
390
- raise ExpectedClosingQuoteError(close_quote)
391
- return "".join(result)
392
-
393
- if current == "\\":
394
- next_char = self.get()
395
- if next_char in _escaped_quotes:
396
- result.append(next_char)
397
- else:
398
- self.undo()
399
- result.append(current)
400
- continue
401
-
402
- if not is_quoted and current in ALL_QUOTES and current not in {"'", "’"}:
403
- close_quote = QUOTE_PAIRS.get(current)
404
- is_quoted = True
405
- _escaped_quotes = (current, close_quote)
406
- continue
407
-
408
- if is_quoted and current == close_quote:
409
- next_char = self.get()
410
- valid_eof = not next_char or next_char.isspace()
411
- if not valid_eof:
412
- self.undo()
413
- close_quote = None
414
- is_quoted = False
415
- _escaped_quotes = ALL_QUOTES
416
- continue
417
- return "".join(result)
418
-
419
- if current.isspace() and not is_quoted:
420
- return "".join(result)
421
-
422
- result.append(current)
423
-
424
-
425
- if __name__ == "__main__": # pragma: no cover
426
- while True:
427
- try:
428
- print(argsplit(input(">>> ")))
429
- except BadArgument as e:
430
- print(e)
@@ -1,67 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import List, Sequence
5
-
6
-
7
- _NUM_PLACEHOLDER_RE = re.compile(r"%(\d+)%|&(\d+)&")
8
-
9
-
10
- def _escape_quotes(arg: str) -> str:
11
- """Escape quotes/backslashes to mimic Avrae's quote-escaping behavior."""
12
- return arg.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'")
13
-
14
-
15
- def _ensure_args(args: Sequence[str] | None, count: int) -> List[str]:
16
- out = list(args or [])
17
- while len(out) < count:
18
- out.append(f"arg{len(out) + 1}")
19
- return out
20
-
21
-
22
- def apply_argument_parsing(text: str, args: Sequence[str] | None = None) -> str:
23
- """
24
- Apply Avrae's argument placeholder replacement rules to an alias body.
25
-
26
- Supports:
27
- - %N% : non-code replacement (quotes added if arg contains spaces)
28
- - %*% : full arg string
29
- - &N& : in-code replacement with quote escaping
30
- - &*& : full arg string with quote escaping
31
- - &ARGS&: Python-style list literal of args
32
- """
33
- max_idx = 0
34
- for match in _NUM_PLACEHOLDER_RE.finditer(text):
35
- groups = [g for g in match.groups() if g]
36
- if groups:
37
- max_idx = max(max_idx, int(groups[0]))
38
-
39
- args_list = _ensure_args(args, max_idx)
40
-
41
- def get_arg(idx: int) -> str:
42
- zero = idx - 1
43
- if zero < 0 or zero >= len(args_list):
44
- return ""
45
- return str(args_list[zero])
46
-
47
- def percent_repl(match: re.Match) -> str:
48
- idx = int(match.group(1))
49
- val = get_arg(idx)
50
- return f'"{val}"' if " " in val else val
51
-
52
- def amp_repl(match: re.Match) -> str:
53
- idx = int(match.group(1))
54
- val = get_arg(idx)
55
- return _escape_quotes(val)
56
-
57
- full_args = " ".join(args_list)
58
- escaped_full_args = _escape_quotes(full_args)
59
- list_literal = "[" + ", ".join(repr(a) for a in args_list) + "]"
60
-
61
- # order: numeric placeholders first, then multi-arg macros
62
- text = re.sub(r"%(\d+)%", percent_repl, text)
63
- text = text.replace("%*%", full_args)
64
- text = re.sub(r"&(\d+)&", amp_repl, text)
65
- text = text.replace("&*&", escaped_full_args)
66
- text = text.replace("&ARGS&", list_literal)
67
- return text