errlens-cli 0.1.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.
- errlens/__init__.py +7 -0
- errlens/__main__.py +6 -0
- errlens/_conpty.py +236 -0
- errlens/_conpty_worker.py +158 -0
- errlens/_pty_child.py +25 -0
- errlens/analyzers/__init__.py +1 -0
- errlens/analyzers/base.py +45 -0
- errlens/analyzers/python.py +362 -0
- errlens/capture.py +92 -0
- errlens/cli.py +177 -0
- errlens/conpty_runner.py +269 -0
- errlens/i18n.py +61 -0
- errlens/locales/en.json +96 -0
- errlens/locales/ru.json +96 -0
- errlens/models.py +120 -0
- errlens/posix_shell.py +71 -0
- errlens/powershell.py +77 -0
- errlens/pty_runner.py +172 -0
- errlens/py.typed +0 -0
- errlens/render.py +96 -0
- errlens/rules/__init__.py +1 -0
- errlens/rules/python.py +84 -0
- errlens/runner.py +241 -0
- errlens/service.py +58 -0
- errlens_cli-0.1.0.dist-info/METADATA +233 -0
- errlens_cli-0.1.0.dist-info/RECORD +30 -0
- errlens_cli-0.1.0.dist-info/WHEEL +5 -0
- errlens_cli-0.1.0.dist-info/entry_points.txt +2 -0
- errlens_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- errlens_cli-0.1.0.dist-info/top_level.txt +1 -0
errlens/conpty_runner.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Windows terminal frontend, with a bounded, disposable ConPTY owner.
|
|
2
|
+
|
|
3
|
+
run_session is the private headless harness used by integration tests. The CLI
|
|
4
|
+
requires real console handles; redirected users keep the default pipe transport.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import queue
|
|
11
|
+
import signal
|
|
12
|
+
import struct
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import BinaryIO
|
|
18
|
+
|
|
19
|
+
from errlens._conpty_worker import receive, send
|
|
20
|
+
from errlens.capture import TerminalCapture
|
|
21
|
+
from errlens.models import ExecutionContext, ExecutionResult
|
|
22
|
+
from errlens.runner import RunnerError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_session(context: ExecutionContext, capture: TerminalCapture, stdout: BinaryIO,
|
|
26
|
+
*, read_input: Callable[[], bytes | None] = lambda: None,
|
|
27
|
+
get_size: Callable[[], tuple[int, int]] = lambda: (80, 24)) -> ExecutionResult:
|
|
28
|
+
"""Run once; callbacks are nonblocking, sizes are (columns, rows)."""
|
|
29
|
+
if sys.platform != "win32":
|
|
30
|
+
raise RunnerError("unsupported", "ConPTY requires Windows")
|
|
31
|
+
if threading.current_thread() is not threading.main_thread():
|
|
32
|
+
raise RunnerError("conpty", "ConPTY mode must run on the main thread")
|
|
33
|
+
process: subprocess.Popen[bytes] | None = None
|
|
34
|
+
incoming: queue.Queue[tuple[bytes, bytes] | Exception] = queue.Queue(16)
|
|
35
|
+
outgoing: queue.Queue[tuple[bytes, bytes]] = queue.Queue(16)
|
|
36
|
+
stop = threading.Event()
|
|
37
|
+
interrupted = False
|
|
38
|
+
previous = signal.getsignal(signal.SIGINT)
|
|
39
|
+
threads: list[threading.Thread] = []
|
|
40
|
+
|
|
41
|
+
def on_interrupt(signum: int, frame: object) -> None:
|
|
42
|
+
nonlocal interrupted
|
|
43
|
+
interrupted = True
|
|
44
|
+
|
|
45
|
+
def publish(item: tuple[bytes, bytes] | Exception) -> None:
|
|
46
|
+
while not stop.is_set():
|
|
47
|
+
try:
|
|
48
|
+
incoming.put(item, timeout=0.05)
|
|
49
|
+
return
|
|
50
|
+
except queue.Full:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def reader(stream: BinaryIO) -> None:
|
|
54
|
+
try:
|
|
55
|
+
while not stop.is_set():
|
|
56
|
+
publish(receive(stream))
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
publish(exc)
|
|
59
|
+
|
|
60
|
+
def writer(stream: BinaryIO) -> None:
|
|
61
|
+
try:
|
|
62
|
+
while not stop.is_set():
|
|
63
|
+
try:
|
|
64
|
+
kind, data = outgoing.get(timeout=0.05)
|
|
65
|
+
except queue.Empty:
|
|
66
|
+
continue
|
|
67
|
+
send(stream, kind, data)
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
publish(exc)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
size = get_size()
|
|
73
|
+
configuration = json.dumps({"command": context.command, "cwd": context.cwd,
|
|
74
|
+
"size": size}, ensure_ascii=False).encode("utf-8")
|
|
75
|
+
if len(configuration) > 65536 or not all(1 <= n <= 32767 for n in size):
|
|
76
|
+
raise RunnerError("conpty", "ConPTY configuration exceeds transport limits")
|
|
77
|
+
# An independent hidden console protects the owner from host Ctrl+C
|
|
78
|
+
# without inheriting CREATE_NEW_PROCESS_GROUP's ignored Ctrl+C flag.
|
|
79
|
+
# Windows venv python.exe is a redirector which starts another process.
|
|
80
|
+
# Own the real CPython process so kill()/wait() bound the actual owner.
|
|
81
|
+
# Reuse the current import path to retain editable/wheel/venv resolution.
|
|
82
|
+
bootstrap = ("import runpy,sys;sys.path[:]=" + repr(sys.path) +
|
|
83
|
+
";runpy.run_module('errlens._conpty_worker',run_name='__main__')")
|
|
84
|
+
executable = getattr(sys, "_base_executable", sys.executable)
|
|
85
|
+
process = subprocess.Popen([executable, "-c", bootstrap],
|
|
86
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
87
|
+
stderr=subprocess.DEVNULL, bufsize=0, shell=False,
|
|
88
|
+
creationflags=subprocess.CREATE_NO_WINDOW)
|
|
89
|
+
assert process.stdin is not None and process.stdout is not None
|
|
90
|
+
threads = [threading.Thread(target=reader, args=(process.stdout,), daemon=True),
|
|
91
|
+
threading.Thread(target=writer, args=(process.stdin,), daemon=True)]
|
|
92
|
+
for thread in threads:
|
|
93
|
+
thread.start()
|
|
94
|
+
outgoing.put_nowait((b"C", configuration))
|
|
95
|
+
signal.signal(signal.SIGINT, on_interrupt)
|
|
96
|
+
ready = False
|
|
97
|
+
status: int | None = None
|
|
98
|
+
deadline: float | None = time.monotonic() + 15 # bounded startup, not execution
|
|
99
|
+
cancel_at: float | None = None
|
|
100
|
+
while True:
|
|
101
|
+
now = time.monotonic()
|
|
102
|
+
if deadline is not None and now > deadline:
|
|
103
|
+
raise RunnerError("conpty", "ConPTY startup/cleanup timed out")
|
|
104
|
+
if interrupted and cancel_at is None:
|
|
105
|
+
cancel_at = now + 1
|
|
106
|
+
if ready and status is None:
|
|
107
|
+
try:
|
|
108
|
+
outgoing.put_nowait((b"I", b"\x03"))
|
|
109
|
+
except queue.Full:
|
|
110
|
+
pass # blocked input still has bounded owner/tree termination
|
|
111
|
+
if cancel_at is not None and now > cancel_at:
|
|
112
|
+
return ExecutionResult(status, True, not capture.truncated)
|
|
113
|
+
if ready and status is None and not outgoing.full():
|
|
114
|
+
new_size = get_size()
|
|
115
|
+
if new_size != size:
|
|
116
|
+
if not all(1 <= n <= 32767 for n in new_size):
|
|
117
|
+
raise RunnerError("conpty", "Invalid host terminal size")
|
|
118
|
+
outgoing.put_nowait((b"W", struct.pack("!HH", *new_size)))
|
|
119
|
+
size = new_size
|
|
120
|
+
if not outgoing.full():
|
|
121
|
+
data = read_input()
|
|
122
|
+
if data:
|
|
123
|
+
if len(data) > 4096:
|
|
124
|
+
raise RunnerError("conpty", "Host input chunk exceeds 4096 bytes")
|
|
125
|
+
outgoing.put_nowait((b"I", data))
|
|
126
|
+
try:
|
|
127
|
+
item = incoming.get(timeout=0.02)
|
|
128
|
+
except queue.Empty:
|
|
129
|
+
if process.poll() is not None and not any(t.is_alive() for t in threads[:1]):
|
|
130
|
+
raise RunnerError("conpty", "ConPTY owner exited without a completion record")
|
|
131
|
+
continue
|
|
132
|
+
if isinstance(item, Exception):
|
|
133
|
+
raise RunnerError("conpty", str(item)) from item
|
|
134
|
+
kind, payload = item
|
|
135
|
+
if kind == b"O":
|
|
136
|
+
capture.combined.append(payload)
|
|
137
|
+
offset = 0
|
|
138
|
+
while offset < len(payload):
|
|
139
|
+
count = stdout.write(payload[offset:])
|
|
140
|
+
if count is None or count <= 0:
|
|
141
|
+
raise OSError("Output stream made no progress")
|
|
142
|
+
offset += count
|
|
143
|
+
stdout.flush()
|
|
144
|
+
elif kind == b"R":
|
|
145
|
+
ready, deadline = True, None
|
|
146
|
+
elif kind == b"S":
|
|
147
|
+
status = struct.unpack("!I", payload)[0]
|
|
148
|
+
deadline = time.monotonic() + 4
|
|
149
|
+
elif kind == b"E":
|
|
150
|
+
error = json.loads(payload)
|
|
151
|
+
raise RunnerError("conpty", error["detail"], error["code"])
|
|
152
|
+
elif kind == b"D" and status is not None:
|
|
153
|
+
return ExecutionResult(status, interrupted or status == 0xc000013a, not capture.truncated)
|
|
154
|
+
else:
|
|
155
|
+
raise RunnerError("conpty", "Invalid ConPTY owner response")
|
|
156
|
+
except BrokenPipeError as exc:
|
|
157
|
+
raise RunnerError("broken_pipe", str(exc), 141) from exc
|
|
158
|
+
except OSError as exc:
|
|
159
|
+
raise RunnerError("conpty", str(exc)) from exc
|
|
160
|
+
finally:
|
|
161
|
+
stop.set()
|
|
162
|
+
signal.signal(signal.SIGINT, previous)
|
|
163
|
+
if process is not None:
|
|
164
|
+
# Closing the owner's job kills all clients, including descendants.
|
|
165
|
+
if process.poll() is None:
|
|
166
|
+
try:
|
|
167
|
+
process.wait(timeout=0.2)
|
|
168
|
+
except subprocess.TimeoutExpired:
|
|
169
|
+
process.kill()
|
|
170
|
+
try:
|
|
171
|
+
process.wait(timeout=2)
|
|
172
|
+
except subprocess.TimeoutExpired as exc:
|
|
173
|
+
raise RunnerError("cleanup", "ConPTY owner did not exit after termination") from exc
|
|
174
|
+
from errlens._conpty import cancel_thread
|
|
175
|
+
for thread in threads:
|
|
176
|
+
if thread.is_alive() and thread.native_id is not None:
|
|
177
|
+
cancel_thread(thread.native_id)
|
|
178
|
+
thread.join(0.5)
|
|
179
|
+
for stream in (process.stdin, process.stdout):
|
|
180
|
+
if stream is not None:
|
|
181
|
+
stream.close()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run(context: ExecutionContext, capture: TerminalCapture, stdout: BinaryIO,
|
|
185
|
+
*, input_fd: int = 0) -> ExecutionResult:
|
|
186
|
+
"""Attach only to actual Windows console handles, restoring both modes."""
|
|
187
|
+
if sys.platform != "win32":
|
|
188
|
+
raise RunnerError("unsupported", "ConPTY requires Windows")
|
|
189
|
+
import ctypes as c
|
|
190
|
+
from ctypes import wintypes as w
|
|
191
|
+
import msvcrt
|
|
192
|
+
from errlens._conpty import api, cancel_thread, check, get_mode, screen_info, SCREEN, set_mode
|
|
193
|
+
try:
|
|
194
|
+
input_handle = msvcrt.get_osfhandle(input_fd)
|
|
195
|
+
output_handle = msvcrt.get_osfhandle(stdout.fileno())
|
|
196
|
+
input_mode, output_mode = w.DWORD(), w.DWORD()
|
|
197
|
+
if not get_mode(input_handle, c.byref(input_mode)) or not get_mode(output_handle, c.byref(output_mode)):
|
|
198
|
+
raise RunnerError("conpty", "ConPTY requires console stdin and stdout; use pipe for redirection")
|
|
199
|
+
except (OSError, ValueError) as exc:
|
|
200
|
+
raise RunnerError("conpty", str(exc)) from exc
|
|
201
|
+
if threading.current_thread() is not threading.main_thread():
|
|
202
|
+
raise RunnerError("conpty", "ConPTY mode must run on the main thread")
|
|
203
|
+
inputs: queue.Queue[bytes | Exception] = queue.Queue(16)
|
|
204
|
+
stop = threading.Event()
|
|
205
|
+
read_console = api("ReadConsoleW", w.BOOL, [w.HANDLE, w.LPVOID, w.DWORD, c.POINTER(w.DWORD), w.LPVOID])
|
|
206
|
+
|
|
207
|
+
def read_host() -> None:
|
|
208
|
+
try:
|
|
209
|
+
pending = b""
|
|
210
|
+
while not stop.is_set():
|
|
211
|
+
buffer, count = c.create_unicode_buffer(512), w.DWORD()
|
|
212
|
+
check(read_console(input_handle, buffer, 512, c.byref(count), None))
|
|
213
|
+
# A read can split a UTF-16 surrogate pair.
|
|
214
|
+
pending += c.string_at(c.addressof(buffer), count.value * 2)
|
|
215
|
+
if pending and 0xd800 <= int.from_bytes(pending[-2:], "little") <= 0xdbff:
|
|
216
|
+
text, pending = pending[:-2], pending[-2:]
|
|
217
|
+
else:
|
|
218
|
+
text, pending = pending, b""
|
|
219
|
+
data = text.decode("utf-16-le", errors="replace").encode("utf-8")
|
|
220
|
+
while not stop.is_set():
|
|
221
|
+
try:
|
|
222
|
+
inputs.put(data, timeout=0.05)
|
|
223
|
+
break
|
|
224
|
+
except queue.Full:
|
|
225
|
+
pass
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
if not stop.is_set():
|
|
228
|
+
try:
|
|
229
|
+
inputs.put_nowait(exc)
|
|
230
|
+
except queue.Full:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
def read_input() -> bytes | None:
|
|
234
|
+
try:
|
|
235
|
+
value = inputs.get_nowait()
|
|
236
|
+
except queue.Empty:
|
|
237
|
+
return None
|
|
238
|
+
if isinstance(value, Exception):
|
|
239
|
+
raise RunnerError("conpty_input", str(value)) from value
|
|
240
|
+
return value
|
|
241
|
+
|
|
242
|
+
def get_size() -> tuple[int, int]:
|
|
243
|
+
info = SCREEN()
|
|
244
|
+
check(screen_info(output_handle, c.byref(info)))
|
|
245
|
+
left, top, right, bottom = info.window
|
|
246
|
+
return right - left + 1, bottom - top + 1
|
|
247
|
+
|
|
248
|
+
thread: threading.Thread | None = None
|
|
249
|
+
try:
|
|
250
|
+
check(set_mode(output_handle, output_mode.value | 4)) # VT output
|
|
251
|
+
check(set_mode(input_handle, (input_mode.value & ~(1 | 2 | 4 | 0x40)) | 0x280))
|
|
252
|
+
thread = threading.Thread(target=read_host, daemon=True)
|
|
253
|
+
thread.start()
|
|
254
|
+
return run_session(context, capture, stdout, read_input=read_input, get_size=get_size)
|
|
255
|
+
except OSError as exc:
|
|
256
|
+
raise RunnerError("conpty", str(exc)) from exc
|
|
257
|
+
finally:
|
|
258
|
+
stop.set()
|
|
259
|
+
if thread is not None:
|
|
260
|
+
if thread.native_id is not None:
|
|
261
|
+
cancel_thread(thread.native_id)
|
|
262
|
+
thread.join(0.5)
|
|
263
|
+
# Restore modes even after a launch, forwarding or cleanup failure.
|
|
264
|
+
restored_input = set_mode(input_handle, input_mode)
|
|
265
|
+
restored_output = set_mode(output_handle, output_mode)
|
|
266
|
+
if not restored_input or not restored_output:
|
|
267
|
+
raise RunnerError("cleanup", "Could not restore Windows console modes")
|
|
268
|
+
if thread is not None and thread.is_alive():
|
|
269
|
+
raise RunnerError("cleanup", "Could not cancel Windows console input")
|
errlens/i18n.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Installed-package JSON catalogs with simple, validated named placeholders."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from string import Formatter
|
|
6
|
+
|
|
7
|
+
from errlens.models import Parameter
|
|
8
|
+
|
|
9
|
+
DEFAULT_LOCALE = "ru"
|
|
10
|
+
SUPPORTED_LOCALES = ("ru", "en")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_template(template: str) -> frozenset[str]:
|
|
14
|
+
"""Accept {name} fields and escaped braces, reject expressions/specifiers."""
|
|
15
|
+
names: set[str] = set()
|
|
16
|
+
for _, name, specification, conversion in Formatter().parse(template):
|
|
17
|
+
if name is None:
|
|
18
|
+
continue
|
|
19
|
+
if not name.isidentifier() or specification or conversion:
|
|
20
|
+
raise ValueError(f"Invalid named placeholder in template: {template!r}")
|
|
21
|
+
names.add(name)
|
|
22
|
+
return frozenset(names)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_catalog(locale: str) -> dict[str, str]:
|
|
26
|
+
"""Load and validate one supported catalog, without filesystem cwd lookup."""
|
|
27
|
+
if locale not in SUPPORTED_LOCALES:
|
|
28
|
+
raise ValueError(f"Unsupported locale: {locale}")
|
|
29
|
+
resource = files("errlens").joinpath("locales", f"{locale}.json")
|
|
30
|
+
data: object = json.loads(resource.read_text(encoding="utf-8"))
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
raise ValueError("Translation catalog must be an object")
|
|
33
|
+
catalog: dict[str, str] = {}
|
|
34
|
+
for key, value in data.items():
|
|
35
|
+
if not isinstance(key, str) or not key or not isinstance(value, str):
|
|
36
|
+
raise ValueError("Translation keys must be nonempty strings and values strings")
|
|
37
|
+
validate_template(value)
|
|
38
|
+
catalog[key] = value
|
|
39
|
+
return catalog
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Translator:
|
|
43
|
+
"""Fallback: selected locale -> Russian -> literal key.
|
|
44
|
+
|
|
45
|
+
Unsupported locales and missing template parameters raise ValueError.
|
|
46
|
+
A missing key is returned literally, without formatting untrusted key text.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, locale: str = DEFAULT_LOCALE) -> None:
|
|
50
|
+
self.locale = locale
|
|
51
|
+
self._catalog = load_catalog(locale)
|
|
52
|
+
self._fallback = load_catalog(DEFAULT_LOCALE)
|
|
53
|
+
|
|
54
|
+
def translate(self, key: str, **parameters: Parameter) -> str:
|
|
55
|
+
template = self._catalog.get(key, self._fallback.get(key))
|
|
56
|
+
if template is None:
|
|
57
|
+
return key
|
|
58
|
+
missing = validate_template(template).difference(parameters)
|
|
59
|
+
if missing:
|
|
60
|
+
raise ValueError(f"Missing translation parameters: {', '.join(sorted(missing))}")
|
|
61
|
+
return template.format_map(parameters)
|
errlens/locales/en.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.description": "ErrLens — local Python error explanations after command execution.",
|
|
3
|
+
"cli.run_help": "Run a program and explain recognized Python errors. Place ErrLens options before COMMAND.",
|
|
4
|
+
"cli.runner_error": "ErrLens | Execution failure ({kind}): {detail}",
|
|
5
|
+
"cli.run_pending": "Command execution and error analysis are not implemented yet. They are planned for later stages.",
|
|
6
|
+
"cli.version": "ErrLens {version}",
|
|
7
|
+
"cli.postprocess_error": "ErrLens | Could not prepare an explanation ({kind}). Original output was preserved.",
|
|
8
|
+
"render.header": "ErrLens · Error detected",
|
|
9
|
+
"render.original": "Python message: {message}",
|
|
10
|
+
"render.file": "Traceback location: {path}",
|
|
11
|
+
"render.line": "Line: {line}",
|
|
12
|
+
"render.incomplete": "Incomplete diagnostic: the exact cause cannot be established.",
|
|
13
|
+
"render.possible": "Possible causes:",
|
|
14
|
+
"render.actions": "What you can do:",
|
|
15
|
+
"render.condition": "Condition:",
|
|
16
|
+
"render.command": "Command to run manually ({shell}):",
|
|
17
|
+
"render.related": "Earlier linked exceptions:",
|
|
18
|
+
"render.chain.context": "The final exception occurred while handling the preceding exception.",
|
|
19
|
+
"render.chain.cause": "Python explicitly linked the final exception to its predecessor with raise … from ….",
|
|
20
|
+
"render.other": "Other diagnostics: {count}. They are not identified as causes of this error.",
|
|
21
|
+
"render.more": "Further linked exceptions: {count}. See the original traceback.",
|
|
22
|
+
"command.install_condition": "Only if requests is a required third-party dependency and is missing from this interpreter. Installation may not help for a local module or a different environment. ErrLens does not execute this command.",
|
|
23
|
+
"detail.module_name": "Module reported: {module_name}",
|
|
24
|
+
"detail.import_name": "Imported name: {import_name}",
|
|
25
|
+
"detail.name": "Undefined name: {name}",
|
|
26
|
+
"detail.attribute_name": "Requested attribute: {attribute_name}",
|
|
27
|
+
"detail.key_repr": "Key reported: {key_repr}",
|
|
28
|
+
"detail.file_path": "Path reported: {file_path}",
|
|
29
|
+
"python.module_not_found.title": "Module unavailable",
|
|
30
|
+
"python.module_not_found.description": "Python could not find the requested module in the current import environment.",
|
|
31
|
+
"python.module_not_found.cause": "The module may be local, belong to another environment, or be absent from the import search path.",
|
|
32
|
+
"python.module_not_found.action": "Check the selected Python, active virtual environment, module spelling and local package layout.",
|
|
33
|
+
"python.module_not_found.condition": "Install only a confirmed third-party dependency; an import name is not always a distribution name.",
|
|
34
|
+
"python.import_error.title": "Import failed",
|
|
35
|
+
"python.import_error.description": "Python could not complete an import operation.",
|
|
36
|
+
"python.import_error.cause": "Circular imports, an incompatible dependency version or an unavailable exported name.",
|
|
37
|
+
"python.import_error.action": "Compare the imported name with the installed version's API and inspect imports between the modules.",
|
|
38
|
+
"python.import_error.condition": "The exception alone does not establish that a library is uninstalled.",
|
|
39
|
+
"python.syntax_error.title": "Invalid syntax",
|
|
40
|
+
"python.syntax_error.description": "Python could not parse the source code.",
|
|
41
|
+
"python.syntax_error.cause": "Unclosed brackets or quotes, a missing colon, or syntax from another Python version.",
|
|
42
|
+
"python.syntax_error.action": "Inspect the pointer and nearby lines: detection can occur after the actual mistake.",
|
|
43
|
+
"python.syntax_error.condition": "Check the code against the Python version that runs the program.",
|
|
44
|
+
"python.indentation_error.title": "Invalid indentation",
|
|
45
|
+
"python.indentation_error.description": "Indentation does not match Python's block structure.",
|
|
46
|
+
"python.indentation_error.cause": "An unexpected indent, a missing block body or an inconsistent nesting level.",
|
|
47
|
+
"python.indentation_error.action": "Compare the reported line with adjacent blocks and enable visible whitespace in your editor.",
|
|
48
|
+
"python.indentation_error.condition": "Adjust indentation to match the intended nesting, not just the pointer position.",
|
|
49
|
+
"python.tab_error.title": "Inconsistent tabs and spaces",
|
|
50
|
+
"python.tab_error.description": "Python detected inconsistent use of tabs and spaces in indentation.",
|
|
51
|
+
"python.tab_error.cause": "Visually similar indentation can represent different nesting levels.",
|
|
52
|
+
"python.tab_error.action": "Show whitespace characters and make indentation consistent throughout the affected block.",
|
|
53
|
+
"python.tab_error.condition": "Check the intended nesting before replacing tabs.",
|
|
54
|
+
"python.name_error.title": "Undefined name",
|
|
55
|
+
"python.name_error.description": "Python could not find a name in the available scope.",
|
|
56
|
+
"python.name_error.cause": "A typo, access before a definition, or use outside the defining scope.",
|
|
57
|
+
"python.name_error.action": "Check the spelling, definition and scope of the variable or function.",
|
|
58
|
+
"python.name_error.condition": "The definition must run before access along the execution path actually taken.",
|
|
59
|
+
"python.type_error.title": "Unsupported types or arguments",
|
|
60
|
+
"python.type_error.description": "Python could not apply an operation or call a function with the supplied values.",
|
|
61
|
+
"python.type_error.cause": "Incompatible operand types, an incorrect argument count or an unsupported operation.",
|
|
62
|
+
"python.type_error.action": "Compare the value types and the function signature with the original message.",
|
|
63
|
+
"python.type_error.condition": "Convert values only when it matches their meaning; there is no universal conversion fix.",
|
|
64
|
+
"python.attribute_error.title": "Attribute unavailable",
|
|
65
|
+
"python.attribute_error.description": "The object does not provide the requested attribute or could not return it.",
|
|
66
|
+
"python.attribute_error.cause": "A typo, an unexpected object type or a version-specific API difference.",
|
|
67
|
+
"python.attribute_error.action": "Check the actual object type, attribute spelling and API documentation.",
|
|
68
|
+
"python.attribute_error.condition": "If the value is None, first establish why it was returned; adding a guard may not fix the cause.",
|
|
69
|
+
"python.key_error.title": "Key unavailable",
|
|
70
|
+
"python.key_error.description": "A requested key is absent from a dictionary-like structure, or its implementation reported that condition with KeyError.",
|
|
71
|
+
"python.key_error.cause": "Unexpected input data or a mismatch in the key's value or type.",
|
|
72
|
+
"python.key_error.action": "Inspect the structure and key membership; consider get() or an explicit membership check.",
|
|
73
|
+
"python.key_error.condition": "A fallback value is appropriate only when a missing key is allowed by the program's logic.",
|
|
74
|
+
"python.file_not_found.title": "File or path component not found",
|
|
75
|
+
"python.file_not_found.description": "The operating system could not find an object required by a file operation.",
|
|
76
|
+
"python.file_not_found.cause": "An incorrect path, a different working directory or a missing parent directory.",
|
|
77
|
+
"python.file_not_found.action": "Check the path, current working directory and existence of the required file or directory.",
|
|
78
|
+
"python.file_not_found.condition": "Create the object only if the program expects it; a traceback source location is not necessarily the missing path.",
|
|
79
|
+
"python.permission_error.title": "Access denied",
|
|
80
|
+
"python.permission_error.description": "The operating system refused an operation on a resource.",
|
|
81
|
+
"python.permission_error.cause": "Access restrictions, a read-only resource or a lock held by another program.",
|
|
82
|
+
"python.permission_error.action": "Check the path, requested operation and current user's permissions; check whether the resource is in use.",
|
|
83
|
+
"python.permission_error.condition": "Change access only for the required resource within your authority; administrator privileges are not a universal fix.",
|
|
84
|
+
"python.unknown.title": "Recognized exception: {error_type}",
|
|
85
|
+
"python.unknown.description": "No verified explanation is available for this type yet. The original message is reported as a fact.",
|
|
86
|
+
"python.unknown.action": "Inspect the original message and documentation for the component that raised the exception.",
|
|
87
|
+
"python.unknown.condition": "The available data does not establish a specific cause or fix.",
|
|
88
|
+
"python.incomplete.title": "Insufficient diagnostic data",
|
|
89
|
+
"python.incomplete.description": "An incomplete or damaged Python diagnostic fragment was detected ({error_type}).",
|
|
90
|
+
"python.incomplete.action": "Obtain the full traceback; for truncated capture, increase the Capture API limit and retry only if rerunning is appropriate.",
|
|
91
|
+
"python.incomplete.condition": "The incomplete fragment does not establish a cause or a corrective command.",
|
|
92
|
+
"python.exception_group.title": "Exception group",
|
|
93
|
+
"python.exception_group.description": "Python reported a group of exceptions. The analyzer retained only the outer level.",
|
|
94
|
+
"python.exception_group.action": "Inspect the nested exceptions in the original traceback and investigate them individually.",
|
|
95
|
+
"python.exception_group.condition": "Nested exceptions may have different causes; there is no single fix for the entire group."
|
|
96
|
+
}
|
errlens/locales/ru.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.description": "ErrLens — локальные объяснения Python-ошибок после выполнения команды.",
|
|
3
|
+
"cli.run_help": "Запустить программу и объяснить распознанную Python-ошибку. Опции ErrLens указываются перед COMMAND.",
|
|
4
|
+
"cli.runner_error": "ErrLens | Ошибка выполнения ({kind}): {detail}",
|
|
5
|
+
"cli.run_pending": "Запуск команд и анализ ошибок ещё не реализованы. Они появятся на следующих этапах.",
|
|
6
|
+
"cli.version": "ErrLens {version}",
|
|
7
|
+
"cli.postprocess_error": "ErrLens | Не удалось подготовить объяснение ({kind}). Исходный вывод сохранён.",
|
|
8
|
+
"render.header": "ErrLens · Обнаружена ошибка",
|
|
9
|
+
"render.original": "Сообщение Python: {message}",
|
|
10
|
+
"render.file": "Расположение в traceback: {path}",
|
|
11
|
+
"render.line": "Строка: {line}",
|
|
12
|
+
"render.incomplete": "Диагностика неполная: точную причину установить нельзя.",
|
|
13
|
+
"render.possible": "Возможные причины:",
|
|
14
|
+
"render.actions": "Что можно сделать:",
|
|
15
|
+
"render.condition": "Условие:",
|
|
16
|
+
"render.command": "Команда для ручного запуска ({shell}):",
|
|
17
|
+
"render.related": "Предшествующие связанные исключения:",
|
|
18
|
+
"render.chain.context": "Итоговое исключение возникло при обработке предыдущего.",
|
|
19
|
+
"render.chain.cause": "Python явно связал итоговое исключение с предыдущим через raise … from ….",
|
|
20
|
+
"render.other": "Других диагностик: {count}. Они не объявляются причинами этой ошибки.",
|
|
21
|
+
"render.more": "Ещё связанных исключений: {count}. См. исходный traceback.",
|
|
22
|
+
"command.install_condition": "Только если requests — нужная сторонняя зависимость проекта и она отсутствует именно у этого интерпретатора. Для локального модуля или другого окружения установка может не помочь. Команда не выполняется ErrLens.",
|
|
23
|
+
"detail.module_name": "Модуль из сообщения: {module_name}",
|
|
24
|
+
"detail.import_name": "Импортируемое имя: {import_name}",
|
|
25
|
+
"detail.name": "Неопределённое имя: {name}",
|
|
26
|
+
"detail.attribute_name": "Запрошенный атрибут: {attribute_name}",
|
|
27
|
+
"detail.key_repr": "Ключ из сообщения: {key_repr}",
|
|
28
|
+
"detail.file_path": "Путь из сообщения: {file_path}",
|
|
29
|
+
"python.module_not_found.title": "Модуль недоступен",
|
|
30
|
+
"python.module_not_found.description": "Python не смог найти запрошенный модуль в используемом окружении.",
|
|
31
|
+
"python.module_not_found.cause": "Модуль может быть локальным, находиться в другом окружении или отсутствовать в пути поиска.",
|
|
32
|
+
"python.module_not_found.action": "Проверьте выбранный Python, активное виртуальное окружение, имя модуля и структуру локальных пакетов.",
|
|
33
|
+
"python.module_not_found.condition": "Установка уместна только для подтверждённой сторонней зависимости; имя импорта не всегда равно имени пакета.",
|
|
34
|
+
"python.import_error.title": "Импорт не выполнен",
|
|
35
|
+
"python.import_error.description": "Python не смог завершить операцию импорта.",
|
|
36
|
+
"python.import_error.cause": "Циклический импорт, несовместимая версия зависимости или отсутствие экспортируемого имени.",
|
|
37
|
+
"python.import_error.action": "Сопоставьте импортируемое имя с API установленной версии и проверьте взаимные импорты модулей.",
|
|
38
|
+
"python.import_error.condition": "Без дополнительных данных нельзя утверждать, что библиотека не установлена.",
|
|
39
|
+
"python.syntax_error.title": "Некорректный синтаксис",
|
|
40
|
+
"python.syntax_error.description": "Python не смог разобрать исходный код.",
|
|
41
|
+
"python.syntax_error.cause": "Незакрытые скобки или кавычки, пропущенное двоеточие либо синтаксис другой версии Python.",
|
|
42
|
+
"python.syntax_error.action": "Посмотрите на указатель и соседние строки: место обнаружения может следовать за самой ошибкой.",
|
|
43
|
+
"python.syntax_error.condition": "Проверяйте код для той версии Python, которой запускается программа.",
|
|
44
|
+
"python.indentation_error.title": "Нарушены отступы",
|
|
45
|
+
"python.indentation_error.description": "Отступы не соответствуют структуре блоков Python.",
|
|
46
|
+
"python.indentation_error.cause": "Лишний отступ, отсутствие тела блока или несовпадение уровня вложенности.",
|
|
47
|
+
"python.indentation_error.action": "Сравните отступ указанной строки с соседними блоками и включите отображение пробелов в редакторе.",
|
|
48
|
+
"python.indentation_error.condition": "Меняйте отступы с учётом задуманной вложенности, а не только положения указателя.",
|
|
49
|
+
"python.tab_error.title": "Неоднозначное смешение табуляции и пробелов",
|
|
50
|
+
"python.tab_error.description": "Python обнаружил несовместимое использование табуляции и пробелов в отступах.",
|
|
51
|
+
"python.tab_error.cause": "Визуально похожие отступы могут обозначать разные уровни вложенности.",
|
|
52
|
+
"python.tab_error.action": "Включите отображение непечатаемых символов и приведите отступы затронутого блока к единому виду.",
|
|
53
|
+
"python.tab_error.condition": "Перед заменой табуляции проверьте задуманную вложенность блока.",
|
|
54
|
+
"python.name_error.title": "Имя не определено",
|
|
55
|
+
"python.name_error.description": "Python не нашёл имя в доступной области видимости.",
|
|
56
|
+
"python.name_error.cause": "Опечатка, обращение до объявления или использование имени вне его области видимости.",
|
|
57
|
+
"python.name_error.action": "Проверьте написание имени, место объявления и область видимости переменной или функции.",
|
|
58
|
+
"python.name_error.condition": "Объявление должно быть выполнено до обращения по той ветке программы, которая действительно запускается.",
|
|
59
|
+
"python.type_error.title": "Операция получила неподходящий тип или аргументы",
|
|
60
|
+
"python.type_error.description": "Python не смог применить операцию или вызвать функцию с переданными значениями.",
|
|
61
|
+
"python.type_error.cause": "Несовместимые типы операндов, неверное число аргументов или неподдерживаемая операция.",
|
|
62
|
+
"python.type_error.action": "Сверьте типы значений и сигнатуру вызываемой функции с исходным сообщением.",
|
|
63
|
+
"python.type_error.condition": "Преобразуйте типы только если это соответствует смыслу данных; универсального преобразования нет.",
|
|
64
|
+
"python.attribute_error.title": "Атрибут недоступен",
|
|
65
|
+
"python.attribute_error.description": "Объект не предоставляет запрошенный атрибут или не смог вернуть его.",
|
|
66
|
+
"python.attribute_error.cause": "Опечатка, неожиданный тип объекта или отличия API используемой версии.",
|
|
67
|
+
"python.attribute_error.action": "Проверьте фактический тип объекта, написание атрибута и документацию его API.",
|
|
68
|
+
"python.attribute_error.condition": "Если объект оказался None, сначала выясните, почему он получен; добавление проверки не всегда исправляет причину.",
|
|
69
|
+
"python.key_error.title": "Ключ недоступен",
|
|
70
|
+
"python.key_error.description": "Запрошенный ключ отсутствует в словаре или аналогичной структуре либо реализация сообщила об этом через KeyError.",
|
|
71
|
+
"python.key_error.cause": "Неожиданное содержимое данных или несовпадение значения и типа ключа.",
|
|
72
|
+
"python.key_error.action": "Проверьте содержимое структуры и наличие ключа; рассмотрите get() или явную проверку принадлежности.",
|
|
73
|
+
"python.key_error.condition": "Безопасный доступ с запасным значением уместен только если отсутствие ключа допустимо по логике программы.",
|
|
74
|
+
"python.file_not_found.title": "Файл или компонент пути не найден",
|
|
75
|
+
"python.file_not_found.description": "Операционная система не нашла объект, необходимый для файловой операции.",
|
|
76
|
+
"python.file_not_found.cause": "Неверный путь, другая рабочая директория или отсутствие одного из родительских каталогов.",
|
|
77
|
+
"python.file_not_found.action": "Проверьте путь, текущую рабочую директорию и наличие нужного файла или каталога.",
|
|
78
|
+
"python.file_not_found.condition": "Создавайте объект только если это предусмотрено программой; существующий путь из traceback может указывать на исходный код, а не на отсутствующий файл.",
|
|
79
|
+
"python.permission_error.title": "Отказано в доступе",
|
|
80
|
+
"python.permission_error.description": "Операционная система отказала в выполнении операции над ресурсом.",
|
|
81
|
+
"python.permission_error.cause": "Ограничения доступа, режим только для чтения или блокировка ресурса другой программой.",
|
|
82
|
+
"python.permission_error.action": "Проверьте путь, требуемую операцию и права текущего пользователя; выясните, не занят ли ресурс.",
|
|
83
|
+
"python.permission_error.condition": "Меняйте доступ только для нужного ресурса в рамках своих полномочий; запуск с правами администратора не является универсальным решением.",
|
|
84
|
+
"python.unknown.title": "Распознано исключение: {error_type}",
|
|
85
|
+
"python.unknown.description": "Для этого типа пока нет проверенного объяснения. Исходное сообщение приведено как факт.",
|
|
86
|
+
"python.unknown.action": "Изучите исходное сообщение и документацию компонента, который сформировал исключение.",
|
|
87
|
+
"python.unknown.condition": "Конкретная причина и способ исправления по доступным данным не установлены.",
|
|
88
|
+
"python.incomplete.title": "Недостаточно диагностических данных",
|
|
89
|
+
"python.incomplete.description": "Обнаружен неполный или повреждённый фрагмент Python-диагностики ({error_type}).",
|
|
90
|
+
"python.incomplete.action": "Получите полный traceback; при усечении увеличьте лимит Capture в API и повторите диагностику, если повторный запуск допустим.",
|
|
91
|
+
"python.incomplete.condition": "Причина и команды исправления по неполному фрагменту не определяются.",
|
|
92
|
+
"python.exception_group.title": "Группа исключений",
|
|
93
|
+
"python.exception_group.description": "Python сообщил о группе исключений. Анализатор сохранил только верхний уровень группы.",
|
|
94
|
+
"python.exception_group.action": "Изучите вложенные исключения в исходном traceback и разберите их по отдельности.",
|
|
95
|
+
"python.exception_group.condition": "Причины вложенных исключений могут различаться; единого исправления для всей группы нет."
|
|
96
|
+
}
|