functualize-inline 0.1.0__tar.gz
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.
- functualize_inline-0.1.0/.gitignore +101 -0
- functualize_inline-0.1.0/PKG-INFO +94 -0
- functualize_inline-0.1.0/README.md +74 -0
- functualize_inline-0.1.0/examples/README.md +22 -0
- functualize_inline-0.1.0/examples/prompted_deploy/deploy.py +26 -0
- functualize_inline-0.1.0/pyproject.toml +41 -0
- functualize_inline-0.1.0/src/functualize_inline/__init__.py +5 -0
- functualize_inline-0.1.0/src/functualize_inline/apps.py +74 -0
- functualize_inline-0.1.0/src/functualize_inline/plugin.py +335 -0
- functualize_inline-0.1.0/src/functualize_inline/py.typed +0 -0
- functualize_inline-0.1.0/src/functualize_inline/widgets.py +437 -0
- functualize_inline-0.1.0/tests/__init__.py +0 -0
- functualize_inline-0.1.0/tests/conftest.py +1 -0
- functualize_inline-0.1.0/tests/test_inline_plugin.py +677 -0
- functualize_inline-0.1.0/tests/test_plugin.py +15 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.whl
|
|
11
|
+
|
|
12
|
+
# Agents
|
|
13
|
+
.spec/archive/
|
|
14
|
+
.spec/features/
|
|
15
|
+
.spec/scrutiny-reports/
|
|
16
|
+
.spec/.agentic-coding
|
|
17
|
+
.spec/STATE.md
|
|
18
|
+
.spec/PROJECT.md
|
|
19
|
+
.spec/REQUIREMENTS.md
|
|
20
|
+
.spec/ROADMAP.md
|
|
21
|
+
.opencode/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.coverage
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
htmlcov/
|
|
33
|
+
.hypothesis/
|
|
34
|
+
snapshot_report.html
|
|
35
|
+
_*_result*.txt
|
|
36
|
+
_debug.txt
|
|
37
|
+
_tui_debug.txt
|
|
38
|
+
_tui_eval_debug.txt
|
|
39
|
+
|
|
40
|
+
# IDE
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*~
|
|
45
|
+
*.code-workspace
|
|
46
|
+
|
|
47
|
+
# Coding-agent tooling state (guards — these dirs are not part of the repo)
|
|
48
|
+
.kiro/
|
|
49
|
+
.moai/
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Environment / secrets
|
|
56
|
+
.env
|
|
57
|
+
.env.*
|
|
58
|
+
!.env.example
|
|
59
|
+
|
|
60
|
+
# Agent scratch space (test output, temp scripts)
|
|
61
|
+
tmp/
|
|
62
|
+
|
|
63
|
+
# Local-only files (not for the repo)
|
|
64
|
+
*.local.md
|
|
65
|
+
*.local.*
|
|
66
|
+
|
|
67
|
+
# Personal notes
|
|
68
|
+
HUMAN_NOTE.md
|
|
69
|
+
|
|
70
|
+
# Distribution
|
|
71
|
+
dist/
|
|
72
|
+
|
|
73
|
+
# Documentation site build output
|
|
74
|
+
site/
|
|
75
|
+
|
|
76
|
+
# uv
|
|
77
|
+
.python-version
|
|
78
|
+
.functualize/cache.json
|
|
79
|
+
.functualize_cache.json
|
|
80
|
+
.todos/
|
|
81
|
+
.sidecar/
|
|
82
|
+
.sidecar-agent
|
|
83
|
+
.sidecar-task
|
|
84
|
+
.sidecar-pr
|
|
85
|
+
.sidecar-start.sh
|
|
86
|
+
.sidecar-base
|
|
87
|
+
.td-root
|
|
88
|
+
.functualize/
|
|
89
|
+
.import_linter_cache/
|
|
90
|
+
.mypy_cache/
|
|
91
|
+
.pytest_cache/
|
|
92
|
+
.ruff_cache/
|
|
93
|
+
|
|
94
|
+
# OmO / OpenCode agent run-continuation scratch state
|
|
95
|
+
.omo/
|
|
96
|
+
.mcp.json
|
|
97
|
+
.agentsroom/handoff-transcript-*.txt
|
|
98
|
+
.agentsroom/handoff-summary-*.md
|
|
99
|
+
|
|
100
|
+
# Internal pre-release audit reports (contain session IDs / local infra notes)
|
|
101
|
+
.release/
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: functualize-inline
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Textual inline interactivity plugin for functualize prompts
|
|
5
|
+
Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: functualize<1.0.0,>=0.1.0
|
|
15
|
+
Requires-Dist: textual>=0.40.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# functualize-inline
|
|
22
|
+
|
|
23
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
24
|
+
|
|
25
|
+
Textual-based inline interactivity provider for the functualize framework. This plugin renders rich terminal widgets (confirmations, selections, text inputs, progress bars) inline within the terminal using [Textual's](https://textual.textualize.io/) inline mode — without taking over the full screen. When Textual is unavailable (non-TTY, CI environments), it gracefully falls back to plain CLI `input()` prompts.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install functualize-inline
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from functualize.plugin import PromptIntent, PromptRequest
|
|
37
|
+
from functualize_inline import InlinePlugin
|
|
38
|
+
|
|
39
|
+
plugin = InlinePlugin()
|
|
40
|
+
|
|
41
|
+
request = PromptRequest(
|
|
42
|
+
intent=PromptIntent.CONFIRM_NEUTRAL,
|
|
43
|
+
question="Deploy to staging?",
|
|
44
|
+
default=True,
|
|
45
|
+
)
|
|
46
|
+
response = plugin.collect(request)
|
|
47
|
+
print(f"User answered: {response.value} (source: {response.source})")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **Rich inline widgets** — Confirmation dialogs, single/multi-select lists, text inputs, secret inputs, and acknowledgment prompts rendered with Textual styling
|
|
53
|
+
- **Automatic CLI fallback** — Detects non-TTY environments and falls back to plain `input()` prompts so jobs run unattended in CI
|
|
54
|
+
- **Structured output rendering** — Implements `OutputRenderer` protocol with `render_log`, `render_phase`, and `render_progress` for formatted terminal output with icons and progress bars
|
|
55
|
+
- **Timeout support** — Prompts can auto-dismiss after a configurable timeout, returning the default value
|
|
56
|
+
- **Entry-point auto-discovery** — Install the package and it registers itself as an interactivity provider via the `functualize.interactivity_providers` entry point
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
Public classes and functions exported by this plugin:
|
|
61
|
+
|
|
62
|
+
### `functualize_inline` (top-level)
|
|
63
|
+
|
|
64
|
+
- `InlinePlugin` — Main plugin class implementing `InputProvider` and `OutputRenderer` protocols. Entry point for prompt collection and terminal output rendering.
|
|
65
|
+
|
|
66
|
+
### `functualize_inline.apps`
|
|
67
|
+
|
|
68
|
+
- `InlinePromptApp` — A short-lived Textual `App` (inline mode) that mounts a prompt widget and returns the user's response as a `(value, source)` tuple.
|
|
69
|
+
|
|
70
|
+
### `functualize_inline.widgets`
|
|
71
|
+
|
|
72
|
+
- `PromptResult` — Textual `Message` posted by widgets when the user provides a response. Carries `.value` and `.source` attributes.
|
|
73
|
+
- `ConfirmDestructiveWidget` — Red-bordered widget requiring the user to type "yes" to confirm dangerous actions.
|
|
74
|
+
- `ConfirmNeutralWidget` — Standard Y/n confirmation widget with keyboard shortcuts.
|
|
75
|
+
- `SelectWidget` — Single-select `OptionList` widget (max 12 visible items).
|
|
76
|
+
- `MultiSelectWidget` — Checkbox-style multi-select widget with toggle and submit.
|
|
77
|
+
- `TextInputWidget` — Free-text input with optional placeholder and default value.
|
|
78
|
+
- `SecretInputWidget` — Masked password input widget.
|
|
79
|
+
- `AcknowledgeWidget` — Press-any-key dismiss widget for informational prompts.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
Run plugin tests:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run pytest plugins/functualize-inline/tests/ -v
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run linting and formatting:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
uv run ruff check plugins/functualize-inline/
|
|
93
|
+
uv run ruff format plugins/functualize-inline/
|
|
94
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# functualize-inline
|
|
2
|
+
|
|
3
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
4
|
+
|
|
5
|
+
Textual-based inline interactivity provider for the functualize framework. This plugin renders rich terminal widgets (confirmations, selections, text inputs, progress bars) inline within the terminal using [Textual's](https://textual.textualize.io/) inline mode — without taking over the full screen. When Textual is unavailable (non-TTY, CI environments), it gracefully falls back to plain CLI `input()` prompts.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install functualize-inline
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from functualize.plugin import PromptIntent, PromptRequest
|
|
17
|
+
from functualize_inline import InlinePlugin
|
|
18
|
+
|
|
19
|
+
plugin = InlinePlugin()
|
|
20
|
+
|
|
21
|
+
request = PromptRequest(
|
|
22
|
+
intent=PromptIntent.CONFIRM_NEUTRAL,
|
|
23
|
+
question="Deploy to staging?",
|
|
24
|
+
default=True,
|
|
25
|
+
)
|
|
26
|
+
response = plugin.collect(request)
|
|
27
|
+
print(f"User answered: {response.value} (source: {response.source})")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Rich inline widgets** — Confirmation dialogs, single/multi-select lists, text inputs, secret inputs, and acknowledgment prompts rendered with Textual styling
|
|
33
|
+
- **Automatic CLI fallback** — Detects non-TTY environments and falls back to plain `input()` prompts so jobs run unattended in CI
|
|
34
|
+
- **Structured output rendering** — Implements `OutputRenderer` protocol with `render_log`, `render_phase`, and `render_progress` for formatted terminal output with icons and progress bars
|
|
35
|
+
- **Timeout support** — Prompts can auto-dismiss after a configurable timeout, returning the default value
|
|
36
|
+
- **Entry-point auto-discovery** — Install the package and it registers itself as an interactivity provider via the `functualize.interactivity_providers` entry point
|
|
37
|
+
|
|
38
|
+
## API Reference
|
|
39
|
+
|
|
40
|
+
Public classes and functions exported by this plugin:
|
|
41
|
+
|
|
42
|
+
### `functualize_inline` (top-level)
|
|
43
|
+
|
|
44
|
+
- `InlinePlugin` — Main plugin class implementing `InputProvider` and `OutputRenderer` protocols. Entry point for prompt collection and terminal output rendering.
|
|
45
|
+
|
|
46
|
+
### `functualize_inline.apps`
|
|
47
|
+
|
|
48
|
+
- `InlinePromptApp` — A short-lived Textual `App` (inline mode) that mounts a prompt widget and returns the user's response as a `(value, source)` tuple.
|
|
49
|
+
|
|
50
|
+
### `functualize_inline.widgets`
|
|
51
|
+
|
|
52
|
+
- `PromptResult` — Textual `Message` posted by widgets when the user provides a response. Carries `.value` and `.source` attributes.
|
|
53
|
+
- `ConfirmDestructiveWidget` — Red-bordered widget requiring the user to type "yes" to confirm dangerous actions.
|
|
54
|
+
- `ConfirmNeutralWidget` — Standard Y/n confirmation widget with keyboard shortcuts.
|
|
55
|
+
- `SelectWidget` — Single-select `OptionList` widget (max 12 visible items).
|
|
56
|
+
- `MultiSelectWidget` — Checkbox-style multi-select widget with toggle and submit.
|
|
57
|
+
- `TextInputWidget` — Free-text input with optional placeholder and default value.
|
|
58
|
+
- `SecretInputWidget` — Masked password input widget.
|
|
59
|
+
- `AcknowledgeWidget` — Press-any-key dismiss widget for informational prompts.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
Run plugin tests:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run pytest plugins/functualize-inline/tests/ -v
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run linting and formatting:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv run ruff check plugins/functualize-inline/
|
|
73
|
+
uv run ruff format plugins/functualize-inline/
|
|
74
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# functualize-inline Examples
|
|
2
|
+
|
|
3
|
+
Inline Textual widgets for prompts, selections, and progress — rendered under your shell prompt during job runs.
|
|
4
|
+
|
|
5
|
+
| Directory | Demonstrates |
|
|
6
|
+
|-----------|--------------|
|
|
7
|
+
| [`prompted_deploy/`](prompted_deploy/) | A job that pauses for a confirmation and a selection, rendered as inline terminal widgets |
|
|
8
|
+
|
|
9
|
+
## Manual verification (interactive widgets aren't headless-testable)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd plugins/functualize-inline/examples/prompted_deploy
|
|
13
|
+
func deploy.py run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Expected behavior:
|
|
17
|
+
|
|
18
|
+
1. A destructive-styled confirmation widget appears inline ("Deploy to production?") — y / n.
|
|
19
|
+
2. On confirm, a selection list of regions appears — pick with arrows + Enter.
|
|
20
|
+
3. The job continues, logging the chosen region; the widgets collapse back into the scrollback.
|
|
21
|
+
|
|
22
|
+
In a headless run (CI, pipe), the same prompts resolve from their defaults instead of blocking. To drive that path in a test, substitute `AutoPrompt` from `functualize.testing`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""A job that asks for confirmation and a region via inline widgets.
|
|
2
|
+
|
|
3
|
+
With functualize-inline installed, `rc.prompt` renders real terminal
|
|
4
|
+
widgets; headless runs resolve from defaults.
|
|
5
|
+
|
|
6
|
+
Run with:
|
|
7
|
+
func deploy.py run
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from functualize.job import RunContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(rc: RunContext) -> str:
|
|
14
|
+
"""Deploy after an interactive confirmation and region pick."""
|
|
15
|
+
if not rc.prompt_confirm("Deploy to production?", destructive=True, default=False):
|
|
16
|
+
rc.log("Deploy cancelled")
|
|
17
|
+
return "cancelled"
|
|
18
|
+
|
|
19
|
+
region = rc.prompt_choice(
|
|
20
|
+
"Which region?",
|
|
21
|
+
["us-east-1", "eu-west-1", "ap-southeast-1"],
|
|
22
|
+
default="us-east-1",
|
|
23
|
+
)
|
|
24
|
+
rc.log(f"Deploying to {region}...")
|
|
25
|
+
rc.log("Deploy complete")
|
|
26
|
+
return region
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "functualize-inline"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Textual inline interactivity plugin for functualize prompts"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Mohammad Hakim Adiprasetya", email = "viltohmyst@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"functualize>=0.1.0,<1.0.0",
|
|
21
|
+
"textual>=0.40.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.entry-points."functualize.interactivity_providers"]
|
|
25
|
+
inline = "functualize_inline:InlinePlugin"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=7.4.0",
|
|
30
|
+
"pytest-cov>=4.1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
functualize = { workspace = true }
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/functualize_inline"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Textual inline applications for prompt rendering.
|
|
2
|
+
|
|
3
|
+
Uses Textual's `inline=True` mode to render widgets inline within the terminal
|
|
4
|
+
without taking over the full screen. Each prompt spawns a short-lived inline app
|
|
5
|
+
that returns the user's response.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from textual import on
|
|
13
|
+
from textual.app import App, ComposeResult
|
|
14
|
+
|
|
15
|
+
from functualize_inline.widgets import (
|
|
16
|
+
PromptResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InlinePromptApp(App[tuple[Any, str]]):
|
|
21
|
+
"""A short-lived Textual app that renders a prompt widget inline.
|
|
22
|
+
|
|
23
|
+
Returns a tuple of (value, source) when the user interacts with the widget.
|
|
24
|
+
The app runs in inline mode and exits once a PromptResult message is received.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
CSS = """
|
|
28
|
+
Screen {
|
|
29
|
+
layout: vertical;
|
|
30
|
+
overflow-y: auto;
|
|
31
|
+
}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
BINDINGS = [
|
|
35
|
+
("ctrl+c", "force_cancel", "Cancel"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
widget_class: type,
|
|
41
|
+
widget_kwargs: dict[str, Any],
|
|
42
|
+
timeout: float | None = None,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(inline=True, **kwargs)
|
|
46
|
+
self._widget_class = widget_class
|
|
47
|
+
self._widget_kwargs = widget_kwargs
|
|
48
|
+
self._timeout = timeout
|
|
49
|
+
self._timeout_timer: Any = None
|
|
50
|
+
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
yield self._widget_class(**self._widget_kwargs)
|
|
53
|
+
|
|
54
|
+
def on_mount(self) -> None:
|
|
55
|
+
"""Start timeout timer if configured."""
|
|
56
|
+
if self._timeout is not None and self._timeout > 0:
|
|
57
|
+
self._timeout_timer = self.set_timer(self._timeout, self._on_timeout)
|
|
58
|
+
|
|
59
|
+
def _on_timeout(self) -> None:
|
|
60
|
+
"""Auto-dismiss on timeout."""
|
|
61
|
+
self.exit((None, "timeout"))
|
|
62
|
+
|
|
63
|
+
@on(PromptResult)
|
|
64
|
+
def _on_prompt_result(self, message: PromptResult) -> None:
|
|
65
|
+
"""Handle result from the prompt widget."""
|
|
66
|
+
if self._timeout_timer is not None:
|
|
67
|
+
self._timeout_timer.stop()
|
|
68
|
+
self.exit((message.value, message.source))
|
|
69
|
+
|
|
70
|
+
def action_force_cancel(self) -> None:
|
|
71
|
+
"""Handle Ctrl+C."""
|
|
72
|
+
if self._timeout_timer is not None:
|
|
73
|
+
self._timeout_timer.stop()
|
|
74
|
+
self.exit((None, "cancelled"))
|