| # -*- coding: utf-8 -*- |
| # |
| # Copyright (C) 2005-2009 Edgewall Software |
| # Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de> |
| # All rights reserved. |
| # |
| # This software is licensed as described in the file COPYING, which |
| # you should have received as part of this distribution. The terms |
| # are also available at http://trac.edgewall.org/wiki/TracLicense. |
| # |
| # This software consists of voluntary contributions made by many |
| # individuals. For the exact contribution history, see the revision |
| # history and logs, available at http://trac.edgewall.org/log/. |
| # |
| # Author: Christopher Lenz <cmlenz@gmx.de> |
| |
| from glob import glob |
| import imp |
| import os.path |
| import pkg_resources |
| from pkg_resources import working_set, DistributionNotFound, VersionConflict, \ |
| UnknownExtra |
| import sys |
| |
| from trac.util import get_doc, get_module_path, get_sources, get_pkginfo |
| from trac.util.text import exception_to_unicode, to_unicode |
| |
| __all__ = ['load_components'] |
| |
| |
| def _enable_plugin(env, module): |
| """Enable the given plugin module if it wasn't disabled explicitly.""" |
| if env.is_component_enabled(module) is None: |
| env.enable_component(module) |
| |
| def load_eggs(entry_point_name): |
| """Loader that loads any eggs on the search path and `sys.path`.""" |
| def _load_eggs(env, search_path, auto_enable=None): |
| # Note that the following doesn't seem to support unicode search_path |
| distributions, errors = working_set.find_plugins( |
| pkg_resources.Environment(search_path) |
| ) |
| for dist in distributions: |
| if dist not in working_set: |
| env.log.debug('Adding plugin %s from %s', dist, dist.location) |
| working_set.add(dist) |
| |
| def _log_error(item, e): |
| ue = exception_to_unicode(e) |
| if isinstance(e, DistributionNotFound): |
| env.log.debug('Skipping "%s": ("%s" not found)', item, ue) |
| elif isinstance(e, VersionConflict): |
| env.log.error('Skipping "%s": (version conflict "%s")', |
| item, ue) |
| elif isinstance(e, UnknownExtra): |
| env.log.error('Skipping "%s": (unknown extra "%s")', item, ue) |
| else: |
| env.log.error('Skipping "%s": %s', item, |
| exception_to_unicode(e, traceback=True)) |
| |
| for dist, e in errors.iteritems(): |
| _log_error(dist, e) |
| |
| for entry in sorted(working_set.iter_entry_points(entry_point_name), |
| key=lambda entry: entry.name): |
| env.log.debug('Loading %s from %s', entry.name, entry.dist.location) |
| try: |
| entry.load(require=True) |
| except Exception, e: |
| _log_error(entry, e) |
| else: |
| if os.path.dirname(entry.dist.location) == auto_enable: |
| _enable_plugin(env, entry.module_name) |
| return _load_eggs |
| |
| def load_py_files(): |
| """Loader that look for Python source files in the plugins directories, |
| which simply get imported, thereby registering them with the component |
| manager if they define any components. |
| """ |
| def _load_py_files(env, search_path, auto_enable=None): |
| for path in search_path: |
| plugin_files = glob(os.path.join(path, '*.py')) |
| for plugin_file in plugin_files: |
| try: |
| plugin_name = os.path.basename(plugin_file[:-3]) |
| env.log.debug('Loading file plugin %s from %s' % \ |
| (plugin_name, plugin_file)) |
| if plugin_name not in sys.modules: |
| module = imp.load_source(plugin_name, plugin_file) |
| if path == auto_enable: |
| _enable_plugin(env, plugin_name) |
| except Exception, e: |
| env.log.error('Failed to load plugin from %s: %s', |
| plugin_file, |
| exception_to_unicode(e, traceback=True)) |
| |
| return _load_py_files |
| |
| def get_plugins_dir(env): |
| """Return the path to the `plugins` directory of the environment.""" |
| plugins_dir = os.path.realpath(os.path.join(env.path, 'plugins')) |
| return os.path.normcase(plugins_dir) |
| |
| def load_components(env, extra_path=None, loaders=(load_eggs('trac.plugins'), |
| load_py_files())): |
| """Load all plugin components found on the given search path.""" |
| plugins_dir = get_plugins_dir(env) |
| search_path = [plugins_dir] |
| if extra_path: |
| search_path += list(extra_path) |
| |
| for loadfunc in loaders: |
| loadfunc(env, search_path, auto_enable=plugins_dir) |
| |
| |
| def get_plugin_info(env, include_core=False): |
| """Return package information about Trac core and installed plugins.""" |
| path_sources = {} |
| |
| def find_distribution(module): |
| name = module.__name__ |
| path = get_module_path(module) |
| sources = path_sources.get(path) |
| if sources is None: |
| sources = path_sources[path] = get_sources(path) |
| dist = sources.get(name.replace('.', '/') + '.py') |
| if dist is None: |
| dist = sources.get(name.replace('.', '/') + '/__init__.py') |
| if dist is None: |
| # This is a plain Python source file, not an egg |
| dist = pkg_resources.Distribution(project_name=name, |
| version='', |
| location=module.__file__) |
| return dist |
| |
| plugins_dir = get_plugins_dir(env) |
| plugins = {} |
| from trac.core import ComponentMeta |
| for component in ComponentMeta._components: |
| module = sys.modules[component.__module__] |
| |
| dist = find_distribution(module) |
| plugin_filename = None |
| if os.path.realpath(os.path.dirname(dist.location)) == plugins_dir: |
| plugin_filename = os.path.basename(dist.location) |
| |
| if dist.project_name not in plugins: |
| readonly = True |
| if plugin_filename and os.access(dist.location, |
| os.F_OK + os.W_OK): |
| readonly = False |
| # retrieve plugin metadata |
| info = get_pkginfo(dist) |
| if not info: |
| info = {} |
| for k in ('author', 'author_email', 'home_page', 'url', |
| 'license', 'trac'): |
| v = getattr(module, k, '') |
| if v and isinstance(v, basestring): |
| if k == 'home_page' or k == 'url': |
| k = 'home_page' |
| v = v.replace('$', '').replace('URL: ', '') |
| else: |
| v = to_unicode(v) |
| info[k] = v |
| else: |
| # Info found; set all those fields to "None" that have the |
| # value "UNKNOWN" as this is the value for fields that |
| # aren't specified in "setup.py" |
| for k in info: |
| if info[k] == 'UNKNOWN': |
| info[k] = '' |
| else: |
| # Must be encoded as unicode as otherwise Genshi |
| # may raise a "UnicodeDecodeError". |
| info[k] = to_unicode(info[k]) |
| |
| # retrieve plugin version info |
| version = dist.version |
| if not version: |
| version = (getattr(module, 'version', '') or |
| getattr(module, 'revision', '')) |
| # special handling for "$Rev$" strings |
| version = version.replace('$', '').replace('Rev: ', 'r') |
| plugins[dist.project_name] = { |
| 'name': dist.project_name, 'version': version, |
| 'path': dist.location, 'plugin_filename': plugin_filename, |
| 'readonly': readonly, 'info': info, 'modules': {}, |
| } |
| modules = plugins[dist.project_name]['modules'] |
| if module.__name__ not in modules: |
| summary, description = get_doc(module) |
| plugins[dist.project_name]['modules'][module.__name__] = { |
| 'summary': summary, 'description': description, |
| 'components': {}, |
| } |
| full_name = module.__name__ + '.' + component.__name__ |
| summary, description = get_doc(component) |
| c = component |
| if c in env and not issubclass(c, env.__class__): |
| c = component(env) |
| modules[module.__name__]['components'][component.__name__] = { |
| 'full_name': full_name, |
| 'summary': summary, 'description': description, |
| 'enabled': env.is_component_enabled(component), |
| 'required': getattr(c, 'required', False), |
| } |
| if not include_core: |
| for name in plugins.keys(): |
| if name.lower() == 'trac': |
| plugins.pop(name) |
| return sorted(plugins.itervalues(), |
| key=lambda p: (p['name'].lower() != 'trac', |
| p['name'].lower())) |
| |
| |
| def match_plugins_to_frames(plugins, frames): |
| """Add a `frame_idx` element to plugin information as returned by |
| `get_plugin_info()`, containing the index of the highest frame in the |
| list that was located in the plugin. |
| """ |
| egg_frames = [(i, f) for i, f in enumerate(frames) |
| if f['filename'].startswith('build/')] |
| |
| def find_egg_frame_index(plugin): |
| for dist in pkg_resources.find_distributions(plugin['path'], |
| only=True): |
| try: |
| sources = dist.get_metadata('SOURCES.txt') |
| for src in sources.splitlines(): |
| if src.endswith('.py'): |
| nsrc = src.replace('\\', '/') |
| for i, f in egg_frames: |
| if f['filename'].endswith(nsrc): |
| plugin['frame_idx'] = i |
| return |
| except KeyError: |
| pass # Metadata not found |
| |
| for plugin in plugins: |
| base, ext = os.path.splitext(plugin['path']) |
| if ext == '.egg' and egg_frames: |
| find_egg_frame_index(plugin) |
| else: |
| for i, f in enumerate(frames): |
| if f['filename'].startswith(base): |
| plugin['frame_idx'] = i |
| break |