X Tutup
# The MIT License # # Copyright (c) 2008 Bob Farrell # Copyright (c) 2012-2021 Sebastian Ramacher # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # To gradually migrate to mypy we aren't setting these globally yet # mypy: disallow_untyped_defs=True # mypy: disallow_untyped_calls=True """ Module to handle command line argument parsing, for all front-ends. """ import argparse import code import importlib.util import logging import os import sys from pathlib import Path from typing import Tuple, List, Optional, NoReturn, Callable from . import __version__, __copyright__ from .config import default_config_path, Config from .translations import _ logger = logging.getLogger(__name__) class ArgumentParserFailed(ValueError): """Raised by the RaisingOptionParser for a bogus commandline.""" class RaisingArgumentParser(argparse.ArgumentParser): def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() def version_banner(base: str = "bpython") -> str: return _("{} version {} on top of Python {} {}").format( base, __version__, sys.version.split()[0], sys.executable, ) def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] def parse( args: Optional[List[str]], extras: Optional[Options] = None, ignore_stdin: bool = False, ) -> Tuple[Config, argparse.Namespace, List[str]]: """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra argument: this should be a tuple of (title, description, callback) title: The title for the argument group description: A full description of the argument group callback: A callback that adds argument to the argument group e.g.: def callback(group): group.add_argument('-f', action='store_true', dest='f', help='Explode') group.add_argument('-l', action='store_true', dest='l', help='Love') parse( ['-i', '-m', 'foo.py'], ( 'Front end-specific options', 'A full description of what these options are for', callback ), ) Return a tuple of (config, options, exec_args) wherein "config" is the config object either parsed from a default/specified config file or default config options, "options" is the parsed options from ArgumentParser.parse_args, and "exec_args" are the args (if any) to be parsed to the executed file (if any). """ if args is None: args = sys.argv[1:] parser = RaisingArgumentParser( usage=_( "Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does " "not know, execution falls back to the " "regular Python interpreter." ) ) parser.add_argument( "--config", default=default_config_path(), type=Path, help=_("Use CONFIG instead of default config file."), ) parser.add_argument( "--interactive", "-i", action="store_true", help=_("Drop to bpython shell after running file instead of exiting."), ) parser.add_argument( "--quiet", "-q", action="store_true", help=_("Don't print version banner."), ) parser.add_argument( "--version", "-V", action="store_true", help=_("Print version and exit."), ) parser.add_argument( "--log-level", "-l", choices=("debug", "info", "warning", "error", "critical"), default="error", help=_("Set log level for logging"), ) parser.add_argument( "--log-output", "-L", help=_("Log output file"), ) if extras is not None: extras_group = parser.add_argument_group(extras[0], extras[1]) extras[2](extras_group) # collect all the remaining arguments into a list parser.add_argument( "args", nargs=argparse.REMAINDER, help=_( "File to execute and additional arguments passed on to the executed script." ), ) try: options = parser.parse_args(args) except ArgumentParserFailed: # Just let Python handle this os.execv(sys.executable, [sys.executable] + args) if options.version: print(version_banner()) print(copyright_banner()) raise SystemExit if not ignore_stdin and not (sys.stdin.isatty() and sys.stdout.isatty()): # Just let Python handle this os.execv(sys.executable, [sys.executable] + args) # Configure logging handler bpython_logger = logging.getLogger("bpython") curtsies_logger = logging.getLogger("curtsies") bpython_logger.setLevel(options.log_level.upper()) curtsies_logger.setLevel(options.log_level.upper()) if options.log_output: handler = logging.FileHandler(filename=options.log_output) handler.setFormatter( logging.Formatter( "%(asctime)s: %(name)s: %(levelname)s: %(message)s" ) ) bpython_logger.addHandler(handler) curtsies_logger.addHandler(handler) bpython_logger.propagate = curtsies_logger.propagate = False else: bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) import cwcwidth import greenlet import pygments import requests import xdg logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) # versions of required dependencies try: import curtsies logger.info("curtsies: %s", curtsies.__version__) except ImportError: # may happen on Windows logger.info("curtsies: not available") logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) # versions of optional dependencies try: import pyperclip logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore except ImportError: logger.info("pyperclip: not available") try: import jedi logger.info("jedi: %s", jedi.__version__) except ImportError: logger.info("jedi: not available") try: import watchdog logger.info("watchdog: available") except ImportError: logger.info("watchdog: not available") logger.info("environment:") for key, value in sorted(os.environ.items()): if key.startswith("LC") or key.startswith("LANG") or key == "TERM": logger.info("%s: %s", key, value) return Config(options.config), options, options.args def exec_code( interpreter: code.InteractiveInterpreter, args: List[str] ) -> None: """ Helper to execute code in a given interpreter, e.g. to implement the behavior of python3 [-i] file.py args should be a [faked] sys.argv. """ try: with open(args[0]) as sourcefile: source = sourcefile.read() except OSError as e: # print an error and exit (if -i is specified the calling code will continue) print(f"bpython: can't open file '{args[0]}: {e}", file=sys.stderr) raise SystemExit(e.errno) old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) spec = importlib.util.spec_from_loader("__main__", loader=None) assert spec mod = importlib.util.module_from_spec(spec) sys.modules["__main__"] = mod interpreter.locals.update(mod.__dict__) # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.locals["__file__"] = args[0] # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.runsource(source, args[0], "exec") sys.argv = old_argv
X Tutup