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: ABC

Base 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.json and metrics.json; providers with a font per style append the style, as in glyphmap-solid.json.

Parameters:

style (str | None) – Style name, or None for the provider’s default.

Returns:

The empty string for single-file providers, otherwise "-<style>".

Return type:

str

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 with python -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.

Parameters:

style (str | None) – Style name, or None for the provider’s default.

Returns:

Icon name to [left, top, width, height] as font-size fractions. Empty when the provider ships no metrics for this style.

Return type:

dict[str, Sequence[float]]

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. A name encoding 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:
  • name (str) – The name as the caller wrote it, with or without a style suffix.

  • style (str | None) – Style to resolve within, or None to infer.

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:

str

resolve_icon_style(name, style=None)#

Resolve a user-supplied icon name and style to the actual style

property default_style: str | None#
property display_name: str#
property font_filename: str | None#
property has_styles: bool#

Return True if this provider defines styles.

property homepage#
property icon_version#
property license_url#
property name: str#
property package: str#
property pad_factor: float#

Padding factor for icon rendering (0.0-1.0).

property render_options: RenderOptions#

Default drawing options for this provider’s glyphs.

Overridable per call — see tkinter_icons.render.RenderOptions.

property scale_to_fit: bool#

Whether to scale down glyphs that exceed the available space.

property style_list: tuple[str, ...]#
property style_map: Mapping[str, Mapping[str, str | Callable[[str], bool]]]#
property uses_single_file: bool#
property y_bias: float#

Vertical bias adjustment for icon rendering.

class tkinter_icons.registry.ProviderRegistry#

Bases: object

Simple 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 as tkinter-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.