termwright 0.2.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.
@@ -0,0 +1,336 @@
1
+ Metadata-Version: 2.5
2
+ Name: termwright
3
+ Version: 0.2.0
4
+ Summary: Semantic side-channel client, automatic Textual probe, and annotation SDK for Termwright
5
+ Project-URL: Homepage, https://github.com/gorce-ai/termwright
6
+ Project-URL: Source, https://github.com/gorce-ai/termwright/tree/main/clients/python
7
+ Author: termwright contributors
8
+ License: MIT
9
+ Keywords: accessibility,terminal,testing,textual,tui
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.9
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
19
+ Requires-Dist: pytest>=7; extra == 'dev'
20
+ Requires-Dist: textual>=0.60; extra == 'dev'
21
+ Provides-Extra: textual
22
+ Requires-Dist: textual>=0.60; extra == 'textual'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # termwright (Python)
26
+
27
+ Semantic side-channel client, automatic probe and optional annotation SDK for
28
+ [Textual](https://textual.textualize.io).
29
+
30
+ An instrumented app publishes its widget tree over a unix socket and commits
31
+ each render with a signed OSC marker, so the driver can assert on *roles and
32
+ names* instead of screen-scraping cells.
33
+
34
+ **Dormant rule.** Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` the
35
+ probe installs nothing, opens no socket, writes no marker, and renders exactly
36
+ the bytes it would have rendered anyway.
37
+
38
+ ## Install
39
+
40
+ ```sh
41
+ pip install termwright # protocol client + probe + annotation SDK
42
+ pip install "termwright[textual]" # + Textual itself
43
+ ```
44
+
45
+ Requires Python 3.9+. The protocol modules have no third-party dependencies.
46
+
47
+ ## Automatic Textual semantics
48
+
49
+ The application is ordinary Textual code with no Termwright import:
50
+
51
+ ```python
52
+ from textual.app import App, ComposeResult
53
+ from textual.widgets import Button, Input, Label
54
+
55
+ class PermissionApp(App):
56
+ def compose(self) -> ComposeResult:
57
+ yield Label("Allow bash to run?", id="prompt")
58
+ yield Button("Approve", id="approve")
59
+ yield Button("Reject", id="reject")
60
+ yield Input(placeholder="Reason", id="reason")
61
+
62
+ if __name__ == "__main__":
63
+ PermissionApp().run()
64
+ ```
65
+
66
+ The Termwright launcher injects the probe at Python startup. The direct form,
67
+ useful for a custom runner, is:
68
+
69
+ ```sh
70
+ python -m termwright_probe -- python app.py
71
+ ```
72
+
73
+ Under the driver this publishes, after every flushed frame:
74
+
75
+ ```
76
+ application "PermissionApp"
77
+ region bounds=(0,0,80,24)
78
+ text "Allow bash to run?" bounds=(0,0,80,1) testId=prompt
79
+ button "Approve" bounds=(1,0,80,3) testId=approve [focused]
80
+ button "Reject" bounds=(4,0,80,3) testId=reject
81
+ textbox "Reason" bounds=(7,0,80,3) testId=reason
82
+ ```
83
+
84
+ ### Roles and names
85
+
86
+ Roles come from the widget class, matched along the MRO, so your own
87
+ `class SaveButton(Button)` is a `button` without any configuration. `Input` and
88
+ `TextArea` map to `textbox`, `DataTable` to `table`, `ListView`/`OptionList` to
89
+ `list`, `Label`/`Static` to `text`, containers to `region`, `ModalScreen` to
90
+ `dialog`. Names come from the widget's label, placeholder, content, `name`, or DOM `id`,
91
+ in that order; the DOM `id` is also published as `testId`.
92
+
93
+ Roles that ARIA names from content — `listitem`, `menuitem`, `tab`, `button`,
94
+ `checkbox`, `radio`, `cell`, `row`, `heading` — fall back to the text of what
95
+ they contain before they fall back to the id. That is what makes a Textual
96
+ `ListItem(Label("Open settings"))` addressable as
97
+ `getByRole('listitem', { name: 'Open settings' })`: the item holds no text of
98
+ its own, the `Label` inside does. Containers are never named this way — a
99
+ `region` would otherwise be named by everything on the screen.
100
+
101
+ ### Custom widgets
102
+
103
+ Automatic geometry, focus, visibility and framework state still come from
104
+ Textual. A decorator supplies only application intent that the framework
105
+ cannot know:
106
+
107
+ ```python
108
+ from termwright.textual import semantic
109
+
110
+ @semantic(
111
+ role="button",
112
+ name=lambda widget: f"Deploy {widget.environment}",
113
+ test_id="deploy-production",
114
+ extended=lambda widget: {"environment": widget.environment},
115
+ actions=("focus", "activate"),
116
+ key=lambda widget: f"deployment:{widget.environment}",
117
+ )
118
+ class DeployWidget(Widget):
119
+ ...
120
+ ```
121
+
122
+ `name`, `description`, `test_id`, `extended`, `labelled_by`, `described_by`,
123
+ `actions` and `key` accept either constants or callables receiving the
124
+ live widget. The declaration is inherited by subclasses. For a third-party
125
+ instance use `annotate(widget, ...)`; the registry is weak and does not keep a
126
+ discarded widget alive.
127
+
128
+ `labelled_by` and `described_by` may return a widget or a sequence of widgets.
129
+ `key` is the stable semantic identity for a domain component that Textual may
130
+ recreate. `actions` uses the protocol's closed descriptive vocabulary; it never
131
+ registers an out-of-band callback, and interaction still becomes real PTY
132
+ input.
133
+
134
+ The API intentionally has no bounds, focus, visibility, rendered-text or
135
+ portable-state arguments. Those physical facts remain probe-owned, and merge
136
+ tests enforce that the annotation cannot replace them.
137
+
138
+ ### Coexisting with `Pilot`
139
+
140
+ The probe only reads the DOM from `post_display_hook`, so `run_test()` and
141
+ `Pilot` keep working unchanged — semantic tests and pilot tests live in the same
142
+ suite.
143
+
144
+ ## Without Textual
145
+
146
+ Any TUI can drive the client directly. You own the render; the client owns the
147
+ revision numbers and hands you the marker to write after the render's last byte.
148
+
149
+ ```python
150
+ from termwright import SemanticNode, SemanticSnapshot, Rect, client_from_env
151
+
152
+ client = client_from_env(adapter_name="my-tui", adapter_version="1.0.0")
153
+ if client is not None and await client.start():
154
+ marker = await client.publish(
155
+ SemanticSnapshot(
156
+ sessionId="", revision=0, columns=80, rows=24, # both are overwritten
157
+ rootIds=["root"],
158
+ nodes=[
159
+ SemanticNode(id="root", role="dialog", name="Permission"),
160
+ SemanticNode(id="ok", parentId="root", role="button", name="Approve",
161
+ bounds=Rect(row=1, column=2, width=9, height=1)),
162
+ ],
163
+ )
164
+ )
165
+ sys.stdout.write(marker) # only after the render is fully written
166
+ sys.stdout.flush()
167
+ ```
168
+
169
+ `publish_nowait` is the same thing for synchronous render callbacks: it returns
170
+ the marker immediately and sends the frames on a background task, in order.
171
+
172
+ ## Application logs
173
+
174
+ ```python
175
+ from termwright import client_from_env
176
+ from termwright.client import CAPABILITIES_WITH_LOGS
177
+ from termwright.logging_bridge import install_log_handler
178
+
179
+ client = client_from_env(adapter_name="my-tui", adapter_version="1.0.0",
180
+ capabilities=CAPABILITIES_WITH_LOGS)
181
+ if client is not None and await client.start():
182
+ install_log_handler(client) # every logging call now reaches the driver
183
+ ```
184
+
185
+ `install_log_handler(None)` is a no-op, so an app can call it unconditionally.
186
+ Levels map onto the wire's closed ladder (anything below `DEBUG` is `trace`,
187
+ `CRITICAL` is `fatal`), `extra=` fields become flat dotted attributes, and the
188
+ client drops what the budget does not allow — leaving a gap in `seq` so the
189
+ driver can report the loss.
190
+
191
+ ## Diagnostics
192
+
193
+ When the adapter does not attach, nothing anywhere says why: the dormant rule
194
+ means a process with no endpoint behaves exactly like a process that never
195
+ heard of termwright. Point `TERMWRIGHT_DEBUG_FILE` at a file and the adapter
196
+ writes down what it decided.
197
+
198
+ ```
199
+ TERMWRIGHT_DEBUG_FILE=/tmp/adapter.log
200
+ ```
201
+
202
+ ```text
203
+ tw:diag [p41207] 0.000s open adapter=textual pid=41207 platform=darwin python=3.12 argv0=app.py
204
+ tw:diag [p41207] 0.001s dormant: TERMWRIGHT_TOKEN not set
205
+ ```
206
+
207
+ or, on a session that came up:
208
+
209
+ ```text
210
+ tw:sem [p41207] 0.002s dial unix:/tmp/tw-8f21/s timeout=5000ms
211
+ tw:sem [p41207] 0.003s hello sent adapter=textual/1.0.0 caps=tree,bounds,…
212
+ tw:sem [3f9c1a04] 0.011s hello-ack session=3f9c1a04… marker=on subscribe=diffs logs=off
213
+ tw:io [3f9c1a04] 0.048s r1 snapshot nodes=17
214
+ ```
215
+
216
+ Three properties are worth knowing before you rely on it:
217
+
218
+ - **It never writes to stderr.** The application owns the terminal, and a
219
+ diagnostic line in the middle of a render corrupts the screen the driver is
220
+ asserting on. There is no stderr mode to turn on by mistake.
221
+ - **It never fails the application.** An unwritable path, a full disk or a
222
+ closed file turns the log off and changes nothing else.
223
+ - **The token never appears in it.** The endpoint does, because the endpoint
224
+ is how you tell one session's socket from another's.
225
+
226
+ `TERMWRIGHT_DEBUG=<path>` works too, for symmetry with the driver's own
227
+ switch. `TERMWRIGHT_DEBUG=1` does **not**: that value means "log to stderr" to
228
+ the driver, it reaches this process as well, and stderr is the one destination
229
+ an adapter cannot use. Set the value to a path or the adapter stays silent.
230
+
231
+ The line format is the driver's, so `TERMWRIGHT_DEBUG=1` on the driver and
232
+ `TERMWRIGHT_DEBUG_FILE=…` on the app produce two halves of one story that a
233
+ single reader can take.
234
+
235
+ ## Injection details
236
+
237
+ ```sh
238
+ python -m termwright_probe -- python app.py
239
+ ```
240
+
241
+ `app.py` imports no termwright, calls nothing of ours, and is not edited. The
242
+ launcher puts a generated `sitecustomize.py` on `PYTHONPATH`; CPython imports
243
+ it during startup, before the script's own directory reaches `sys.path`; the
244
+ probe waits there until the application imports Textual and attaches to
245
+ `App.post_display_hook`. A driver that already sets `TERMWRIGHT_ENDPOINT` and
246
+ `TERMWRIGHT_TOKEN` can compose the same thing itself:
247
+
248
+ ```python
249
+ from termwright_probe import with_probe
250
+
251
+ command, env, bootstrap = with_probe(["python", "app.py"])
252
+ # run `command` with `env`; call bootstrap.cleanup() when the session ends
253
+ ```
254
+
255
+ Nothing is written into the project. The temporary directory holds one
256
+ generated file and is named only in the child's environment.
257
+
258
+ **Dormant without instrumentation.** No endpoint and no token means the
259
+ launcher creates no directory, and the generated module — if one survived from
260
+ an earlier run — installs nothing. A test runs the same application on a pty
261
+ with and without the bootstrap and compares the two byte streams: they are
262
+ identical. A second test compares an instrumented run against the baseline
263
+ after removing the render-commit markers, and those are identical too, which is
264
+ the claim that the probe observes rather than redraws.
265
+
266
+ **What it reports automatically.**
267
+
268
+ | Fact | Where it comes from |
269
+ |---|---|
270
+ | `bounds` = what is on screen | `MapGeometry.visible_region`, Textual's `clip ∩ region` |
271
+ | `occlusion: "known"` | widgets ranked by `MapGeometry.order`, the compositor's own sort key |
272
+ | roles for your own widget classes | the MRO, so `SaveButton(Button)` is a button with no registration |
273
+ | `frameworkType` on anything unrecognised | the widget's class name |
274
+ | scrolled out of view vs `display = False` | both `hidden`; the first also `state.offscreen`, with a zero-area rect |
275
+
276
+ Because paint order is real here, the driver allows pointer actions against
277
+ Textual nodes; it refuses them for producers that cannot say whether a node's
278
+ cells are covered.
279
+
280
+ **Where the injection reaches**, measured on CPython 3.12 (see
281
+ `docs/architecture/audit/textual.md` for the full table): a plain script,
282
+ `-m`, `-c`, a console-script entry point and `uv run` all work. `python -S`
283
+ and `python -E` do not, and are not meant to — the first disables `site`
284
+ entirely and the second makes the interpreter ignore `PYTHONPATH`. Both are
285
+ the person running the interpreter opting out.
286
+
287
+ ## Deviations
288
+
289
+ Measured against the probe conventions in the protocol README. Everything
290
+ not listed here follows them.
291
+
292
+ - **Windows support is written, not yet observed here.** A `\\.\pipe\…`
293
+ endpoint is opened through the proactor loop's `create_pipe_connection`,
294
+ which exists only on Windows; every test in this repository runs on POSIX,
295
+ so the verdict for a live pipe comes from CI. On a loop without that method
296
+ the connect fails quietly and the application keeps rendering.
297
+
298
+ - **`multiline` is derived from the widget type, not a flag** (rule 4). Textual
299
+ has no `multiline` property: `TextArea` accepts newlines and `Input` does
300
+ not, as a matter of what the classes are. The state is published from the
301
+ type for that reason and for no other — no other state here is inferred.
302
+ - **Widgets on an inactive screen are absent, not `hidden`** (rule 4). The
303
+ probe walks `app.screen`, so a pushed-over screen's widgets are not in the
304
+ tree at all. A widget hidden on the *active* screen (`display = False`) does
305
+ publish `hidden: true`. Textual owns the screen stack; reaching into it would
306
+ mean publishing widgets that no longer receive events.
307
+ - **`poetry` is unverified.** Poetry was not installed on the machine where the
308
+ injection table was measured. It runs the interpreter from the project venv
309
+ as a subprocess and passes the environment through, so it is *expected* to
310
+ behave like the venv row, and that expectation has not been confirmed. Treat
311
+ a poetry-run application as untested for the probe until somebody watches it
312
+ work.
313
+ - **The probe instruments grandchildren too.** `PYTHONPATH` is inherited, so a
314
+ process the application spawns is also instrumented unless the variable is
315
+ scrubbed. Visible to the application as well: it can read its own
316
+ environment. This is a property of the injection mechanism, not a decision.
317
+ - **The probe does not report `frame-begin`** (probe capability). Textual calls
318
+ `post_display_hook` from the `finally` of `App._display`, *after* the frame
319
+ has been flushed, so there is no instant the probe could honestly call the
320
+ start of a frame. Consumers must not read a missing `frame-begin` as "no
321
+ frame in progress".
322
+ - **A `Static` subclass with a custom `render()` is named by its `content`**
323
+ (rule 2), which is the markup it was given rather than what it draws. Textual
324
+ renders to a strip of segments with no text handle the adapter can read.
325
+
326
+ ## Conformance
327
+
328
+ `tests/` runs against `clients/test-vectors/`, which is generated from the
329
+ normative TypeScript implementation in `packages/protocol`. Framing bytes,
330
+ marker MACs, message parsing and snapshot validation are all asserted against
331
+ the same vectors in Python, Go and Rust.
332
+
333
+ ```sh
334
+ pip install -e ".[dev]"
335
+ pytest
336
+ ```
@@ -0,0 +1,25 @@
1
+ termwright/__init__.py,sha256=uTLnSENyzsM8LXkglx_GxfZhEtbUD6yI4TIqmcHzLyM,3220
2
+ termwright/client.py,sha256=ry9GDeTlm4upHiDov2qiYLABJ7E8Ujhqt_PZ-VdqvIk,28326
3
+ termwright/debug.py,sha256=2DkRkrwuaERwUkGkELjyxW5-IUlYS-RUy-B7TxkVtWo,6408
4
+ termwright/diffing.py,sha256=siautntcW8IEasC95suPweZ2FfOyjYw8GBH_hiJbrWw,4790
5
+ termwright/errors.py,sha256=sJOh-OogA3h97D7AVPpJOeWdbfgul1G1efFen0usk1c,595
6
+ termwright/framing.py,sha256=If_hiSF0_jRcUi3RbORclQ_rGKytF1GXGp70UMWfM30,7890
7
+ termwright/limits.py,sha256=d6Vsu1JN81ZBDD9Q_gW8UL0JFHf9aAlFDfDMDmx-uIY,2207
8
+ termwright/logging_bridge.py,sha256=yLQvmwITXjG7BjG2Zv70JdtDy3PsUpuAdgJtBMEB6Mg,4122
9
+ termwright/logs.py,sha256=pPzZ8r2fmqhcAcVbMXKtznzn6MTWa46s8AE0y1dZsc0,7961
10
+ termwright/marker.py,sha256=_ic7cEu38M8YW4ipryTzAF7VlhKvgwhSK31606J0hig,4760
11
+ termwright/messages.py,sha256=NEhsCnpgyvJMtTUx8xmrdRBTaHlNP9B631Y2ABuknS8,17510
12
+ termwright/roles.py,sha256=lt1dL8MYhHLVUF9Rvw97AMEu_LVC_YzixM_xEydXe_o,953
13
+ termwright/textual.py,sha256=ke55s-47kuEayy2vdPm25lzBRVe7R8YM-q39oLqSZEs,8955
14
+ termwright/tree.py,sha256=F5SjOBmauVPx7rVaUXzJEkC16r4FQjDpeaWS_xbPFD4,9961
15
+ termwright/validate.py,sha256=1dT0g8_sDzaA-26LhUj7ld4-V1pN5Zko7xNIT-1q34E,34889
16
+ termwright_probe/__init__.py,sha256=glZAVLAApQBEsjBrwEr3O1fP7Z178oUQ5RZbjvniSDU,2234
17
+ termwright_probe/__main__.py,sha256=j1ENKbPI3qyi7Ub5orNxR31wiC6bnN7dxigNqltMmA0,1427
18
+ termwright_probe/bootstrap.py,sha256=JnDhdZdJj40YiBOz0V-L8oveY9PyJ2hiMcYiUSwLxhU,8019
19
+ termwright_probe/defer.py,sha256=W2pDqUCqtVbgKszyE5QQNvrBp57VKr0TYvHXatkfnNk,4801
20
+ termwright_probe/session.py,sha256=95ylWR86votXeRqa7Xeeledm0p_al1W-HuNfxU8QHug,7361
21
+ termwright_probe/textual_probe.py,sha256=J5MrZhh_0fTKvO4EmJwh9N4w5_re8btXiJaEQkStQDI,8483
22
+ termwright_probe/textual_tree.py,sha256=9mPOElzFpGSDzwwslYQcTJj7JjuZQ2iJlkpkT3zoZ34,24268
23
+ termwright-0.2.0.dist-info/METADATA,sha256=N_LRtQ_GKL-iz8gxCOfBWwV0CUrAW_iKnGAthBdnKAE,14483
24
+ termwright-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
25
+ termwright-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,73 @@
1
+ """termwright probe for Textual — semantics from an app that imports nothing of ours.
2
+
3
+ The application is launched with one extra environment variable. CPython's
4
+ startup imports a generated `sitecustomize`, which installs this probe, which
5
+ waits for the application to import Textual and attaches to it there. Nothing
6
+ is written into the project, no configuration file is needed, and the
7
+ application's source is untouched.
8
+
9
+ **Dormant without instrumentation.** With no `TERMWRIGHT_ENDPOINT` and
10
+ `TERMWRIGHT_TOKEN` in the environment the probe installs nothing at all — no
11
+ import hook, no patch, no socket — so a `PYTHONPATH` that outlived the run it
12
+ was written for is inert.
13
+
14
+ The probe depends on the protocol half of `termwright` (framing, marker,
15
+ messages, validation, client) and on the metadata-only Textual annotation SDK.
16
+ It does not import Textual until the application itself chooses to.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Mapping, Optional
23
+
24
+ from .bootstrap import (
25
+ ENV_ENDPOINT,
26
+ ENV_TOKEN,
27
+ Bootstrap,
28
+ is_instrumented,
29
+ with_probe,
30
+ write_bootstrap,
31
+ )
32
+ from .defer import when_imported
33
+
34
+ __version__ = "0.2.0"
35
+
36
+ #: Set once :func:`install` has run, so a second `sitecustomize` on the path
37
+ #: cannot attach the probe twice.
38
+ _installed = False
39
+
40
+
41
+ def install(env: Optional[Mapping[str, str]] = None) -> bool:
42
+ """Attach the probe to Textual, as soon as the application imports it.
43
+
44
+ Returns whether anything was installed. Safe to call more than once, and
45
+ safe to call in a process that never imports Textual: all that happens
46
+ then is one finder sitting unused on `sys.meta_path`.
47
+ """
48
+ global _installed
49
+ source: Mapping[str, str] = os.environ if env is None else env
50
+ if _installed or not is_instrumented(source):
51
+ return False
52
+ _installed = True
53
+
54
+ def attach(module: object) -> None:
55
+ from .textual_probe import attach_to_app_module
56
+
57
+ attach_to_app_module(module)
58
+
59
+ when_imported("textual.app", attach)
60
+ return True
61
+
62
+
63
+ __all__ = [
64
+ "Bootstrap",
65
+ "ENV_ENDPOINT",
66
+ "ENV_TOKEN",
67
+ "__version__",
68
+ "install",
69
+ "is_instrumented",
70
+ "when_imported",
71
+ "with_probe",
72
+ "write_bootstrap",
73
+ ]
@@ -0,0 +1,45 @@
1
+ """Launcher: `python -m termwright_probe -- <command>`.
2
+
3
+ Composes the instrumented environment and execs the command in it, so a driver
4
+ can prefix any Python invocation without knowing where the probe is installed.
5
+
6
+ Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` the command runs exactly
7
+ as it would have: no directory is created and no variable is added. That makes
8
+ the launcher safe to leave in a wrapper script that is also used outside tests.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ from typing import List, Sequence
17
+
18
+ from .bootstrap import with_probe
19
+
20
+ USAGE = "usage: python -m termwright_probe [--] <command> [args...]"
21
+
22
+
23
+ def main(argv: Sequence[str]) -> int:
24
+ arguments: List[str] = list(argv)
25
+ if arguments and arguments[0] == "--":
26
+ arguments = arguments[1:]
27
+ if not arguments:
28
+ print(USAGE, file=sys.stderr)
29
+ return 2
30
+
31
+ command, env, bootstrap = with_probe(arguments)
32
+ try:
33
+ # subprocess rather than execve: the ephemeral directory has to
34
+ # outlive the child, and an exec would drop our chance to remove it.
35
+ return subprocess.call(command, env=env)
36
+ except FileNotFoundError:
37
+ print(f"termwright: cannot run {command[0]!r}", file=sys.stderr)
38
+ return 127
39
+ finally:
40
+ if bootstrap is not None:
41
+ bootstrap.cleanup()
42
+
43
+
44
+ if __name__ == "__main__":
45
+ sys.exit(main(sys.argv[1:]))