1#  Licensed under the Apache License, Version 2.0 (the "License"); you may
2#  not use this file except in compliance with the License. You may obtain
3#  a copy of the License at
4#
5#       http://www.apache.org/licenses/LICENSE-2.0
6#
7#  Unless required by applicable law or agreed to in writing, software
8#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10#  License for the specific language governing permissions and limitations
11#  under the License.
12
13"""Discover and lookup command plugins.
14"""
15
16import logging
17
18import pkg_resources
19
20from . import utils
21
22
23LOG = logging.getLogger(__name__)
24
25
26def _get_commands_by_partial_name(args, commands):
27    n = len(args)
28    candidates = []
29    for command_name in commands:
30        command_parts = command_name.split()
31        if len(command_parts) != n:
32            continue
33        if all(command_parts[i].startswith(args[i]) for i in range(n)):
34            candidates.append(command_name)
35    return candidates
36
37
38class EntryPointWrapper(object):
39    """Wrap up a command class already imported to make it look like a plugin.
40    """
41
42    def __init__(self, name, command_class):
43        self.name = name
44        self.command_class = command_class
45
46    def load(self, require=False):
47        return self.command_class
48
49
50class CommandManager(object):
51    """Discovers commands and handles lookup based on argv data.
52
53    :param namespace: String containing the setuptools entrypoint namespace
54                      for the plugins to be loaded. For example,
55                      ``'cliff.formatter.list'``.
56    :param convert_underscores: Whether cliff should convert underscores to
57                                spaces in entry_point commands.
58    """
59    def __init__(self, namespace, convert_underscores=True):
60        self.commands = {}
61        self._legacy = {}
62        self.namespace = namespace
63        self.convert_underscores = convert_underscores
64        self.group_list = []
65        self._load_commands()
66
67    def _load_commands(self):
68        # NOTE(jamielennox): kept for compatibility.
69        if self.namespace:
70            self.load_commands(self.namespace)
71
72    def load_commands(self, namespace):
73        """Load all the commands from an entrypoint"""
74        self.group_list.append(namespace)
75        for ep in pkg_resources.iter_entry_points(namespace):
76            LOG.debug('found command %r', ep.name)
77            cmd_name = (ep.name.replace('_', ' ')
78                        if self.convert_underscores
79                        else ep.name)
80            self.commands[cmd_name] = ep
81        return
82
83    def __iter__(self):
84        return iter(self.commands.items())
85
86    def add_command(self, name, command_class):
87        self.commands[name] = EntryPointWrapper(name, command_class)
88
89    def add_legacy_command(self, old_name, new_name):
90        """Map an old command name to the new name.
91
92        :param old_name: The old command name.
93        :type old_name: str
94        :param new_name: The new command name.
95        :type new_name: str
96
97        """
98        self._legacy[old_name] = new_name
99
100    def find_command(self, argv):
101        """Given an argument list, find a command and
102        return the processor and any remaining arguments.
103        """
104        start = self._get_last_possible_command_index(argv)
105        for i in range(start, 0, -1):
106            name = ' '.join(argv[:i])
107            search_args = argv[i:]
108            # The legacy command handling may modify name, so remember
109            # the value we actually found in argv so we can return it.
110            return_name = name
111            # Convert the legacy command name to its new name.
112            if name in self._legacy:
113                name = self._legacy[name]
114
115            found = None
116            if name in self.commands:
117                found = name
118            else:
119                candidates = _get_commands_by_partial_name(
120                    argv[:i], self.commands)
121                if len(candidates) == 1:
122                    found = candidates[0]
123            if found:
124                cmd_ep = self.commands[found]
125                if hasattr(cmd_ep, 'resolve'):
126                    cmd_factory = cmd_ep.resolve()
127                else:
128                    # NOTE(dhellmann): Some fake classes don't take
129                    # require as an argument. Yay?
130                    arg_spec = utils.getargspec(cmd_ep.load)
131                    if 'require' in arg_spec[0]:
132                        cmd_factory = cmd_ep.load(require=False)
133                    else:
134                        cmd_factory = cmd_ep.load()
135                return (cmd_factory, return_name, search_args)
136        else:
137            raise ValueError('Unknown command %r' %
138                             (argv,))
139
140    def _get_last_possible_command_index(self, argv):
141        """Returns the index after the last argument
142        in argv that can be a command word
143        """
144        for i, arg in enumerate(argv):
145            if arg.startswith('-'):
146                return i
147        return len(argv)
148
149    def add_command_group(self, group=None):
150        """Adds another group of command entrypoints"""
151        if group:
152            self.load_commands(group)
153
154    def get_command_groups(self):
155        """Returns a list of the loaded command groups"""
156        return self.group_list
157
158    def get_command_names(self, group=None):
159        """Returns a list of commands loaded for the specified group"""
160        group_list = []
161        if group is not None:
162            for ep in pkg_resources.iter_entry_points(group):
163                cmd_name = (
164                    ep.name.replace('_', ' ')
165                    if self.convert_underscores
166                    else ep.name
167                )
168                group_list.append(cmd_name)
169            return group_list
170        return list(self.commands.keys())
171