'''
Generic file resouces repository.
This package provide a way to register some folders under a given name
(:meth:`add_folder`) and later retreive files by their basename and
registered folder name (:meth:`get`).
When registering a folder with the same name as another one, the lookup will
favorize the last one and fall back to the first one.
This way you can extend and/or override the resources available under a given
folder name.
You can deal with any kind of files, but in the case of images there are a couple
helper functions: :meth:`get_pixmap()` and :meth:`get_icon()`
Every search result is cached so disk access is minimized. You don't need to
cache your resources next to their usage code, just can keep calling
``resources.get()`` as much as you want.
You can inspect the available resources with :meth:`get_folder_names()`,
:meth:`list_folder()` and :meth:`list_folder_paths()`
.. note::
Files starting with an underscore ``_`` are ignored.
'''
import logging
import os
import glob
_PATHS = {}
_CACHE = {}
_PIXCACHE = {}
_ICONCACHE = {}
[docs]class ResourcesError(Exception):
'''
Raised when the ``folder_name`` is unknown.
'''
[docs]class NotFoundError(ResourcesError):
'''
Raised when the the resource was not found.
Note that this is a :class:`ResourcesError` too.
'''
[docs]def add_folder(name, path):
'''
Register a folder to the search path.
The ``name`` will be used to access files.
You can use :meth:`get_folder_names` to get a list of all declared names.
The ``path`` can be a file inside the folder
to register, or the path of the folder itself.
If a folder with the same name was previously added, the
files from the last one overrides the previous one but
the previous files not available in the last one are
still accessible.
In other words: the last call overrides and extends the previous ones.
Typical resource declaration:
.. code-block:: python
:caption: my_studio/icons/emoji/__init__.py
# We're in a package folder containing some png files
# Every file will be accessible, and overrides previous
# declarations of 'icons.emoji' files
import kabaret.app.resources
kabaret.app.resources.add_folder('icons.emoji', __file__)
.. code-block:: python
:caption: my_studio/gui.py
# Be sure to have the resource modules imported at least once
# This is typically done in the module defining your session
from my_studio.icons import emoji
Typical resource access:
.. code-block:: python
:caption: my_studio/flows/my_dope_flow/__init__.py
# Use anywhere
from kabaret.app import resources
icon = resources.get('icons.emoji', 'sunglasses')
.. seealso::
:meth:`get_icon()`
:meth:`get_pixmap()`
'''
global _PATHS, _CACHE
if not os.path.isdir(path):
path = os.path.dirname(path)
if name not in _PATHS:
_PATHS[name] = []
_PATHS[name].insert(0, path)
# Remove cache for this folder name:
if name in _CACHE:
_CACHE.pop(name)
[docs]def get_folder_names():
'''
Returns a list of available folder names.
'''
global _PATHS
return list(_PATHS)
[docs]def list_folder(folder_name):
'''
Returns a list of resource names available in ``folder_name``.
'''
global _PATHS
EXCLUDE_LIST = ('Thumbs.db',)
folder_name = folder_name.lower()
ret = set()
for path in _PATHS[folder_name]:
ret.update(
[
name.split('.', 1)[0]
for name in os.listdir(path)
if not name.startswith('_')
if not name.startswith('.')
and name not in EXCLUDE_LIST
]
)
return sorted(ret)
[docs]def list_folder_paths(folder_name):
'''
Return a list of list of file_name for ``folder_name``.
(overrides come first)
'''
global _PATHS
folder_name = folder_name.lower()
ret = []
for path in _PATHS[folder_name]:
ret.append(sorted(
[
name.split('.', 1)[0]
for name in os.listdir(path)
if not name.startswith('_')
]
))
return ret
[docs]def get(folder_name, file_name):
'''
Returns the file named ``file_name`` in the
folder registered as ``folder_name``.
'''
global _PATHS, _CACHE
# avoid case sensitive look-up in order to have the same
# behavior in win$ and Linux:
folder_name = folder_name.lower()
file_name = file_name.lower()
cache_key = (folder_name, file_name)
if cache_key in _CACHE:
if _CACHE[cache_key]:
return _CACHE[cache_key]
else:
raise NotFoundError("Resource not found: " + repr(cache_key))
try:
path_list = _PATHS[folder_name]
except KeyError:
raise ResourcesError(
"Unknown resource folder " +
repr(folder_name) + ". (Did you install it first?)"
)
files = None
for path in path_list:
files = glob.glob(
os.path.join(
path,
file_name + ('.' not in file_name and ".*" or "")
)
)
if files:
break
if not files:
_CACHE[cache_key] = None
raise NotFoundError("Resource not found: " + repr(cache_key))
if len(files) > 1:
logging.getLogger('kabaret.resources').warn(
"Several resources found for: " + repr(cache_key))
_CACHE[cache_key] = files[0]
return files[0]
[docs]def get_pixmap(folder_name, pixmap_name):
'''
Same as :meth:`get()`, but returns a QPixmap
'''
global _PIXCACHE
path = get(folder_name, pixmap_name)
if path in _PIXCACHE:
return _PIXCACHE[path]
from qtpy import QtGui
ret = QtGui.QPixmap(path)
_PIXCACHE[path] = ret
return ret
[docs]def get_icon(icon_ref, for_widget=None, disabled_ref=None):
'''
Same as :meth:`get_pixmap()`, but returns a QIcon
If ``icon_ref`` is an **int**, ``for_widget`` must not be **None**
and its current QStyle will be used to return
the QIcon pointed by icon_ref.
If icon_ref is a 2D tuple, it is used to call
:meth:`get_pixmap(*icon_ref)`
If icon_ref is a file path, an icon is created from this file.
if disable_ref is not None, it must be a 2D tuple suitable for
:meth:`get_pixmap(*disabled_ref)` and will be used as the "disabled"
state of the icon.
Warning: calls with different disabled_ref for the same icon_ref has
undefined behavior.
'''
if isinstance(icon_ref, int):
style = for_widget.style()
return style.standardIcon(icon_ref)
global _ICONCACHE
if icon_ref in _ICONCACHE:
return _ICONCACHE[icon_ref]
from qtpy import QtGui
pix = None
try:
pix = get_pixmap(*icon_ref)
except Exception:
try:
if os.path.isfile(icon_ref):
pix = QtGui.QPixmap(icon_ref)
except TypeError:
pass
if pix is None:
raise NotFoundError('Cannot find icon for %r' % (icon_ref,))
icon = QtGui.QIcon(pix)
if disabled_ref is not None:
pix = get_pixmap(*disabled_ref)
icon.addPixmap(pix, mode=icon.Disabled)
_ICONCACHE[icon_ref] = icon
return icon