pyside-iconify 1.0.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.
Files changed (73) hide show
  1. pyside_iconify-1.0.0/.gitignore +23 -0
  2. pyside_iconify-1.0.0/CONTRIBUTING.md +88 -0
  3. pyside_iconify-1.0.0/LICENSE +21 -0
  4. pyside_iconify-1.0.0/PKG-INFO +192 -0
  5. pyside_iconify-1.0.0/README.md +158 -0
  6. pyside_iconify-1.0.0/benchmarks/leak_probe.py +54 -0
  7. pyside_iconify-1.0.0/benchmarks/list_scroll.py +251 -0
  8. pyside_iconify-1.0.0/benchmarks/memory_soak.py +123 -0
  9. pyside_iconify-1.0.0/docs/architecture.md +167 -0
  10. pyside_iconify-1.0.0/docs/readme/README.de.md +161 -0
  11. pyside_iconify-1.0.0/docs/readme/README.es.md +163 -0
  12. pyside_iconify-1.0.0/docs/readme/README.fr.md +161 -0
  13. pyside_iconify-1.0.0/docs/readme/README.ja.md +155 -0
  14. pyside_iconify-1.0.0/docs/readme/README.ko.md +153 -0
  15. pyside_iconify-1.0.0/docs/readme/README.pt-BR.md +161 -0
  16. pyside_iconify-1.0.0/docs/readme/README.ru.md +158 -0
  17. pyside_iconify-1.0.0/docs/readme/README.zh-CN.md +147 -0
  18. pyside_iconify-1.0.0/docs/roadmap.md +43 -0
  19. pyside_iconify-1.0.0/examples/_common.py +29 -0
  20. pyside_iconify-1.0.0/examples/_i18n.py +75 -0
  21. pyside_iconify-1.0.0/examples/_theme.py +52 -0
  22. pyside_iconify-1.0.0/examples/async_loading.py +105 -0
  23. pyside_iconify-1.0.0/examples/button_states.py +116 -0
  24. pyside_iconify-1.0.0/examples/network_showcase.py +365 -0
  25. pyside_iconify-1.0.0/examples/offline_showcase.py +740 -0
  26. pyside_iconify-1.0.0/examples/set_icon_targets.py +121 -0
  27. pyside_iconify-1.0.0/pyproject.toml +83 -0
  28. pyside_iconify-1.0.0/src/pyside_iconify/__init__.py +212 -0
  29. pyside_iconify-1.0.0/src/pyside_iconify/__init__.pyi +163 -0
  30. pyside_iconify-1.0.0/src/pyside_iconify/_api.py +215 -0
  31. pyside_iconify-1.0.0/src/pyside_iconify/_config.py +169 -0
  32. pyside_iconify-1.0.0/src/pyside_iconify/_errors.py +93 -0
  33. pyside_iconify-1.0.0/src/pyside_iconify/_logging.py +7 -0
  34. pyside_iconify-1.0.0/src/pyside_iconify/core/__init__.py +6 -0
  35. pyside_iconify-1.0.0/src/pyside_iconify/core/colors.py +261 -0
  36. pyside_iconify-1.0.0/src/pyside_iconify/core/icon_data.py +315 -0
  37. pyside_iconify-1.0.0/src/pyside_iconify/core/names.py +38 -0
  38. pyside_iconify-1.0.0/src/pyside_iconify/core/registry.py +242 -0
  39. pyside_iconify-1.0.0/src/pyside_iconify/core/sizes.py +88 -0
  40. pyside_iconify-1.0.0/src/pyside_iconify/core/svg.py +205 -0
  41. pyside_iconify-1.0.0/src/pyside_iconify/core/types.py +181 -0
  42. pyside_iconify-1.0.0/src/pyside_iconify/integrations/__init__.py +13 -0
  43. pyside_iconify-1.0.0/src/pyside_iconify/integrations/set_icon.py +222 -0
  44. pyside_iconify-1.0.0/src/pyside_iconify/network/__init__.py +33 -0
  45. pyside_iconify-1.0.0/src/pyside_iconify/network/client.py +533 -0
  46. pyside_iconify-1.0.0/src/pyside_iconify/network/disk_cache.py +287 -0
  47. pyside_iconify-1.0.0/src/pyside_iconify/network/downloader.py +359 -0
  48. pyside_iconify-1.0.0/src/pyside_iconify/pack.py +332 -0
  49. pyside_iconify-1.0.0/src/pyside_iconify/py.typed +0 -0
  50. pyside_iconify-1.0.0/src/pyside_iconify/rendering/__init__.py +6 -0
  51. pyside_iconify-1.0.0/src/pyside_iconify/rendering/cache.py +131 -0
  52. pyside_iconify-1.0.0/src/pyside_iconify/rendering/engine.py +454 -0
  53. pyside_iconify-1.0.0/src/pyside_iconify/rendering/options.py +171 -0
  54. pyside_iconify-1.0.0/src/pyside_iconify/rendering/placeholder.py +57 -0
  55. pyside_iconify-1.0.0/src/pyside_iconify/rendering/qt_adapter.py +156 -0
  56. pyside_iconify-1.0.0/src/pyside_iconify/rendering/renderer.py +138 -0
  57. pyside_iconify-1.0.0/src/pyside_iconify/widgets/__init__.py +5 -0
  58. pyside_iconify-1.0.0/src/pyside_iconify/widgets/animation.py +130 -0
  59. pyside_iconify-1.0.0/src/pyside_iconify/widgets/icon_widget.py +630 -0
  60. pyside_iconify-1.0.0/tests/__init__.py +1 -0
  61. pyside_iconify-1.0.0/tests/conftest.py +81 -0
  62. pyside_iconify-1.0.0/tests/helpers.py +43 -0
  63. pyside_iconify-1.0.0/tests/test_api.py +94 -0
  64. pyside_iconify-1.0.0/tests/test_colors.py +93 -0
  65. pyside_iconify-1.0.0/tests/test_core.py +88 -0
  66. pyside_iconify-1.0.0/tests/test_integrations.py +229 -0
  67. pyside_iconify-1.0.0/tests/test_network.py +256 -0
  68. pyside_iconify-1.0.0/tests/test_network_integration.py +116 -0
  69. pyside_iconify-1.0.0/tests/test_out_of_box.py +71 -0
  70. pyside_iconify-1.0.0/tests/test_pack_integration.py +84 -0
  71. pyside_iconify-1.0.0/tests/test_render_cache.py +122 -0
  72. pyside_iconify-1.0.0/tests/test_threads.py +59 -0
  73. pyside_iconify-1.0.0/tests/test_widget.py +208 -0
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .venv-*/
5
+ /build/
6
+ /dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ /.cache/
14
+ /.env
15
+ /.env.*
16
+ !/.env.example
17
+ /.workbuddy/
18
+ /outputs/
19
+ .idea/
20
+ .vscode/
21
+ Thumbs.db
22
+ .DS_Store
23
+ .workbuddy/
@@ -0,0 +1,88 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in contributing. This page covers setup, conventions,
4
+ and the release checklist.
5
+
6
+ ## Development setup
7
+
8
+ Requires [uv](https://docs.astral.sh/uv/) and Python 3.10–3.14.
9
+
10
+ ```powershell
11
+ uv sync --extra pyside6 --group test
12
+ $env:QT_QPA_PLATFORM = "offscreen"
13
+ pytest -v
14
+ ```
15
+
16
+ On macOS/Linux use `export QT_QPA_PLATFORM=offscreen` instead. The test suite
17
+ runs fully offscreen — no desktop session needed.
18
+
19
+ ## Project layout
20
+
21
+ See [docs/architecture.md](docs/architecture.md) for the layering rules
22
+ (`widgets/ → rendering/ → core/`, no Qt in `core/`) and the reasoning behind
23
+ each directory. The short version of "where does my change go":
24
+
25
+ | Change touches | Directory |
26
+ | ---------------------------------------------- | --------------- |
27
+ | Icon names, JSON parsing, colors, sizes | `core/` |
28
+ | QIcon/QPixmap rendering, caching, placeholders | `rendering/` |
29
+ | `IconWidget`, QSS, animation | `widgets/` |
30
+ | Downloads, disk cache | `network/` |
31
+ | Putting icons on existing widgets | `integrations/` |
32
+ | Public API surface, global config | package root |
33
+
34
+ ## Conventions
35
+
36
+ - **Type hints everywhere**; the package ships `py.typed`.
37
+ No bare `Any` in public signatures.
38
+ - **Errors are loud.** Unparseable input raises a specific exception
39
+ (`InvalidColorError`, `InvalidSizeError`, …) — never a silent fallback.
40
+ - **Defaults are side-effect free.** Visual effects (hover, selection) are
41
+ explicit opt-in via parameters.
42
+ - **Platform behavior is verified with probes before being designed against**
43
+ (offscreen platforms can differ from real ones; measure, then design).
44
+ - Comments stay minimal — what/why, no debugging history.
45
+ - No emoji in code, docs, or commit messages.
46
+
47
+ ## Testing expectations
48
+
49
+ - Every bug fix ships with a regression test that fails without the fix.
50
+ - Rendering changes: assert actual pixels (`pixmap.toImage().pixelColor(...)`)
51
+ or rendered output, not just that "no exception occurred".
52
+ - Remember `update()` is an asynchronous repaint request — call `grab()` to
53
+ force synchronous rendering before asserting on cache keys or pixels.
54
+ - Performance-relevant changes: run `uv run benchmarks/list_scroll.py`
55
+ before and after, and quote both numbers in the PR.
56
+
57
+ ## Translations
58
+
59
+ The README ships in several languages under `docs/readme/`. English
60
+ (`README.md`) is the source of truth; fixes to translations are welcome and
61
+ are among the easiest first contributions.
62
+
63
+ ## Release checklist
64
+
65
+ Maintainers only. All build/publish commands are run by a maintainer.
66
+
67
+ 1. Run the full suite: `pytest -v` (all green, including
68
+ `tests/test_render_cache.py`).
69
+ 2. Long-run memory soak: `uv run benchmarks/memory_soak.py` for at least
70
+ 30 minutes; RSS must not grow without bound.
71
+ 3. Build: `uv build` — then audit the artifacts:
72
+ - no `__pycache__`, `.pytest_cache`, `.workbuddy`, or temp files inside
73
+ - no secrets, tokens, or local paths
74
+ - `py.typed` present in the wheel
75
+ - `LICENSE` and README present in the sdist
76
+ 4. Bump `__version__` in `src/pyside_iconify/__init__.py`, commit and push.
77
+ 5. Tag and push the tag; the publish workflow does the rest (build, wait for
78
+ approval of the `pypi-release` environment, upload to PyPI, then create
79
+ the GitHub Release with generated notes):
80
+
81
+ ```powershell
82
+ git tag v1.0.0
83
+ git push origin v1.0.0
84
+ ```
85
+
86
+ The tag must equal `v` + `__version__`, otherwise the build job fails.
87
+ To rehearse on TestPyPI, run the "Publish to PyPI" workflow manually and
88
+ choose `testpypi`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pyside-iconify 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,192 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyside-iconify
3
+ Version: 1.0.0
4
+ Summary: An independent Iconify icon integration for PySide6, producing standard QIcon objects.
5
+ Project-URL: Homepage, https://github.com/LingChen-tsjmdlc/pyside-iconify
6
+ Project-URL: Source, https://github.com/LingChen-tsjmdlc/pyside-iconify
7
+ Project-URL: Issues, https://github.com/LingChen-tsjmdlc/pyside-iconify/issues
8
+ Author: jerrylu
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: iconify,icons,pyside,pyside6,qt,svg
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: MacOS X
14
+ Classifier: Environment :: Win32 (MS Windows)
15
+ Classifier: Environment :: X11 Applications :: Qt
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Software Development :: User Interfaces
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: <3.15,>=3.10
28
+ Provides-Extra: pyside2
29
+ Requires-Dist: pyside2==5.15.2.1; (python_version < '3.11') and extra == 'pyside2'
30
+ Provides-Extra: pyside6
31
+ Requires-Dist: pyside6-essentials<7,>=6.11.2; (python_version >= '3.14') and extra == 'pyside6'
32
+ Requires-Dist: pyside6-essentials<7,>=6.8.3; (python_version >= '3.10' and python_version < '3.14') and extra == 'pyside6'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # pyside-iconify
36
+
37
+ **Give `QIcon` a data source.**
38
+
39
+ English | [中文](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.zh-CN.md) | [日本語](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ja.md) | [한국어](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ko.md) | [Español](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.es.md) | [Français](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.fr.md) | [Deutsch](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.de.md) | [Русский](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ru.md) | [Português (BR)](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.pt-BR.md)
40
+
41
+ ```python
42
+ IconWidget("mdi:home", color="red")
43
+ ```
44
+
45
+ One line, one icon. No SVG or PNG files to manage — icons are fetched on demand
46
+ from the [Iconify](https://iconify.design) ecosystem by name and cached
47
+ automatically. Everything still ends up as a standard `QIcon`, so it works with
48
+ any widget that accepts icons.
49
+
50
+ > **Status**: under active development, not yet published to PyPI. The public
51
+ > API may still change before the first release.
52
+ >
53
+ > This project is **not** affiliated with the Iconify project.
54
+
55
+ ## Why
56
+
57
+ | Without | With pyside-iconify |
58
+ | ----------------------------------------- | ------------------------------------------ |
59
+ | `QIcon("assets/home.svg")` | `get_icon("mdi:home")` |
60
+ | Managing SVG/PNG files and resource paths | Icons by name from the Iconify ecosystem |
61
+ | Multiple asset sets for colors and sizes | `color`, `size`, `rotate` as parameters |
62
+ | Per-widget icon plumbing | Standard `QIcon`, works everywhere |
63
+ | Manual light/dark theme icon variants | Follows the widget's palette automatically |
64
+
65
+ ## Quick start
66
+
67
+ ```powershell
68
+ pip install "pyside-iconify[pyside6]"
69
+ ```
70
+
71
+ ```python
72
+ from PySide6.QtWidgets import QApplication, QPushButton
73
+ from pyside_iconify import IconWidget, get_icon
74
+
75
+ app = QApplication([])
76
+
77
+ # A standalone icon widget
78
+ icon = IconWidget("mdi:home", size=24, color="red")
79
+
80
+ # Or hand a standard QIcon to any widget
81
+ button = QPushButton("Settings")
82
+ button.setIcon(get_icon("mdi:cog", size=20))
83
+
84
+ app.exec()
85
+ ```
86
+
87
+ That is the whole API for the common case. Icons are downloaded on first use,
88
+ cached on disk, and rendered locally afterwards.
89
+
90
+ ## Highlights
91
+
92
+ - **Theme aware** — with no color set, icons follow the widget's text color and
93
+ switch automatically between light and dark themes; disabled icons fade
94
+ instead of turning gray (so multi-color icons fade correctly too)
95
+ - **Full color control** — single color, `{original: replacement}` mapping for
96
+ multi-color icons, and per-theme overrides (`color_light_theme` /
97
+ `color_dark_theme`)
98
+ - **Sizes the way you think** — numbers, `(w, h)`, `QSize`, `"24px"`, or
99
+ `"1.5em"` (follows the widget's font)
100
+ - **HiDPI native** — sizes are logical pixels; physical pixels are rendered
101
+ per screen scale factor, no @2x/@3x asset sets
102
+ - **Loading vs. missing are distinct** — loading shows blank, a confirmed miss
103
+ shows a built-in placeholder and emits `failed`; optional `fallback` icon
104
+ - **QSS friendly** — `qss="toolbar primary"` tags for grouping, style with
105
+ native `setStyleSheet()`; priority: parameters → QSS → theme
106
+ - **Typed and logged** — full type hints with `py.typed`, standard `logging`
107
+ (silent by default)
108
+
109
+ ## Parameters
110
+
111
+ Shared by `IconWidget` and `get_icon`:
112
+
113
+ ```python
114
+ get_icon(name, *, size=24, color=None, color_light_theme=None,
115
+ color_dark_theme=None, selected_color=None, opacity=1.0,
116
+ width=None, height=None, rotate=0, spin=False,
117
+ h_flip=False, v_flip=False)
118
+
119
+ IconWidget(name, *, ..., hover_color=None, qss=None, parent=None)
120
+ ```
121
+
122
+ | Parameter | Meaning |
123
+ | ---------------------------------------- | ---------------------------------------------------------------------- |
124
+ | `name` | Icon name like `"mdi:home"`, or inline icon data |
125
+ | `size` | Number, `(w, h)`, `QSize`, `"24px"`, or `"1.5em"` |
126
+ | `color` | A color, or a `{original: replacement}` mapping |
127
+ | `color_light_theme` / `color_dark_theme` | Per-theme overrides, win over `color` |
128
+ | `selected_color` | Color when the item is selected (item views) |
129
+ | `hover_color` | IconWidget / `set_icon` hover color — explicit opt-in |
130
+ | `opacity` | Overall opacity 0–1; multiplied with color alpha |
131
+ | `width` / `height` | Non-square icons; win over `size` |
132
+ | `rotate` | Any angle in degrees, auto-shrunk to avoid clipping |
133
+ | `spin` | Continuous rotation for loading indicators |
134
+ | `h_flip` / `v_flip` | Horizontal / vertical flip |
135
+ | `qss` | QSS grouping tags, `IconWidget[qss~="tag"]` selector (IconWidget only) |
136
+
137
+ Colors accept CSS/SVG names, HEX 3/4/6/8 digit (`#RRGGBBAA` order),
138
+ `rgb()/rgba()/hsl()/hsla()`, tuples, `QColor`, and `Qt.GlobalColor`.
139
+ Unparseable colors raise instead of silently turning black.
140
+
141
+ ## Offline packaging
142
+
143
+ For air-gapped machines or zero-external-dependency builds, pack icons at
144
+ release time:
145
+
146
+ ```powershell
147
+ python -m pyside_iconify.pack -o icons.json
148
+ ```
149
+
150
+ ```python
151
+ from pyside_iconify import load_bundle
152
+
153
+ load_bundle("icons.json") # at app startup; works inside PyInstaller too
154
+ ```
155
+
156
+ See `src/pyside_iconify/pack.py` and `tests/test_pack_integration.py` for the
157
+ full CLI and API. Local data always takes priority over the network.
158
+
159
+ ## Performance
160
+
161
+ Three-layer cache (icon data → SVG text → rendered bitmaps) with a memory
162
+ budget, per-frame work kept to a cache lookup and one `drawPixmap`. A 1000-row
163
+ `QListWidget` builds in ~270 ms and scrolls at native speed. For large grids
164
+ use item views with `get_icon`; `IconWidget` is meant for standalone icon
165
+ slots.
166
+
167
+ ## Support scope
168
+
169
+ | Item | Scope |
170
+ | ------------ | --------------------------------- |
171
+ | Python | 3.10 – 3.14 |
172
+ | Qt binding | PySide6 |
173
+ | OS | Windows, macOS, Linux (CI on macOS) |
174
+ | PySide2/PyQt | Not supported (PyQt6 maybe later) |
175
+
176
+ ## Development
177
+
178
+ ```powershell
179
+ uv sync --extra pyside6 --group test
180
+ $env:QT_QPA_PLATFORM = "offscreen"
181
+ pytest -v
182
+ ```
183
+
184
+ - Contributing guide and release checklist: [CONTRIBUTING.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/CONTRIBUTING.md)
185
+ - Architecture and design decisions: [docs/architecture.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/architecture.md)
186
+ - Roadmap: [docs/roadmap.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/roadmap.md)
187
+ - Benchmark: `uv run benchmarks/list_scroll.py`
188
+
189
+ ## License
190
+
191
+ [MIT](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/LICENSE). Icon collections are subject to their own licenses; Qt is
192
+ licensed separately by its copyright holders.
@@ -0,0 +1,158 @@
1
+ # pyside-iconify
2
+
3
+ **Give `QIcon` a data source.**
4
+
5
+ English | [中文](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.zh-CN.md) | [日本語](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ja.md) | [한국어](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ko.md) | [Español](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.es.md) | [Français](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.fr.md) | [Deutsch](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.de.md) | [Русский](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.ru.md) | [Português (BR)](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/readme/README.pt-BR.md)
6
+
7
+ ```python
8
+ IconWidget("mdi:home", color="red")
9
+ ```
10
+
11
+ One line, one icon. No SVG or PNG files to manage — icons are fetched on demand
12
+ from the [Iconify](https://iconify.design) ecosystem by name and cached
13
+ automatically. Everything still ends up as a standard `QIcon`, so it works with
14
+ any widget that accepts icons.
15
+
16
+ > **Status**: under active development, not yet published to PyPI. The public
17
+ > API may still change before the first release.
18
+ >
19
+ > This project is **not** affiliated with the Iconify project.
20
+
21
+ ## Why
22
+
23
+ | Without | With pyside-iconify |
24
+ | ----------------------------------------- | ------------------------------------------ |
25
+ | `QIcon("assets/home.svg")` | `get_icon("mdi:home")` |
26
+ | Managing SVG/PNG files and resource paths | Icons by name from the Iconify ecosystem |
27
+ | Multiple asset sets for colors and sizes | `color`, `size`, `rotate` as parameters |
28
+ | Per-widget icon plumbing | Standard `QIcon`, works everywhere |
29
+ | Manual light/dark theme icon variants | Follows the widget's palette automatically |
30
+
31
+ ## Quick start
32
+
33
+ ```powershell
34
+ pip install "pyside-iconify[pyside6]"
35
+ ```
36
+
37
+ ```python
38
+ from PySide6.QtWidgets import QApplication, QPushButton
39
+ from pyside_iconify import IconWidget, get_icon
40
+
41
+ app = QApplication([])
42
+
43
+ # A standalone icon widget
44
+ icon = IconWidget("mdi:home", size=24, color="red")
45
+
46
+ # Or hand a standard QIcon to any widget
47
+ button = QPushButton("Settings")
48
+ button.setIcon(get_icon("mdi:cog", size=20))
49
+
50
+ app.exec()
51
+ ```
52
+
53
+ That is the whole API for the common case. Icons are downloaded on first use,
54
+ cached on disk, and rendered locally afterwards.
55
+
56
+ ## Highlights
57
+
58
+ - **Theme aware** — with no color set, icons follow the widget's text color and
59
+ switch automatically between light and dark themes; disabled icons fade
60
+ instead of turning gray (so multi-color icons fade correctly too)
61
+ - **Full color control** — single color, `{original: replacement}` mapping for
62
+ multi-color icons, and per-theme overrides (`color_light_theme` /
63
+ `color_dark_theme`)
64
+ - **Sizes the way you think** — numbers, `(w, h)`, `QSize`, `"24px"`, or
65
+ `"1.5em"` (follows the widget's font)
66
+ - **HiDPI native** — sizes are logical pixels; physical pixels are rendered
67
+ per screen scale factor, no @2x/@3x asset sets
68
+ - **Loading vs. missing are distinct** — loading shows blank, a confirmed miss
69
+ shows a built-in placeholder and emits `failed`; optional `fallback` icon
70
+ - **QSS friendly** — `qss="toolbar primary"` tags for grouping, style with
71
+ native `setStyleSheet()`; priority: parameters → QSS → theme
72
+ - **Typed and logged** — full type hints with `py.typed`, standard `logging`
73
+ (silent by default)
74
+
75
+ ## Parameters
76
+
77
+ Shared by `IconWidget` and `get_icon`:
78
+
79
+ ```python
80
+ get_icon(name, *, size=24, color=None, color_light_theme=None,
81
+ color_dark_theme=None, selected_color=None, opacity=1.0,
82
+ width=None, height=None, rotate=0, spin=False,
83
+ h_flip=False, v_flip=False)
84
+
85
+ IconWidget(name, *, ..., hover_color=None, qss=None, parent=None)
86
+ ```
87
+
88
+ | Parameter | Meaning |
89
+ | ---------------------------------------- | ---------------------------------------------------------------------- |
90
+ | `name` | Icon name like `"mdi:home"`, or inline icon data |
91
+ | `size` | Number, `(w, h)`, `QSize`, `"24px"`, or `"1.5em"` |
92
+ | `color` | A color, or a `{original: replacement}` mapping |
93
+ | `color_light_theme` / `color_dark_theme` | Per-theme overrides, win over `color` |
94
+ | `selected_color` | Color when the item is selected (item views) |
95
+ | `hover_color` | IconWidget / `set_icon` hover color — explicit opt-in |
96
+ | `opacity` | Overall opacity 0–1; multiplied with color alpha |
97
+ | `width` / `height` | Non-square icons; win over `size` |
98
+ | `rotate` | Any angle in degrees, auto-shrunk to avoid clipping |
99
+ | `spin` | Continuous rotation for loading indicators |
100
+ | `h_flip` / `v_flip` | Horizontal / vertical flip |
101
+ | `qss` | QSS grouping tags, `IconWidget[qss~="tag"]` selector (IconWidget only) |
102
+
103
+ Colors accept CSS/SVG names, HEX 3/4/6/8 digit (`#RRGGBBAA` order),
104
+ `rgb()/rgba()/hsl()/hsla()`, tuples, `QColor`, and `Qt.GlobalColor`.
105
+ Unparseable colors raise instead of silently turning black.
106
+
107
+ ## Offline packaging
108
+
109
+ For air-gapped machines or zero-external-dependency builds, pack icons at
110
+ release time:
111
+
112
+ ```powershell
113
+ python -m pyside_iconify.pack -o icons.json
114
+ ```
115
+
116
+ ```python
117
+ from pyside_iconify import load_bundle
118
+
119
+ load_bundle("icons.json") # at app startup; works inside PyInstaller too
120
+ ```
121
+
122
+ See `src/pyside_iconify/pack.py` and `tests/test_pack_integration.py` for the
123
+ full CLI and API. Local data always takes priority over the network.
124
+
125
+ ## Performance
126
+
127
+ Three-layer cache (icon data → SVG text → rendered bitmaps) with a memory
128
+ budget, per-frame work kept to a cache lookup and one `drawPixmap`. A 1000-row
129
+ `QListWidget` builds in ~270 ms and scrolls at native speed. For large grids
130
+ use item views with `get_icon`; `IconWidget` is meant for standalone icon
131
+ slots.
132
+
133
+ ## Support scope
134
+
135
+ | Item | Scope |
136
+ | ------------ | --------------------------------- |
137
+ | Python | 3.10 – 3.14 |
138
+ | Qt binding | PySide6 |
139
+ | OS | Windows, macOS, Linux (CI on macOS) |
140
+ | PySide2/PyQt | Not supported (PyQt6 maybe later) |
141
+
142
+ ## Development
143
+
144
+ ```powershell
145
+ uv sync --extra pyside6 --group test
146
+ $env:QT_QPA_PLATFORM = "offscreen"
147
+ pytest -v
148
+ ```
149
+
150
+ - Contributing guide and release checklist: [CONTRIBUTING.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/CONTRIBUTING.md)
151
+ - Architecture and design decisions: [docs/architecture.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/architecture.md)
152
+ - Roadmap: [docs/roadmap.md](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/docs/roadmap.md)
153
+ - Benchmark: `uv run benchmarks/list_scroll.py`
154
+
155
+ ## License
156
+
157
+ [MIT](https://github.com/LingChen-tsjmdlc/pyside-iconify/blob/main/LICENSE). Icon collections are subject to their own licenses; Qt is
158
+ licensed separately by its copyright holders.
@@ -0,0 +1,54 @@
1
+ """Diagnose the soak leak: diff two tracemalloc snapshots across cycles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+ import sys
7
+ import tracemalloc
8
+
9
+ sys.path.insert(0, "benchmarks")
10
+ sys.path.insert(0, "src")
11
+
12
+ from PySide6.QtCore import QEvent # noqa: E402
13
+ from PySide6.QtWidgets import QApplication, QTabWidget # noqa: E402
14
+
15
+ import memory_soak # noqa: E402
16
+
17
+ app = QApplication([])
18
+ memory_soak.register_icons()
19
+
20
+ window = QTabWidget()
21
+ window.resize(800, 600)
22
+ window.show()
23
+ app.processEvents()
24
+
25
+ tracemalloc.start(25)
26
+
27
+
28
+ def run_cycles(n: int) -> None:
29
+ for _ in range(n):
30
+ lw, area = memory_soak.build_tabs()
31
+ while window.count():
32
+ page = window.widget(0)
33
+ window.removeTab(0)
34
+ page.deleteLater()
35
+ window.addTab(lw, "list")
36
+ window.addTab(area, "grid")
37
+ app.processEvents()
38
+ app.sendPostedEvents(None, QEvent.Type.DeferredDelete)
39
+ gc.collect()
40
+
41
+
42
+ run_cycles(3)
43
+ gc.collect()
44
+ snap1 = tracemalloc.take_snapshot()
45
+
46
+ run_cycles(10)
47
+ gc.collect()
48
+ snap2 = tracemalloc.take_snapshot()
49
+
50
+ current, _ = tracemalloc.get_traced_memory()
51
+ print(f"traced now: {current / 1e6:.1f} MB")
52
+ print("=== top growth between snapshots ===")
53
+ for stat in snap2.compare_to(snap1, "lineno")[:12]:
54
+ print(f"{stat.size_diff / 1e6:+8.2f} MB {stat.count_diff:+6d} {stat.traceback[0]}")