1# -*- Mode: Python -*-
2# pygobject - Python bindings for the GObject library
3# Copyright (C) 2006  Johannes Hoelzl
4#
5#   glib/option.py: GOption command line parser
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2.1 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, see <http://www.gnu.org/licenses/>.
19
20"""GOption command line parser
21
22Extends optparse to use the GOptionGroup, GOptionEntry and GOptionContext
23objects. So it is possible to use the gtk, gnome_program and gstreamer command
24line groups and contexts.
25
26Use this interface instead of the raw wrappers of GOptionContext and
27GOptionGroup in glib.
28"""
29
30import sys
31import optparse
32from optparse import OptParseError, OptionError, OptionValueError, \
33    BadOptionError, OptionConflictError
34from .module import get_introspection_module
35
36from gi import _gi
37from gi._error import GError
38GLib = get_introspection_module('GLib')
39
40OPTION_CONTEXT_ERROR_QUARK = GLib.quark_to_string(GLib.option_error_quark())
41
42__all__ = [
43    "OptParseError",
44    "OptionError",
45    "OptionValueError",
46    "BadOptionError",
47    "OptionConflictError",
48    "Option",
49    "OptionGroup",
50    "OptionParser",
51    "make_option",
52]
53
54
55class Option(optparse.Option):
56    """Represents a command line option
57
58    To use the extended possibilities of the GOption API Option
59    (and make_option) are extended with new types and attributes.
60
61    Types:
62        filename   The supplied arguments are read as filename, GOption
63                   parses this type in with the GLib filename encoding.
64
65    :ivar optional_arg:
66        This does not need a arguement, but it can be supplied.
67    :ivar hidden:
68        The help list does not show this option
69    :ivar in_main:
70        This option apears in the main group, this should only
71        be used for backwards compatibility.
72
73    Use Option.REMAINING as option name to get all positional arguments.
74
75    .. NOTE::
76        Every argument to an option is passed as utf-8 coded string, the only
77        exception are options which use the 'filename' type, its arguments
78        are passed as strings in the GLib filename encoding.
79
80    For further help, see optparse.Option.
81    """
82    TYPES = optparse.Option.TYPES + (
83        'filename',
84    )
85
86    ATTRS = optparse.Option.ATTRS + [
87        'hidden',
88        'in_main',
89        'optional_arg',
90    ]
91
92    REMAINING = '--' + GLib.OPTION_REMAINING
93
94    def __init__(self, *args, **kwargs):
95        optparse.Option.__init__(self, *args, **kwargs)
96        if not self._long_opts:
97            raise ValueError("%s at least one long option name.")
98
99        if len(self._long_opts) < len(self._short_opts):
100            raise ValueError(
101                "%s at least more long option names than short option names.")
102
103        if not self.help:
104            raise ValueError("%s needs a help message.", self._long_opts[0])
105
106    def _set_opt_string(self, opts):
107        if self.REMAINING in opts:
108            self._long_opts.append(self.REMAINING)
109        optparse.Option._set_opt_string(self, opts)
110        if len(self._short_opts) > len(self._long_opts):
111            raise OptionError("goption.Option needs more long option names "
112                              "than short option names")
113
114    def _to_goptionentries(self):
115        flags = 0
116
117        if self.hidden:
118            flags |= GLib.OptionFlags.HIDDEN
119
120        if self.in_main:
121            flags |= GLib.OptionFlags.IN_MAIN
122
123        if self.takes_value():
124            if self.optional_arg:
125                flags |= GLib.OptionFlags.OPTIONAL_ARG
126        else:
127            flags |= GLib.OptionFlags.NO_ARG
128
129        if self.type == 'filename':
130            flags |= GLib.OptionFlags.FILENAME
131
132        for (long_name, short_name) in zip(self._long_opts, self._short_opts):
133            short_bytes = short_name[1]
134            if not isinstance(short_bytes, bytes):
135                short_bytes = short_bytes.encode("utf-8")
136            yield (long_name[2:], short_bytes, flags, self.help, self.metavar)
137
138        for long_name in self._long_opts[len(self._short_opts):]:
139            yield (long_name[2:], b'\0', flags, self.help, self.metavar)
140
141
142class OptionGroup(optparse.OptionGroup):
143    """A group of command line options.
144
145    :param str name:
146        The groups name, used to create the --help-{name} option
147    :param str description:
148        Shown as title of the groups help view
149    :param str help_description:
150        Shown as help to the --help-{name} option
151    :param list option_list:
152        The options used in this group, must be option.Option()
153    :param dict defaults:
154        A dicitionary of default values
155    :param translation_domain:
156           Sets the translation domain for gettext().
157
158    .. NOTE::
159        This OptionGroup does not exactly map the optparse.OptionGroup
160        interface. There is no parser object to supply, but it is possible
161        to set default values and option_lists. Also the default values and
162        values are not shared with the OptionParser.
163
164    To pass a OptionGroup into a function which expects a GOptionGroup (e.g.
165    gnome_program_init() ). OptionGroup.get_option_group() can be used.
166
167    For further help, see optparse.OptionGroup.
168    """
169    def __init__(self, name, description, help_description="",
170                 option_list=None, defaults=None,
171                 translation_domain=None):
172        optparse.OptionContainer.__init__(self, Option, 'error', description)
173        self.name = name
174        self.parser = None
175        self.help_description = help_description
176        if defaults:
177            self.defaults = defaults
178
179        self.values = None
180
181        self.translation_domain = translation_domain
182
183        if option_list:
184            for option in option_list:
185                self.add_option(option)
186
187    def _create_option_list(self):
188        self.option_list = []
189        self._create_option_mappings()
190
191    def _to_goptiongroup(self, parser):
192        def callback(option_name, option_value, group):
193            if option_name.startswith('--'):
194                opt = self._long_opt[option_name]
195            else:
196                opt = self._short_opt[option_name]
197
198            try:
199                opt.process(option_name, option_value, self.values, parser)
200            except OptionValueError:
201                error = sys.exc_info()[1]
202                gerror = GError(str(error))
203                gerror.domain = OPTION_CONTEXT_ERROR_QUARK
204                gerror.code = GLib.OptionError.BAD_VALUE
205                gerror.message = str(error)
206                raise gerror
207
208        group = _gi.OptionGroup(self.name, self.description,
209                                self.help_description, callback)
210        if self.translation_domain:
211            group.set_translation_domain(self.translation_domain)
212
213        entries = []
214        for option in self.option_list:
215            entries.extend(option._to_goptionentries())
216
217        group.add_entries(entries)
218
219        return group
220
221    def get_option_group(self, parser=None):
222        """ Returns the corresponding GOptionGroup object.
223
224        Can be used as parameter for gnome_program_init(), gtk_init().
225        """
226        self.set_values_to_defaults()
227        return self._to_goptiongroup(parser)
228
229    def set_values_to_defaults(self):
230        for option in self.option_list:
231            default = self.defaults.get(option.dest)
232            if isinstance(default, str):
233                opt_str = option.get_opt_string()
234                self.defaults[option.dest] = option.check_value(
235                    opt_str, default)
236        self.values = optparse.Values(self.defaults)
237
238
239class OptionParser(optparse.OptionParser):
240    """Command line parser with GOption support.
241
242    :param bool help_enabled:
243        The --help, --help-all and --help-{group} options are enabled (default).
244    :param bool ignore_unknown_options:
245        Do not throw a exception when a option is not knwon, the option
246        will be in the result list.
247
248    .. NOTE::
249        The OptionParser interface is not the exactly the same as the
250        optparse.OptionParser interface. Especially the usage parameter
251        is only used to show the metavar of the arguements.
252
253    OptionParser.add_option_group() does not only accept OptionGroup instances
254    but also glib.OptionGroup, which is returned by gtk_get_option_group().
255
256    Only glib.option.OptionGroup and glib.option.Option instances should
257    be passed as groups and options.
258
259    For further help, see optparse.OptionParser.
260    """
261
262    def __init__(self, *args, **kwargs):
263        if 'option_class' not in kwargs:
264            kwargs['option_class'] = Option
265        self.help_enabled = kwargs.pop('help_enabled', True)
266        self.ignore_unknown_options = kwargs.pop('ignore_unknown_options',
267                                                 False)
268        optparse.OptionParser.__init__(self, add_help_option=False,
269                                       *args, **kwargs)
270
271    def set_usage(self, usage):
272        if usage is None:
273            self.usage = ''
274        elif usage.startswith("%prog"):
275            self.usage = usage[len("%prog"):]
276        else:
277            self.usage = usage
278
279    def _to_goptioncontext(self, values):
280        if self.description:
281            parameter_string = self.usage + " - " + self.description
282        else:
283            parameter_string = self.usage
284        context = _gi.OptionContext(parameter_string)
285        context.set_help_enabled(self.help_enabled)
286        context.set_ignore_unknown_options(self.ignore_unknown_options)
287
288        for option_group in self.option_groups:
289            if isinstance(option_group, _gi.OptionGroup):
290                g_group = option_group
291            else:
292                g_group = option_group.get_option_group(self)
293            context.add_group(g_group)
294
295        def callback(option_name, option_value, group):
296            if option_name.startswith('--'):
297                opt = self._long_opt[option_name]
298            else:
299                opt = self._short_opt[option_name]
300            opt.process(option_name, option_value, values, self)
301
302        main_group = _gi.OptionGroup(None, None, None, callback)
303        main_entries = []
304        for option in self.option_list:
305            main_entries.extend(option._to_goptionentries())
306        main_group.add_entries(main_entries)
307        context.set_main_group(main_group)
308
309        return context
310
311    def add_option_group(self, *args, **kwargs):
312        if isinstance(args[0], str):
313            optparse.OptionParser.add_option_group(self,
314                                                   OptionGroup(self, *args, **kwargs))
315            return
316        elif len(args) == 1 and not kwargs:
317            if isinstance(args[0], OptionGroup):
318                if not args[0].parser:
319                    args[0].parser = self
320                if args[0].parser is not self:
321                    raise ValueError("invalid OptionGroup (wrong parser)")
322            if isinstance(args[0], _gi.OptionGroup):
323                self.option_groups.append(args[0])
324                return
325        optparse.OptionParser.add_option_group(self, *args, **kwargs)
326
327    def _get_all_options(self):
328        options = self.option_list[:]
329        for group in self.option_groups:
330            if isinstance(group, optparse.OptionGroup):
331                options.extend(group.option_list)
332        return options
333
334    def _process_args(self, largs, rargs, values):
335        context = self._to_goptioncontext(values)
336
337        # _process_args() returns the remaining parameters in rargs.
338        # The prepended program name is used to all g_set_prgname()
339        # The program name is cut away so it doesn't appear in the result.
340        rargs[:] = context.parse([sys.argv[0]] + rargs)[1:]
341
342    def parse_args(self, args=None, values=None):
343        try:
344            options, args = optparse.OptionParser.parse_args(
345                self, args, values)
346        except GError:
347            error = sys.exc_info()[1]
348            if error.domain != OPTION_CONTEXT_ERROR_QUARK:
349                raise
350            if error.code == GLib.OptionError.BAD_VALUE:
351                raise OptionValueError(error.message)
352            elif error.code == GLib.OptionError.UNKNOWN_OPTION:
353                raise BadOptionError(error.message)
354            elif error.code == GLib.OptionError.FAILED:
355                raise OptParseError(error.message)
356            else:
357                raise
358
359        for group in self.option_groups:
360            for key, value in group.values.__dict__.items():
361                options.ensure_value(key, value)
362
363        return options, args
364
365
366make_option = Option
367