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.
- pyside_iconify-1.0.0/.gitignore +23 -0
- pyside_iconify-1.0.0/CONTRIBUTING.md +88 -0
- pyside_iconify-1.0.0/LICENSE +21 -0
- pyside_iconify-1.0.0/PKG-INFO +192 -0
- pyside_iconify-1.0.0/README.md +158 -0
- pyside_iconify-1.0.0/benchmarks/leak_probe.py +54 -0
- pyside_iconify-1.0.0/benchmarks/list_scroll.py +251 -0
- pyside_iconify-1.0.0/benchmarks/memory_soak.py +123 -0
- pyside_iconify-1.0.0/docs/architecture.md +167 -0
- pyside_iconify-1.0.0/docs/readme/README.de.md +161 -0
- pyside_iconify-1.0.0/docs/readme/README.es.md +163 -0
- pyside_iconify-1.0.0/docs/readme/README.fr.md +161 -0
- pyside_iconify-1.0.0/docs/readme/README.ja.md +155 -0
- pyside_iconify-1.0.0/docs/readme/README.ko.md +153 -0
- pyside_iconify-1.0.0/docs/readme/README.pt-BR.md +161 -0
- pyside_iconify-1.0.0/docs/readme/README.ru.md +158 -0
- pyside_iconify-1.0.0/docs/readme/README.zh-CN.md +147 -0
- pyside_iconify-1.0.0/docs/roadmap.md +43 -0
- pyside_iconify-1.0.0/examples/_common.py +29 -0
- pyside_iconify-1.0.0/examples/_i18n.py +75 -0
- pyside_iconify-1.0.0/examples/_theme.py +52 -0
- pyside_iconify-1.0.0/examples/async_loading.py +105 -0
- pyside_iconify-1.0.0/examples/button_states.py +116 -0
- pyside_iconify-1.0.0/examples/network_showcase.py +365 -0
- pyside_iconify-1.0.0/examples/offline_showcase.py +740 -0
- pyside_iconify-1.0.0/examples/set_icon_targets.py +121 -0
- pyside_iconify-1.0.0/pyproject.toml +83 -0
- pyside_iconify-1.0.0/src/pyside_iconify/__init__.py +212 -0
- pyside_iconify-1.0.0/src/pyside_iconify/__init__.pyi +163 -0
- pyside_iconify-1.0.0/src/pyside_iconify/_api.py +215 -0
- pyside_iconify-1.0.0/src/pyside_iconify/_config.py +169 -0
- pyside_iconify-1.0.0/src/pyside_iconify/_errors.py +93 -0
- pyside_iconify-1.0.0/src/pyside_iconify/_logging.py +7 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/__init__.py +6 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/colors.py +261 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/icon_data.py +315 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/names.py +38 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/registry.py +242 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/sizes.py +88 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/svg.py +205 -0
- pyside_iconify-1.0.0/src/pyside_iconify/core/types.py +181 -0
- pyside_iconify-1.0.0/src/pyside_iconify/integrations/__init__.py +13 -0
- pyside_iconify-1.0.0/src/pyside_iconify/integrations/set_icon.py +222 -0
- pyside_iconify-1.0.0/src/pyside_iconify/network/__init__.py +33 -0
- pyside_iconify-1.0.0/src/pyside_iconify/network/client.py +533 -0
- pyside_iconify-1.0.0/src/pyside_iconify/network/disk_cache.py +287 -0
- pyside_iconify-1.0.0/src/pyside_iconify/network/downloader.py +359 -0
- pyside_iconify-1.0.0/src/pyside_iconify/pack.py +332 -0
- pyside_iconify-1.0.0/src/pyside_iconify/py.typed +0 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/__init__.py +6 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/cache.py +131 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/engine.py +454 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/options.py +171 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/placeholder.py +57 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/qt_adapter.py +156 -0
- pyside_iconify-1.0.0/src/pyside_iconify/rendering/renderer.py +138 -0
- pyside_iconify-1.0.0/src/pyside_iconify/widgets/__init__.py +5 -0
- pyside_iconify-1.0.0/src/pyside_iconify/widgets/animation.py +130 -0
- pyside_iconify-1.0.0/src/pyside_iconify/widgets/icon_widget.py +630 -0
- pyside_iconify-1.0.0/tests/__init__.py +1 -0
- pyside_iconify-1.0.0/tests/conftest.py +81 -0
- pyside_iconify-1.0.0/tests/helpers.py +43 -0
- pyside_iconify-1.0.0/tests/test_api.py +94 -0
- pyside_iconify-1.0.0/tests/test_colors.py +93 -0
- pyside_iconify-1.0.0/tests/test_core.py +88 -0
- pyside_iconify-1.0.0/tests/test_integrations.py +229 -0
- pyside_iconify-1.0.0/tests/test_network.py +256 -0
- pyside_iconify-1.0.0/tests/test_network_integration.py +116 -0
- pyside_iconify-1.0.0/tests/test_out_of_box.py +71 -0
- pyside_iconify-1.0.0/tests/test_pack_integration.py +84 -0
- pyside_iconify-1.0.0/tests/test_render_cache.py +122 -0
- pyside_iconify-1.0.0/tests/test_threads.py +59 -0
- 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]}")
|