Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
options.py746 linesDownload Raw Back to tornado
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""A command line parsing module that lets modules define their own options.
17
18This module is inspired by Google's `gflags
19<https://github.com/google/python-gflags>`_. The primary difference
20with libraries such as `argparse` is that a global registry is used so
21that options may be defined in any module (it also enables
22`tornado.log` by default). The rest of Tornado does not depend on this
23module, so feel free to use `argparse` or other configuration
24libraries if you prefer them.
25
26Options must be defined with `tornado.options.define` before use,
27generally at the top level of a module. The options are then
28accessible as attributes of `tornado.options.options`::
29
30    # myapp/db.py
31    from tornado.options import define, options
32
33    define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
34    define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
35           help="Main user memcache servers")
36
37    def connect():
38        db = database.Connection(options.mysql_host)
39        ...
40
41    # myapp/server.py
42    from tornado.options import define, options
43
44    define("port", default=8080, help="port to listen on")
45
46    def start_server():
47        app = make_app()
48        app.listen(options.port)
49
50The ``main()`` method of your application does not need to be aware of all of
51the options used throughout your program; they are all automatically loaded
52when the modules are loaded.  However, all modules that define options
53must have been imported before the command line is parsed.
54
55Your ``main()`` method can parse the command line or parse a config file with
56either `parse_command_line` or `parse_config_file`::
57
58    import myapp.db, myapp.server
59    import tornado
60
61    if __name__ == '__main__':
62        tornado.options.parse_command_line()
63        # or
64        tornado.options.parse_config_file("/etc/server.conf")
65
66.. note::
67
68   When using multiple ``parse_*`` functions, pass ``final=False`` to all
69   but the last one, or side effects may occur twice (in particular,
70   this can result in log messages being doubled).
71
72`tornado.options.options` is a singleton instance of `OptionParser`, and
73the top-level functions in this module (`define`, `parse_command_line`, etc)
74simply call methods on it.  You may create additional `OptionParser`
75instances to define isolated sets of options, such as for subcommands.
76
77.. note::
78
79   By default, several options are defined that will configure the
80   standard `logging` module when `parse_command_line` or `parse_config_file`
81   are called.  If you want Tornado to leave the logging configuration
82   alone so you can manage it yourself, either pass ``--logging=none``
83   on the command line or do the following to disable it in code::
84
85       from tornado.options import options, parse_command_line
86       options.logging = None
87       parse_command_line()
88
89.. note::
90
91   `parse_command_line` or `parse_config_file` function should called after
92   logging configuration and user-defined command line flags using the
93   ``callback`` option definition, or these configurations will not take effect.
94
95.. versionchanged:: 4.3
96   Dashes and underscores are fully interchangeable in option names;
97   options can be defined, set, and read with any mix of the two.
98   Dashes are typical for command-line usage while config files require
99   underscores.
100"""
101
102import datetime
103import numbers
104import re
105import sys
106import os
107import textwrap
108
109from tornado.escape import _unicode, native_str
110from tornado.log import define_logging_options
111from tornado.util import basestring_type, exec_in
112
113from typing import (
114    Any,
115    Iterator,
116    Iterable,
117    Tuple,
118    Set,
119    Dict,
120    Callable,
121    List,
122    TextIO,
123    Optional,
124)
125
126
127class Error(Exception):
128    """Exception raised by errors in the options module."""
129
130    pass
131
132
133class OptionParser:
134    """A collection of options, a dictionary with object-like access.
135
136    Normally accessed via static functions in the `tornado.options` module,
137    which reference a global instance.
138    """
139
140    def __init__(self) -> None:
141        # we have to use self.__dict__ because we override setattr.
142        self.__dict__["_options"] = {}
143        self.__dict__["_parse_callbacks"] = []
144        self.define(
145            "help",
146            type=bool,
147            help="show this help information",
148            callback=self._help_callback,
149        )
150
151    def _normalize_name(self, name: str) -> str:
152        return name.replace("_", "-")
153
154    def __getattr__(self, name: str) -> Any:
155        name = self._normalize_name(name)
156        if isinstance(self._options.get(name), _Option):
157            return self._options[name].value()
158        raise AttributeError("Unrecognized option %r" % name)
159
160    def __setattr__(self, name: str, value: Any) -> None:
161        name = self._normalize_name(name)
162        if isinstance(self._options.get(name), _Option):
163            return self._options[name].set(value)
164        raise AttributeError("Unrecognized option %r" % name)
165
166    def __iter__(self) -> Iterator:
167        return (opt.name for opt in self._options.values())
168
169    def __contains__(self, name: str) -> bool:
170        name = self._normalize_name(name)
171        return name in self._options
172
173    def __getitem__(self, name: str) -> Any:
174        return self.__getattr__(name)
175
176    def __setitem__(self, name: str, value: Any) -> None:
177        return self.__setattr__(name, value)
178
179    def items(self) -> Iterable[Tuple[str, Any]]:
180        """An iterable of (name, value) pairs.
181
182        .. versionadded:: 3.1
183        """
184        return [(opt.name, opt.value()) for name, opt in self._options.items()]
185
186    def groups(self) -> Set[str]:
187        """The set of option-groups created by ``define``.
188
189        .. versionadded:: 3.1
190        """
191        return {opt.group_name for opt in self._options.values()}
192
193    def group_dict(self, group: str) -> Dict[str, Any]:
194        """The names and values of options in a group.
195
196        Useful for copying options into Application settings::
197
198            from tornado.options import define, parse_command_line, options
199
200            define('template_path', group='application')
201            define('static_path', group='application')
202
203            parse_command_line()
204
205            application = Application(
206                handlers, **options.group_dict('application'))
207
208        .. versionadded:: 3.1
209        """
210        return {
211            opt.name: opt.value()
212            for name, opt in self._options.items()
213            if not group or group == opt.group_name
214        }
215
216    def as_dict(self) -> Dict[str, Any]:
217        """The names and values of all options.
218
219        .. versionadded:: 3.1
220        """
221        return {opt.name: opt.value() for name, opt in self._options.items()}
222
223    def define(
224        self,
225        name: str,
226        default: Any = None,
227        type: Optional[type] = None,
228        help: Optional[str] = None,
229        metavar: Optional[str] = None,
230        multiple: bool = False,
231        group: Optional[str] = None,
232        callback: Optional[Callable[[Any], None]] = None,
233    ) -> None:
234        """Defines a new command line option.
235
236        ``type`` can be any of `str`, `int`, `float`, `bool`,
237        `~datetime.datetime`, or `~datetime.timedelta`. If no ``type``
238        is given but a ``default`` is, ``type`` is the type of
239        ``default``. Otherwise, ``type`` defaults to `str`.
240
241        If ``multiple`` is True, the option value is a list of ``type``
242        instead of an instance of ``type``.
243
244        ``help`` and ``metavar`` are used to construct the
245        automatically generated command line help string. The help
246        message is formatted like::
247
248           --name=METAVAR      help string
249
250        ``group`` is used to group the defined options in logical
251        groups. By default, command line options are grouped by the
252        file in which they are defined.
253
254        Command line option names must be unique globally.
255
256        If a ``callback`` is given, it will be run with the new value whenever
257        the option is changed.  This can be used to combine command-line
258        and file-based options::
259
260            define("config", type=str, help="path to config file",
261                   callback=lambda path: parse_config_file(path, final=False))
262
263        With this definition, options in the file specified by ``--config`` will
264        override options set earlier on the command line, but can be overridden
265        by later flags.
266
267        """
268        normalized = self._normalize_name(name)
269        if normalized in self._options:
270            raise Error(
271                "Option %r already defined in %s"
272                % (normalized, self._options[normalized].file_name)
273            )
274        frame = sys._getframe(0)
275        if frame is not None:
276            options_file = frame.f_code.co_filename
277
278            # Can be called directly, or through top level define() fn, in which
279            # case, step up above that frame to look for real caller.
280            if (
281                frame.f_back is not None
282                and frame.f_back.f_code.co_filename == options_file
283                and frame.f_back.f_code.co_name == "define"
284            ):
285                frame = frame.f_back
286
287            assert frame.f_back is not None
288            file_name = frame.f_back.f_code.co_filename
289        else:
290            file_name = "<unknown>"
291        if file_name == options_file:
292            file_name = ""
293        if type is None:
294            if not multiple and default is not None:
295                type = default.__class__
296            else:
297                type = str
298        if group:
299            group_name = group  # type: Optional[str]
300        else:
301            group_name = file_name
302        option = _Option(
303            name,
304            file_name=file_name,
305            default=default,
306            type=type,
307            help=help,
308            metavar=metavar,
309            multiple=multiple,
310            group_name=group_name,
311            callback=callback,
312        )
313        self._options[normalized] = option
314
315    def parse_command_line(
316        self, args: Optional[List[str]] = None, final: bool = True
317    ) -> List[str]:
318        """Parses all options given on the command line (defaults to
319        `sys.argv`).
320
321        Options look like ``--option=value`` and are parsed according
322        to their ``type``. For boolean options, ``--option`` is
323        equivalent to ``--option=true``
324
325        If the option has ``multiple=True``, comma-separated values
326        are accepted. For multi-value integer options, the syntax
327        ``x:y`` is also accepted and equivalent to ``range(x, y)``.
328
329        Note that ``args[0]`` is ignored since it is the program name
330        in `sys.argv`.
331
332        We return a list of all arguments that are not parsed as options.
333
334        If ``final`` is ``False``, parse callbacks will not be run.
335        This is useful for applications that wish to combine configurations
336        from multiple sources.
337
338        """
339        if args is None:
340            args = sys.argv
341        remaining = []  # type: List[str]
342        for i in range(1, len(args)):
343            # All things after the last option are command line arguments
344            if not args[i].startswith("-"):
345                remaining = args[i:]
346                break
347            if args[i] == "--":
348                remaining = args[i + 1 :]
349                break
350            arg = args[i].lstrip("-")
351            name, equals, value = arg.partition("=")
352            name = self._normalize_name(name)
353            if name not in self._options:
354                self.print_help()
355                raise Error("Unrecognized command line option: %r" % name)
356            option = self._options[name]
357            if not equals:
358                if option.type == bool:
359                    value = "true"
360                else:
361                    raise Error("Option %r requires a value" % name)
362            option.parse(value)
363
364        if final:
365            self.run_parse_callbacks()
366
367        return remaining
368
369    def parse_config_file(self, path: str, final: bool = True) -> None:
370        """Parses and loads the config file at the given path.
371
372        The config file contains Python code that will be executed (so
373        it is **not safe** to use untrusted config files). Anything in
374        the global namespace that matches a defined option will be
375        used to set that option's value.
376
377        Options may either be the specified type for the option or
378        strings (in which case they will be parsed the same way as in
379        `.parse_command_line`)
380
381        Example (using the options defined in the top-level docs of
382        this module)::
383
384            port = 80
385            mysql_host = 'mydb.example.com:3306'
386            # Both lists and comma-separated strings are allowed for
387            # multiple=True.
388            memcache_hosts = ['cache1.example.com:11011',
389                              'cache2.example.com:11011']
390            memcache_hosts = 'cache1.example.com:11011,cache2.example.com:11011'
391
392        If ``final`` is ``False``, parse callbacks will not be run.
393        This is useful for applications that wish to combine configurations
394        from multiple sources.
395
396        .. note::
397
398            `tornado.options` is primarily a command-line library.
399            Config file support is provided for applications that wish
400            to use it, but applications that prefer config files may
401            wish to look at other libraries instead.
402
403        .. versionchanged:: 4.1
404           Config files are now always interpreted as utf-8 instead of
405           the system default encoding.
406
407        .. versionchanged:: 4.4
408           The special variable ``__file__`` is available inside config
409           files, specifying the absolute path to the config file itself.
410
411        .. versionchanged:: 5.1
412           Added the ability to set options via strings in config files.
413
414        """
415        config = {"__file__": os.path.abspath(path)}
416        with open(path, "rb") as f:
417            exec_in(native_str(f.read()), config, config)
418        for name in config:
419            normalized = self._normalize_name(name)
420            if normalized in self._options:
421                option = self._options[normalized]
422                if option.multiple:
423                    if not isinstance(config[name], (list, str)):
424                        raise Error(
425                            "Option %r is required to be a list of %s "
426                            "or a comma-separated string"
427                            % (option.name, option.type.__name__)
428                        )
429
430                if type(config[name]) is str and (
431                    option.type is not str or option.multiple
432                ):
433                    option.parse(config[name])
434                else:
435                    option.set(config[name])
436
437        if final:
438            self.run_parse_callbacks()
439
440    def print_help(self, file: Optional[TextIO] = None) -> None:
441        """Prints all the command line options to stderr (or another file)."""
442        if file is None:
443            file = sys.stderr
444        print("Usage: %s [OPTIONS]" % sys.argv[0], file=file)
445        print("\nOptions:\n", file=file)
446        by_group = {}  # type: Dict[str, List[_Option]]
447        for option in self._options.values():
448            by_group.setdefault(option.group_name, []).append(option)
449
450        for filename, o in sorted(by_group.items()):
451            if filename:
452                print("\n%s options:\n" % os.path.normpath(filename), file=file)
453            o.sort(key=lambda option: option.name)
454            for option in o:
455                # Always print names with dashes in a CLI context.
456                prefix = self._normalize_name(option.name)
457                if option.metavar:
458                    prefix += "=" + option.metavar
459                description = option.help or ""
460                if option.default is not None and option.default != "":
461                    description += " (default %s)" % option.default
462                lines = textwrap.wrap(description, 79 - 35)
463                if len(prefix) > 30 or len(lines) == 0:
464                    lines.insert(0, "")
465                print("  --%-30s %s" % (prefix, lines[0]), file=file)
466                for line in lines[1:]:
467                    print("%-34s %s" % (" ", line), file=file)
468        print(file=file)
469
470    def _help_callback(self, value: bool) -> None:
471        if value:
472            self.print_help()
473            sys.exit(0)
474
475    def add_parse_callback(self, callback: Callable[[], None]) -> None:
476        """Adds a parse callback, to be invoked when option parsing is done."""
477        self._parse_callbacks.append(callback)
478
479    def run_parse_callbacks(self) -> None:
480        for callback in self._parse_callbacks:
481            callback()
482
483    def mockable(self) -> "_Mockable":
484        """Returns a wrapper around self that is compatible with
485        `unittest.mock.patch`.
486
487        The `unittest.mock.patch` function is incompatible with objects like ``options`` that
488        override ``__getattr__`` and ``__setattr__``.  This function returns an object that can be
489        used with `mock.patch.object <unittest.mock.patch.object>` to modify option values::
490
491            with mock.patch.object(options.mockable(), 'name', value):
492                assert options.name == value
493        """
494        return _Mockable(self)
495
496
497class _Mockable:
498    """`mock.patch` compatible wrapper for `OptionParser`.
499
500    As of ``mock`` version 1.0.1, when an object uses ``__getattr__``
501    hooks instead of ``__dict__``, ``patch.__exit__`` tries to delete
502    the attribute it set instead of setting a new one (assuming that
503    the object does not capture ``__setattr__``, so the patch
504    created a new attribute in ``__dict__``).
505
506    _Mockable's getattr and setattr pass through to the underlying
507    OptionParser, and delattr undoes the effect of a previous setattr.
508    """
509
510    def __init__(self, options: OptionParser) -> None:
511        # Modify __dict__ directly to bypass __setattr__
512        self.__dict__["_options"] = options
513        self.__dict__["_originals"] = {}
514
515    def __getattr__(self, name: str) -> Any:
516        return getattr(self._options, name)
517
518    def __setattr__(self, name: str, value: Any) -> None:
519        assert name not in self._originals, "don't reuse mockable objects"
520        self._originals[name] = getattr(self._options, name)
521        setattr(self._options, name, value)
522
523    def __delattr__(self, name: str) -> None:
524        setattr(self._options, name, self._originals.pop(name))
525
526
527class _Option:
528    # This class could almost be made generic, but the way the types
529    # interact with the multiple argument makes this tricky. (default
530    # and the callback use List[T], but type is still Type[T]).
531    UNSET = object()
532
533    def __init__(
534        self,
535        name: str,
536        default: Any = None,
537        type: Optional[type] = None,
538        help: Optional[str] = None,
539        metavar: Optional[str] = None,
540        multiple: bool = False,
541        file_name: Optional[str] = None,
542        group_name: Optional[str] = None,
543        callback: Optional[Callable[[Any], None]] = None,
544    ) -> None:
545        if default is None and multiple:
546            default = []
547        self.name = name
548        if type is None:
549            raise ValueError("type must not be None")
550        self.type = type
551        self.help = help
552        self.metavar = metavar
553        self.multiple = multiple
554        self.file_name = file_name
555        self.group_name = group_name
556        self.callback = callback
557        self.default = default
558        self._value = _Option.UNSET  # type: Any
559
560    def value(self) -> Any:
561        return self.default if self._value is _Option.UNSET else self._value
562
563    def parse(self, value: str) -> Any:
564        _parse = {
565            datetime.datetime: self._parse_datetime,
566            datetime.timedelta: self._parse_timedelta,
567            bool: self._parse_bool,
568            basestring_type: self._parse_string,
569        }.get(
570            self.type, self.type
571        )  # type: Callable[[str], Any]
572        if self.multiple:
573            self._value = []
574            for part in value.split(","):
575                if issubclass(self.type, numbers.Integral):
576                    # allow ranges of the form X:Y (inclusive at both ends)
577                    lo_str, _, hi_str = part.partition(":")
578                    lo = _parse(lo_str)
579                    hi = _parse(hi_str) if hi_str else lo
580                    self._value.extend(range(lo, hi + 1))
581                else:
582                    self._value.append(_parse(part))
583        else:
584            self._value = _parse(value)
585        if self.callback is not None:
586            self.callback(self._value)
587        return self.value()
588
589    def set(self, value: Any) -> None:
590        if self.multiple:
591            if not isinstance(value, list):
592                raise Error(
593                    "Option %r is required to be a list of %s"
594                    % (self.name, self.type.__name__)
595                )
596            for item in value:
597                if item is not None and not isinstance(item, self.type):
598                    raise Error(
599                        "Option %r is required to be a list of %s"
600                        % (self.name, self.type.__name__)
601                    )
602        else:
603            if value is not None and not isinstance(value, self.type):
604                raise Error(
605                    "Option %r is required to be a %s (%s given)"
606                    % (self.name, self.type.__name__, type(value))
607                )
608        self._value = value
609        if self.callback is not None:
610            self.callback(self._value)
611
612    # Supported date/time formats in our options
613    _DATETIME_FORMATS = [
614        "%a %b %d %H:%M:%S %Y",
615        "%Y-%m-%d %H:%M:%S",
616        "%Y-%m-%d %H:%M",
617        "%Y-%m-%dT%H:%M",
618        "%Y%m%d %H:%M:%S",
619        "%Y%m%d %H:%M",
620        "%Y-%m-%d",
621        "%Y%m%d",
622        "%H:%M:%S",
623        "%H:%M",
624    ]
625
626    def _parse_datetime(self, value: str) -> datetime.datetime:
627        for format in self._DATETIME_FORMATS:
628            try:
629                return datetime.datetime.strptime(value, format)
630            except ValueError:
631                pass
632        raise Error("Unrecognized date/time format: %r" % value)
633
634    _TIMEDELTA_ABBREV_DICT = {
635        "h": "hours",
636        "m": "minutes",
637        "min": "minutes",
638        "s": "seconds",
639        "sec": "seconds",
640        "ms": "milliseconds",
641        "us": "microseconds",
642        "d": "days",
643        "w": "weeks",
644    }
645
646    _FLOAT_PATTERN = r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?"
647
648    _TIMEDELTA_PATTERN = re.compile(
649        r"\s*(%s)\s*(\w*)\s*" % _FLOAT_PATTERN, re.IGNORECASE
650    )
651
652    def _parse_timedelta(self, value: str) -> datetime.timedelta:
653        try:
654            sum = datetime.timedelta()
655            start = 0
656            while start < len(value):
657                m = self._TIMEDELTA_PATTERN.match(value, start)
658                if not m:
659                    raise Exception()
660                num = float(m.group(1))
661                units = m.group(2) or "seconds"
662                units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
663
664                sum += datetime.timedelta(**{units: num})
665                start = m.end()
666            return sum
667        except Exception:
668            raise
669
670    def _parse_bool(self, value: str) -> bool:
671        return value.lower() not in ("false", "0", "f")
672
673    def _parse_string(self, value: str) -> str:
674        return _unicode(value)
675
676
677options = OptionParser()
678"""Global options object.
679
680All defined options are available as attributes on this object.
681"""
682
683
684def define(
685    name: str,
686    default: Any = None,
687    type: Optional[type] = None,
688    help: Optional[str] = None,
689    metavar: Optional[str] = None,
690    multiple: bool = False,
691    group: Optional[str] = None,
692    callback: Optional[Callable[[Any], None]] = None,
693) -> None:
694    """Defines an option in the global namespace.
695
696    See `OptionParser.define`.
697    """
698    return options.define(
699        name,
700        default=default,
701        type=type,
702        help=help,
703        metavar=metavar,
704        multiple=multiple,
705        group=group,
706        callback=callback,
707    )
708
709
710def parse_command_line(
711    args: Optional[List[str]] = None, final: bool = True
712) -> List[str]:
713    """Parses global options from the command line.
714
715    See `OptionParser.parse_command_line`.
716    """
717    return options.parse_command_line(args, final=final)
718
719
720def parse_config_file(path: str, final: bool = True) -> None:
721    """Parses global options from a config file.
722
723    See `OptionParser.parse_config_file`.
724    """
725    return options.parse_config_file(path, final=final)
726
727
728def print_help(file: Optional[TextIO] = None) -> None:
729    """Prints all the command line options to stderr (or another file).
730
731    See `OptionParser.print_help`.
732    """
733    return options.print_help(file)
734
735
736def add_parse_callback(callback: Callable[[], None]) -> None:
737    """Adds a parse callback, to be invoked when option parsing is done.
738
739    See `OptionParser.add_parse_callback`
740    """
741    options.add_parse_callback(callback)
742
743
744# Default options
745define_logging_options(options)
746 
codekingpro/portable-devtools · Team Ai