Contributing#
This page covers working on the library rather than with it: the repository layout, the developer API that packs are built against, and how a new pack is made.
The repository#
Eighteen distributions live in one repository — the base package, sixteen icon packs, and a compatibility shim:
packages/
tkinter-icons/ the renderer, the browser, the pack catalogue
tkinter-icons-bs/ one directory per pack ...
tkinter-icons-mat/
...
ttkbootstrap-icons-shim/ builds the `ttkbootstrap-icons` distribution
tests/ one suite, covering all of them
docs/ this documentation
The shim’s directory is deliberately not named after the distribution it builds: the plain name belonged to the package being renamed away from. Anything mapping tags to directories has to go through .github/scripts/packages.py, which reads every pyproject.toml rather than assuming packages/<dist>.
Setting up#
python -m venv .venv
.venv/Scripts/python -m pip install -e packages/tkinter-icons
.venv/Scripts/python -m pip install --no-deps -e packages/tkinter-icons-bs
.venv/Scripts/python -m pip install -r docs/requirements.txt
.venv/Scripts/python -m pytest -q
Important
--no-deps on the packs is not optional in a working tree. Every pack requires tkinter-icons>=5.0.0, and the base package’s version comes from git describe — so before a v5.0.0 tag exists it reports something like 4.0.1.dev32+g4f9beca, which is below the floor. Without --no-deps, pip decides your local base package will not do and fetches one from PyPI. Their only other dependency is Pillow, which the base install already brought in.
The alternative is to tell setuptools-scm what to claim: SETUPTOOLS_SCM_PRETEND_VERSION_FOR_TKINTER_ICONS=5.0.0.
Install every pack when you are working on the packs themselves, on the packs page of these docs, or on anything that measures glyphs:
.venv/Scripts/python -m pip install --no-deps $(printf -- '-e %s ' packages/tkinter-icons-*/)
Tests#
python -m pytest -q
Most of the suite renders through Icon.render_pil, which is pure Pillow and needs no display. The Tk-level tests skip themselves where there is no display; on Linux CI they run under xvfb-run.
Note
Tk 8.6 cannot reliably create a second interpreter in one process — reloading ttk themes intermittently fails. Tests needing a fresh root guard themselves with pytest.skip on TclError, and which test trips it depends on ordering, so a run reporting one skip and a run reporting none are both correct. This is a Tk limitation, not a bug to fix here.
Building the docs#
python -m sphinx docs docs/_build/html -b html -W -n
The packs page reads each pack’s styles, upstream version, and glyph count from the installed provider rather than from a table someone has to remember to update, and each pack’s preview is drawn at build time with the real font — so a build without the packs installed leaves those columns blank, renders no previews, and warns. -W turns that into a failure; a published table with holes in it would be worse than none. -n does the same for unresolved cross-references, so a renamed API leaves a failing build rather than a dead link.
That same command runs as the docs job in ci.yml, so a pull request that breaks a cross-reference or a curated icon name fails as a status check rather than as a red build somewhere else after merging.
Publishing is Read the Docs, configured by .readthedocs.yaml — there is no deploy workflow to run and no repository setting to flip. It installs all sixteen packs for the same reason the local command needs them, and unshallows the checkout so setuptools-scm can see the tags. The gh-pages branch is a leftover from the mkdocs gh-deploy era and serves nothing.
The developer API#
An icon pack is a distribution that ships a font, a glyph map, measured metrics, and a provider class registered on an entry point. These are the pieces it is built from.
- class tkinter_icons.providers.BaseFontProvider(**kwargs)#
Bases:
ABCBase class for icon providers with class-level caches.
- static format_glyph_name(glyph_name)#
- asset_suffix(style=None)#
Return the filename suffix for this provider’s per-style assets.
Providers backed by one font file name their assets
glyphmap.jsonandmetrics.json; providers with a font per style append the style, as inglyphmap-solid.json.
- build_display_index()#
- build_name_lookup()#
- get_icons_names_for_display()#
- load_assets(style=None)#
- load_metrics(style=None)#
Load precomputed ink bounds for this provider’s glyphs.
The metrics file is what lets the renderer center on a glyph’s true ink instead of Pillow’s
getbbox, which under-reports it. Generate it withpython -m tkinter_icons.tools.generate_metrics <package>.A missing or unreadable file is not an error: providers published before metrics existed simply have none, and the renderer falls back to measuring at draw time.
- resolve_icon_name(name, style=None)#
Resolve a user-supplied icon name to the actual glyph name.
A name may carry its own style as a suffix, so the two arguments can disagree; the rules settle which one wins.
With an explicit
style, resolution happens within that style only. Anameencoding a conflicting style -"-fill"against a requested"outline"- raises rather than silently preferring one of them.Without one, the style is inferred from a
"-<style>"suffix when the name has one, and otherwise falls back to the provider’s default style (or"base"for a provider with no styles).
- Parameters:
- Returns:
The glyph name as it appears in this provider’s glyph map.
- Raises:
ValueError – If the name does not resolve, or if it encodes a style that contradicts
style.- Return type:
- resolve_icon_style(name, style=None)#
Resolve a user-supplied icon name and style to the actual style
- property homepage#
- property icon_version#
- property license_url#
- property render_options: RenderOptions#
Default drawing options for this provider’s glyphs.
Overridable per call — see
tkinter_icons.render.RenderOptions.
- class tkinter_icons.registry.ProviderRegistry#
Bases:
objectSimple registry for icon providers.
This lets applications discover external providers and create icon subclasses bound to those providers.
- get_provider(name)#
- names()#
- register_provider(name, provider)#
- tkinter_icons.registry.load_external_providers(registry)#
Discover installed icon packs and register their providers.
Problems are raised as warnings rather than printed, so an application can route them through
warnings(silence them, turn them into errors, or send them to a log) instead of finding text on its stdout.Both the current and the pre-rename entry-point groups are scanned, so a pack published as
ttkbootstrap-icons-*stays discoverable alongside one published astkinter-icons-*. A pack appearing in both groups is registered once — its provider name is the identity, not the group.- Parameters:
registry (ProviderRegistry) – The registry to populate.
- tkinter_icons.registry.PROVIDER_GROUP = 'tkinter_icons.providers'#
Entry-point group icon packs register their provider under.
- tkinter_icons.registry.LEGACY_PROVIDER_GROUP = 'ttkbootstrap_icons.providers'#
The group used before the rename from ttkbootstrap-icons. Still scanned so packs published under the old name keep working with this package.
Note
Both entry-point groups are scanned, and that is deliberate. Dropping the pre-rename group would mean anyone upgrading the base package with an old ttkbootstrap-icons-* pack installed silently loses every icon set.
Building a pack#
A pack is roughly a hundred lines, most of it data. Copy the closest existing one — the packs are near-identical by design.
1. The provider describes the set to the renderer:
from tkinter_icons.providers import BaseFontProvider
class ExampleFontProvider(BaseFontProvider):
def __init__(self):
super().__init__(
name="example",
display_name="Example Icons",
package="tkinter_icons_example",
filename="fonts/example.ttf",
homepage="https://example.com/icons",
license_url="https://example.com/icons/LICENSE",
icon_version="1.0.0",
pad_factor=0.10,
)
Styles are a mapping when a set has them, each naming a font file, a predicate on the glyph name, or both:
styles={
"outline": {"filename": "fonts/example.ttf", "predicate": lambda n: not n.endswith("-fill")},
"fill": {"filename": "fonts/example.ttf", "predicate": lambda n: n.endswith("-fill")},
},
default_style="outline",
2. The icon class resolves names and hands off:
from tkinter_icons.icon import Icon
from tkinter_icons_example.provider import ExampleFontProvider
class ExampleIcon(Icon):
def __init__(self, name, size=24, color="black", style=None):
provider = ExampleFontProvider()
ExampleIcon.initialize_with_provider(provider)
super().__init__(provider.resolve_icon_name(name, style), size, color)
3. The glyph map, glyphmap.json beside the font — or glyphmap-<style>.json per style when the styles use different fonts. Three shapes are accepted, since the packs grew up separately:
{"house": "EA01"}
{"house": {"unicode": "EA01"}}
[{"name": "house", "unicode": "EA01"}]
4. The entry point, in the pack’s pyproject.toml:
[project.entry-points."tkinter_icons.providers"]
example = "tkinter_icons_example.provider:ExampleFontProvider"
Warning
The registry registers a provider under provider.name, not under the entry-point key. They match for every pack but one — the fa entry point registers fontawesome — so anything passing a pack name to a tool has to import the provider to get it. Reading the key gives an argument generate_metrics rejects.
5. The metrics. Measure the glyph ink, or the renderer falls back to getbbox and your icons sit slightly wrong:
python -m tkinter_icons.tools.generate_metrics example
6. The catalogue. Add a Pack entry to KNOWN_PACKS in packs.py and an extra to the base package’s pyproject.toml. That is what makes pip install "tkinter-icons[example]" and from tkinter_icons import ExampleIcon work, and what every install message reads from.
7. The PyInstaller hook, one file in tkinter_icons/_pyinstaller/:
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('tkinter_icons_example')
Without it, a frozen application ships without your font and draws nothing — silently, since a missing glyph renders transparent. The test suite checks every pack in the catalogue has one.
8. The upstream license, verbatim, under LICENSES/ in the pack. The preflight fails without it.
Maintainer tools#
These regenerate assets into a source tree, so they do nothing from an installed wheel and deliberately ship in none of them. Run them with python -m from a checkout:
python -m tkinter_icons.tools.generate_metrics --all # measure every installed pack
python -m tkinter_icons.tools.generate_metrics --check # verify without writing
python -m tkinter_icons.tools.build_all # rebuild pack assets from upstream
python .github/scripts/verify_packages.py # packaging preflight
python .github/scripts/verify_packages.py --strict # what the release runs
verify_packages.py asks one question of every distribution: would this still work if the only thing that existed were the built distribution? Package-data globs that match nothing, license files that are declared but absent, metrics that exist on disk but outside the glob that ships them — none of those are visible in a working tree, where the files are simply there.
Pull requests#
Branch from 5.0 while 5.0.0 is in flight, and target it. Name the issue the change closes; the whole branch merges to main once, at release, which is when those issues close.
Releasing is tag-driven through Trusted Publishing, and publish order is load-bearing — the packs first, then the base package, then the shim. RELEASE.md has the details.