cjm-substrate-qt-kit 0.0.1__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.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-substrate-qt-kit
3
+ Version: 0.0.1
4
+ Summary: Shared foundations for the PySide6 application lane (DEC c4b0d6e5): the loop-thread session base (a private asyncio loop behind a Qt shell), the row-style palette, and the keybinding helper — extracted at their first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt, never speculatively. Born on-graph.
5
+ Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/cj-mills/cjm-substrate-qt-kit
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: PySide6
13
+
14
+ # cjm-substrate-qt-kit
15
+
16
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
17
+
18
+ Shared foundations for the PySide6 application lane (DEC c4b0d6e5): LoopThreadSession (a private asyncio loop behind a Qt shell — GraphSession and CapabilitySession both subclass it), the row-style palette (STYLE_COLORS/apply_row_style), and the keybinding helper. Every piece extracted at its first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt — the kit grows by demand, never speculation. Semantic theming (a94fa56a) and in-app config (812beb51) land here at their demand points.
19
+
20
+ ## Modules
21
+
22
+ - **`cjm_substrate_qt_kit`** — Shared foundations for the PySide6 application lane — extracted at first duplication (DEC dcf8a712).
23
+ - **`cjm_substrate_qt_kit.keys`** — The keybinding helper both shells grew independently.
24
+ - **`cjm_substrate_qt_kit.loopthread`** — The loop-thread session base: a private asyncio loop behind a Qt shell.
25
+ - **`cjm_substrate_qt_kit.style`** — Row-style vocabulary for the lane's list widgets.
26
+
27
+ ## API
28
+
29
+ ### `cjm_substrate_qt_kit.keys`
30
+
31
+ - `bind` _function_ — One QShortcut: `key` on `parent` (default: the owner window) fires `fn`.
32
+
33
+ ### `cjm_substrate_qt_kit.loopthread`
34
+
35
+ - `LoopThreadSession` _class_ — Owns one daemon asyncio loop thread; subclasses put their subsystem's
36
+
37
+ ### `cjm_substrate_qt_kit.style`
38
+
39
+ - `apply_row_style` _function_ — Map a row's style words onto a list item (color words + bold).
40
+
41
+ ## Dependencies
42
+
43
+ **Depends on:** `PySide6`
44
+ **Used by:** `cjm-graph-workbench-qt`, `cjm-transcription-qt`
@@ -0,0 +1,31 @@
1
+ # cjm-substrate-qt-kit
2
+
3
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
4
+
5
+ Shared foundations for the PySide6 application lane (DEC c4b0d6e5): LoopThreadSession (a private asyncio loop behind a Qt shell — GraphSession and CapabilitySession both subclass it), the row-style palette (STYLE_COLORS/apply_row_style), and the keybinding helper. Every piece extracted at its first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt — the kit grows by demand, never speculation. Semantic theming (a94fa56a) and in-app config (812beb51) land here at their demand points.
6
+
7
+ ## Modules
8
+
9
+ - **`cjm_substrate_qt_kit`** — Shared foundations for the PySide6 application lane — extracted at first duplication (DEC dcf8a712).
10
+ - **`cjm_substrate_qt_kit.keys`** — The keybinding helper both shells grew independently.
11
+ - **`cjm_substrate_qt_kit.loopthread`** — The loop-thread session base: a private asyncio loop behind a Qt shell.
12
+ - **`cjm_substrate_qt_kit.style`** — Row-style vocabulary for the lane's list widgets.
13
+
14
+ ## API
15
+
16
+ ### `cjm_substrate_qt_kit.keys`
17
+
18
+ - `bind` _function_ — One QShortcut: `key` on `parent` (default: the owner window) fires `fn`.
19
+
20
+ ### `cjm_substrate_qt_kit.loopthread`
21
+
22
+ - `LoopThreadSession` _class_ — Owns one daemon asyncio loop thread; subclasses put their subsystem's
23
+
24
+ ### `cjm_substrate_qt_kit.style`
25
+
26
+ - `apply_row_style` _function_ — Map a row's style words onto a list item (color words + bold).
27
+
28
+ ## Dependencies
29
+
30
+ **Depends on:** `PySide6`
31
+ **Used by:** `cjm-graph-workbench-qt`, `cjm-transcription-qt`
@@ -0,0 +1 @@
1
+ """Shared foundations for the PySide6 application lane — extracted at first duplication (DEC dcf8a712)."""
@@ -0,0 +1,16 @@
1
+ """The keybinding helper both shells grew independently."""
2
+
3
+ from PySide6.QtCore import Qt
4
+ from PySide6.QtGui import QKeySequence, QShortcut
5
+
6
+
7
+ def bind(owner, key: str, fn, parent=None, context=Qt.WindowShortcut) -> QShortcut:
8
+ """One QShortcut: `key` on `parent` (default: the owner window) fires `fn`.
9
+
10
+ Returns the shortcut so callers can retune context/enabled later; the
11
+ default WindowShortcut context pairs with modal dialogs for text entry
12
+ (single-letter bindings never fight a focused editor)."""
13
+ shortcut = QShortcut(QKeySequence(key), parent or owner)
14
+ shortcut.setContext(context)
15
+ shortcut.activated.connect(fn)
16
+ return shortcut
@@ -0,0 +1,64 @@
1
+ """The loop-thread session base: a private asyncio loop behind a Qt shell.
2
+
3
+ Extracted from the third repetition of the pattern (GraphSession in
4
+ cjm-graph-workbench-qt, CapabilitySession in cjm-transcription-qt): async
5
+ subsystems — the projection lens layer, the capability stack — live on a
6
+ daemon loop thread; the Qt shell submits coroutines and resolves the returned
7
+ concurrent Futures through queued Signals, so the paint thread never blocks.
8
+ Qt-free on purpose: sessions test headless, and non-Qt callers (probes,
9
+ scripts) can drive them the same way."""
10
+
11
+ import asyncio
12
+ import threading
13
+ from concurrent.futures import Future
14
+ from typing import Optional
15
+
16
+
17
+ class LoopThreadSession:
18
+ """Owns one daemon asyncio loop thread; subclasses put their subsystem's
19
+ verbs on top as submit()-wrapped coroutines.
20
+
21
+ start() spins the loop (subclasses may extend it to open resources ON the
22
+ loop); close() runs the on_close() coroutine hook, then stops the thread.
23
+ submit() is the non-blocking bridge (Future -> queued Signal in a shell);
24
+ call() blocks — teardown paths and headless tests."""
25
+
26
+ thread_name = "loop-session"
27
+
28
+ def __init__(self, timeout: float = 60.0):
29
+ self.timeout = timeout
30
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
31
+ self._thread: Optional[threading.Thread] = None
32
+
33
+ @property
34
+ def running(self) -> bool:
35
+ return self._loop is not None
36
+
37
+ def start(self) -> None:
38
+ self._loop = asyncio.new_event_loop()
39
+ self._thread = threading.Thread(target=self._loop.run_forever,
40
+ name=self.thread_name, daemon=True)
41
+ self._thread.start()
42
+
43
+ def submit(self, coro) -> Future:
44
+ """Schedule a coroutine on the loop thread; resolves there."""
45
+ return asyncio.run_coroutine_threadsafe(coro, self._loop)
46
+
47
+ def call(self, coro):
48
+ """Blocking submit (the sync facade)."""
49
+ return self.submit(coro).result(self.timeout)
50
+
51
+ async def on_close(self) -> None:
52
+ """Subclass hook: release loop-side resources before the loop stops."""
53
+
54
+ def close(self) -> None:
55
+ if self._loop is None:
56
+ return
57
+ try:
58
+ self.call(self.on_close())
59
+ finally:
60
+ self._loop.call_soon_threadsafe(self._loop.stop)
61
+ if self._thread is not None:
62
+ self._thread.join(timeout=5)
63
+ self._loop = None
64
+ self._thread = None
@@ -0,0 +1,26 @@
1
+ """Row-style vocabulary for the lane's list widgets.
2
+
3
+ Spine row dicts carry Rich-ish style words ("green", "dim", "bold", …); the
4
+ shells map them onto QListWidgetItems. Theme-neutral hexes readable on light
5
+ and dark — the same palette both shells shipped with before extraction."""
6
+
7
+ from typing import Optional
8
+
9
+ from PySide6.QtGui import QColor
10
+ from PySide6.QtWidgets import QListWidgetItem
11
+
12
+ STYLE_COLORS = {"red": "#c74a3c", "yellow": "#b9770e", "cyan": "#2b8a9d",
13
+ "magenta": "#9b59b6", "green": "#3f9d55", "blue": "#4a6fb5",
14
+ "dim": "#8a9299"}
15
+
16
+
17
+ def apply_row_style(item: QListWidgetItem, style: Optional[str]) -> None:
18
+ """Map a row's style words onto a list item (color words + bold)."""
19
+ parts = str(style or "").split()
20
+ for word in parts:
21
+ if word in STYLE_COLORS:
22
+ item.setForeground(QColor(STYLE_COLORS[word]))
23
+ if "bold" in parts:
24
+ font = item.font()
25
+ font.setBold(True)
26
+ item.setFont(font)
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: cjm-substrate-qt-kit
3
+ Version: 0.0.1
4
+ Summary: Shared foundations for the PySide6 application lane (DEC c4b0d6e5): the loop-thread session base (a private asyncio loop behind a Qt shell), the row-style palette, and the keybinding helper — extracted at their first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt, never speculatively. Born on-graph.
5
+ Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/cj-mills/cjm-substrate-qt-kit
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: PySide6
13
+
14
+ # cjm-substrate-qt-kit
15
+
16
+ <!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
17
+
18
+ Shared foundations for the PySide6 application lane (DEC c4b0d6e5): LoopThreadSession (a private asyncio loop behind a Qt shell — GraphSession and CapabilitySession both subclass it), the row-style palette (STYLE_COLORS/apply_row_style), and the keybinding helper. Every piece extracted at its first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt — the kit grows by demand, never speculation. Semantic theming (a94fa56a) and in-app config (812beb51) land here at their demand points.
19
+
20
+ ## Modules
21
+
22
+ - **`cjm_substrate_qt_kit`** — Shared foundations for the PySide6 application lane — extracted at first duplication (DEC dcf8a712).
23
+ - **`cjm_substrate_qt_kit.keys`** — The keybinding helper both shells grew independently.
24
+ - **`cjm_substrate_qt_kit.loopthread`** — The loop-thread session base: a private asyncio loop behind a Qt shell.
25
+ - **`cjm_substrate_qt_kit.style`** — Row-style vocabulary for the lane's list widgets.
26
+
27
+ ## API
28
+
29
+ ### `cjm_substrate_qt_kit.keys`
30
+
31
+ - `bind` _function_ — One QShortcut: `key` on `parent` (default: the owner window) fires `fn`.
32
+
33
+ ### `cjm_substrate_qt_kit.loopthread`
34
+
35
+ - `LoopThreadSession` _class_ — Owns one daemon asyncio loop thread; subclasses put their subsystem's
36
+
37
+ ### `cjm_substrate_qt_kit.style`
38
+
39
+ - `apply_row_style` _function_ — Map a row's style words onto a list item (color words + bold).
40
+
41
+ ## Dependencies
42
+
43
+ **Depends on:** `PySide6`
44
+ **Used by:** `cjm-graph-workbench-qt`, `cjm-transcription-qt`
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ cjm_substrate_qt_kit/__init__.py
4
+ cjm_substrate_qt_kit/keys.py
5
+ cjm_substrate_qt_kit/loopthread.py
6
+ cjm_substrate_qt_kit/style.py
7
+ cjm_substrate_qt_kit.egg-info/PKG-INFO
8
+ cjm_substrate_qt_kit.egg-info/SOURCES.txt
9
+ cjm_substrate_qt_kit.egg-info/dependency_links.txt
10
+ cjm_substrate_qt_kit.egg-info/requires.txt
11
+ cjm_substrate_qt_kit.egg-info/top_level.txt
12
+ tests/test_loopthread.py
@@ -0,0 +1 @@
1
+ cjm_substrate_qt_kit
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cjm-substrate-qt-kit"
7
+ version = "0.0.1"
8
+ description = "Shared foundations for the PySide6 application lane (DEC c4b0d6e5): the loop-thread session base (a private asyncio loop behind a Qt shell), the row-style palette, and the keybinding helper — extracted at their first real duplication between cjm-graph-workbench-qt and cjm-transcription-qt, never speculatively. Born on-graph."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [{name = "Christian J. Mills", email = "9126128+cj-mills@users.noreply.github.com"}]
13
+ classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
14
+ dependencies = ['PySide6']
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/cj-mills/cjm-substrate-qt-kit"
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["cjm_substrate_qt_kit*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ """LoopThreadSession contract: submit/call resolve on the loop thread, the
2
+ on_close hook runs before the loop stops, close is idempotent."""
3
+
4
+ import asyncio
5
+ import threading
6
+
7
+ from cjm_substrate_qt_kit.loopthread import LoopThreadSession
8
+
9
+
10
+ class Probe(LoopThreadSession):
11
+ thread_name = "probe-loop"
12
+
13
+ def __init__(self):
14
+ super().__init__(timeout=5.0)
15
+ self.closed_on: str = ""
16
+
17
+ async def where(self) -> str:
18
+ return threading.current_thread().name
19
+
20
+ async def on_close(self) -> None:
21
+ await asyncio.sleep(0)
22
+ self.closed_on = threading.current_thread().name
23
+
24
+
25
+ def test_submit_and_call_run_on_the_loop_thread():
26
+ p = Probe()
27
+ assert not p.running
28
+ p.start()
29
+ assert p.running
30
+ assert p.call(p.where()) == "probe-loop"
31
+ assert p.submit(p.where()).result(5) == "probe-loop"
32
+ p.close()
33
+
34
+
35
+ def test_on_close_runs_on_loop_then_thread_stops():
36
+ p = Probe()
37
+ p.start()
38
+ thread = p._thread
39
+ p.close()
40
+ assert p.closed_on == "probe-loop"
41
+ assert not thread.is_alive()
42
+ assert not p.running
43
+ p.close() # idempotent