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/runner.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Binary forwarding with independent readers and bounded process cleanup."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import errno
|
|
5
|
+
import queue
|
|
6
|
+
import select
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable, Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import BinaryIO, Literal
|
|
15
|
+
from types import FrameType
|
|
16
|
+
|
|
17
|
+
from errlens.capture import Capture
|
|
18
|
+
from errlens.models import ExecutionContext, ExecutionResult
|
|
19
|
+
|
|
20
|
+
CHUNK_SIZE = 64 * 1024
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunnerError(Exception):
|
|
24
|
+
"""ErrLens infrastructure failure, distinct from a child's nonzero status."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, kind: str, detail: str, exit_code: int = 125) -> None:
|
|
27
|
+
super().__init__(detail)
|
|
28
|
+
self.kind = kind
|
|
29
|
+
self.exit_code = exit_code
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _broken_pipe(error: OSError | ValueError, destination: BinaryIO) -> bool:
|
|
33
|
+
if isinstance(error, BrokenPipeError):
|
|
34
|
+
return True
|
|
35
|
+
# Windows CRT can surface a closed redirected pipe as EINVAL, not EPIPE.
|
|
36
|
+
# Restrict the mapping to actual pipe handles; EINVAL on files stays an error.
|
|
37
|
+
if sys.platform == "win32" and isinstance(error, OSError) and error.errno == errno.EINVAL:
|
|
38
|
+
import ctypes
|
|
39
|
+
import msvcrt
|
|
40
|
+
from ctypes import wintypes
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
44
|
+
get_type = kernel.GetFileType
|
|
45
|
+
get_type.argtypes = [wintypes.HANDLE]
|
|
46
|
+
get_type.restype = wintypes.DWORD
|
|
47
|
+
return bool(get_type(msvcrt.get_osfhandle(destination.fileno())) == 3)
|
|
48
|
+
except (OSError, ValueError):
|
|
49
|
+
return False
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _available(stream: BinaryIO) -> int:
|
|
54
|
+
"""Poll pipes without an uncancellable blocking read (also on Python 3.11)."""
|
|
55
|
+
if sys.platform != "win32":
|
|
56
|
+
return CHUNK_SIZE if select.select([stream], [], [], 0.02)[0] else 0
|
|
57
|
+
import ctypes
|
|
58
|
+
import msvcrt
|
|
59
|
+
from ctypes import wintypes
|
|
60
|
+
|
|
61
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
62
|
+
peek = kernel.PeekNamedPipe
|
|
63
|
+
peek.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
|
|
64
|
+
wintypes.LPVOID, ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
|
|
65
|
+
peek.restype = wintypes.BOOL
|
|
66
|
+
available = wintypes.DWORD()
|
|
67
|
+
if not peek(msvcrt.get_osfhandle(stream.fileno()), None, 0, None,
|
|
68
|
+
ctypes.byref(available), None):
|
|
69
|
+
error = ctypes.get_last_error()
|
|
70
|
+
if error in (109, 233): # broken pipe / no process on other end
|
|
71
|
+
return -1
|
|
72
|
+
raise ctypes.WinError(error)
|
|
73
|
+
return min(available.value, CHUNK_SIZE)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _pump(
|
|
77
|
+
source: BinaryIO, destination: BinaryIO, name: Literal["stdout", "stderr"],
|
|
78
|
+
capture: Capture, stop: threading.Event, errors: queue.SimpleQueue[RunnerError],
|
|
79
|
+
) -> None:
|
|
80
|
+
try:
|
|
81
|
+
while not stop.is_set():
|
|
82
|
+
try:
|
|
83
|
+
available = _available(source)
|
|
84
|
+
if available < 0:
|
|
85
|
+
return
|
|
86
|
+
if available == 0:
|
|
87
|
+
stop.wait(0.01)
|
|
88
|
+
continue
|
|
89
|
+
chunk = os.read(source.fileno(), available)
|
|
90
|
+
except (OSError, ValueError) as exc:
|
|
91
|
+
errors.put(RunnerError("read", f"{name}: {exc}"))
|
|
92
|
+
return
|
|
93
|
+
if not chunk:
|
|
94
|
+
return
|
|
95
|
+
capture.append(name, chunk)
|
|
96
|
+
try:
|
|
97
|
+
offset = 0
|
|
98
|
+
while offset < len(chunk):
|
|
99
|
+
written = destination.write(chunk[offset:])
|
|
100
|
+
if written is None or written <= 0:
|
|
101
|
+
raise OSError("Output stream made no progress")
|
|
102
|
+
offset += written
|
|
103
|
+
destination.flush()
|
|
104
|
+
except (OSError, ValueError) as exc:
|
|
105
|
+
broken = _broken_pipe(exc, destination)
|
|
106
|
+
errors.put(RunnerError("broken_pipe" if broken else "write", f"{name}: {exc}",
|
|
107
|
+
141 if broken else 125))
|
|
108
|
+
return
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
errors.put(RunnerError("reader", f"{name}: {type(exc).__name__}: {exc}"))
|
|
111
|
+
finally:
|
|
112
|
+
try:
|
|
113
|
+
source.close()
|
|
114
|
+
except OSError as exc:
|
|
115
|
+
errors.put(RunnerError("read", f"{name}: {exc}"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@contextmanager
|
|
119
|
+
def _interrupts(cancel: threading.Event) -> Iterator[None]:
|
|
120
|
+
"""Handlers record cancellation; no asynchronous exceptions during cleanup."""
|
|
121
|
+
previous: dict[signal.Signals, Callable[[int, FrameType | None], object] | int | None] = {}
|
|
122
|
+
if threading.current_thread() is threading.main_thread():
|
|
123
|
+
signals = [signal.SIGINT]
|
|
124
|
+
if sys.platform == "win32":
|
|
125
|
+
signals.append(signal.SIGBREAK)
|
|
126
|
+
for number in signals:
|
|
127
|
+
previous[number] = signal.getsignal(number)
|
|
128
|
+
if previous[number] != signal.SIG_IGN:
|
|
129
|
+
signal.signal(number, lambda signum, frame: cancel.set())
|
|
130
|
+
try:
|
|
131
|
+
yield
|
|
132
|
+
finally:
|
|
133
|
+
for number, handler in previous.items():
|
|
134
|
+
signal.signal(number, handler)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _signal_group(process: subprocess.Popen[bytes], *, force: bool = False) -> None:
|
|
138
|
+
try:
|
|
139
|
+
if sys.platform != "win32":
|
|
140
|
+
os.killpg(process.pid, signal.SIGKILL if force else signal.SIGINT)
|
|
141
|
+
elif process.poll() is None:
|
|
142
|
+
if force:
|
|
143
|
+
process.kill()
|
|
144
|
+
else:
|
|
145
|
+
process.send_signal(signal.CTRL_BREAK_EVENT)
|
|
146
|
+
except ProcessLookupError:
|
|
147
|
+
pass
|
|
148
|
+
except OSError:
|
|
149
|
+
if process.poll() is None:
|
|
150
|
+
process.kill()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def run(
|
|
154
|
+
context: ExecutionContext, capture: Capture, stdout: BinaryIO, stderr: BinaryIO,
|
|
155
|
+
*, cancel: threading.Event | None = None, interrupt_timeout: float = 1.0,
|
|
156
|
+
drain_timeout: float = 1.0,
|
|
157
|
+
) -> ExecutionResult:
|
|
158
|
+
"""Run an argv sequence, inheriting stdin and environment (metadata is inert).
|
|
159
|
+
|
|
160
|
+
Single-use capture is supplied by the caller. Main-thread use handles Ctrl+C;
|
|
161
|
+
callers in other threads must supply a cancellation Event. Exit signals remain
|
|
162
|
+
negative in the result. No analyzers, rendering or translations are invoked.
|
|
163
|
+
"""
|
|
164
|
+
if interrupt_timeout <= 0 or drain_timeout <= 0:
|
|
165
|
+
raise ValueError("Timeouts must be positive")
|
|
166
|
+
cancellation = cancel if cancel is not None else threading.Event()
|
|
167
|
+
errors: queue.SimpleQueue[RunnerError] = queue.SimpleQueue()
|
|
168
|
+
stop = threading.Event()
|
|
169
|
+
readers: list[threading.Thread] = []
|
|
170
|
+
process: subprocess.Popen[bytes] | None = None
|
|
171
|
+
with _interrupts(cancellation):
|
|
172
|
+
try:
|
|
173
|
+
try:
|
|
174
|
+
creation_flags = 0
|
|
175
|
+
if sys.platform == "win32":
|
|
176
|
+
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
177
|
+
process = subprocess.Popen(
|
|
178
|
+
context.command, cwd=context.cwd, stdout=subprocess.PIPE,
|
|
179
|
+
stderr=subprocess.PIPE, bufsize=0, shell=False,
|
|
180
|
+
start_new_session=os.name == "posix",
|
|
181
|
+
creationflags=creation_flags,
|
|
182
|
+
)
|
|
183
|
+
except FileNotFoundError as exc:
|
|
184
|
+
raise RunnerError("not_found", str(exc), 127) from exc
|
|
185
|
+
except PermissionError as exc:
|
|
186
|
+
raise RunnerError("permission", str(exc), 126) from exc
|
|
187
|
+
except (OSError, ValueError) as exc:
|
|
188
|
+
raise RunnerError("launch", str(exc)) from exc
|
|
189
|
+
assert process.stdout is not None and process.stderr is not None
|
|
190
|
+
for name, source, target in (
|
|
191
|
+
("stdout", process.stdout, stdout), ("stderr", process.stderr, stderr),
|
|
192
|
+
):
|
|
193
|
+
reader = threading.Thread(
|
|
194
|
+
target=_pump, args=(source, target, name, capture, stop, errors),
|
|
195
|
+
name=f"errlens-{name}", daemon=True,
|
|
196
|
+
)
|
|
197
|
+
reader.start()
|
|
198
|
+
readers.append(reader)
|
|
199
|
+
interrupted = False
|
|
200
|
+
interrupt_deadline: float | None = None
|
|
201
|
+
drain_deadline: float | None = None
|
|
202
|
+
while True:
|
|
203
|
+
if not errors.empty():
|
|
204
|
+
raise errors.get()
|
|
205
|
+
now = time.monotonic()
|
|
206
|
+
if cancellation.is_set() and not interrupted:
|
|
207
|
+
interrupted = True
|
|
208
|
+
_signal_group(process)
|
|
209
|
+
interrupt_deadline = now + interrupt_timeout
|
|
210
|
+
if interrupt_deadline is not None and now >= interrupt_deadline:
|
|
211
|
+
_signal_group(process, force=True)
|
|
212
|
+
interrupt_deadline = None
|
|
213
|
+
status = process.poll()
|
|
214
|
+
if status is not None:
|
|
215
|
+
if not any(reader.is_alive() for reader in readers):
|
|
216
|
+
break
|
|
217
|
+
if drain_deadline is None:
|
|
218
|
+
drain_deadline = now + drain_timeout
|
|
219
|
+
if now >= drain_deadline:
|
|
220
|
+
raise RunnerError("drain", "Output pipes did not close after process exit")
|
|
221
|
+
time.sleep(0.01)
|
|
222
|
+
if not errors.empty():
|
|
223
|
+
raise errors.get()
|
|
224
|
+
return ExecutionResult(status, interrupted, not capture.truncated)
|
|
225
|
+
finally:
|
|
226
|
+
stop.set()
|
|
227
|
+
if process is not None:
|
|
228
|
+
if process.poll() is None or cancellation.is_set() or any(r.is_alive() for r in readers):
|
|
229
|
+
_signal_group(process, force=True)
|
|
230
|
+
try:
|
|
231
|
+
process.wait(timeout=2)
|
|
232
|
+
except subprocess.TimeoutExpired as exc:
|
|
233
|
+
raise RunnerError("cleanup", "Process did not exit after kill") from exc
|
|
234
|
+
for reader in readers:
|
|
235
|
+
reader.join(timeout=0.5)
|
|
236
|
+
# Readers own their source handles; never close/reuse an fd while
|
|
237
|
+
# a reader is active. Normally polling makes both joins immediate.
|
|
238
|
+
if not any(reader.is_alive() for reader in readers):
|
|
239
|
+
for pipe in (process.stdout, process.stderr):
|
|
240
|
+
if pipe is not None and not pipe.closed:
|
|
241
|
+
pipe.close()
|
errlens/service.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Post-execution analysis/explanation coordination, without process or terminal I/O."""
|
|
2
|
+
|
|
3
|
+
from errlens.analyzers.base import AnalyzerRegistry
|
|
4
|
+
from errlens.analyzers.python import PythonAnalyzer
|
|
5
|
+
from errlens.capture import Capture, TerminalCapture
|
|
6
|
+
from errlens.models import Diagnostic, ExecutionContext, ExecutionResult, ExplanationReport
|
|
7
|
+
from errlens.rules.python import explain
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def analyze_capture(
|
|
11
|
+
context: ExecutionContext, result: ExecutionResult, capture: Capture | TerminalCapture,
|
|
12
|
+
*, registry: AnalyzerRegistry | None = None, encoding: str = "utf-8",
|
|
13
|
+
) -> list[Diagnostic]:
|
|
14
|
+
"""Analyze bounded tails once the runner has finished. Inputs are not modified.
|
|
15
|
+
|
|
16
|
+
Callers can inspect structured results without reparsing the original bytes.
|
|
17
|
+
"""
|
|
18
|
+
if registry is None:
|
|
19
|
+
registry = AnalyzerRegistry()
|
|
20
|
+
registry.register(PythonAnalyzer())
|
|
21
|
+
data = capture.decoded(encoding)
|
|
22
|
+
diagnostics: list[Diagnostic] = []
|
|
23
|
+
for analyzer in registry.all():
|
|
24
|
+
diagnostics.extend(analyzer.analyze(context, result, data))
|
|
25
|
+
return diagnostics
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def explain_capture(
|
|
29
|
+
context: ExecutionContext, result: ExecutionResult, capture: Capture | TerminalCapture,
|
|
30
|
+
*, registry: AnalyzerRegistry | None = None, encoding: str = "utf-8",
|
|
31
|
+
) -> ExplanationReport | None:
|
|
32
|
+
"""One report after a failed, non-interrupted execution; no output on success.
|
|
33
|
+
|
|
34
|
+
Prefer stderr, then combined, then stdout. Follow explicit chain links only;
|
|
35
|
+
independent diagnostics are counted, never labelled as causes.
|
|
36
|
+
"""
|
|
37
|
+
if result.exit_code in (0, None) or result.interrupted:
|
|
38
|
+
return None
|
|
39
|
+
diagnostics = analyze_capture(context, result, capture, registry=registry, encoding=encoding)
|
|
40
|
+
if not diagnostics:
|
|
41
|
+
return None
|
|
42
|
+
primary = next((items[-1] for source in ("stderr", "combined", "stdout")
|
|
43
|
+
if (items := [item for item in diagnostics if item.source == source])),
|
|
44
|
+
diagnostics[-1])
|
|
45
|
+
indexed = {item.diagnostic_id: item for item in diagnostics}
|
|
46
|
+
related: list[Diagnostic] = []
|
|
47
|
+
seen = {primary.diagnostic_id}
|
|
48
|
+
current = primary
|
|
49
|
+
while current.related_diagnostic_id and current.related_diagnostic_id not in seen:
|
|
50
|
+
predecessor = indexed.get(current.related_diagnostic_id)
|
|
51
|
+
if predecessor is None:
|
|
52
|
+
break
|
|
53
|
+
related.append(predecessor)
|
|
54
|
+
seen.add(predecessor.diagnostic_id)
|
|
55
|
+
current = predecessor
|
|
56
|
+
related.reverse()
|
|
57
|
+
return ExplanationReport(primary, explain(primary, context), related,
|
|
58
|
+
max(0, len(diagnostics) - len(related) - 1))
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: errlens-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local Python error explanations with opt-in terminal and shell integration.
|
|
5
|
+
Author: ErrLens contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ErrLens/errlens-cli
|
|
8
|
+
Project-URL: Source, https://github.com/ErrLens/errlens-cli
|
|
9
|
+
Project-URL: Documentation, https://github.com/ErrLens/errlens-cli/blob/main/README.md
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: typer<1,>=0.16
|
|
19
|
+
Requires-Dist: rich<15,>=14
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: build<2,>=1.2; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy<2,>=1.15; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# ErrLens
|
|
27
|
+
|
|
28
|
+
ErrLens is a local, open-source CLI that adds readable Russian or English explanations to recognized Python errors. It helps developers understand tracebacks without copying them to an external service. Explanations are rule-based and work offline after installation; no API key or AI model is required.
|
|
29
|
+
|
|
30
|
+
ErrLens runs a command through `errlens run`. Its default pipe mode forwards the original stdout and stderr, then appends an explanation to stderr when an error is recognized. Optional shell wrappers intercept only `python`; explicit terminal mode uses merged terminal output.
|
|
31
|
+
|
|
32
|
+
## Requirements and installation
|
|
33
|
+
|
|
34
|
+
Python 3.11 or newer is required. On Windows, Linux or macOS, install from a local checkout:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
python -m pip install .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For development, use a virtual environment and install `python -m pip install -e ".[dev]"`. Installing dependencies requires an available package index or local wheels.
|
|
41
|
+
|
|
42
|
+
The PyPI distribution name is **errlens-cli**; the Python import and executable
|
|
43
|
+
are both **errlens**. This candidate has not been uploaded. After publication,
|
|
44
|
+
install with `python -m pip install errlens-cli==0.1.0`, then run `errlens --version`.
|
|
45
|
+
Until then use this checkout or a verified supplied wheel:
|
|
46
|
+
`python -m pip install /path/to/errlens_cli-0.1.0-py3-none-any.whl`.
|
|
47
|
+
The existing PyPI package named `errlens` is not the distribution selected for
|
|
48
|
+
this repository. See [release preparation](https://github.com/ErrLens/errlens-cli/blob/main/docs/releasing.md).
|
|
49
|
+
CI covers CPython 3.11–3.13; newer Python versions are not yet qualified.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
errlens --help
|
|
55
|
+
errlens --version
|
|
56
|
+
errlens run python main.py
|
|
57
|
+
errlens run --lang en -- python main.py
|
|
58
|
+
errlens run --no-explain -- python main.py
|
|
59
|
+
errlens run python -c "print(undefined_variable)"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Russian is the default; `--lang en` selects English. Place ErrLens options before the executable; arguments after the executable belong to the child process. `--no-explain` skips analysis. The optional `--` makes the boundary explicit.
|
|
63
|
+
|
|
64
|
+
The original traceback remains visible. For example, a `NameError` can be followed by an explanation that the name is unavailable in the current scope and suggestions to check its spelling and definition. Causes are described as possibilities, not established facts. Successful commands and nonzero exits without recognized diagnostics do not produce an explanation.
|
|
65
|
+
|
|
66
|
+
## Supported diagnostics
|
|
67
|
+
|
|
68
|
+
Rules cover `ModuleNotFoundError`, `ImportError`, `SyntaxError`, `IndentationError`, `TabError`, `NameError`, `TypeError`, `AttributeError`, `KeyError`, `FileNotFoundError` and `PermissionError`. Other structured exceptions receive a neutral notice. Incomplete diagnostics are marked as such; `ExceptionGroup` is summarized at its outer level. For a chain of exceptions, the final diagnostic is explained and linked predecessors are noted.
|
|
69
|
+
|
|
70
|
+
Suggested commands are never executed. A pip command is shown only for the exact `requests` import and an explicitly specified absolute Python executable path, with a condition to confirm that the distribution is actually needed in that environment. Import names do not generally identify package names, and a missing module may be local.
|
|
71
|
+
|
|
72
|
+
## Behavior and limitations
|
|
73
|
+
|
|
74
|
+
- ErrLens launches argument sequences without `shell=True`. Shell pipelines and builtins are interpreted by the invoking shell, not by ErrLens. On Windows, the operating system may invoke a command interpreter for `.bat` or `.cmd` files even with `shell=False`.
|
|
75
|
+
- Separate bounded buffers retain up to 1 MiB per stream for analysis. Original bytes are forwarded as they arrive; the diagnostic copy may be truncated. No output logs are saved to disk.
|
|
76
|
+
- Pipes change `isatty()` and may affect child buffering and colors. Interactive programs, job control and full-screen applications are not guaranteed to work. The relative ordering of stdout and stderr cannot be reconstructed.
|
|
77
|
+
- The child exit code is preserved for normal exits. On POSIX, signal termination becomes `128 + signal`; user interruption returns 130. Runner failures have their own codes: 125 for internal failures, 126 for permission errors, 127 for missing executables and 141 for broken pipes. Child programs can also use these codes; ErrLens identifies its own failures in stderr when possible.
|
|
78
|
+
- In pipe mode, Ctrl+C handling uses a separate process group. Windows pipe cleanup does not guarantee termination of an arbitrary descendant tree. After the child exits, pipes held open by descendants are subject to a one-second drain timeout. ConPTY uses the Job Object cleanup described below.
|
|
79
|
+
- Recognition targets standard CPython traceback formatting; IPython/Jupyter and logger-prefixed tracebacks are not supported. A traceback-like log can be ambiguous. Stdout diagnostics require a failed Python command. Unknown or incomplete data is not presented as a certain cause.
|
|
80
|
+
- Rich markup and terminal control characters from diagnostic data are rendered literally or escaped in the ErrLens block. Redirected explanations are plain text without ANSI. Original child output is not sanitized.
|
|
81
|
+
- Explanation fields are capped at 2,048 characters; linked exception messages at 256 characters. The forwarded traceback is not shortened by this limit.
|
|
82
|
+
- ErrLens does not fix code, install packages, collect telemetry or contact a service. Shell wrappers and terminal mode are explicit opt-ins; VS Code extension support remains future work.
|
|
83
|
+
|
|
84
|
+
## Opt-in PowerShell 7 integration (Windows)
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
errlens shell install powershell
|
|
88
|
+
errlens shell status powershell
|
|
89
|
+
errlens shell uninstall powershell
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Installation explicitly edits the current user's **all-hosts PowerShell 7 profile**
|
|
93
|
+
(shared with VS Code), discovered using `pwsh -NoProfile`. Restart PowerShell after
|
|
94
|
+
installation/removal. ExecutionPolicy is never changed; your policy must allow
|
|
95
|
+
loading the profile and the integration script. No administrator access is needed.
|
|
96
|
+
For an isolated trial or a custom host profile use `--profile 'path/to/profile.ps1'`
|
|
97
|
+
on each management command, then dot-source that profile manually.
|
|
98
|
+
|
|
99
|
+
An owned script is placed in an `ErrLens` subdirectory next to the selected profile.
|
|
100
|
+
Only one guarded dot-source line is appended. Existing profile bytes are preserved;
|
|
101
|
+
each modification of an existing profile creates a uniquely named backup alongside
|
|
102
|
+
it. Backups are retained on removal. Edited connection lines and unowned integration
|
|
103
|
+
files are refused rather than overwritten. A newly created profile is left empty
|
|
104
|
+
after removal. Existing `python` aliases/functions are preserved with a warning.
|
|
105
|
+
|
|
106
|
+
After loading, plain `python main.py`, `python -c "print(undefined_variable)"` and
|
|
107
|
+
`python --version` delegate to `errlens run`. Only the command name `python` is
|
|
108
|
+
wrapped. Absolute executable paths, `py`, pip and other commands bypass it.
|
|
109
|
+
The native Python application is resolved from PATH on every invocation, so venv
|
|
110
|
+
activation is respected. The ErrLens runtime is bound to the absolute Python path
|
|
111
|
+
used during installation; `errlens` need not be in PATH. Keep that environment
|
|
112
|
+
installed. To migrate it, uninstall the integration and reinstall from the new runtime.
|
|
113
|
+
|
|
114
|
+
Temporarily bypass analysis with `$env:ERRLENS_DISABLE='1'`; restore it with
|
|
115
|
+
`Remove-Item Env:ERRLENS_DISABLE`. To remove the wrapper from the current session,
|
|
116
|
+
use `Remove-Item Function:python`. Persistent removal takes effect in new sessions.
|
|
117
|
+
|
|
118
|
+
This is a PowerShell function calling a native program, not transparent terminal
|
|
119
|
+
capture. `$LASTEXITCODE` is explicitly propagated. `$?` may report function success
|
|
120
|
+
even when Python fails; inspect `$LASTEXITCODE`. PowerShell 7.3+ uses Standard
|
|
121
|
+
native argument passing locally inside the wrapper; 7.0–7.2 retain their legacy
|
|
122
|
+
quoting/empty-argument limitations. Global preferences are not changed.
|
|
123
|
+
Pipeline output and `>` pass through PowerShell's object/text conversion and do
|
|
124
|
+
not guarantee byte identity (including PowerShell 7.4 native redirection behavior).
|
|
125
|
+
Pipeline input is not forwarded by this wrapper. Avoid binary pipelines, REPLs,
|
|
126
|
+
interactive and full-screen applications; use an absolute Python executable when
|
|
127
|
+
native semantics are needed. ExecutionPolicy, host-specific profile loading and
|
|
128
|
+
VS Code itself require manual verification on the target machine.
|
|
129
|
+
|
|
130
|
+
## Opt-in Bash and Zsh integration
|
|
131
|
+
|
|
132
|
+
On Linux/macOS, explicitly install with `errlens shell install bash` or
|
|
133
|
+
`errlens shell install zsh`; use `status` or `uninstall` in place of `install`.
|
|
134
|
+
Bash uses `~/.bashrc`; Zsh uses `${ZDOTDIR:-$HOME}/.zshrc`. Bash login shells
|
|
135
|
+
do not necessarily load `.bashrc`: configure your login profile to source it
|
|
136
|
+
yourself if needed. These are interactive startup files; noninteractive shells
|
|
137
|
+
do not automatically load them. `--profile PATH` selects an isolated/custom file.
|
|
138
|
+
|
|
139
|
+
An owned script in `.errlens` beside the profile is sourced by one guarded line.
|
|
140
|
+
Existing profile bytes are retained, changes receive unique backups, repeated
|
|
141
|
+
installation is idempotent, and removal removes only the exact owned line and script.
|
|
142
|
+
Edited lines and unowned scripts are refused. Backups remain after removal.
|
|
143
|
+
Existing `python` functions and aliases take precedence and are preserved.
|
|
144
|
+
|
|
145
|
+
The `python` function resolves the external interpreter and `errlens` on each
|
|
146
|
+
invocation, forwards `"$@"`, working directory and exit status, and respects an
|
|
147
|
+
activated virtual environment. `ERRLENS_DISABLE=1 python ...` bypasses analysis;
|
|
148
|
+
if ErrLens is absent from PATH, the native Python is run directly. Use `unset -f
|
|
149
|
+
python` to disable the wrapper in the current shell. `command python`, an absolute
|
|
150
|
+
path, `python3`, pip and other commands bypass it. Restart or source the profile
|
|
151
|
+
after installation; restart or unset the function after uninstallation.
|
|
152
|
+
|
|
153
|
+
When starting Zsh from Bash with an active virtual environment, the Bash
|
|
154
|
+
activation script may have modified and exported prompt variables. Bash prompt
|
|
155
|
+
escapes are not Zsh prompt syntax and may appear literally. Start a clean Zsh,
|
|
156
|
+
then run `source /path/to/venv/bin/activate` within Zsh. The ErrLens integration
|
|
157
|
+
does not change `PS1` or `PROMPT`; your Zsh configuration can define its own prompt.
|
|
158
|
+
Manual reports of Windows 11/PowerShell and Ubuntu 24.04/WSL Bash and Zsh are
|
|
159
|
+
distinct from the automated CI checks; consult CI results for verified runs.
|
|
160
|
+
|
|
161
|
+
Redirection and pipelines follow the shell and `errlens run` pipe semantics.
|
|
162
|
+
Input is inherited, but interactive/terminal-dependent programs and exact stdout/
|
|
163
|
+
stderr interleaving are not guaranteed. Ctrl+C follows Runner process-group handling;
|
|
164
|
+
PTY and job-control equivalence are not guaranteed. Paths with spaces and Unicode
|
|
165
|
+
are quoted without `eval`. No system profiles or shell settings are modified.
|
|
166
|
+
|
|
167
|
+
## Explicit terminal mode
|
|
168
|
+
|
|
169
|
+
Use `errlens run --terminal-mode pty -- python script.py` when terminal stdin
|
|
170
|
+
and stdout are available. Linux/macOS use POSIX PTY; Windows 10 1809+ and Windows
|
|
171
|
+
11 use native ConPTY. Pipe remains the default, including all shell wrappers.
|
|
172
|
+
A transport error never triggers an automatic second run. Windows requires
|
|
173
|
+
actual console handles and a native executable (.exe or .com), with no batch
|
|
174
|
+
file or legacy winpty fallback. No extra runtime dependency or compiler is needed.
|
|
175
|
+
|
|
176
|
+
Terminal mode supplies a terminal, input, window-size propagation and terminal
|
|
177
|
+
Ctrl+C handling. stdout and stderr merge onto ErrLens stdout; echo and LF-to-CRLF
|
|
178
|
+
conversion are normal terminal behavior. Binary identity and original stream
|
|
179
|
+
separation are not guaranteed. Redirected host stdin/stdout are rejected; use
|
|
180
|
+
pipe mode for pipelines. The combined diagnostic tail is limited to 1 MiB.
|
|
181
|
+
Recognized failed Python commands receive one explanation on stderr after terminal
|
|
182
|
+
restoration. Interactive REPL exceptions followed by a successful exit do not
|
|
183
|
+
produce cards. ANSI forwarding is supported, but screen reconstruction, job-control
|
|
184
|
+
transparency, suspended children and arbitrary full-screen applications are not
|
|
185
|
+
guaranteed. A permanently blocked output sink can still block forwarding.
|
|
186
|
+
|
|
187
|
+
The host terminal modes and signal handlers are saved and restored on ordinary
|
|
188
|
+
exit, cancellation and transport errors. SIGKILL or a host crash cannot provide
|
|
189
|
+
that guarantee. POSIX processes that deliberately detach from the session may
|
|
190
|
+
outlive cleanup. ConPTY clients belong to a kill-on-close Windows Job Object:
|
|
191
|
+
remaining descendants are terminated when the main client ends or transport fails.
|
|
192
|
+
Windows uses a separate, short-lived owner process to bound ConPTY shutdown on
|
|
193
|
+
older Windows builds. Input/output are UTF-8/VT text, so invalid bytes, NUL, cursor
|
|
194
|
+
movement, wrapping and screen redraws need not survive as original log bytes.
|
|
195
|
+
The analyzer strips ANSI but does not reconstruct a screen; complex redraws can
|
|
196
|
+
produce incomplete or unrecognized diagnostics.
|
|
197
|
+
|
|
198
|
+
Automated tests use isolated real ConPTY sessions and nested console handles on
|
|
199
|
+
Windows, and nested POSIX PTYs on Linux and in the manual macOS workflow. User-reported manual checks
|
|
200
|
+
cover Windows Terminal and VS Code ConPTY plus selected WSL Bash/Zsh sessions;
|
|
201
|
+
the [acceptance record](https://github.com/ErrLens/errlens-cli/blob/main/docs/manual-acceptance.md) preserves untested combinations.
|
|
202
|
+
Legacy console hosts, detailed REPL editing, Windows 10 lifecycle behavior and
|
|
203
|
+
physical macOS hosts are not fully manually qualified. See
|
|
204
|
+
[Windows ConPTY notes](https://github.com/ErrLens/errlens-cli/blob/main/docs/windows-conpty.md) for lifecycle details.
|
|
205
|
+
|
|
206
|
+
## Development and checks
|
|
207
|
+
|
|
208
|
+
```console
|
|
209
|
+
python -m pytest
|
|
210
|
+
python -m mypy
|
|
211
|
+
python -m pip check
|
|
212
|
+
python -m build
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The build creates a wheel and source distribution in `dist/`. GitHub Actions #6
|
|
216
|
+
passed all 11 jobs for `84dbb9c927b1b0eaf3ac6efc50863b9f0ef72836`, covering
|
|
217
|
+
Windows, Ubuntu and macOS on Python 3.11–3.13 plus Windows/Ubuntu wheel installation.
|
|
218
|
+
Hosted macOS runners were unavailable in [CI #8](https://github.com/ErrLens/errlens-cli/actions/runs/43235376928).
|
|
219
|
+
The required Tests workflow temporarily runs Linux and Windows on Python 3.11–3.13
|
|
220
|
+
and both packaging jobs. macOS tests remain available through a separate manual
|
|
221
|
+
workflow. Earlier macOS passes apply to their tested commits. The release candidate
|
|
222
|
+
has yet to be tested on macOS. The [release guide](https://github.com/ErrLens/errlens-cli/blob/main/docs/releasing.md)
|
|
223
|
+
records when to restore macOS to the required matrix. The manual publishing workflow
|
|
224
|
+
can retain candidate artifacts for review.
|
|
225
|
+
|
|
226
|
+
See [CONTRIBUTING.md](https://github.com/ErrLens/errlens-cli/blob/main/CONTRIBUTING.md) for contribution guidance. ErrLens is distributed under the [MIT License](https://github.com/ErrLens/errlens-cli/blob/main/LICENSE).
|
|
227
|
+
|
|
228
|
+
## Roadmap
|
|
229
|
+
|
|
230
|
+
The current CLI focuses on local Python diagnostics, localized explanations,
|
|
231
|
+
opt-in shell wrappers and explicit terminal sessions. Future stages may improve
|
|
232
|
+
terminal compatibility and add VS Code integration, additional analyzers and
|
|
233
|
+
optional AI providers. Those future features are not part of the current CLI.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
errlens/__init__.py,sha256=AkRndmVGIVs_sUxxABG7XRcPS3OPBa_r-ErjfPWqmj8,186
|
|
2
|
+
errlens/__main__.py,sha256=I_fRhsEjjNCRvt75dfF4Bik486DYs49IWxi-_S2D7u4,118
|
|
3
|
+
errlens/_conpty.py,sha256=zLMY7uoKLGtNrbixeRcei-bs6TQaF30-FVuA2IDcuY8,9983
|
|
4
|
+
errlens/_conpty_worker.py,sha256=IOhUg9ycO3drOqLH97hk_vjn1f63iD30njycCtScwKM,6020
|
|
5
|
+
errlens/_pty_child.py,sha256=S_4Sl7bAZuo0Chj0RTE0exj2LSYLJdzLw1h0u3Qop08,719
|
|
6
|
+
errlens/capture.py,sha256=xZZlP_LhXqUDmkV_YAUFZKHDvHUEXzYu_x8pVIUZMfA,2994
|
|
7
|
+
errlens/cli.py,sha256=y4P2Vka0h9i_43kxt-jogRRSGwZET9vZVjKGev05ZoM,6954
|
|
8
|
+
errlens/conpty_runner.py,sha256=ABp8xGH-4UGVBSDuGTKz30A30fqIALpJKcq2LkEq1XQ,12408
|
|
9
|
+
errlens/i18n.py,sha256=reY8VjIdUL5kC-S-GdiIa997wrVafmTS7LnoOao4ffM,2390
|
|
10
|
+
errlens/models.py,sha256=Enjn-C4FwJ1BzFIrki5wFOOMAXFo-NJO_YR6UFtn_Cw,4060
|
|
11
|
+
errlens/posix_shell.py,sha256=3i4AV3SjaDoM8TIVS9_YOS9WX9ub_-wtVn_qGW78iq8,3074
|
|
12
|
+
errlens/powershell.py,sha256=nPlGZDWJ_EREGMmBj85LqwzqKkWHvxTJT6t5CMBDR5s,3698
|
|
13
|
+
errlens/pty_runner.py,sha256=33oskB5IlpB3PXAGAVk32m270VPccYvRNRIKFqASnW0,7084
|
|
14
|
+
errlens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
errlens/render.py,sha256=1PX78Vtau3BkxEzoaR_tRVx6XUfxdPEaS-oa-YMmeHo,4696
|
|
16
|
+
errlens/runner.py,sha256=tZ1zGFf9-ipOP2TnRa4YbKvI_OKYmD0837HCk4mBn9A,10020
|
|
17
|
+
errlens/service.py,sha256=WpKo9R9OiTlI4ggFID2_nBfirRdSkgi9apw_2ETkpTA,2602
|
|
18
|
+
errlens/analyzers/__init__.py,sha256=Lf3L5CqPi18geW4Fe7gdsvDeQHDHz--j1cdLFDhLaSA,78
|
|
19
|
+
errlens/analyzers/base.py,sha256=04kj74wNdB-0tZ0pjky--o2kN8Q6cD25TqamrW4_URE,1424
|
|
20
|
+
errlens/analyzers/python.py,sha256=HYcfe9h1wqrNcUHbK7f-BHJJeKCxfyhTF9-Cj3drdDA,16244
|
|
21
|
+
errlens/locales/en.json,sha256=AsFTM4rThOqyqPipZtlIq-qnZhrSTT1VQLnMUn5ZPTc,9107
|
|
22
|
+
errlens/locales/ru.json,sha256=XFJW_d_8nzJrTJsG65xqq-s7j8W5tXr2MSAkDAgyEHI,14288
|
|
23
|
+
errlens/rules/__init__.py,sha256=imlafOfMafltS594ewopLR1cd_0O348cJFpkaQ4LoyM,74
|
|
24
|
+
errlens/rules/python.py,sha256=fmxGawpIoPYOT3JVCf9p_j8AeqKZkkmsKrX6tBnnNpo,3681
|
|
25
|
+
errlens_cli-0.1.0.dist-info/licenses/LICENSE,sha256=w-U6mi6O-gzqwvlP9IB3m6Zr81MW1wNkqa3I_VBEPfs,1077
|
|
26
|
+
errlens_cli-0.1.0.dist-info/METADATA,sha256=VGQgNhOuH3Go0y6vJ_r2lbdHK7ulPEAoAB-LHnBZ_1Q,15870
|
|
27
|
+
errlens_cli-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
28
|
+
errlens_cli-0.1.0.dist-info/entry_points.txt,sha256=huj9rHpK07NvEqo5_Mluz5IvcEL3aENtZCr2e-gHj8k,51
|
|
29
|
+
errlens_cli-0.1.0.dist-info/top_level.txt,sha256=MlJH5FSVsEJQu1-spNcKDl9aXfrLT2B1ANsP6xBgxzI,8
|
|
30
|
+
errlens_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ErrLens contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
errlens
|