1from __future__ import unicode_literals
2# Copyright (c) 2006-2021  Andrey Golovizin
3# Copyright (c) 2014  Matthias C. M. Troffaes
4#
5# Permission is hereby granted, free of charge, to any person obtaining
6# a copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish,
9# distribute, sublicense, and/or sell copies of the Software, and to
10# permit persons to whom the Software is furnished to do so, subject to
11# the following conditions:
12#
13# The above copyright notice and this permission notice shall be
14# included in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24
25import os.path  # splitext
26import pkg_resources
27
28from pybtex.exceptions import PybtexError
29
30
31class Plugin(object):
32    pass
33
34
35#: default pybtex plugins
36_DEFAULT_PLUGINS = {
37    "pybtex.database.input": "bibtex",
38    "pybtex.database.output": "bibtex",
39    "pybtex.backends": "latex",
40    "pybtex.style.labels": "number",
41    "pybtex.style.names": "plain",
42    "pybtex.style.sorting": "none",
43    "pybtex.style.formatting": "unsrt",
44}
45
46
47class PluginGroupNotFound(PybtexError):
48
49    def __init__(self, group_name):
50        message = u'plugin group {group_name} not found'.format(
51            group_name=group_name,
52        )
53        super(PluginGroupNotFound, self).__init__(message)
54
55
56class PluginNotFound(PybtexError):
57
58    def __init__(self, plugin_group, name):
59        if not name.startswith('.'):
60            message = u'plugin {plugin_group}.{name} not found'.format(
61                plugin_group=plugin_group,
62                name=name,
63            )
64        else:
65            assert plugin_group.endswith('.suffixes')
66            message = (
67                u'plugin {plugin_group} for suffix {suffix} not found'.format(
68                    plugin_group=plugin_group,
69                    suffix=name,
70                )
71            )
72
73        super(PluginNotFound, self).__init__(message)
74
75
76def _load_entry_point(group, name, use_aliases=False):
77    groups = [group, group + '.aliases'] if use_aliases else [group]
78    for search_group in groups:
79        for entry_point in pkg_resources.iter_entry_points(search_group, name):
80            return entry_point.load()
81    raise PluginNotFound(group, name)
82
83
84def find_plugin(plugin_group, name=None, filename=None):
85    """Find a :class:`Plugin` class within *plugin_group* which
86    matches *name*, or *filename* if *name* is not specified, or
87    the default plugin if neither *name* nor *filename* is
88    specified.
89
90    If *name* is specified, return the :class:`Plugin` class
91    registered under *name*. If *filename* is specified, look at
92    its suffix (i.e. extension) and return the :class:`Plugin`
93    class registered for this suffix.
94
95    If *name* is not a string, but a plugin class, just return it back.
96    (Used to make functions like :function:`make_bibliography` accept already
97    loaded plugins as well as plugin names.
98
99    """
100    if isinstance(name, type) and issubclass(name, Plugin):
101        return name
102
103    if plugin_group not in _DEFAULT_PLUGINS:
104        raise PluginGroupNotFound(plugin_group)
105    if name:
106        return _load_entry_point(plugin_group, name, use_aliases=True)
107    elif filename:
108        suffix = os.path.splitext(filename)[1]
109        return _load_entry_point(plugin_group + '.suffixes', suffix)
110    else:
111        return _load_entry_point(plugin_group, _DEFAULT_PLUGINS[plugin_group])
112
113
114def enumerate_plugin_names(plugin_group):
115    """Enumerate all plugin names for the given *plugin_group*."""
116    return (entry_point.name
117            for entry_point in pkg_resources.iter_entry_points(plugin_group))
118
119
120class _FakeEntryPoint(pkg_resources.EntryPoint):
121
122    def __init__(self, name, klass):
123        self.name = name
124        self.klass = klass
125
126    def __str__(self):
127        return "%s = :%s" % (self.name, self.klass.__name__)
128
129    def __repr__(self):
130        return (
131            "_FakeEntryPoint(name=%r, klass=%s)"
132            % (self.name, self.klass.__name__))
133
134    def load(self, require=True, env=None, installer=None):
135        return self.klass
136
137    def require(self, env=None, installer=None):
138        pass
139
140
141def register_plugin(plugin_group, name, klass, force=False):
142    """Register a plugin on the fly.
143
144    This works by adding *klass* as a pybtex entry point, under entry
145    point group *plugin_group* and entry point name *name*.
146
147    To register a suffix, append ".suffixes" to the plugin group, and
148    *name* is then simply the suffix, which should start with a
149    period.
150
151    To register an alias plugin name, append ".aliases" to the plugin group.
152    Aliases work just as real names, but are not returned by
153    :function:`enumerate_plugin_names` and not advertised in the command line
154    help.
155
156    If *force* is ``False``, then existing entry points are not
157    overwritten. If an entry point with the given group and name
158    already exists, then returns ``False``, otherwise returns
159    ``True``.
160
161    If *force* is ``True``, then existing entry points are
162    overwritten, and the function always returns ``True``.
163
164    """
165    if plugin_group.endswith(".suffixes"):
166        base_group, _ = plugin_group.rsplit(".", 1)
167        if not name.startswith('.'):
168            raise ValueError("a suffix must start with a period")
169    elif plugin_group.endswith(".aliases"):
170        base_group, _ = plugin_group.rsplit(".", 1)
171    else:
172        base_group = plugin_group
173    if base_group not in _DEFAULT_PLUGINS:
174        raise PluginGroupNotFound(base_group)
175
176    dist = pkg_resources.get_distribution('pybtex')
177    ep_map = pkg_resources.get_entry_map(dist)
178    if plugin_group not in ep_map:
179        ep_map[plugin_group] = {}
180    if name in ep_map[plugin_group] and not force:
181        return False
182    else:
183        ep_map[plugin_group][name] = _FakeEntryPoint(name, klass)
184        return True
185