#!/usr/bin/env python """ ptpython: Interactive Python shell. positional arguments: args Script and arguments optional arguments: -h, --help show this help message and exit --vi Enable Vi key bindings -i, --interactive Start interactive shell after executing this file. --asyncio Run an asyncio event loop to support top-level "await". --light-bg Run on a light background (use dark colors for text). --dark-bg Run on a dark background (use light colors for text). --config-file CONFIG_FILE Location of configuration file. --history-file HISTORY_FILE Location of history file. -V, --version show program's version number and exit environment variables: PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """ from __future__ import annotations import argparse import asyncio import os import pathlib import sys from textwrap import dedent from typing import IO import appdirs from prompt_toolkit.formatted_text import HTML from prompt_toolkit.shortcuts import print_formatted_text from ptpython.repl import PythonRepl, embed, enable_deprecation_warnings, run_config try: from importlib import metadata # type: ignore except ImportError: import importlib_metadata as metadata # type: ignore __all__ = ["create_parser", "get_config_and_history_file", "run"] class _Parser(argparse.ArgumentParser): def print_help(self, file: IO[str] | None = None) -> None: super().print_help() print( dedent( """ environment variables: PTPYTHON_CONFIG_HOME: a configuration directory to use PYTHONSTARTUP: file executed on interactive startup (no default) """, ).rstrip(), ) def create_parser() -> _Parser: parser = _Parser(description="ptpython: Interactive Python shell.") parser.add_argument("--vi", action="store_true", help="Enable Vi key bindings") parser.add_argument( "-i", "--interactive", action="store_true", help="Start interactive shell after executing this file.", ) parser.add_argument( "--asyncio", action="store_true", help='Run an asyncio event loop to support top-level "await".', ) parser.add_argument( "--light-bg", action="store_true", help="Run on a light background (use dark colors for text).", ) parser.add_argument( "--dark-bg", action="store_true", help="Run on a dark background (use light colors for text).", ) parser.add_argument( "--config-file", type=str, help="Location of configuration file." ) parser.add_argument("--history-file", type=str, help="Location of history file.") parser.add_argument( "-V", "--version", action="version", version=metadata.version("ptpython"), ) parser.add_argument("args", nargs="*", help="Script and arguments") return parser def get_config_and_history_file(namespace: argparse.Namespace) -> tuple[str, str]: """ Check which config/history files to use, ensure that the directories for these files exist, and return the config and history path. """ config_dir = os.environ.get( "PTPYTHON_CONFIG_HOME", appdirs.user_config_dir("ptpython", "prompt_toolkit"), ) data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit") # Create directories. for d in (config_dir, data_dir): pathlib.Path(d).mkdir(parents=True, exist_ok=True) # Determine config file to be used. config_file = os.path.join(config_dir, "config.py") legacy_config_file = os.path.join(os.path.expanduser("~/.ptpython"), "config.py") warnings = [] # Config file if namespace.config_file: # Override config_file. config_file = os.path.expanduser(namespace.config_file) elif os.path.isfile(legacy_config_file): # Warn about the legacy configuration file. warnings.append( HTML( " ~/.ptpython/config.py is deprecated, move your configuration to %s\n" ) % config_file ) config_file = legacy_config_file # Determine history file to be used. history_file = os.path.join(data_dir, "history") legacy_history_file = os.path.join(os.path.expanduser("~/.ptpython"), "history") if namespace.history_file: # Override history_file. history_file = os.path.expanduser(namespace.history_file) elif os.path.isfile(legacy_history_file): # Warn about the legacy history file. warnings.append( HTML( " ~/.ptpython/history is deprecated, move your history to %s\n" ) % history_file ) history_file = legacy_history_file # Print warnings. if warnings: print_formatted_text(HTML("Warning:")) for w in warnings: print_formatted_text(w) return config_file, history_file def run() -> None: a = create_parser().parse_args() config_file, history_file = get_config_and_history_file(a) # Startup path startup_paths = [] if "PYTHONSTARTUP" in os.environ: startup_paths.append(os.environ["PYTHONSTARTUP"]) # --interactive if a.interactive and a.args: # Note that we shouldn't run PYTHONSTARTUP when -i is given. startup_paths = [a.args[0]] sys.argv = a.args # Add the current directory to `sys.path`. if sys.path[0] != "": sys.path.insert(0, "") # When a file has been given, run that, otherwise start the shell. if a.args and not a.interactive: sys.argv = a.args path = a.args[0] with open(path, "rb") as f: code = compile(f.read(), path, "exec") # NOTE: We have to pass a dict as namespace. Omitting this argument # causes imports to not be found. See issue #326. # However, an empty dict sets __name__ to 'builtins', which # breaks `if __name__ == '__main__'` checks. See issue #444. exec(code, {"__name__": "__main__", "__file__": path}) # Run interactive shell. else: enable_deprecation_warnings() # Apply config file def configure(repl: PythonRepl) -> None: if os.path.exists(config_file): run_config(repl, config_file) # Adjust colors if dark/light background flag has been given. if a.light_bg: repl.min_brightness = 0.0 repl.max_brightness = 0.60 elif a.dark_bg: repl.min_brightness = 0.60 repl.max_brightness = 1.0 import __main__ embed_result = embed( # type: ignore vi_mode=a.vi, history_filename=history_file, configure=configure, locals=__main__.__dict__, globals=__main__.__dict__, startup_paths=startup_paths, title="Python REPL (ptpython)", return_asyncio_coroutine=a.asyncio, ) if a.asyncio: print("Starting ptpython asyncio REPL") print('Use "await" directly instead of "asyncio.run()".') asyncio.run(embed_result) if __name__ == "__main__": run()