pywinui 0.1.0a1__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.
pywinui/__init__.py ADDED
@@ -0,0 +1,553 @@
1
+ """
2
+ PyWinUI — a small, Pythonic wrapper over PyWinRT's ``winui3`` (Windows App SDK)
3
+ bindings.
4
+
5
+ Goal: declare native WinUI 3 UIs the way you'd write Flet/Flutter-style Python,
6
+ while all the verbose WinRT glue stays in ONE place (the `_Native` layer below).
7
+
8
+ import pywinui as ui
9
+
10
+ class Demo(ui.App):
11
+ def build(self):
12
+ count = ui.TextBlock("0", font_size=32)
13
+
14
+ def bump(sender, args):
15
+ count.text = str(int(count.text) + 1)
16
+
17
+ return ui.Window(
18
+ title="PyWinUI Demo",
19
+ content=ui.StackPanel(
20
+ spacing=12, padding=24,
21
+ children=[
22
+ ui.TextBlock("Counter", font_size=20),
23
+ count,
24
+ ui.Button("Increment", on_click=bump),
25
+ ],
26
+ ),
27
+ )
28
+
29
+ Demo().run()
30
+
31
+ --------------------------------------------------------------------------------
32
+ STATUS: working proof of concept — verified end-to-end on Windows 11 against
33
+ winui3 3.2.1 / Python 3.13. A real window opens, native controls realize, click
34
+ handlers mutate them, and off-thread writes marshal back via the DispatcherQueue.
35
+
36
+ The import paths are confirmed: `winui3.microsoft.ui.xaml` is correct (the
37
+ PyWinRT docs' `winui3.microsoft.windows.ui.xaml` was indeed a typo).
38
+
39
+ The async bridge is verified too: `async def` handlers, awaiting WinRT async
40
+ operations, and off-thread writes marshalling back to the UI. No `# VERIFY`
41
+ tags remain — `TextBox` and `Grid` are the only controls not yet instantiated,
42
+ and they follow the same pattern as the verified ones.
43
+ --------------------------------------------------------------------------------
44
+ """
45
+
46
+ from __future__ import annotations
47
+ import asyncio
48
+ import contextlib
49
+ import contextvars
50
+ import inspect
51
+ import threading
52
+ from typing import Any, Callable, Optional
53
+
54
+
55
+ # =============================================================================
56
+ # NATIVE GLUE — the only place that imports winui3.
57
+ # Swap/verify these against the installed packages; the rest of the file is
58
+ # plain Python and never mentions WinRT directly.
59
+ # =============================================================================
60
+ class _Native:
61
+ """Lazy holder for winui3 modules. Imported on first use so that importing
62
+ `pywinui` on a non-Windows box (e.g. for tests of the pure-Python parts)
63
+ doesn't explode."""
64
+
65
+ _loaded = False
66
+
67
+ def load(self) -> None:
68
+ if self._loaded:
69
+ return
70
+ try:
71
+ # Import paths confirmed against winui3 3.2.1 on Windows 11.
72
+ from winui3.microsoft.ui.xaml import Application, Window, Thickness
73
+ from winui3.microsoft.ui.xaml import Visibility
74
+ from winui3.microsoft.ui.xaml.controls import (
75
+ Button, TextBlock, TextBox, StackPanel, Grid,
76
+ )
77
+ # Separate package from the Xaml one — the dispatcher lives in
78
+ # Microsoft.UI.Dispatching, not under Xaml.
79
+ from winui3.microsoft.ui.dispatching import DispatcherQueue
80
+ from winui3.microsoft.windows.applicationmodel.dynamicdependency import (
81
+ bootstrap,
82
+ )
83
+ # WinRT object-typed properties (Button.Content etc.) won't accept a
84
+ # bare Python str — it has to be boxed into an IInspectable first.
85
+ from winrt.windows.foundation import PropertyValue
86
+ except ImportError as exc: # give a useful message, not a raw traceback
87
+ raise RuntimeError(
88
+ "PyWinUI requires the winui3 bindings and the Windows App "
89
+ "Runtime.\n"
90
+ # ASCII only: this prints to consoles still using cp1252, where
91
+ # non-ASCII punctuation shows up as mojibake.
92
+ " 1) pip install the namespace packages (each namespace is its\n"
93
+ " own wheel - .Controls and .Bootstrap are NOT included by\n"
94
+ " their parent):\n"
95
+ " winui3-Microsoft.UI.Xaml\n"
96
+ " winui3-Microsoft.UI.Xaml.Controls\n"
97
+ " winui3-Microsoft.UI.Dispatching\n"
98
+ " winui3-Microsoft.Windows.ApplicationModel.DynamicDependency\n"
99
+ " winui3-Microsoft.Windows.ApplicationModel."
100
+ "DynamicDependency.Bootstrap\n"
101
+ " winrt-Windows.Foundation (boxing + event delegates)\n"
102
+ " 2) Install the Windows App Runtime: "
103
+ "https://aka.ms/windowsappsdk/runtime\n"
104
+ " 3) Use python.org Python, NOT the Microsoft Store build "
105
+ "(the Store build fails bootstrap with ERROR_NOT_SUPPORTED).\n"
106
+ " 4) If the cause above is 'DLL load failed ... filename or "
107
+ "extension is too long', your environment sits too deep: the\n"
108
+ " winui3 DLL names are long enough to exceed Windows'"
109
+ " 260-char MAX_PATH. Use a shorter venv path or enable long\n"
110
+ " paths (LongPathsEnabled)."
111
+ ) from exc
112
+
113
+ # Stash the pieces the wrapper needs.
114
+ self.Application = Application
115
+ self.Window = Window
116
+ self.Thickness = Thickness
117
+ self.Visibility = Visibility
118
+ self.Button = Button
119
+ self.TextBlock = TextBlock
120
+ self.TextBox = TextBox
121
+ self.StackPanel = StackPanel
122
+ self.Grid = Grid
123
+ self.DispatcherQueue = DispatcherQueue
124
+ self.bootstrap = bootstrap
125
+ self.PropertyValue = PropertyValue
126
+ self._loaded = True
127
+
128
+ def box(self, value: Any):
129
+ """Box a Python primitive into an IInspectable for object-typed
130
+ properties (``Button.Content``, ``ContentControl.Content``, ...).
131
+ WinRT objects and realized controls pass through untouched."""
132
+ if isinstance(value, str):
133
+ return self.PropertyValue.create_string(value)
134
+ if isinstance(value, bool):
135
+ return self.PropertyValue.create_boolean(value)
136
+ if isinstance(value, int):
137
+ return self.PropertyValue.create_int32(value)
138
+ if isinstance(value, float):
139
+ return self.PropertyValue.create_double(value)
140
+ return value
141
+
142
+ def thickness(self, value: Any):
143
+ """Turn a Pythonic padding/margin (number or 4-tuple) into a WinRT
144
+ Thickness struct."""
145
+ if isinstance(value, (int, float)):
146
+ l = t = r = b = float(value)
147
+ else:
148
+ l, t, r, b = (float(v) for v in value)
149
+ return self.Thickness(l, t, r, b)
150
+
151
+ def visibility(self, value: bool):
152
+ """Map a Pythonic bool to WinUI's Visibility enum (Visible/Collapsed —
153
+ note WinUI has no 'Hidden'; Collapsed removes it from layout)."""
154
+ return self.Visibility.VISIBLE if value else self.Visibility.COLLAPSED
155
+
156
+
157
+ _native = _Native()
158
+
159
+
160
+ # =============================================================================
161
+ # CONTEXT-MANAGER TREE BUILDING
162
+ # `with SomeContainer():` opens a scope; any Widget constructed inside it
163
+ # auto-attaches to that container. Implemented with a ContextVar (async-safe)
164
+ # instead of threading.local, so it survives an asyncio/dispatcher layer later.
165
+ # Nesting works via the token stack: each __enter__ set()s and each __exit__
166
+ # reset()s, so `_open_parent.get()` is always the innermost open container.
167
+ # =============================================================================
168
+ _open_parent: contextvars.ContextVar[Optional["Widget"]] = contextvars.ContextVar(
169
+ "pywinui_open_parent", default=None
170
+ )
171
+
172
+
173
+ @contextlib.contextmanager
174
+ def detached():
175
+ """Suppress auto-attach inside a `with` block. Widgets constructed in this
176
+ scope are NOT added to the enclosing container, so you can build one for
177
+ manual placement later:
178
+
179
+ with ui.StackPanel() as panel:
180
+ ui.TextBlock("in tree") # attaches
181
+ with ui.detached():
182
+ loose = ui.Button("later") # does NOT attach
183
+ panel.add(loose) # place it explicitly
184
+ """
185
+ token = _open_parent.set(None)
186
+ try:
187
+ yield
188
+ finally:
189
+ _open_parent.reset(token)
190
+
191
+
192
+ # =============================================================================
193
+ # DISPATCHER + ASYNC BRIDGE
194
+ # WinUI has ONE UI thread; touching a control from another thread throws, and
195
+ # WinRT I/O returns IAsyncOperation objects instead of plain values. This
196
+ # section hides both so handlers can be ordinary `async def`:
197
+ # * run_on_ui(fn) -> marshal a call back onto the UI thread (any thread ok)
198
+ # * as_future(op) -> await a WinRT async op like a normal coroutine
199
+ # * async handlers -> auto-scheduled on a background loop
200
+ # * off-thread widget writes -> auto-marshalled (see Widget._set_native)
201
+ #
202
+ # ARCHITECTURE (v1 — simple + robust): Application.start owns the UI thread with
203
+ # the WinUI message pump, so we can't also run asyncio there. Instead asyncio
204
+ # runs on a separate daemon thread; async handlers run there, and anything that
205
+ # mutates UI hops back via the DispatcherQueue. The nicer alternative — one
206
+ # event loop *driven by* the DispatcherQueue so handlers run on the UI thread —
207
+ # is noted as future work; it removes the marshalling but is far more code.
208
+ # =============================================================================
209
+ class _Dispatcher:
210
+ def __init__(self) -> None:
211
+ self._queue: Any = None
212
+ self._ui_thread_id: Optional[int] = None
213
+
214
+ @property
215
+ def bound(self) -> bool:
216
+ return self._queue is not None
217
+
218
+ def bind_current_thread(self) -> None:
219
+ """Call once from the UI thread during app startup."""
220
+ self._queue = _native.DispatcherQueue.get_for_current_thread()
221
+ self._ui_thread_id = threading.get_ident()
222
+
223
+ def on_ui_thread(self) -> bool:
224
+ return threading.get_ident() == self._ui_thread_id
225
+
226
+ def post(self, fn: Callable[[], Any]) -> None:
227
+ if self._queue is None:
228
+ fn() # no UI yet (e.g. building before start) — just run inline
229
+ return
230
+ self._queue.try_enqueue(fn) # a plain Python callable satisfies the delegate
231
+
232
+
233
+ _dispatcher = _Dispatcher()
234
+ _loop: Optional[asyncio.AbstractEventLoop] = None
235
+
236
+
237
+ def run_on_ui(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
238
+ """Run `fn` on the UI thread. Safe from any thread."""
239
+ _dispatcher.post(lambda: fn(*args, **kwargs))
240
+
241
+
242
+ def as_future(winrt_async_op: Any) -> "asyncio.Future":
243
+ """Adapt a WinRT IAsyncOperation / IAsyncAction into an asyncio Future.
244
+
245
+ text = await ui.as_future(FileIO.read_text_async(file))
246
+
247
+ NOTE: as of PyWinRT 3.2 the projected async types implement ``__await__``
248
+ already, so plain ``await op`` works and is the idiomatic path (errors
249
+ surface as normal Python exceptions either way). This wrapper is kept for
250
+ the cases where you want a real Future — to pass to ``asyncio.gather``,
251
+ ``wait_for``, or to hold and cancel later — and as a stable seam if the
252
+ projection ever changes. It no longer hand-rolls the Completed delegate.
253
+ """
254
+ return asyncio.ensure_future(winrt_async_op)
255
+
256
+
257
+ def _schedule_coro(coro: Any) -> None:
258
+ """Schedule an async-handler coroutine on the background loop."""
259
+ if _loop is None:
260
+ raise RuntimeError("async handler fired before the app loop started")
261
+ asyncio.run_coroutine_threadsafe(coro, _loop)
262
+
263
+
264
+ def _wrap_async(handler: Callable[..., Any]) -> Callable[..., Any]:
265
+ """If a handler is `async def`, return a sync shim that schedules it and
266
+ returns immediately, so the WinRT event callback doesn't block the UI."""
267
+ # inspect, not asyncio: asyncio.iscoroutinefunction is deprecated in 3.14.
268
+ if inspect.iscoroutinefunction(handler):
269
+ def _fire(sender: Any, args: Any, _h: Callable[..., Any] = handler) -> None:
270
+ _schedule_coro(_h(sender, args))
271
+ return _fire
272
+ return handler
273
+
274
+
275
+ # =============================================================================
276
+ # BASE WIDGET
277
+ # Declaration and native realization are deliberately separate: you build a
278
+ # cheap Python tree first, then `_realize()` walks it and creates native
279
+ # objects (which must happen after bootstrap, on the UI thread).
280
+ # =============================================================================
281
+ class Widget:
282
+ # Attribute names that live on the Python object, never forwarded to native.
283
+ _INTERNAL = {"_native", "_props", "_events"}
284
+
285
+ def __init__(self, **props: Any) -> None:
286
+ # Set internals via object.__setattr__ so our custom __setattr__ below
287
+ # doesn't try to forward them to a native object that doesn't exist yet.
288
+ object.__setattr__(self, "_native", None)
289
+ # `attached` is a construct-time-only flag (NOT a live property): it
290
+ # controls the one-shot auto-parenting below. Consume it so it never
291
+ # gets forwarded to native.
292
+ attached = props.pop("attached", True)
293
+ # Split "on_*" handlers out from ordinary properties.
294
+ events = {k[3:]: props.pop(k) for k in list(props) if k.startswith("on_")}
295
+ object.__setattr__(self, "_events", events)
296
+ object.__setattr__(self, "_props", props)
297
+
298
+ # If we're being constructed inside a `with container:` block, attach
299
+ # ourselves to that container. Merely constructing the widget is the
300
+ # side effect that adds it to the tree (nicegui-style). `attached=False`
301
+ # (or a surrounding ui.detached()) opts out for manual placement.
302
+ if attached:
303
+ parent = _open_parent.get()
304
+ if parent is not None:
305
+ parent._adopt(self)
306
+
307
+ # ---- container protocol -------------------------------------------------
308
+ def _adopt(self, child: "Widget") -> None:
309
+ """Attach a child constructed inside this widget's `with` block.
310
+ Leaf controls reject children; containers override this."""
311
+ raise TypeError(f"{type(self).__name__} cannot contain children")
312
+
313
+ def __enter__(self) -> "Widget":
314
+ # Push self as the current parent; remember the token so nested blocks
315
+ # unwind correctly on exit.
316
+ object.__setattr__(self, "_ctx_token", _open_parent.set(self))
317
+ return self
318
+
319
+ def __exit__(self, *exc: Any) -> bool:
320
+ token = self.__dict__.pop("_ctx_token", None)
321
+ if token is not None:
322
+ _open_parent.reset(token)
323
+ return False # never swallow exceptions
324
+
325
+ # ---- override points for subclasses -------------------------------------
326
+ def _construct(self) -> Any:
327
+ """Return a freshly created native WinRT control."""
328
+ raise NotImplementedError
329
+
330
+ def _apply(self) -> None:
331
+ """Apply queued properties to the native control. Default: forward each
332
+ kwarg by name, since PyWinRT already exposes properties as snake_case."""
333
+ for name, value in self._props.items():
334
+ self._set_native(name, value)
335
+
336
+ # ---- property forwarding ------------------------------------------------
337
+ def _set_native(self, name: str, value: Any) -> None:
338
+ # Map Pythonic property names/values onto the WinRT surface.
339
+ if name in ("padding", "margin"):
340
+ value = _native.thickness(value)
341
+ elif name == "visible":
342
+ name, value = "visibility", _native.visibility(value)
343
+ elif name == "content":
344
+ # Object-typed slot: primitives must be boxed, controls must not.
345
+ value = _native.box(value)
346
+ if _dispatcher.bound and not _dispatcher.on_ui_thread():
347
+ # Write coming from an async handler on the background loop: hop it
348
+ # onto the UI thread so the caller never has to think about threads.
349
+ _dispatcher.post(lambda: setattr(self._native, name, value))
350
+ else:
351
+ setattr(self._native, name, value)
352
+
353
+ # ---- realization machinery ----------------------------------------------
354
+ def _realize(self) -> Any:
355
+ if self._native is not None:
356
+ return self._native
357
+ self._native = self._construct()
358
+ self._apply()
359
+ for event, handler in self._events.items():
360
+ # PyWinRT projects WinRT events as add_<event>/remove_<event>.
361
+ # _wrap_async lets handlers be `async def` transparently.
362
+ getattr(self._native, f"add_{event}")(_wrap_async(handler))
363
+ return self._native
364
+
365
+ # ---- the escape hatch ---------------------------------------------------
366
+ @property
367
+ def native(self) -> Any:
368
+ """The underlying WinUI 3 control — the blessed way to reach anything
369
+ the wrapper doesn't cover. Realizes on access (idempotent), so advanced
370
+ users can grab and tweak the real object even mid-build:
371
+
372
+ btn = ui.Button("Save")
373
+ btn.native.background = some_brush # raw WinUI, fully supported
374
+ """
375
+ return self._realize()
376
+
377
+ # ---- Pythonic attribute access ------------------------------------------
378
+ # Lets you write `label.text = "hi"` before OR after realization and read
379
+ # `label.text` back, transparently forwarding to the native control.
380
+ def __setattr__(self, name: str, value: Any) -> None:
381
+ if name in self._INTERNAL or name.startswith("_"):
382
+ object.__setattr__(self, name, value)
383
+ return
384
+ native = self.__dict__.get("_native")
385
+ if native is not None:
386
+ self._set_native(name, value)
387
+ else:
388
+ self._props[name] = value
389
+
390
+ def __getattr__(self, name: str) -> Any:
391
+ # __getattr__ only fires when normal lookup fails, so methods/internals
392
+ # are unaffected. Forward reads to the native control once realized,
393
+ # otherwise return whatever was queued.
394
+ native = self.__dict__.get("_native")
395
+ if native is not None:
396
+ return getattr(native, name)
397
+ props = self.__dict__.get("_props", {})
398
+ if name in props:
399
+ return props[name]
400
+ raise AttributeError(name)
401
+
402
+
403
+ # =============================================================================
404
+ # CONTROLS
405
+ # Adding a new control is the whole point of the design: subclass Widget,
406
+ # implement `_construct`, and (optionally) map a positional arg to a property.
407
+ # =============================================================================
408
+ class TextBlock(Widget):
409
+ def __init__(self, text: str = "", **props: Any) -> None:
410
+ props.setdefault("text", text)
411
+ super().__init__(**props)
412
+
413
+ def _construct(self):
414
+ return _native.TextBlock()
415
+
416
+
417
+ class TextBox(Widget):
418
+ def __init__(self, text: str = "", **props: Any) -> None:
419
+ props.setdefault("text", text)
420
+ super().__init__(**props)
421
+
422
+ def _construct(self):
423
+ return _native.TextBox() # not yet exercised on Windows (same pattern)
424
+
425
+
426
+ class Button(Widget):
427
+ def __init__(self, label: Optional[str] = None, **props: Any) -> None:
428
+ if label is not None:
429
+ props.setdefault("content", label) # WinUI Button uses Content
430
+ super().__init__(**props)
431
+
432
+ def _construct(self):
433
+ return _native.Button()
434
+
435
+
436
+ class _Panel(Widget):
437
+ """Base for layout containers that hold a list of child Widgets."""
438
+
439
+ def __init__(self, children: Optional[list["Widget"]] = None, **props: Any) -> None:
440
+ super().__init__(**props)
441
+ object.__setattr__(self, "_child_widgets", children or [])
442
+
443
+ def _adopt(self, child: "Widget") -> None:
444
+ # Panels hold many children; `with`-adopted ones append to whatever the
445
+ # `children=[...]` kwarg already seeded, so the two styles compose.
446
+ self._child_widgets.append(child)
447
+
448
+ def add(self, *children: "Widget") -> "_Panel":
449
+ """Attach children manually — e.g. ones built inside ui.detached().
450
+ Returns self for chaining."""
451
+ self._child_widgets.extend(children)
452
+ return self
453
+
454
+ def _apply(self) -> None:
455
+ super()._apply() # spacing, padding, orientation, ...
456
+ container = self._native.children
457
+ for child in self._child_widgets:
458
+ container.append(child._realize())
459
+
460
+
461
+ class StackPanel(_Panel):
462
+ def _construct(self):
463
+ return _native.StackPanel()
464
+
465
+
466
+ class Grid(_Panel):
467
+ # Row/column definitions and Grid.Row/Grid.Column attached properties are
468
+ # left as an exercise — attached properties need their own helper. Kept here
469
+ # so the container hierarchy is visible.
470
+ def _construct(self):
471
+ return _native.Grid() # not yet exercised on Windows (same pattern)
472
+
473
+
474
+ # =============================================================================
475
+ # WINDOW + APP LIFECYCLE
476
+ # This is the least-trodden part of the binding; treat _on_start as the piece
477
+ # most likely to need adjustment against the real packages.
478
+ # =============================================================================
479
+ class Window(Widget):
480
+ def __init__(self, content: Optional[Widget] = None, **props: Any) -> None:
481
+ super().__init__(**props)
482
+ object.__setattr__(self, "_content", content)
483
+
484
+ def _adopt(self, child: "Widget") -> None:
485
+ # A Window has a single content slot, so a second child is an error —
486
+ # the "slot" semantics (single vs list) are what let one protocol serve
487
+ # both container kinds.
488
+ if self._content is not None:
489
+ raise TypeError("Window already has content; it holds a single child")
490
+ object.__setattr__(self, "_content", child)
491
+
492
+ def _construct(self):
493
+ return _native.Window()
494
+
495
+ def _apply(self) -> None:
496
+ super()._apply() # title, etc.
497
+ if self._content is not None:
498
+ self._native.content = self._content._realize()
499
+
500
+
501
+ class App:
502
+ """Subclass and implement `build()` returning a Window, then call `run()`."""
503
+
504
+ def build(self) -> Window:
505
+ raise NotImplementedError("App subclasses must implement build() -> Window")
506
+
507
+ def run(self) -> None:
508
+ _native.load()
509
+ self._start_background_loop()
510
+ boot = _native.bootstrap
511
+ # Start the Windows App Runtime; prompt the user to install it if missing.
512
+ opts = boot.InitializeOptions.ON_NO_MATCH_SHOW_UI
513
+ try:
514
+ with boot.initialize(options=opts):
515
+ # Application.start blocks and pumps the UI message loop until exit.
516
+ # The callback runs on the freshly created UI thread.
517
+ _native.Application.start(self._on_start)
518
+ finally:
519
+ self._stop_background_loop()
520
+
521
+ def _on_start(self, _params: Any) -> None:
522
+ # We're on the UI thread now: capture it + the dispatcher queue so
523
+ # off-thread widget writes and run_on_ui() can marshal back here.
524
+ _dispatcher.bind_current_thread()
525
+ # Keep the window on the app: it's the root of the live tree, and both
526
+ # user code and tooling need a handle on it after startup.
527
+ self.window = self.build()
528
+ self.window._realize()
529
+ self.window._native.activate()
530
+
531
+ # -- background asyncio loop (see DISPATCHER + ASYNC BRIDGE) ---------------
532
+ def _start_background_loop(self) -> None:
533
+ global _loop
534
+ _loop = asyncio.new_event_loop()
535
+ self._loop_thread = threading.Thread(
536
+ target=_loop.run_forever, name="pywinui-async", daemon=True
537
+ )
538
+ self._loop_thread.start()
539
+
540
+ def _stop_background_loop(self) -> None:
541
+ global _loop
542
+ if _loop is not None:
543
+ _loop.call_soon_threadsafe(_loop.stop)
544
+ _loop = None
545
+
546
+
547
+ __all__ = [
548
+ "App", "Window",
549
+ "TextBlock", "TextBox", "Button",
550
+ "StackPanel", "Grid",
551
+ "Widget",
552
+ "detached", "run_on_ui", "as_future",
553
+ ]
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywinui
3
+ Version: 0.1.0a1
4
+ Summary: Build native WinUI 3 (Windows App SDK) desktop apps in idiomatic Python
5
+ Project-URL: Homepage, https://github.com/israel-dryer/pywinui
6
+ Project-URL: Repository, https://github.com/israel-dryer/pywinui
7
+ Project-URL: Issues, https://github.com/israel-dryer/pywinui/issues
8
+ Author-email: Israel Dryer <israel.dryer@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Israel Dryer
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: desktop,gui,windows,windows-app-sdk,winrt,winui,winui3
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Environment :: Win32 (MS Windows)
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
37
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Topic :: Software Development :: User Interfaces
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.10
46
+ Requires-Dist: winrt-windows-foundation<4,>=3.2; sys_platform == 'win32'
47
+ Requires-Dist: winui3-microsoft-ui-dispatching<4,>=3.2; sys_platform == 'win32'
48
+ Requires-Dist: winui3-microsoft-ui-xaml-controls<4,>=3.2; sys_platform == 'win32'
49
+ Requires-Dist: winui3-microsoft-ui-xaml<4,>=3.2; sys_platform == 'win32'
50
+ Requires-Dist: winui3-microsoft-windows-applicationmodel-dynamicdependency-bootstrap<4,>=3.2; sys_platform == 'win32'
51
+ Requires-Dist: winui3-microsoft-windows-applicationmodel-dynamicdependency<4,>=3.2; sys_platform == 'win32'
52
+ Provides-Extra: dev
53
+ Requires-Dist: pytest>=8.0; extra == 'dev'
54
+ Provides-Extra: examples
55
+ Requires-Dist: winrt-windows-storage<4,>=3.2; (sys_platform == 'win32') and extra == 'examples'
56
+ Description-Content-Type: text/markdown
57
+
58
+ # PyWinUI
59
+
60
+ Build **native WinUI 3** (Windows App SDK) desktop applications in idiomatic Python.
61
+
62
+ Not a themed look-alike and not a reimplementation — these are real WinUI 3
63
+ controls, wrapped so that you never have to touch WinRT unless you want to.
64
+
65
+ ```python
66
+ import pywinui as ui
67
+
68
+
69
+ class CounterApp(ui.App):
70
+ def build(self):
71
+ count = ui.TextBlock("0", font_size=32)
72
+
73
+ def bump(sender, args):
74
+ count.text = str(int(count.text) + 1)
75
+
76
+ return ui.Window(
77
+ title="PyWinUI Counter",
78
+ content=ui.StackPanel(
79
+ spacing=12, padding=24,
80
+ children=[
81
+ ui.TextBlock("Counter", font_size=20),
82
+ count,
83
+ ui.Button("Increment", on_click=bump),
84
+ ],
85
+ ),
86
+ )
87
+
88
+
89
+ CounterApp().run()
90
+ ```
91
+
92
+ > **Status: early alpha.** The architecture is verified end-to-end against the
93
+ > real bindings on Windows 11, but only five controls are wrapped so far. The API
94
+ > may change. See [Current scope](#current-scope) before depending on it.
95
+
96
+ ## Why now
97
+
98
+ The WinUI 3 projections for Python ([PyWinRT](https://github.com/pywinrt/pywinrt))
99
+ only landed in March 2025. Before that, a Python WinUI 3 app meant hand-rolling
100
+ raw WinRT. The bindings now exist and work; PyWinUI is the ergonomics layer on
101
+ top of them.
102
+
103
+ ## Requirements
104
+
105
+ - **Windows 10/11** with the [Windows App Runtime](https://aka.ms/windowsappsdk/runtime)
106
+ - **python.org Python 3.10+** — *not* the Microsoft Store build, which is a
107
+ packaged app and fails the runtime bootstrap with `ERROR_NOT_SUPPORTED`
108
+
109
+ ## Install
110
+
111
+ ```
112
+ pip install pywinui
113
+ ```
114
+
115
+ The `winui3-*` dependencies are Windows-only and install automatically there.
116
+ On other platforms the package still installs and imports (the native layer is
117
+ loaded lazily), so the test suite and editor tooling work anywhere — but
118
+ anything that realizes a control needs Windows.
119
+
120
+ ## Two ways to build a tree
121
+
122
+ Both are first-class and they compose.
123
+
124
+ ```python
125
+ # Flutter style — the tree is a value. Best for reusable components.
126
+ ui.StackPanel(children=[ui.TextBlock("a"), ui.TextBlock("b")])
127
+
128
+ # With style — handles loops, conditionals and local references far better.
129
+ with ui.StackPanel() as panel:
130
+ ui.TextBlock("Items")
131
+ for name in items:
132
+ ui.Button(name, on_click=make_handler(name))
133
+ ```
134
+
135
+ ## Async without the ceremony
136
+
137
+ Handlers may be `async def`. They're scheduled off the UI thread automatically,
138
+ WinRT async operations are awaited like any coroutine, and writes back to
139
+ widgets are marshalled onto the UI thread for you.
140
+
141
+ ```python
142
+ async def read_file(sender, args):
143
+ status.text = "Reading..."
144
+ file = await StorageFile.get_file_from_path_async(path)
145
+ text = await FileIO.read_text_async(file) # real WinRT I/O
146
+ status.text = f"{len(text)} chars" # marshalled back to the UI
147
+ ```
148
+
149
+ Failures arrive as ordinary Python exceptions — a missing path raises
150
+ `FileNotFoundError`.
151
+
152
+ ## The escape hatch
153
+
154
+ The curated surface covers the common path with type hints, validation and value
155
+ conversions. Anything not wrapped still forwards by name, and `widget.native`
156
+ gives you the raw WinUI control:
157
+
158
+ ```python
159
+ btn = ui.Button("Save")
160
+ btn.native.background = some_brush # raw WinUI, fully supported
161
+ ```
162
+
163
+ ## Current scope
164
+
165
+ | Working | Not yet |
166
+ |---|---|
167
+ | `TextBlock`, `TextBox`, `Button`, `StackPanel`, `Grid`, `Window` | The other ~100 WinUI controls |
168
+ | Both tree-building styles, attach/detach | Data binding, styles, resource dictionaries |
169
+ | `async def` handlers, awaiting WinRT ops | Control templates, virtualized lists |
170
+ | Off-thread writes auto-marshalled | Off-thread *reads* (wrap in `run_on_ui`) |
171
+ | `padding`/`margin`, `visible`, content boxing | `Grid.Row`/`Grid.Column` attached properties |
172
+ | Running from a Python environment | Packaging a double-clickable app (MSIX) |
173
+
174
+ **Packaging is unproven.** Shipping an app to end users means bundling Python and
175
+ the Windows App Runtime, likely as MSIX, and that path has not been prototyped.
176
+ Today this is a library for developers who already have Python installed.
177
+
178
+ ## Troubleshooting
179
+
180
+ **`DLL load failed ... The filename or extension is too long`** — your virtual
181
+ environment path is too deep. The winui3 extension modules have very long file
182
+ names (`_winui3_microsoft_windows_applicationmodel_dynamicdependency_bootstrap`),
183
+ and a nested venv can push them past Windows' 260-character `MAX_PATH`. Use a
184
+ shorter path, or [enable long paths](https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation).
185
+
186
+ **`ERROR_NOT_SUPPORTED` during bootstrap** — you're on the Microsoft Store build
187
+ of Python, which is itself a packaged app. Use the python.org installer.
188
+
189
+ ## Examples
190
+
191
+ ```
192
+ pip install -e ".[examples,dev]"
193
+ python examples/counter.py
194
+ python examples/async_file_read.py
195
+ ```
196
+
197
+ ## Tests
198
+
199
+ ```
200
+ pytest
201
+ ```
202
+
203
+ The suite runs **anywhere** — no Windows required. It swaps in a fake native
204
+ layer to lock down the tree model, both building styles, attach/detach, property
205
+ forwarding, value conversions and thread marshalling. What it deliberately
206
+ cannot cover is the binding boundary itself; that is verified by running the
207
+ examples on Windows.
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,5 @@
1
+ pywinui/__init__.py,sha256=fUhGBee1BVCVBxd-4acHEbghaUPlOWU3LlPqvrNFY38,23678
2
+ pywinui-0.1.0a1.dist-info/METADATA,sha256=CfNZbSV3TTM6nVSG7LtOjhwo3z5Eip9VPZCth5mvRD4,8378
3
+ pywinui-0.1.0a1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
4
+ pywinui-0.1.0a1.dist-info/licenses/LICENSE,sha256=RU7fnTshexL9FueMYVgwu_WNKsNlw1hP0roNhPZaqS8,1069
5
+ pywinui-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Israel Dryer
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.