From 5fcca5912068d9178ea578341760efe254b1a1a4 Mon Sep 17 00:00:00 2001 From: tgoddessana Date: Sun, 19 Nov 2023 00:33:47 +0900 Subject: [PATCH 1/3] test: add flake8 config --- .flake8 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.flake8 b/.flake8 index b851b0a..397dc0f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,9 +1,8 @@ [flake8] -select = B,B9,C,D,DAR,E,F,N,RST,W -ignore = E203,E501,RST201,RST203,RST301,W503,D100,D104,C901,B904,B307 +select = B,B9,C,D,DAR,E,F,N,RST,S,W +ignore = E203,E501,RST201,RST203,RST301,W503 max-line-length = 80 max-complexity = 10 -docstring-convention = google per-file-ignores = tests/*:S101 rst-roles = class,const,func,meth,mod,ref rst-directives = deprecated From c7937a8137bbae622d3d7e487ebc9a89affb3e0e Mon Sep 17 00:00:00 2001 From: tgoddessana Date: Sun, 19 Nov 2023 00:38:26 +0900 Subject: [PATCH 2/3] test: add flake8 config to remove docstring checks --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 397dc0f..ddd7b7f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] select = B,B9,C,D,DAR,E,F,N,RST,S,W -ignore = E203,E501,RST201,RST203,RST301,W503 +ignore = E203,E501,RST201,RST203,RST301,W503,D100,D101,D102,D104 max-line-length = 80 max-complexity = 10 per-file-ignores = tests/*:S101 From 3da669bbdba7af447239bc45d85930f5beecb343 Mon Sep 17 00:00:00 2001 From: tgoddessana Date: Sun, 19 Nov 2023 00:42:17 +0900 Subject: [PATCH 3/3] refactor: Change each shell to be class-based --- src/flask_moreshell/__main__.py | 157 +++++-------------- src/flask_moreshell/shells/__init__.py | 14 ++ src/flask_moreshell/shells/base_shell.py | 50 ++++++ src/flask_moreshell/shells/bpython_shell.py | 20 +++ src/flask_moreshell/shells/ipython_shell.py | 37 +++++ src/flask_moreshell/shells/ptpython_shell.py | 20 +++ src/flask_moreshell/shells/python_shell.py | 28 ++++ tests/shells/__init__.py | 0 8 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 src/flask_moreshell/shells/__init__.py create mode 100644 src/flask_moreshell/shells/base_shell.py create mode 100644 src/flask_moreshell/shells/bpython_shell.py create mode 100644 src/flask_moreshell/shells/ipython_shell.py create mode 100644 src/flask_moreshell/shells/ptpython_shell.py create mode 100644 src/flask_moreshell/shells/python_shell.py create mode 100644 tests/shells/__init__.py diff --git a/src/flask_moreshell/__main__.py b/src/flask_moreshell/__main__.py index 05708b9..d12aa04 100644 --- a/src/flask_moreshell/__main__.py +++ b/src/flask_moreshell/__main__.py @@ -1,15 +1,30 @@ -import os import sys import click from flask.cli import with_appcontext -from flask.globals import current_app + +from flask_moreshell.shells import BPythonShell +from flask_moreshell.shells import IpythonShell +from flask_moreshell.shells import PTPythonShell +from flask_moreshell.shells import PythonShell + + +shells = { + "ipython": IpythonShell, + "bpython": BPythonShell, + "ptpython": PTPythonShell, + "python": PythonShell, +} @click.command(context_settings=dict(ignore_unknown_options=True)) -@click.option("--shelltype", type=click.STRING, default=None) +@click.option( + "--shelltype", + type=click.Choice(["ipython", "bpython", "ptpython", "python"]), + default=None, +) @with_appcontext -def shell(shelltype: str): +def shell(shelltype: str) -> None: """Run `flask shell` command with IPython, BPython, PTPython. If you have IPython, PYTPython, or BPython installed, run them with your @@ -20,124 +35,22 @@ def shell(shelltype: str): :param shelltype: type of shell to use. """ - if shelltype: - try: - if shelltype == "ipython": - _load_ipython() - elif shelltype == "bpython": - _load_bpython() - elif shelltype == "ptpython": - _load_ptpython() - elif shelltype == "python": - _load_python() - except ModuleNotFoundError: - raise ModuleNotFoundError(f"{shelltype} is not installed on your system.") - else: - try: - _load_ipython() - sys.exit() - except ModuleNotFoundError: - pass - try: - _load_bpython() - sys.exit() - except ModuleNotFoundError: - pass - try: - _load_ptpython() - sys.exit() - except ModuleNotFoundError: - _load_python() - -def _load_ipython(): - """Load ipython shell, with current application.""" - import IPython - from IPython.terminal.ipapp import load_default_config - from traitlets.config.loader import Config + def try_load_shell(_shelltype: str) -> None: + shell_class = shells.get(_shelltype) + shell_class().load() - if "IPYTHON_CONFIG" in current_app.config: - config = Config(current_app.config["IPYTHON_CONFIG"]) + # If the user specifies a shell type, try to load it + if shelltype: + try_load_shell(shelltype) else: - config = load_default_config() - - config.TerminalInteractiveShell.banner1 = "".join( - f"Python {sys.version} on {sys.platform}\n" - f"IPython: {IPython.__version__}\n" - f"App: {current_app.import_name} [{current_app.debug}]\n" - f"Instance: {current_app.instance_path}\n" - ) - - IPython.start_ipython( - argv=[], - user_ns=current_app.make_shell_context(), - config=config, - ) - - -def _load_bpython(): - """Load bpython shell, with current application.""" - import bpython # type: ignore[import] - - banner = "".join( - f"Python {sys.version} on {sys.platform}\n" - f"BPython: {bpython.__version__}\n" - f"App: {current_app.import_name} [{current_app.debug}]\n" - f"Instance: {current_app.instance_path}\n" - ) - - ctx = {} - ctx.update(current_app.make_shell_context()) - - bpython.embed(banner=banner, locals_=ctx) - - -def _load_ptpython(): - from importlib.metadata import version - - from flask.globals import _app_ctx_stack - from ptpython.repl import embed - - banner = "".join( - f"Python {sys.version} on {sys.platform}\n" - f"PTPython: {version('ptpython')}\n" - f"App: {current_app.import_name} [{current_app.debug}]\n" - f"Instance: {current_app.instance_path}\n" - ) - - app = _app_ctx_stack.top.app - ctx = {} - - # Support the regular Python interpreter startup script if someone - # is using it. - startup = os.environ.get("PYTHONSTARTUP") - if startup and os.path.isfile(startup): - with open(startup) as f: - eval(compile(f.read(), startup, "exec"), ctx) - - ctx.update(app.make_shell_context()) - print(banner) - embed(globals=ctx) - - -def _load_python(): - """Load default python shell.""" - import code - - ctx: dict = {} - startup = os.environ.get("PYTHONSTARTUP") - if startup and os.path.isfile(startup): - with open(startup) as f: - eval(compile(f.read(), startup, "exec"), ctx) - ctx.update(current_app.make_shell_context()) - interactive_hook = getattr(sys, "__interactivehook__", None) - if interactive_hook is not None: - try: - import readline - from rlcompleter import Completer - except ImportError: - pass - else: - readline.set_completer(Completer(ctx).complete) - interactive_hook() - code.interact(local=ctx) + preferred_shells = ["ipython", "bpython", "ptpython", "python"] + for shelltype in preferred_shells: + try: + try_load_shell(shelltype) + break + except ModuleNotFoundError: + continue + if not shelltype: + print("No shell type is installed or recognized on your system.") + sys.exit(1) diff --git a/src/flask_moreshell/shells/__init__.py b/src/flask_moreshell/shells/__init__.py new file mode 100644 index 0000000..2e83013 --- /dev/null +++ b/src/flask_moreshell/shells/__init__.py @@ -0,0 +1,14 @@ +from .base_shell import BaseShell +from .bpython_shell import BPythonShell +from .ipython_shell import IpythonShell +from .ptpython_shell import PTPythonShell +from .python_shell import PythonShell + + +__all__ = [ + "BaseShell", + "BPythonShell", + "IpythonShell", + "PTPythonShell", + "PythonShell", +] diff --git a/src/flask_moreshell/shells/base_shell.py b/src/flask_moreshell/shells/base_shell.py new file mode 100644 index 0000000..96626f7 --- /dev/null +++ b/src/flask_moreshell/shells/base_shell.py @@ -0,0 +1,50 @@ +import os +import sys +from abc import ABC +from abc import abstractmethod + +from flask.globals import current_app + + +class BaseShell(ABC): + """Base class for all shells. + + you can extend this class to implement your own shell. + """ + + def get_banner(self) -> str: + """ + Return banner text to be displayed when shell starts. + + override this method to customize banner text. + """ + python_info = f"Python {sys.version} on {sys.platform}" + shell_info = f"{self.get_shell_name()} {self.get_shell_version()}" + app_info = ( + f"App: {current_app.import_name} " + f"[debug: {current_app.debug}, testing: {current_app.testing}]" + ) + config = f"Config: {current_app.config_class.__name__}" + return f"{python_info}\n{shell_info}\n{app_info}\n{config}" + + @staticmethod + def load_context(ctx: dict) -> None: # type: ignore + startup = os.environ.get("PYTHONSTARTUP") + + if startup and os.path.isfile(startup): + with open(startup) as f: + exec(f.read(), ctx) # noqa: S102 + + ctx.update(current_app.make_shell_context()) + + @abstractmethod + def get_shell_name(self) -> str: + pass + + @abstractmethod + def get_shell_version(self) -> str: + pass + + @abstractmethod + def load(self) -> None: + pass diff --git a/src/flask_moreshell/shells/bpython_shell.py b/src/flask_moreshell/shells/bpython_shell.py new file mode 100644 index 0000000..b14942d --- /dev/null +++ b/src/flask_moreshell/shells/bpython_shell.py @@ -0,0 +1,20 @@ +from flask_moreshell.shells import BaseShell + + +try: + import bpython # type: ignore +except ModuleNotFoundError as e: + raise ModuleNotFoundError("BPython is not installed on your system.") from e + + +class BPythonShell(BaseShell): + def get_shell_name(self) -> str: + return "BPython" + + def get_shell_version(self) -> str: + return str(bpython.__version__) + + def load(self) -> None: + ctx = {} # type: ignore + self.load_context(ctx) + bpython.embed(banner=self.get_banner(), locals_=ctx) diff --git a/src/flask_moreshell/shells/ipython_shell.py b/src/flask_moreshell/shells/ipython_shell.py new file mode 100644 index 0000000..5cb487e --- /dev/null +++ b/src/flask_moreshell/shells/ipython_shell.py @@ -0,0 +1,37 @@ +from typing import Any + +from flask.globals import current_app + +from flask_moreshell.shells import BaseShell + + +try: + import IPython + from IPython.terminal.ipapp import load_default_config + from traitlets.config.loader import Config +except ModuleNotFoundError as e: + raise ModuleNotFoundError("IPython is not installed on your system.") from e + + +class IpythonShell(BaseShell): + def get_shell_name(self) -> str: + return "IPython" + + def get_shell_version(self) -> str: + return IPython.__version__ + + def load(self) -> None: + config = self._get_config() + config.TerminalInteractiveShell.banner1 = self.get_banner() + + IPython.start_ipython( + argv=[], + user_ns=current_app.make_shell_context(), + config=config, + ) + + @staticmethod + def _get_config() -> Any | None: + if "IPYTHON_CONFIG" in current_app.config: + return Config(current_app.config["IPYTHON_CONFIG"]) + return load_default_config() diff --git a/src/flask_moreshell/shells/ptpython_shell.py b/src/flask_moreshell/shells/ptpython_shell.py new file mode 100644 index 0000000..7690a9c --- /dev/null +++ b/src/flask_moreshell/shells/ptpython_shell.py @@ -0,0 +1,20 @@ +import sys +from importlib.metadata import version + +from ptpython.repl import embed + +from flask_moreshell.shells import BaseShell + + +class PTPythonShell(BaseShell): + def get_shell_name(self) -> str: + return "PTPython" + + def get_shell_version(self) -> str: + return version("ptpython") + + def load(self) -> None: + ctx = {} # type: ignore + self.load_context(ctx) + sys.stdout.write(f"{self.get_banner()}\n") + embed(globals=ctx) diff --git a/src/flask_moreshell/shells/python_shell.py b/src/flask_moreshell/shells/python_shell.py new file mode 100644 index 0000000..9ed285c --- /dev/null +++ b/src/flask_moreshell/shells/python_shell.py @@ -0,0 +1,28 @@ +import code +import sys + +from flask_moreshell.shells import BaseShell + + +class PythonShell(BaseShell): + def get_shell_name(self) -> str: + return "Python" + + def get_shell_version(self) -> str: + return sys.version + + def load(self) -> None: + ctx = {} # type: ignore + self.load_context(ctx) + + interactive_hook = getattr(sys, "__interactivehook__", None) + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + readline.set_completer(Completer(ctx).complete) + interactive_hook() + code.interact(local=ctx, banner=self.get_banner()) diff --git a/tests/shells/__init__.py b/tests/shells/__init__.py new file mode 100644 index 0000000..e69de29