Skip to main content

Python resource manager to work with the standard library for regular python code and executables.

Project description

Resource manager to work with Python standards and executables.

Standard Resource Functions
  • files - importlib.resources files this function is the standard for retrieving resources for Python 3.9+

  • as_file - context manager for retrieving a true filepath for Python 3.9+.

  • read_binary - Return the bytes found in the package with the given basename.

  • read_text - Return the text found in the package with the given basename.

  • contents - Return an iterable of basenames in the given package.

  • is_resource - Return if the given package, basename exists.

Custom Helpers
  • clear - clear all registered resources.

  • register - Register a package and basename.

  • register_data - Register a plain data resource that does not have a file.

  • register_directory - Register the contents of a package directory.

  • unregister - Remove a resource from the registration.

  • has_resource - Return if the given resource, package_path, or alias has a registered resource.

  • get_resources - Return a list of Resources. If multiple Resources register the same alias the last one will be used.

  • get_resource - Return the Resource object for the given alias, fallback alias, or default value.

  • get_binary - Return the binary data read from the found resource.

  • get_text - Return the text data read from the found resource.

Notes:

I added support for subdirectories which importlib.resources specifically does not support. I believe this was a terrible mistake. If I am using any library with css, html, or js there will naturally be a couple of subdirectories. If I include these projects as sub-repositories I do not want to add __init__.py files to everything.

# mylib/run.py
# File Structure:
#     mylib/
#         __init__.py
#         run.py
#         subrepo/
#             html/
#                 index.html
#             css/
#                 my.css
#             js/
#                 my.js
#             edit-cut.png
#             README.md
import resource_man


RESOURCES = [
    resource_man.register('mylib', 'subrepo/html/index.html')
    resource_man.register('mylib', 'subrepo/css/my.css')
    resource_man.register('mylib', 'subrepo/js/my.js')
    resource_man.register('mylib', 'subrepo/edit-cut.png')
    ]
# RESOURCES = resource_man.register_directory('mylib', 'subrepo', recursive=True,
#                                             exclude=['README.md'],  # exclude from directory ('subrepo')
#                                             extensions=['.png', '.html', '.js', '.css'])

Resource Man Example

Register resources. This example requires that the file exists. If you use PyInstaller to make an executable “mylib/actions/edit-cut.png” needs to be included in datas. This should also work if the resource is bundled in a zip file according to some of the Python documentation.

# mylib/run.py
# File Structure:
#     mylib/
#         __init__.py
#         run.py
#         actions/
#             __init__.py
#             edit-cut.png
import resource_man as rsc

rsc.register('mylib.actions', 'edit-cut.png', alias='edit-cut')

if __name__ == '__main__':
    edit_cut_bin = resource_man.get_binary('edit-cut')

Best practice is probably to register the resource file in the __init__.py that it exists with. Then you have access to all resources on import.

# mylib/actions/__init__.py
import resource_man as rsc

EDIT_CUT = rsc.register('mylib.actions', 'edit-cut.png')

if __name__ == '__main__':
     edit_cut_bin = EDIT_CUT.read_bytes()

# Use with `import mylib.actions`

Register directories by registering all objects

import resource_man as rsc

DIRECTORY = rsc.register_directory('check_lib.check_sub', extensions=['.txt', '.png'])

assert len(DIRECTORY) > 0
assert isinstance(DIRECTORY[0], rsc.Resource)

for resource in DIRECTORY:
    print(resource)

importlib.resources Example

Using filenames and paths. As stated earlier Python recommends that you use importlib.resources to read the resource data. Filenames still have some support with importlib.resources, but it must be used as a context manager.

# my_interface.py
# sdl2 with sld2.dll in package
# File Structure:
#     my_sdl/
#         sdl2_dll_path/
#             __init__.py  # Is probably still required. Was required for pkg_resources
#             SDL2.dll
#         __init__.py
#         my_interface.py
import os
from resource_man import files, as_file
import my_sdl.sdl2_dll_path  # Required for PyInstaller to include the package

# ".sdl2_dll_path" would require __init__.py
binary = files('my_sdl.sdl2_dll_path').joinpath('SDL2.dll').read_binary()

with as_file(files('my_sdl.sdl2_dll_path').joinpath('SDL2.dll')) as sdl_path:
    os.environ.setdefault('PYSDL2_DLL_PATH', os.path.dirname(str(sdl_path)))
    import sdl2

# Use sdl2
assert sdl2 is not None

PyInstaller Helper

This library has a collect_datas helper function. I believe this function to be more useful than PyInstallers built in tool.

# hook-mylib.py
#
# File Structure:
#     mylib/
#         __init__.py
#         run.py
#         edit-cut.png
#     pyinstaller-hooks/
#         hook-mylib.py
from resource_man.pyinstaller import find_datas, registered_datas

# datas = find_datas('mylib')  # Will also find resources in sub packages
datas = registered_datas(use_dest_dirs=False)  # Return a list of registered resources

Use the pyinstaller helper with pylibimp to import all resources for your project.

# build_exe.py
from resource_man.pyinstaller import registered_datas
from PyInstaller import config
from pylibimp import import_module
import subprocess

if __name__ == '__main__':
    main_module = 'mylib/run.py'

    # Import the main module to register all of the data files.
    import_module(main_module, reset_modules=True)

    # Get registered datas
    datas = registered_datas(use_dest_dirs=False)
    args = []
    for data in datas:
        args.extend(['--add-data', os.pathsep.join(data)])

    subprocess.run(['pyinstaller', main_module] + args)

You could also make your own PyInstaller hook using these helper functions.

Qt Example

The importlib.resources library prefers reading data from a resource instead of using filename paths. This is to speed up execution and support with zip files. Qt Primarily uses filenames, but also has it’s own system of importing compiled resources. I have created several utilities to help with this.

Compiled Resources

The best way is probably to use compiled resources.

The resource_man library helps with utilities for registering resources, create .qrc files, and compiling .qrc files.

1. Register the Resource

Use resource_man to register resources when the file is imported.

# main_qt.py
# File Structure:
#    main_qt.py
#    check_lib/
#        __init__.py
#        check_sub/
#            __init__.py
#            edit-cut.png
import resource_man.qt as rsc

# Register on import outside of main
rsc.register('check_lib.check_sub', 'edit-cut.png', alias='edit-cut')
DOCUMENT_NEW = rsc.register('check_lib.check_sub', 'document-new.png')  # QFile name is ":/check_lib/check_sub/document-new.png"
RSC2 = rsc.register('check_lib.check_sub', 'rsc2.txt', ...)  # ... uses name as alias ("rsc2.txt")

After registering, resource_man can create the list of resources in a .qrc file.

2. Create .qrc File

Create the .qrc file that can compile all resources into a binary data file.

python -m resource_man.qt create ./main_qt.py

This creates a file that looks like.

<!DOCTYPE RCC><RCC version="1.0">
<qresource>
    <file alias="edit-cut">check_lib\check_sub\edit-cut.png</file>
    <file>check_lib\check_sub\document-new.png</file>
    <file alias="rsc2.txt">check_lib\check_sub\rsc2.txt</file>
</qresource>
</RCC>

3. Compile the .qrc file

Compile the .qrc file into an importable .py file. PySide can also make a C++ .rcc file that can be registered as well.

python -m resource_man.qt compile

This creates a large .py file with the binary data.

4. Load the compiled file

Load the compiled .py file.

...

if __name__ == '__main__':
    app = QtWidgets.QApplication([])

    # Load the Qt RCC after QApplication
    success = load_resource()

5. Use the qrc resource

Use the QIcon or QPixmap to use the registered resource.

from resource_man.qt import QIcon, QPixmap

...

icon = QIcon('edit-cut')
icon = QIcon(':/edit-cut')
icon = QIcon(':/document-new.png')

Use with importlib.resources.

from resource_man.qt import read_binary
import check_lib.check_sub
...

# Need to --add-datas with PyInstaller to use this in an executable
binary_img = read_binary('check_lib.check_sub', 'edit-cut.png')

Full Example

The resource_man library includes a QIcon and QPixmap class to use registered resources. This QIcon and QPixmap can take in binary data as the first argument to create the icon. This QIcon and QPixmap can also take the registered alias. This library uses QtPy to support PySide or PyQt.

# mylib/run.py
# File Structure:
#     check_lib/
#         __init__.py
#         run.py
#         check_sub/
#             __init__.py
#             edit-cut.png
#             document-new.png
#             document-save-as.svg
    import check_lib.check_sub
from qtpy import QtWidgets, QtCore
import resource_man.qt as rsc


# Register on import outside of main
EDIT_CUT = rsc.register('check_lib.check_sub', 'edit-cut.png', None)  # None uses name no ext as alias ('edit-cut')
rsc.register('check_lib.check_sub', 'document-save-as.svg', None)  # None uses name no ext as alias ('document-save-as')
RSC = rsc.register('check_lib', 'rsc.txt', ...)  # ... uses name as alias ("rsc.txt")
RSC2 = rsc.register('check_lib.check_sub', 'rsc2.txt', ...)  # ... uses name as alias ("rsc2.txt")
DOCUMENT_NEW = rsc.register('check_lib.check_sub', 'document-new.png')  # QFile ":/check_lib/check_sub/document-new.png"

DOC_NEW_DATA = rsc.register_data(DOCUMENT_NEW.read_bytes(), 'readme_qt', 'data_resource', 'data_resource')


if __name__ == '__main__':
    app = QtWidgets.QApplication([])

    # Load the Qt RCC after QApplication
    success = rsc.load_resource()

    widg = QtWidgets.QWidget()
    widg.setLayout(QtWidgets.QVBoxLayout())

    # Use the Resource as the Path. This is not recommended. Use 'as_file' or 'read_text'.
    with open(DOCUMENT_NEW, 'rb') as f:  # Need str for some objects QtCore.QFile(str(DOCUMENT_NEW))
        assert len(f.read()) > 0

    # Resource file (Must be compiled and loaded)
    file = QtCore.QFile(':/rsc2.txt')
    if not file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text):
        text = 'File Not Available'
    else:
        text = file.readAll().data().decode('utf-8')
        file.close()
    msg = 'READ FILE\n' \
          'File Path = {}\nread_text = {}\nQFile :/rsc2.txt = {}'.format(str(RSC2), repr(RSC2.read_text()), repr(text))
    lbl = QtWidgets.QLabel(msg)
    widg.layout().addWidget(lbl)

    # Use resource_man register alias
    btn = QtWidgets.QPushButton(rsc.QIcon('edit-cut'), 'resource_man alias "edit-cut"', None)
    widg.layout().addWidget(btn)

    # Use Qt QResource alias name
    btn = QtWidgets.QPushButton(rsc.QIcon(':/edit-cut'), 'QFile alias ":/edit-cut"', None)
    widg.layout().addWidget(btn)

    # Use Qt QResource File name - DOES NOT WORK! CAN ONLY USE QRC ALIAS IDENTIFIER!
    # btn = QtWidgets.QPushButton(QIcon(':\\check_lib\\check_sub\\edit-cut.png'),
    #                                   'QFile name ":\\check_lib\\check_sub\\edit-cut.png"', None)
    # widg.layout().addWidget(btn)

    # Use resource_man register alias
    btn = QtWidgets.QPushButton(rsc.QIcon(DOCUMENT_NEW), 'resource_man object DOCUMENT_NEW', None)
    widg.layout().addWidget(btn)

    # Use Qt QResource File name alias
    btn = QtWidgets.QPushButton(rsc.QIcon(':/check_lib/check_sub/document-new.png'), 'QFile alias ":/check_lib/check_sub/document-new.png"', None)
    widg.layout().addWidget(btn)

    # Use Qt QResource File name alias
    btn = QtWidgets.QPushButton(rsc.QIcon('data_resource'), 'data_resource', None)
    widg.layout().addWidget(btn)

    # Use Qt QResource File name alias - DOES NOT WORK! CAN ONLY USE QRC ALIAS IDENTIFIER!
    # btn = QtWidgets.QPushButton(rsc.QIcon(':/check_lib/check_sub/document-new.png'),
    #                                   '":/check_lib/check_sub/document-new.png"', None)
    # widg.layout().addWidget(btn)

    # ===== The two methods below only work if the resource files exist in the executable =====
    # you need to include the .png files as data files in PyInstaller
    # you also need to import the package (`import check_lib.check_sub`) for PyInstaller to include the package.

    # resource_man binary (resource_man register alias support)
    try:
        btn_binary_resource_man = QtWidgets.QPushButton(rsc.QIcon(rsc.get_binary('edit-cut')), 'resource_man get_binary("edit-cut")')
        widg.layout().addWidget(btn_binary_resource_man)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass

    # importlib.resources binary
    try:
        btn_binary_importlib = QtWidgets.QPushButton(rsc.QIcon(rsc.read_binary('check_lib.check_sub', 'edit-cut.png')),
                                                     'importlib.resources read_binary("check_lib.check_sub", "edit-cut.png")')
        widg.layout().addWidget(btn_binary_importlib)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass

    # Show Images
    hlay = QtWidgets.QHBoxLayout()
    widg.layout().addLayout(hlay)
    try:
        lbl = QtWidgets.QLabel()
        lbl.setPixmap(rsc.QPixmap(rsc.files('check_lib.check_sub').joinpath('edit-cut.png')).scaledToHeight(32))
        hlay.addWidget(lbl)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass
    try:
        # QSvg Cannot load png images. This will be blank
        invalid = rsc.QSvgWidget("check_lib/check_sub/document-new.png")
        w = QtWidgets.QWidget()
        w.setLayout(QtWidgets.QVBoxLayout())
        w.layout().addWidget(QtWidgets.QLabel('QSvgWidget NO PNG'))
        w.layout().addWidget(invalid)
        hlay.addWidget(w)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass
    try:
        lbl = QtWidgets.QLabel()
        lbl.setPixmap(rsc.QPixmap("document-save-as").scaledToHeight(32))
        hlay.addWidget(lbl)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass
    try:
        svg = rsc.QSvgWidget("document-save-as")
        svg.setFixedSize(32, 32)
        hlay.addWidget(svg)
    except (rsc.ResourceNotAvailable, OSError, TypeError) as err:
        pass

    widg.show()
    app.exec_()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

resource_man-2.2.8.tar.gz (44.0 kB view hashes)

Uploaded Source

Built Distribution

resource_man-2.2.8-py3-none-any.whl (44.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page