renamed BackendsLoader to ModulesLoader

This commit is contained in:
Romain Bignon 2010-08-24 11:03:39 +02:00
commit 9f5c9aeebc
9 changed files with 301 additions and 252 deletions

View file

@ -46,8 +46,8 @@ class WeboobCfg(ConsoleApplication):
@ConsoleApplication.command('Add a configured backend') @ConsoleApplication.command('Add a configured backend')
def command_add(self, name, *options): def command_add(self, name, *options):
self.weboob.backends_loader.load_all() self.weboob.modules_loader.load_all()
if name not in [_name for _name, backend in self.weboob.backends_loader.loaded.iteritems()]: if name not in [_name for _name, backend in self.weboob.modules_loader.loaded.iteritems()]:
logging.error(u'Backend "%s" does not exist.' % name) logging.error(u'Backend "%s" does not exist.' % name)
return 1 return 1
@ -61,7 +61,7 @@ class WeboobCfg(ConsoleApplication):
return 1 return 1
params[key] = value params[key] = value
# ask for params non-specified on command-line arguments # ask for params non-specified on command-line arguments
backend = self.weboob.backends_loader.get_or_load_backend(name) backend = self.weboob.modules_loader.get_or_load_module(name)
asked_config = False asked_config = False
for key, value in backend.config.iteritems(): for key, value in backend.config.iteritems():
if not asked_config: if not asked_config:
@ -105,7 +105,7 @@ class WeboobCfg(ConsoleApplication):
def command_listconfigured(self): def command_listconfigured(self):
self.set_default_formatter('table') self.set_default_formatter('table')
for instance_name, name, params in sorted(self.weboob.backends_config.iter_backends()): for instance_name, name, params in sorted(self.weboob.backends_config.iter_backends()):
backend = self.weboob.backends_loader.get_or_load_backend(name) backend = self.weboob.modules_loader.get_or_load_module(name)
row = OrderedDict([('Instance name', instance_name), row = OrderedDict([('Instance name', instance_name),
('Backend name', name), ('Backend name', name),
('Configuration', ', '.join('%s=%s' % (key, ('*****' if backend.config[key].is_masked else value)) for key, value in params.iteritems())), ('Configuration', ', '.join('%s=%s' % (key, ('*****' if backend.config[key].is_masked else value)) for key, value in params.iteritems())),
@ -127,8 +127,8 @@ class WeboobCfg(ConsoleApplication):
@ConsoleApplication.command('Show available backends') @ConsoleApplication.command('Show available backends')
def command_backends(self, *caps): def command_backends(self, *caps):
self.set_default_formatter('table') self.set_default_formatter('table')
self.weboob.backends_loader.load_all() self.weboob.modules_loader.load_all()
for name, backend in sorted(self.weboob.backends_loader.loaded.iteritems()): for name, backend in sorted(self.weboob.modules_loader.loaded.iteritems()):
if caps and not self.caps_included(backend.iter_caps(), caps): if caps and not self.caps_included(backend.iter_caps(), caps):
continue continue
row = OrderedDict([('Name', name), row = OrderedDict([('Name', name),
@ -140,7 +140,7 @@ class WeboobCfg(ConsoleApplication):
@ConsoleApplication.command('Display information about a backend') @ConsoleApplication.command('Display information about a backend')
def command_info(self, name): def command_info(self, name):
try: try:
backend = self.weboob.backends_loader.get_or_load_backend(name) backend = self.weboob.modules_loader.get_or_load_module(name)
except KeyError: except KeyError:
logging.error('No such backend: "%s"' % name) logging.error('No such backend: "%s"' % name)
return 1 return 1

View file

@ -1,225 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2010 Romain Bignon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import with_statement
from ConfigParser import RawConfigParser
import logging
from logging import debug, error, exception, warning
import os
import re
import stat
from weboob.capabilities.base import IBaseCap
from weboob.tools.backend import BaseBackend
__all__ = ['Backend', 'BackendsConfig', 'BackendsLoader']
class Backend(object):
def __init__(self, package):
self.package = package
self.klass = None
for attrname in dir(self.package):
attr = getattr(self.package, attrname)
if isinstance(attr, type) and issubclass(attr, BaseBackend) and attr != BaseBackend:
self.klass = attr
if not self.klass:
raise ImportError('%s is not a backend (no BaseBackend class found)' % package)
@property
def name(self):
return self.klass.NAME
@property
def maintainer(self):
return '%s <%s>' % (self.klass.MAINTAINER, self.klass.EMAIL)
@property
def version(self):
return self.klass.VERSION
@property
def description(self):
return self.klass.DESCRIPTION
@property
def license(self):
return self.klass.LICENSE
@property
def config(self):
return self.klass.CONFIG
@property
def website(self):
if self.klass.BROWSER and self.klass.BROWSER.DOMAIN:
return '%s://%s' % (self.klass.BROWSER.PROTOCOL, self.klass.BROWSER.DOMAIN)
else:
return None
@property
def icon_path(self):
if self.klass.ICON is None:
try:
import xdg.IconTheme
except ImportError:
debug(u'Python xdg module was not found. Please install it to read icon files.')
else:
self.klass.ICON = xdg.IconTheme.getIconPath(self.klass.NAME)
return self.klass.ICON
def iter_caps(self):
for cap in self.klass.__bases__:
if issubclass(cap, IBaseCap) and cap != IBaseCap:
yield cap
def has_caps(self, *caps):
for c in caps:
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
(type(c) == type and issubclass(self.klass, c)):
return True
return False
def create_instance(self, weboob, instance_name, config, storage):
backend_instance = self.klass(weboob, instance_name, config, storage)
debug(u'Created backend instance "%s" for backend "%s"' % (instance_name, self.name))
return backend_instance
class BackendsConfig(object):
class WrongPermissions(Exception):
pass
def __init__(self, confpath):
self.confpath = confpath
try:
mode = os.stat(confpath).st_mode
except OSError:
os.mknod(confpath, 0600)
else:
if mode & stat.S_IRGRP or mode & stat.S_IROTH:
raise self.WrongPermissions(
u'Weboob will not start until config file %s is readable by group or other users.' % confpath)
def iter_backends(self):
config = RawConfigParser()
config.read(self.confpath)
for instance_name in config.sections():
params = dict(config.items(instance_name))
try:
backend_name = params.pop('_backend')
except KeyError:
try:
backend_name = params.pop('_type')
logging.warning(u'Please replace _type with _backend in your config file "%s", for backend "%s"' % (
self.confpath, backend_name))
except KeyError:
warning('Missing field "_backend" for configured backend "%s"', instance_name)
continue
yield instance_name, backend_name, params
def add_backend(self, instance_name, backend_name, params, edit=False):
if not instance_name:
raise ValueError(u'Please give a name to the configured backend.')
config = RawConfigParser()
config.read(self.confpath)
if not edit:
config.add_section(instance_name)
config.set(instance_name, '_backend', backend_name)
for key, value in params.iteritems():
config.set(instance_name, key, value)
with open(self.confpath, 'wb') as f:
config.write(f)
def edit_backend(self, instance_name, backend_name, params):
return self.add_backend(instance_name, backend_name, params, True)
def get_backend(self, instance_name):
config = RawConfigParser()
config.read(self.confpath)
if not config.has_section(instance_name):
raise KeyError(u'Configured backend "%s" not found' % instance_name)
items = dict(config.items(instance_name))
try:
backend_name = items.pop('_backend')
except KeyError:
try:
backend_name = items.pop('_type')
logging.warning(u'Please replace _type with _backend in your config file "%s"' % self.confpath)
except KeyError:
warning('Missing field "_backend" for configured backend "%s"', instance_name)
raise KeyError(u'Configured backend "%s" not found' % instance_name)
return backend_name, items
def remove_backend(self, instance_name):
config = RawConfigParser()
config.read(self.confpath)
config.remove_section(instance_name)
with open(self.confpath, 'w') as f:
config.write(f)
class BackendsLoader(object):
def __init__(self):
self.loaded = {}
def get_or_load_backend(self, backend_name):
if backend_name not in self.loaded:
self.load_backend(backend_name)
if backend_name in self.loaded:
return self.loaded[backend_name]
else:
return None
def iter_existing_backend_names(self):
try:
import weboob.backends
except ImportError:
return
for path in weboob.backends.__path__:
regexp = re.compile('^%s/([\w\d_]+)$' % path)
for root, dirs, files in os.walk(path):
m = regexp.match(root)
if m and '__init__.py' in files:
yield m.group(1)
def load_all(self):
for existing_backend_name in self.iter_existing_backend_names():
self.load_backend(existing_backend_name)
def load_backend(self, backend_name):
try:
package_name = 'weboob.backends.%s' % backend_name
backend = Backend(__import__(package_name, fromlist=[str(package_name)]))
except ImportError, e:
msg = u'Unable to load backend "%s": %s' % (backend_name, e)
if logging.root.level == logging.DEBUG:
exception(msg)
return
else:
error(msg)
return
if backend.name in self.loaded:
debug('Backend "%s" is already loaded from %s' % (backend_name, backend.package.__path__[0]))
return
self.loaded[backend.name] = backend
debug('Loaded backend "%s" from %s' % (backend_name, backend.package.__path__[0]))

103
weboob/core/backendscfg.py Normal file
View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2010 Romain Bignon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import with_statement
import stat
import os
from ConfigParser import RawConfigParser
from logging import warning
__all__ = ['BackendsConfig']
class BackendsConfig(object):
class WrongPermissions(Exception):
pass
def __init__(self, confpath):
self.confpath = confpath
try:
mode = os.stat(confpath).st_mode
except OSError:
os.mknod(confpath, 0600)
else:
if mode & stat.S_IRGRP or mode & stat.S_IROTH:
raise self.WrongPermissions(
u'Weboob will not start until config file %s is readable by group or other users.' % confpath)
def iter_backends(self):
config = RawConfigParser()
config.read(self.confpath)
for instance_name in config.sections():
params = dict(config.items(instance_name))
try:
backend_name = params.pop('_backend')
except KeyError:
try:
backend_name = params.pop('_type')
warning(u'Please replace _type with _backend in your config file "%s", for backend "%s"' % (
self.confpath, backend_name))
except KeyError:
warning('Missing field "_backend" for configured backend "%s"', instance_name)
continue
yield instance_name, backend_name, params
def add_backend(self, instance_name, backend_name, params, edit=False):
if not instance_name:
raise ValueError(u'Please give a name to the configured backend.')
config = RawConfigParser()
config.read(self.confpath)
if not edit:
config.add_section(instance_name)
config.set(instance_name, '_backend', backend_name)
for key, value in params.iteritems():
config.set(instance_name, key, value)
with open(self.confpath, 'wb') as f:
config.write(f)
def edit_backend(self, instance_name, backend_name, params):
return self.add_backend(instance_name, backend_name, params, True)
def get_backend(self, instance_name):
config = RawConfigParser()
config.read(self.confpath)
if not config.has_section(instance_name):
raise KeyError(u'Configured backend "%s" not found' % instance_name)
items = dict(config.items(instance_name))
try:
backend_name = items.pop('_backend')
except KeyError:
try:
backend_name = items.pop('_type')
warning(u'Please replace _type with _backend in your config file "%s"' % self.confpath)
except KeyError:
warning('Missing field "_backend" for configured backend "%s"', instance_name)
raise KeyError(u'Configured backend "%s" not found' % instance_name)
return backend_name, items
def remove_backend(self, instance_name):
config = RawConfigParser()
config.read(self.confpath)
config.remove_section(instance_name)
with open(self.confpath, 'w') as f:
config.write(f)

141
weboob/core/modules.py Normal file
View file

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2010 Romain Bignon
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import with_statement
import logging
from logging import debug, error, exception
import os
import re
from weboob.capabilities.base import IBaseCap
from weboob.tools.backend import BaseBackend
__all__ = ['Module', 'ModulesLoader']
class Module(object):
def __init__(self, package):
self.package = package
self.klass = None
for attrname in dir(self.package):
attr = getattr(self.package, attrname)
if isinstance(attr, type) and issubclass(attr, BaseBackend) and attr != BaseBackend:
self.klass = attr
if not self.klass:
raise ImportError('%s is not a backend (no BaseBackend class found)' % package)
@property
def name(self):
return self.klass.NAME
@property
def maintainer(self):
return '%s <%s>' % (self.klass.MAINTAINER, self.klass.EMAIL)
@property
def version(self):
return self.klass.VERSION
@property
def description(self):
return self.klass.DESCRIPTION
@property
def license(self):
return self.klass.LICENSE
@property
def config(self):
return self.klass.CONFIG
@property
def website(self):
if self.klass.BROWSER and self.klass.BROWSER.DOMAIN:
return '%s://%s' % (self.klass.BROWSER.PROTOCOL, self.klass.BROWSER.DOMAIN)
else:
return None
@property
def icon_path(self):
return self.klass.ICON
def iter_caps(self):
for cap in self.klass.__bases__:
if issubclass(cap, IBaseCap) and cap != IBaseCap:
yield cap
def has_caps(self, *caps):
for c in caps:
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
(type(c) == type and issubclass(self.klass, c)):
return True
return False
def create_instance(self, weboob, instance_name, config, storage):
backend_instance = self.klass(weboob, instance_name, config, storage)
debug(u'Created backend instance "%s" for backend "%s"' % (instance_name, self.name))
return backend_instance
class ModulesLoader(object):
def __init__(self):
self.loaded = {}
def get_or_load_module(self, module_name):
if module_name not in self.loaded:
self.load_module(module_name)
if module_name in self.loaded:
return self.loaded[module_name]
else:
return None
def iter_existing_module_names(self):
try:
import weboob.backends
except ImportError:
return
for path in weboob.backends.__path__:
regexp = re.compile('^%s/([\w\d_]+)$' % path)
for root, dirs, files in os.walk(path):
m = regexp.match(root)
if m and '__init__.py' in files:
yield m.group(1)
def load_all(self):
for existing_module_name in self.iter_existing_module_names():
self.load_module(existing_module_name)
def load_module(self, module_name):
try:
package_name = 'weboob.backends.%s' % module_name
module = Module(__import__(package_name, fromlist=[str(package_name)]))
except ImportError, e:
msg = u'Unable to load module "%s": %s' % (module_name, e)
if logging.root.level == logging.DEBUG:
exception(msg)
return
else:
error(msg)
return
if module.name in self.loaded:
debug('Module "%s" is already loaded from %s' % (module_name, module.package.__path__[0]))
return
self.loaded[module.name] = module
debug('Loaded module "%s" from %s' % (module_name, module.package.__path__[0]))

View file

@ -22,7 +22,8 @@ from logging import warning
import os import os
from weboob.core.bcall import BackendsCall from weboob.core.bcall import BackendsCall
from weboob.core.backends import BackendsConfig, BackendsLoader from weboob.core.modules import ModulesLoader
from weboob.core.backendscfg import BackendsConfig
from weboob.core.scheduler import Scheduler from weboob.core.scheduler import Scheduler
from weboob.tools.backend import BaseBackend from weboob.tools.backend import BaseBackend
@ -50,7 +51,7 @@ class Weboob(object):
warning(u'"%s" is not a directory' % self.workdir) warning(u'"%s" is not a directory' % self.workdir)
# Backends loader # Backends loader
self.backends_loader = BackendsLoader() self.modules_loader = ModulesLoader()
# Backend instances config # Backend instances config
if not backends_filename: if not backends_filename:
@ -73,25 +74,25 @@ class Weboob(object):
if storage is None: if storage is None:
storage = self.storage storage = self.storage
for instance_name, backend_name, params in self.backends_config.iter_backends(): for instance_name, module_name, params in self.backends_config.iter_backends():
if '_enabled' in params and not params['_enabled'] or \ if '_enabled' in params and not params['_enabled'] or \
names is not None and instance_name not in names or \ names is not None and instance_name not in names or \
modules is not None and backend_name not in modules: modules is not None and module_name not in modules:
continue continue
backend = self.backends_loader.get_or_load_backend(backend_name) module = self.modules_loader.get_or_load_module(module_name)
if backend is None: if module is None:
warning(u'Backend "%s" is referenced in ~/.weboob/backends ' warning(u'Backend "%s" is referenced in ~/.weboob/backends '
'configuration file, but was not found. ' 'configuration file, but was not found. '
'Hint: is it installed?' % backend_name) 'Hint: is it installed?' % module_name)
continue continue
if caps is not None and not backend.has_caps(caps): if caps is not None and not module.has_caps(caps):
continue continue
if instance_name in self.backend_instances: if instance_name in self.backend_instances:
warning(u'Oops, the backend "%s" is already loaded. Unload it before reloading...' % instance_name) warning(u'Oops, the backend "%s" is already loaded. Unload it before reloading...' % instance_name)
self.unload_backends(instance_name) self.unload_backends(instance_name)
backend_instance = backend.create_instance(self, instance_name, params, storage) backend_instance = module.create_instance(self, instance_name, params, storage)
self.backend_instances[instance_name] = loaded[instance_name] = backend_instance self.backend_instances[instance_name] = loaded[instance_name] = backend_instance
return loaded return loaded

View file

@ -77,7 +77,10 @@ class BaseApplication(object):
# Default storage tree # Default storage tree
STORAGE = {} STORAGE = {}
# Synopsis # Synopsis
SYNOPSIS = 'Usage: %prog [options (-h for help)] ...' SYNOPSIS = 'Usage: %prog [-h] [-dqv] [-b backends] ...'
SYNOPSIS += ' %prog [--help] [--version]'
# Description
DESCRIPTION = None
# Version # Version
VERSION = None VERSION = None
# Copyright # Copyright
@ -109,6 +112,8 @@ class BaseApplication(object):
self._parser = OptionParser(self.SYNOPSIS, version=self._get_optparse_version()) self._parser = OptionParser(self.SYNOPSIS, version=self._get_optparse_version())
else: else:
self._parser = option_parser self._parser = option_parser
if self.DESCRIPTION:
self._parser.description = self.DESCRIPTION
self._parser.add_option('-b', '--backends', help='what backend(s) to enable (comma separated)') self._parser.add_option('-b', '--backends', help='what backend(s) to enable (comma separated)')
logging_options = OptionGroup(self._parser, 'Logging Options') logging_options = OptionGroup(self._parser, 'Logging Options')
logging_options.add_option('-d', '--debug', action='store_true', help='display debug messages') logging_options.add_option('-d', '--debug', action='store_true', help='display debug messages')

View file

@ -28,7 +28,7 @@ import subprocess
import sys import sys
from weboob.core import CallErrors from weboob.core import CallErrors
from weboob.core.backends import BackendsConfig from weboob.core.backendscfg import BackendsConfig
from .base import BackendNotFound, BaseApplication from .base import BackendNotFound, BaseApplication
from .formatters.load import formatters, load_formatter from .formatters.load import formatters, load_formatter
@ -44,7 +44,8 @@ class ConsoleApplication(BaseApplication):
Base application class for CLI applications. Base application class for CLI applications.
""" """
SYNOPSIS = 'Usage: %prog [options (-h for help)] command [parameters...]' SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] command [arguments..]\n'
SYNOPSIS += ' %prog [--help] [--version]'
def __init__(self): def __init__(self):
option_parser = OptionParser(self.SYNOPSIS, version=self._get_optparse_version()) option_parser = OptionParser(self.SYNOPSIS, version=self._get_optparse_version())
@ -62,6 +63,8 @@ class ConsoleApplication(BaseApplication):
if self._parser.description is None: if self._parser.description is None:
self._parser.description = '' self._parser.description = ''
else:
self._parser.description += '\n\n'
self._parser.description += 'Available commands:\n' self._parser.description += 'Available commands:\n'
for name, arguments, doc_string in self._commands: for name, arguments, doc_string in self._commands:
command = '%s %s' % (name, arguments) command = '%s %s' % (name, arguments)

View file

@ -45,12 +45,12 @@ class BackendCfg(QDialog):
# is_enabling is a counter to prevent race conditions. # is_enabling is a counter to prevent race conditions.
self.is_enabling = 0 self.is_enabling = 0
self.weboob.backends_loader.load_all() self.weboob.modules_loader.load_all()
self.ui.configuredBackendsList.header().setResizeMode(QHeaderView.ResizeToContents) self.ui.configuredBackendsList.header().setResizeMode(QHeaderView.ResizeToContents)
self.ui.configFrame.hide() self.ui.configFrame.hide()
for name, backend in self.weboob.backends_loader.loaded.iteritems(): for name, backend in self.weboob.modules_loader.loaded.iteritems():
if not self.caps or backend.has_caps(*self.caps): if not self.caps or backend.has_caps(*self.caps):
item = QListWidgetItem(name.capitalize()) item = QListWidgetItem(name.capitalize())
@ -74,10 +74,8 @@ class BackendCfg(QDialog):
def loadConfiguredBackendsList(self): def loadConfiguredBackendsList(self):
self.ui.configuredBackendsList.clear() self.ui.configuredBackendsList.clear()
for instance_name, name, params in self.weboob.backends_config.iter_backends(): for instance_name, name, params in self.weboob.backends_config.iter_backends():
if name not in self.weboob.backends_loader.loaded: backend = self.weboob.modules_loader.get_or_load_module(name)
continue if not backend or self.caps and not backend.has_caps(*self.caps):
backend = self.weboob.backends_loader.loaded[name]
if self.caps and not backend.has_caps(*self.caps):
continue continue
item = QTreeWidgetItem(None, [instance_name, name]) item = QTreeWidgetItem(None, [instance_name, name])
@ -195,7 +193,12 @@ class BackendCfg(QDialog):
self.tr('Please select a backend')) self.tr('Please select a backend'))
return return
backend = self.weboob.backends_loader.loaded[unicode(selection[0].text()).lower()] backend = self.weboob.modules_loader.get_or_load_module(unicode(selection[0].text()).lower())
if not backend:
QMessageBox.critical(self, self.tr('Unable to add a configured backend'),
self.tr('The selected backend does not exist.'))
return
params = {} params = {}
missing = [] missing = []
@ -261,7 +264,7 @@ class BackendCfg(QDialog):
if not selection: if not selection:
return return
backend = self.weboob.backends_loader.loaded[unicode(selection[0].text()).lower()] backend = self.weboob.modules_loader.loaded[unicode(selection[0].text()).lower()]
if backend.icon_path: if backend.icon_path:
img = QImage(backend.icon_path) img = QImage(backend.icon_path)

View file

@ -147,6 +147,24 @@ class BaseBackend(object):
""" """
pass pass
class classprop(object):
def __init__(self, fget):
self.fget = fget
def __get__(self, inst, objtype=None):
if inst:
return self.fget(inst)
else:
return self.fget(objtype)
@classprop
def ICON(self):
try:
import xdg.IconTheme
except ImportError:
debug(u'Python xdg module was not found. Please install it to read icon files.')
else:
return xdg.IconTheme.getIconPath(self.NAME)
@property @property
def browser(self): def browser(self):
""" """