utils.py PySideAbdhUI — Resource Handling, Theme Management & Color Utilities
utils.py is the central utility module for the PySideAbdhUI framework, providing three
distinct but interconnected capabilities: resource path resolution,
theme management, and contrast-aware color generation. Together, these
utilities ensure that the framework can locate its packaged assets (SVG icons, QSS templates,
JSON color-role definitions) regardless of installation location, apply user-selectable
themes at runtime by replacing placeholder tokens in a QSS template, and generate random
colors that meet WCAG accessibility contrast requirements against any background.
The module is designed around a resource-loading pipeline that uses Python's
importlib.resources to resolve packaged resources, a ThemeManager
class that reads/writes a JSON-based theme configuration and applies themes by performing
placeholder substitution on a QSS template file, and a standalone function
random_contrasting_hex() that computes perceptual luminance and contrast ratios
using the WCAG 2.0 algorithm to guarantee readable foreground colors.
Dependencies
| Module | Components Used | Purpose |
|---|---|---|
random |
random.uniform |
Generating random HSL values for the contrast-aware color utility. |
re |
re.compile |
Regular expression matching for widget property editing in QSS templates. |
json |
json.load, json.dump |
Reading and writing the JSON-based theme configuration file. |
importlib.resources |
importlib.resources.path |
Loading packaged resources (icons, templates, JSON) from the package's data directories using Python's standard resource API. |
pathlib |
Path |
Type annotation for resource path return values. |
PySide6.QtWidgets |
QApplication |
Applying stylesheets to the running application instance in ThemeManager.apply_theme(). |
PySide6.QtGui |
QColor |
Color representation, HSL construction, and component access for contrast calculations. |
Architecture
The module follows a layered architecture where low-level resource resolution functions feed into higher-level theme management. The following diagram shows the data flow:
Resource Resolution Functions
The resource resolution layer provides a unified API for locating packaged resources
(icons, stylesheets, configuration files) using Python's importlib.resources
module. This approach ensures that resources can be found regardless of whether the package
is installed as a regular directory, a zip-imported egg, or a wheel. All functions in this
layer delegate to get_resource_path(), which is the core resolver.
get_resource_path(package: str, resource: str, ext: str = 'svg') -> Path
Function
Retrieves the full filesystem path to a specified resource located within a given
Python package. This is the foundational function upon which all other resource
accessors depend. It uses importlib.resources.path() as a context manager
to resolve the resource, which handles the complexity of different package
installation formats transparently.
The function automatically appends the file extension to the resource name. If
ext is provided (defaulting to 'svg'), the resource is looked
up as f'{resource}.{ext}'. If ext is falsy (empty string or
None), the extension is derived from the last segment of the package
path — for example, a package path of 'PySideAbdhUI.resources.styles'
would yield ext = 'styles', which is likely incorrect. This fallback
behavior appears to be a design artifact rather than a useful feature.
If the resource cannot be located, a RuntimeError is raised with a
descriptive message that includes both the resource name and the package path,
and the original exception is chained via from e.
| Parameter | Type | Default | Description |
|---|---|---|---|
package | str | — | Dotted package path where the resource is located. E.g., 'PySideAbdhUI.resources.icons.svg'. |
resource | str | — | Base filename of the resource without extension. E.g., 'menu' for menu.svg. |
ext | str | 'svg' | File extension to append. If falsy, derived from the package path (usually incorrect). |
Path — The full filesystem path to the resource.Known Issue: The default value of ext='svg' means that calling get_resource_path() without specifying ext will always look for an .svg file, even for non-SVG resources. The convenience functions (get_styles_template, get_color_roles) override this correctly, but direct callers must remember to pass the appropriate extension.
get_icon(name: str, package: str = 'PySideAbdhUI.resources.icons.svg', ext: str = 'svg') -> str
Function
Convenience function that resolves the path to an SVG icon resource and returns
it as a POSIX-style string. This is the primary function used by the framework's
widgets to load icons — every QIcon(get_icon('icon-name')) call in
the codebase goes through this function. The default package path points to the
PySideAbdhUI.resources.icons.svg subpackage, where all SVG icons are
expected to reside.
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | — | The icon name without extension. E.g., 'menu' resolves to menu.svg. |
package | str | 'PySideAbdhUI.resources.icons.svg' | Dotted package path where icons are stored. |
ext | str | 'svg' | File extension of the icon resource. |
str — POSIX-style path string suitable for QIcon() or QAction() constructors.get_styles_template(package: str = 'PySideAbdhUI.resources.styles') -> str
Function
Convenience function that resolves the path to the QSS template file
(qss-template.qss) within the styles package. The template contains
CSS rules with placeholder tokens in the format --token-name-- that
are replaced with actual color values during theme application. The template
serves as the single source of truth for the application's stylesheet structure,
while the color values come from the theme JSON file.
| Parameter | Type | Default | Description |
|---|---|---|---|
package | str | 'PySideAbdhUI.resources.styles' | Dotted package path where style resources are stored. |
str — POSIX-style path string to the qss-template.qss file.get_color_roles(package: str = 'PySideAbdhUI.resources.styles') -> str
Function
Convenience function that resolves the path to the color-roles JSON file
(color-roles.json) within the styles package. This JSON file defines
all available themes, their color categories and roles, and the currently active
theme. The file is the persistence layer for the ThemeManager — it is
read on initialization and written back when themes are switched or properties
are modified.
| Parameter | Type | Default | Description |
|---|---|---|---|
package | str | 'PySideAbdhUI.resources.styles' | Dotted package path where style resources are stored. |
str — POSIX-style path string to the color-roles.json file.ThemeManager
ThemeManager
Manages the application's theme lifecycle: loading theme definitions from a JSON file,
switching between themes, applying themes by performing placeholder substitution on a
QSS template, and editing CSS properties within the template. The class acts as the
bridge between the structured theme data (stored in color-roles.json) and
the flat QSS template (stored in qss-template.qss), converting structured
color definitions into a complete, application-ready stylesheet.
The theme system uses a simple but effective placeholder replacement approach: the QSS
template contains tokens in the format --token-name--, and the theme JSON
provides a nested dictionary of categories, role names, and color values. During
application, each placeholder is replaced with its corresponding color value. This
approach avoids the complexity of a full CSS preprocessor while still providing
theme-switching capability.
Constructor
ThemeManager.__init__(self)
Public
Initializes the theme manager by resolving the paths to the color-roles JSON file and the QSS template, then loading the theme data. The constructor performs the following steps:
- Calls
get_color_roles()to resolve the path tocolor-roles.json. - Calls
get_styles_template()to resolve the path toqss-template.qss. - Stores both paths as
self.color_rolesandself.template_path. - Calls
self.load()to read and parse the JSON file intoself.data.
| Attribute | Type | Description |
|---|---|---|
color_roles | str | POSIX path to the color-roles.json file. |
template_path | str | POSIX path to the qss-template.qss file. |
data | dict | Parsed JSON data containing theme definitions and the active theme name. |
Persistence (load / save)
load(self) -> dict
Public
Reads and parses the color-roles.json file, returning the resulting
dictionary. The file is opened with UTF-8 encoding and parsed with
json.load(). After reading, the file is explicitly closed with
f.close() (though the with statement would handle this
automatically).
If the file cannot be opened or parsed, the with block exits and the
code after it (return {"active-theme": "", "themes": {}}) is reached.
However, this fallback is unreachable because the return
statement inside the with block always executes first if the file is
successfully read, and an exception would propagate rather than falling through.
dict — The parsed JSON data with theme definitions.Unreachable Code: The line return {"active-theme": "", "themes": {}} is unreachable because it appears after a return inside a with block. If the file read fails, an exception will be raised rather than reaching this fallback. A proper implementation would use a try/except around the file operation.
save(self)
Public
Writes the current self.data dictionary back to the
color-roles.json file with 4-space indentation. The file is opened with
UTF-8 encoding in write mode. Like load(), the file is explicitly closed
with f.close() despite the with statement handling this
automatically. This method is called by switch_theme() to persist the
active theme name change.
Theme Queries
get_current_theme_name(self) -> str
Public
Returns the name of the currently active theme. The name is stored under the
"active-theme" key in the JSON data. If the key is missing, an empty
string is returned.
str — The active theme name, or an empty string if not set.get_current_theme(self) -> dict
Public
Returns the full theme dictionary for the currently active theme. The theme
dictionary is a nested structure organized by color categories and role names.
For example, a theme might have categories like "surface",
"text", "accent", each containing role entries with a
"color" field. If the active theme name does not exist in the themes
dictionary, an empty dict is returned.
dict — The active theme's color definitions, or {} if not found.get_color(self, role_category: str, role_name: str) -> str | None
Public
Retrieves a specific color value from the current theme by navigating the nested
dictionary structure: theme[role_category][role_name]["color"]. This
provides a convenient lookup API that abstracts away the dictionary structure.
If the category, role, or color key does not exist, None is returned
due to the chained .get() calls.
| Parameter | Type | Description |
|---|---|---|
role_category | str | The top-level category in the theme (e.g., "surface", "text", "accent"). |
role_name | str | The specific role within the category (e.g., "primary", "secondary", "disabled"). |
str | None — The color value (typically a hex string like "#1E1E2E"), or None if not found.get_all_themes(self) -> list
Public
Returns a list of all available theme names in the configuration. This is useful for populating a theme selector UI where the user can choose from the installed themes.
list[str] — List of theme name strings.Theme Switching
switch_theme(self, new_theme_name: str) -> bool
Public
Switches the active theme to the specified theme name. The method first checks
whether the requested theme exists in the "themes" dictionary. If it
does, the "active-theme" key in self.data is updated and
self.save() is called to persist the change to disk. If the theme name
does not exist, the method returns False without making any changes.
| Parameter | Type | Description |
|---|---|---|
new_theme_name | str | The name of the theme to switch to. Must match a key in the "themes" dictionary. |
True if the switch was successful; False if the theme name was not found.Applying Themes
apply_theme(self, app: QApplication, theme_name: str = 'default-dark')
Public
The main entry point for applying a theme to the application. This method performs a complete theme application cycle:
- Switch theme: Calls
switch_theme(theme_name)to update the active theme in the JSON data and persist the change. - Load template: Reads the QSS template file from
self.template_pathinto a string. - Replace placeholders: Iterates over the current theme's categories and roles, replacing each placeholder token
--role_name--in the QSS string with the corresponding color value. The iteration order ensures all nested categories and roles are covered. - Apply stylesheet: Calls
app.setStyleSheet(qss)on theQApplicationinstance to apply the completed stylesheet.
If the QSS template file cannot be read, an error message is printed to the console and the method returns without applying any changes. The existing stylesheet remains in effect.
| Parameter | Type | Default | Description |
|---|---|---|---|
app | QApplication | — | The application instance to apply the stylesheet to. |
theme_name | str | 'default-dark' | The name of the theme to apply. |
Note: The placeholder format --role_name-- uses double dashes on both sides, which is different from CSS custom property syntax (--name). This avoids conflicts with CSS custom properties that might exist in the template. The replacement is a simple string substitution (qss.replace(placeholder, color)), not a regex, so placeholders must match exactly.
Widget Property Editing
add_property_to_widget(self, widget_name: str, property_name: str, property_value: str)
Public
Adds or updates a CSS property within a specific widget's style block in the QSS template file. This method provides a programmatic way to customize the stylesheet without manually editing the template file. It uses regular expressions to locate and modify the target widget block.
The method performs the following steps:
- Read template: Reads the entire QSS template file into a string.
- Match widget block: Uses the regex
widget_name\s*\{[^}]*\}to find the CSS block for the specified widget. This pattern matches the widget name followed by a brace-enclosed block of properties. - Check for existing property: Within the matched block, searches for
property_name\s*:\s*[^;]+;to determine if the property already exists. - Update or insert: If the property exists, its value is replaced. If it does not exist, the new property is appended before the closing brace with proper indentation.
- Write template: The modified QSS string is written back to the template file.
| Parameter | Type | Description |
|---|---|---|
widget_name | str | The CSS selector name (e.g., "QPushButton", "QLabel"). |
property_name | str | The CSS property name (e.g., "font-family", "border-radius"). |
property_value | str | The CSS property value (e.g., "'Arial'", "8px"). |
Limitations: The regex pattern widget_name\s*\{[^}]*\} does not support nested braces, which means it will fail for widget blocks that contain nested selectors (e.g., QPushButton:hover { ... }). Additionally, if the widget block spans multiple lines with complex formatting, the regex may not match correctly. The method only writes back to the template file if a match is found — if the widget does not exist in the template, no changes are made.
Note: After modifying the template, the commented-out line #self.apply_theme(QApplication.instance(), self.get_current_theme_name()) suggests that the theme was originally re-applied automatically after property changes. This is currently disabled, meaning the user must manually call apply_theme() after using this method to see the changes.
random_contrasting_hex
random_contrasting_hex(background: QColor | str, theme: str = "auto", min_contrast: float = 4.5) -> str
Function
Generates a random hex color that is guaranteed to be readable against the given background color, meeting a specified minimum WCAG contrast ratio. The function uses the WCAG 2.0 relative luminance algorithm to compute perceptual brightness and the standard contrast ratio formula to ensure accessibility. The generated color is never extremely bright (no white or near-white) and tends toward bolder, more saturated colors due to the minimum saturation threshold.
The function supports two operating modes controlled by the theme
parameter:
- "auto" (default): Automatically determines whether a light or dark foreground is needed based on the background's luminance. If the background luminance is below 0.5, a light foreground is generated; otherwise, a dark one.
- "dark" or "light": Forces the generation of a specific foreground type regardless of the background brightness. Use
"dark"to generate light foreground colors (for dark backgrounds) and"light"for dark foreground colors (for light backgrounds).
| Parameter | Type | Default | Description |
|---|---|---|---|
background | QColor | str | — | The background color to contrast against. Accepts a QColor object or a CSS color string (e.g., "#1E1E2E"). |
theme | str | "auto" | Controls foreground brightness mode: "auto", "dark" (light foreground), or any other value (dark foreground). |
min_contrast | float | 4.5 | Minimum WCAG contrast ratio. The default of 4.5 meets WCAG AA for normal text. Use 7.0 for AAA compliance. |
str — A hex color string (e.g., "#C8A2FF") that meets the contrast requirement against the background.Contrast Algorithm Details
The function implements the WCAG 2.0 contrast ratio algorithm, which is the industry
standard for evaluating text readability on colored backgrounds. The algorithm consists
of two nested helper functions defined inside random_contrasting_hex():
Luminance Calculation
The luminance(color) function computes the relative luminance of a color
using the sRGB linearization formula defined in WCAG 2.0 specification. Each RGB channel
is first normalized to the 0-1 range, then linearized using the standard transfer function:
values below 0.04045 are divided by 12.92, while values above are transformed using
((c + 0.055) / 1.055) ^ 2.4. The linearized values are then combined using
the weighted sum: 0.2126 * R + 0.7152 * G + 0.0722 * B, where the weights
reflect the human eye's varying sensitivity to different wavelengths of light.
The color generation loop runs up to 1000 attempts, generating random HSL colors and
testing each against the contrast threshold. The HSL parameters are constrained to produce
visually appealing colors: saturation is always at least 40% to avoid washed-out pastels,
and lightness is bounded to prevent extremely bright or dark results. Pure white
(#ffffff) and colors with a lightness value above 85% are explicitly rejected
regardless of contrast ratio, ensuring the output is never glaringly bright.
If no suitable color is found after 1000 attempts (which is extremely rare given the
constrained parameter ranges), a safe fallback color is returned: "#C8C8C8"
for light foregrounds (a medium gray readable on dark backgrounds) or "#1A1A1A"
for dark foregrounds (a near-black readable on light backgrounds). These fallbacks
deliberately avoid pure white and pure black, maintaining the function's guarantee of
"never extremely bright" output.
Tip: When calling this function, the default min_contrast=4.5 meets WCAG AA requirements for normal-sized text. For large text (18pt+ or 14pt+ bold), min_contrast=3.0 is sufficient. For the highest accessibility standard (WCAG AAA), use min_contrast=7.0.
Full Method / Function Index
| Type | Name | Brief Description |
|---|---|---|
| Functions | get_resource_path | Resolve filesystem path to a packaged resource via importlib |
get_icon | Resolve SVG icon path as POSIX string | |
get_styles_template | Resolve QSS template path as POSIX string | |
get_color_roles | Resolve color-roles JSON path as POSIX string | |
| ThemeManager | __init__ | Resolve resource paths and load theme data |
load | Read and parse color-roles.json | |
save | Write theme data back to color-roles.json | |
get_current_theme_name | Get the active theme name | |
get_current_theme | Get the full active theme dictionary | |
get_color | Look up a specific color by category and role | |
get_all_themes | List all available theme names | |
switch_theme | Change active theme and persist to disk | |
apply_theme | Apply theme to QApplication via placeholder replacement | |
add_property_to_widget | Add/update CSS property in QSS template via regex | |
| Color Utility | random_contrasting_hex | Generate WCAG-compliant contrasting random color |
Resource Package Structure: The utility functions expect the following package structure within PySideAbdhUI: resources/icons/svg/ (SVG icon files), resources/styles/ (qss-template.qss and color-roles.json). If the package is installed without these resources, all resource resolution functions will raise RuntimeError.