├── tests ├── __init__.py └── test_utils_python.py ├── peepshow ├── cmds │ ├── __init__.py │ ├── base.py │ └── cmds.py ├── core │ ├── __init__.py │ ├── exceptions.py │ ├── goto.py │ ├── show.py │ ├── env.py │ ├── context.py │ ├── explorer.py │ ├── peep.py │ ├── probes.py │ ├── trans.py │ └── dialect.py ├── pager │ ├── __init__.py │ ├── cache.py │ └── pager.py ├── utils │ ├── __init__.py │ ├── system.py │ ├── magic_peep.py │ ├── terminal.py │ ├── traceback.py │ └── python.py ├── __init__.py ├── peep.py ├── show.py └── peepshow.1 ├── poetry.toml ├── docs ├── demo.gif ├── macros.py ├── index.md ├── custom.css ├── show.md ├── peep.md └── demo.cast ├── demo.py ├── .gitignore ├── samples ├── sample02.py ├── sample01.py ├── sample03.py └── sample99.py ├── LICENSE ├── pyproject.toml ├── mkdocs.yml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peepshow/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peepshow/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peepshow/pager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peepshow/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /peepshow/core/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class CommandError(Exception): pass 3 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gergelyk/peepshow/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from peepshow import peep 2 | 3 | x = 123 4 | y = {'name': 'John', 'age': 33, 'skills': ['python', 'django', 'rest', 'sql']} 5 | z = "Hello World!" 6 | 7 | peep() 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # cpython 2 | __pycache__ 3 | *.egg-info 4 | *.pyc 5 | 6 | # poetry 7 | .venv 8 | dist/ 9 | 10 | # mypy 11 | .mypy_cache/ 12 | 13 | # mkdocs 14 | site/ 15 | 16 | # pytest-cov 17 | htmlcov/ 18 | .coverage 19 | -------------------------------------------------------------------------------- /peepshow/core/goto.py: -------------------------------------------------------------------------------- 1 | 2 | class GoToLabel(Exception): 3 | pass 4 | 5 | class Stop(GoToLabel): 6 | def __init__(self, *, exit): 7 | self.exit = exit 8 | 9 | class NextCommand(GoToLabel): 10 | pass 11 | 12 | -------------------------------------------------------------------------------- /docs/macros.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | def define_env(env): 4 | 5 | @env.macro 6 | def random_digits(n): 7 | """Sample macro""" 8 | 9 | digits = [str(random.randint(0, 9)) for _ in range(n)] 10 | return ', '.join(digits) 11 | -------------------------------------------------------------------------------- /peepshow/core/show.py: -------------------------------------------------------------------------------- 1 | from peepshow.utils.python import prettify_expr, pformat 2 | 3 | def show(names, values): 4 | """Show names of variables together with corresponding values.""" 5 | for name, value in zip(names, values): 6 | print(f'{prettify_expr(name)} = {pformat(value)}') 7 | -------------------------------------------------------------------------------- /samples/sample02.py: -------------------------------------------------------------------------------- 1 | x = 123 2 | y = {'name': 'John', 'age': 123} 3 | z = "Hello World!" 4 | 5 | # peep dictionary that consists of all variables in scope 6 | peep() 7 | 8 | # or only selected variable 9 | peep(x) 10 | 11 | # use 'peep_' to specify variable name as a string 12 | peep_('x') 13 | -------------------------------------------------------------------------------- /peepshow/core/env.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | 3 | class Env(SimpleNamespace): 4 | def __init__(self, glo, loc): 5 | super().__init__(glo=glo, loc=loc) 6 | self.initial = {**glo, **loc} 7 | self.current = {**self.initial} 8 | 9 | def update(self, items): 10 | self.current.update(items) 11 | -------------------------------------------------------------------------------- /peepshow/utils/system.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from math import ceil, log2 4 | 5 | OS_IS_WINDOWS = os.name == 'nt' 6 | 7 | # https://docs.python.org/3/library/platform.html#cross-platform 8 | # read paragraph on platform.architecture() 9 | OS_BITS = 2**ceil(log2(log2(sys.maxsize))) 10 | 11 | dev_mode_enabled = bool(int(os.getenv('PEEPSHOW_DEV_MODE', 0))) 12 | -------------------------------------------------------------------------------- /samples/sample01.py: -------------------------------------------------------------------------------- 1 | x = 123 2 | y = {'name': 'John', 'age': 123} 3 | z = "Hello World!" 4 | 5 | # show all variables in scope 6 | show() 7 | 8 | # or only selected variables 9 | show(x, y) 10 | 11 | # you can also rename them 12 | show(my_var=x) 13 | 14 | # use 'show_' to specify variable names as a string 15 | show_('x') 16 | 17 | # expressions and renaming are also allowed 18 | show_('x + 321', zet='z') 19 | -------------------------------------------------------------------------------- /peepshow/utils/magic_peep.py: -------------------------------------------------------------------------------- 1 | from IPython.core.magic import register_line_magic 2 | 3 | @register_line_magic 4 | def peep(target): 5 | """Peep the target. 6 | 7 | Usage: 8 | %peep target. 9 | 10 | Calls peep() from peepshow library. 11 | """ 12 | 13 | import peepshow 14 | target = target.strip() 15 | if target: 16 | return peepshow.peep_(target) 17 | else: 18 | return peepshow.peep_() 19 | 20 | del peep 21 | del register_line_magic 22 | -------------------------------------------------------------------------------- /peepshow/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | supported_versions = ('3.6', '3.7', '3.8') 4 | python_version = '.'.join(map(str, sys.version_info[:2])) 5 | if python_version not in supported_versions: 6 | raise RuntimeError('python version ' + python_version + ' is not supported') 7 | 8 | from peepshow.peep import peep, peep_ 9 | from peepshow.show import show, show_ 10 | from peepshow.utils.python import catch 11 | from peepshow.utils.python import nth 12 | from peepshow.utils.traceback import enable_except_hook 13 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ![](https://user-images.githubusercontent.com/11185582/51219128-b3127780-192f-11e9-8618-ecfff642b87f.gif) 4 | 5 | `peepshow` package provides following utilities for debugging Python applications: 6 | 7 | * `show` - lightweight function that prints name and value of your variable(s) to the console. 8 | * `peep` - featured, interactive interface for data inspection. 9 | 10 | Following sections explain how to use these utilities. 11 | 12 | Installation instructions can be found in [README](https://github.com/gergelyk/peepshow/) file. 13 | -------------------------------------------------------------------------------- /docs/custom.css: -------------------------------------------------------------------------------- 1 | div.doc-contents:not(.first) { 2 | padding-left: 25px; 3 | border-left: 4px solid rgba(230, 230, 230); 4 | margin-bottom: 80px; 5 | } 6 | 7 | h5.doc-heading { 8 | text-transform: none !important; 9 | } 10 | 11 | h6.hidden-toc { 12 | margin: 0 !important; 13 | position: relative; 14 | top: -70px; 15 | } 16 | 17 | h6.hidden-toc::before { 18 | margin-top: 0 !important; 19 | padding-top: 0 !important; 20 | } 21 | 22 | h6.hidden-toc a.headerlink { 23 | display: none; 24 | } 25 | 26 | td code { 27 | word-break: normal !important; 28 | } 29 | 30 | td p { 31 | margin-top: 0 !important; 32 | margin-bottom: 0 !important; 33 | } 34 | -------------------------------------------------------------------------------- /samples/sample03.py: -------------------------------------------------------------------------------- 1 | 2 | class Shelf: 3 | def __init__(self): 4 | self.items = [] 5 | 6 | def put(self, item): 7 | self.items.append(item) 8 | 9 | def show(self): 10 | for item in self.items: 11 | print(item) 12 | 13 | class Book: 14 | def __init__(self, title, pages): 15 | self.title = title 16 | self.pages = pages 17 | 18 | def __repr__(self): 19 | return f'' 20 | 21 | def __len__(self): 22 | return pages 23 | 24 | 25 | shelf = Shelf() 26 | book1 = Book("Ulysses", 272) 27 | book2 = Book("Hamlet", 342) 28 | 29 | shelf.put(book1) 30 | shelf.put(book2) 31 | 32 | peep(shelf) 33 | 34 | shelf.show() 35 | -------------------------------------------------------------------------------- /docs/show.md: -------------------------------------------------------------------------------- 1 | # Show 2 | 3 | `show` is a function that displays variables given as arguments. Names of the variables that are provided as positional arguments are determined based on Python reflection. 4 | 5 | ```python 6 | >>> x = 123 7 | >>> y = [1, 2, 3] 8 | >>> show(x, y, x*2+1) 9 | x = 123 10 | y = [1, 2, 3] 11 | x * 2 + 1 = 247 12 | ``` 13 | 14 | Variables that are provided as keyword arguments inherit names from corresponding arguments. 15 | 16 | ```python 17 | >>> x = 123 18 | >>> y = 234 19 | >>> show(foo=x+y) 20 | foo = 357 21 | ``` 22 | 23 | There is also `show_` function that expects their arguments to be expressions that should be evaluated in context of the caller. 24 | 25 | ```python 26 | >>> x = 123 27 | >>> y = 234 28 | >>> show_('x+y', py_ver='sys.version.split()[0]') 29 | x + y = 357 30 | py_ver = '3.6.2' 31 | ``` 32 | 33 | `show` and `show_` functions can be also called without arguments to display all the variables in context of the caller. 34 | -------------------------------------------------------------------------------- /peepshow/core/context.py: -------------------------------------------------------------------------------- 1 | import peepshow.core.dialect as dialect 2 | from peepshow.utils import terminal 3 | from peepshow.core.explorer import Explorer 4 | from peepshow.utils.python import catch, nth 5 | from peepshow.core.trans import TransformationMgr 6 | 7 | class Context: 8 | def __init__(self, target, env): 9 | self.mgr = TransformationMgr(target, self) 10 | self._env = env 11 | self.explorer = Explorer(self) 12 | 13 | @property 14 | def env(self): 15 | predefined = {'_': self.target, 'catch': catch, 'nth': nth} 16 | self._env.update(predefined) 17 | return self._env 18 | 19 | @property 20 | def target(self): 21 | return self.mgr.selected.result 22 | 23 | @property 24 | def readable(self): 25 | return dialect.Readable().stringify(self.mgr.selected) 26 | 27 | def eval_(self, expr): 28 | return eval(expr, {}, self.env.current) 29 | 30 | def exec_(self, expr): 31 | exec(expr, {}, self.env.current) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Grzegorz Krasoń 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "peepshow" 3 | version = "0.3.0" 4 | description = "Python data explorer." 5 | authors = ["Grzegorz Krasoń "] 6 | homepage = "https://gergelyk.github.io/peepshow" 7 | documentation = "https://gergelyk.github.io/peepshow" 8 | repository = "https://github.com/gergelyk/peepshow" 9 | license = "MIT" 10 | readme = "README.md" 11 | keywords = ["debug", "data", "explore", "programming"] 12 | include = ["peepshow/peepshow.1"] 13 | 14 | [tool.poetry.dependencies] 15 | python = "^3.6" 16 | astor = "^0.8.1" 17 | astunparse = "^1.6.3" 18 | colorama = "^0.4.4" 19 | pprintpp = "^0.4.0" 20 | pygments = "^2.7.2" 21 | miscutils = "^1.4.0" 22 | py-getch = "^1.0.1" 23 | 24 | [tool.poetry.dev-dependencies] 25 | mkdocs-material = "^4.6.0" 26 | pytest = "^5.3.5" 27 | bs4 = "^0.0.1" 28 | mkdocs-plugin-mermaid = "^0.1.1" 29 | mdx_include = "^1.3.3" 30 | mkdocs-htmlproofer-plugin = "^0.0.3" 31 | mkdocs-plugin-progress = "^1.1.4" 32 | mkdocs-macros-plugin = "0.4.9" 33 | pytest-cov = "^2.8.1" 34 | pytest-html = "^2.1.1" 35 | lxml = "^4.5.0" 36 | yapf = "^0.29.0" 37 | docformatter = "^1.3.1" 38 | flake8 = "^3.7.9" 39 | isort = "^4.3.21" 40 | mkdocs = "^1.1" 41 | mkdocstrings = "^0.10.1" 42 | 43 | [build-system] 44 | requires = ["poetry>=0.12"] 45 | build-backend = "poetry.masonry.api" 46 | -------------------------------------------------------------------------------- /samples/sample99.py: -------------------------------------------------------------------------------- 1 | from peepshow import show, peep 2 | from peepshow import show_, peep_ 3 | 4 | class NS: 5 | class Foo: 6 | """ Bleh! 7 | """ 8 | x = 12 9 | _a = 123 10 | __b = None 11 | 12 | w = [1,2,3] 13 | 14 | def __init__(self): 15 | self.y = 23 16 | self._c = 123 17 | self.__d = None 18 | 19 | def __bool__(self): 20 | return True 21 | 22 | def __gtx__(self): 23 | pass 24 | 25 | xn = None 26 | xl = [1,2,3] 27 | xt = (1,2,3) 28 | xi = (x for x in range(1000)) 29 | xd1 = {'k1':'v1', 'k2':'v2', 'k3':'v3'} 30 | xd2 = {1: 11, 2: 22, 3: 33} 31 | xd3 = {'k1': 11, 'k2': 22, 'k3': 33} 32 | xs = {1,2,3} 33 | 34 | idx = 1 35 | 36 | txt = 'abcdefghijklmnopqrstuvwxyz'*5 37 | 38 | 39 | def xf1(*args, **kwargs): 40 | #return (*args, *kwargs.values()) 41 | return args[0] 42 | 43 | def xf2(x: "descr") -> "ret": 44 | return 45 | 46 | class xc: 47 | def foo(self): 48 | return 123 49 | 50 | def foo2(self): 51 | return 321 52 | 53 | def _bar(self): 54 | pass 55 | 56 | def __baz(self): 57 | pass 58 | 59 | class xc2(xc): 60 | def __init__(self, a,b,c): 61 | self.aaa = 123 62 | 63 | xo2 = xc2(0,0,0) 64 | 65 | class xc3(str): 66 | pass 67 | 68 | xo = xc() 69 | 70 | def xg(): 71 | while True: 72 | yield 123 73 | 74 | 75 | def rai(x, y): 76 | if x > y: 77 | raise Exception('x is greater than y') 78 | return y-x 79 | 80 | 81 | peep() 82 | -------------------------------------------------------------------------------- /peepshow/peep.py: -------------------------------------------------------------------------------- 1 | 2 | def peep(*args): 3 | """Examine local data. 4 | peep() # examine all variables in the scope (locals cover globals) 5 | peep(x) # examine x (name will be determined only if possible) 6 | """ 7 | 8 | from peepshow.core import peep as core 9 | from peepshow.utils import python as utils 10 | from peepshow.core.trans import GloLoc, Given 11 | 12 | if len(args) > 1: 13 | raise TypeError("Too many arguments.") 14 | 15 | env = utils.caller_gloloc() 16 | 17 | if args: 18 | expr = utils.arg_names()[0] 19 | target = Given(args[0], expr) 20 | else: 21 | target = GloLoc(env.initial) 22 | 23 | last_target = core.peep(target, env) 24 | 25 | 26 | def peep_(*args): 27 | """Examine local data. 28 | peep_() # examine all variables in the scope (locals cover globals) 29 | peep_('x') # examine x (name will be known as it is explicitely given) 30 | """ 31 | 32 | from peepshow.core import peep as core 33 | from peepshow.utils import python as utils 34 | from peepshow.core.trans import GloLoc, Given 35 | 36 | if len(args) > 1: 37 | raise TypeError("Too many arguments.") 38 | 39 | expr = args[0] 40 | env = utils.caller_gloloc() 41 | 42 | if args: 43 | if not isinstance(expr, str): 44 | raise TypeError("Expression must be a string or None.") 45 | 46 | try: 47 | target = eval(expr, {}, env.initial) 48 | except: 49 | raise SyntaxError('Invalid expression.') 50 | 51 | target = Given(target, expr) 52 | else: 53 | target = GloLoc(env.initial) 54 | 55 | last_target = core.peep(target, env) 56 | -------------------------------------------------------------------------------- /peepshow/utils/terminal.py: -------------------------------------------------------------------------------- 1 | import os 2 | from colorama import Fore, Back, Style 3 | import colorama 4 | import rlcompleter 5 | import readline 6 | from peepshow.utils.system import OS_IS_WINDOWS 7 | 8 | _completer_suggestions = {} 9 | 10 | class Completer(rlcompleter.Completer): 11 | def complete_ex(self, *args, **kwargs): 12 | buf = readline.get_line_buffer().strip() 13 | if buf and buf[0] in ['!', '$']: 14 | return self.complete(*args, **kwargs) 15 | 16 | def update_suggestions(suggestions): 17 | _completer_suggestions.update(suggestions) 18 | 19 | def init(suggestions): 20 | update_suggestions(suggestions) 21 | completer = Completer(_completer_suggestions) 22 | readline.set_completer(completer.complete_ex) 23 | readline.parse_and_bind("tab: complete") 24 | colorama.init() 25 | readline.set_history_length(1000) 26 | clear() 27 | 28 | def clear(): 29 | os.system('cls' if OS_IS_WINDOWS else 'clear') 30 | 31 | def print_error(msg='', *args, **kwargs): 32 | print(style(Back.RED + Fore.WHITE, msg), *args, **kwargs) 33 | 34 | def print_help(msg='', *args, **kwargs): 35 | print(style(Fore.LIGHTYELLOW_EX, msg), *args, **kwargs) 36 | 37 | def prefill_input(text=None): 38 | if text: 39 | readline.set_startup_hook(lambda: readline.insert_text(text)) 40 | else: 41 | readline.set_startup_hook() 42 | 43 | def style(spec, text, for_readline=False): 44 | # Thanks to Samuele Santi fot the article: 45 | # 9https://wiki.hackzine.org/development/misc/readline-color-prompt.html 46 | 47 | RL_PROMPT_START_IGNORE = '\001' 48 | RL_PROMPT_END_IGNORE = '\002' 49 | term = Style.RESET_ALL 50 | 51 | def wrap(text): 52 | if for_readline: 53 | return RL_PROMPT_START_IGNORE + text + RL_PROMPT_END_IGNORE 54 | else: 55 | return text 56 | 57 | return wrap(spec) + text + wrap(term) 58 | -------------------------------------------------------------------------------- /peepshow/cmds/base.py: -------------------------------------------------------------------------------- 1 | 2 | # command name: e.g. DoSomething 3 | # command nick: e.g. ds 4 | # command alias: e.g. dosomething, DOSOMETHING, ds, DS, ... 5 | 6 | from types import SimpleNamespace 7 | 8 | def get_nick(name): 9 | return ''.join(filter(lambda k: 'A' <= k <= 'Z', name)).lower() 10 | 11 | def command(name, qualifier=None, syntax=None): 12 | cmd_descr = SimpleNamespace(**locals()) 13 | cmd_descr.nick = get_nick(name) 14 | 15 | def decorator(func): 16 | func.cmd_descr = cmd_descr 17 | return func 18 | return decorator 19 | 20 | 21 | class CommandsBase: 22 | 23 | def __iter__(self): 24 | attrs = vars(type(self)).values() 25 | attrs = filter(callable, attrs) 26 | attrs = filter(lambda attr: hasattr(attr, 'cmd_descr'), attrs) 27 | yield from attrs 28 | 29 | def __getitem__(self, alias): 30 | if not alias: 31 | raise KeyError 32 | 33 | cmds_simple = [cmd for cmd in self if cmd.cmd_descr.qualifier is None] 34 | qualification = None 35 | 36 | # try to find command by name 37 | cmds_simple_by_name = {cmd.cmd_descr.name.lower(): cmd for cmd in cmds_simple} 38 | try: 39 | return cmds_simple_by_name[alias.lower()], qualification 40 | except KeyError: 41 | pass 42 | 43 | # try to find command by alias 44 | cmds_simple_by_nick = {cmd.cmd_descr.nick: cmd for cmd in cmds_simple} 45 | try: 46 | return cmds_simple_by_nick[alias.lower()], qualification 47 | except KeyError: 48 | pass 49 | 50 | # try to find command by qualifier 51 | cmds_custom = (cmd for cmd in self if callable(cmd.cmd_descr.qualifier)) 52 | for cmd in cmds_custom: 53 | try: 54 | qualification = cmd.cmd_descr.qualifier(alias) 55 | return cmd, qualification 56 | except: 57 | pass 58 | 59 | raise KeyError 60 | -------------------------------------------------------------------------------- /peepshow/pager/cache.py: -------------------------------------------------------------------------------- 1 | from peepshow.pager.pager import Pager 2 | 3 | class TooManyInitialIterations(StopIteration): pass 4 | 5 | class PagedCache: 6 | 7 | def __init__(self, content, str_func=str): 8 | self.iterator = content.__iter__() 9 | self.index = 0 10 | self.offset = 0 11 | self.cache = [] 12 | self.str_func = str_func 13 | 14 | def iterate(self, times): 15 | for i in range(times): 16 | next(self.iterator) 17 | self.offset += 1 18 | self.index += 1 19 | 20 | def __iter__(self): 21 | while True: 22 | try: 23 | obj = next(self.iterator) 24 | except StopIteration: 25 | break 26 | line = self.str_func(obj) 27 | self.cache.append(obj) 28 | yield f'[{self.index:>5}] {line}' 29 | self.index += 1 30 | 31 | def clear_cache(self): 32 | self.offset += len(self.cache) 33 | self.cache[:] = [] 34 | 35 | def __getitem__(self, index): 36 | cache_index = index - self.offset 37 | if not cache_index >= 0 or not cache_index < len(self.cache): 38 | raise IndexError("You can use only indices visible on the screen.") 39 | 40 | return self.cache[cache_index] 41 | 42 | def recall_cache(self): 43 | def content_gen(): 44 | for index, entry in enumerate(self.cache): 45 | key, obj = entry 46 | line = self.str_func(entry) 47 | index_ = index + self.offset 48 | yield f'[{index_:>5}] {line}' 49 | p = Pager(numeric=True) 50 | p.page(content_gen()) 51 | 52 | def page(content, str_func=str, offset=0): 53 | cache = PagedCache(content, str_func) 54 | try: 55 | cache.iterate(offset) 56 | except StopIteration as ex: 57 | raise TooManyInitialIterations(f'Only {cache.index} iterations possible.') from ex 58 | pager = Pager( (PagedCache.clear_cache, cache), numeric=True ) 59 | pager.page(cache) 60 | return cache 61 | -------------------------------------------------------------------------------- /peepshow/show.py: -------------------------------------------------------------------------------- 1 | 2 | def show(*args, **kwargs): 3 | """show(x, y, z=z) # print names & values of arguments 4 | names of args will be determined only if possible 5 | names of kwargs will be known as it is explicitely given 6 | """ 7 | 8 | import miscutils.insp as insp 9 | from peepshow.core import show as core 10 | from peepshow.utils import python as utils 11 | 12 | if args or kwargs: 13 | # show specified variables 14 | if args: 15 | names = utils.arg_names() 16 | else: 17 | names = [] 18 | names += [*kwargs.keys()] 19 | values = [*args] + [*kwargs.values()] 20 | else: 21 | # show all the user variables in scope of the caller 22 | env = utils.caller_gloloc() 23 | is_user_var = lambda item: not insp.isaccess(item[0]).special 24 | user_vars = filter(is_user_var, env.initial.items()) 25 | names, values = zip(*user_vars) 26 | 27 | core.show(names, values) 28 | 29 | def show_(*args, **kwargs): 30 | """show_('x', 'y', z='z') # print names & values of arguments 31 | values of args and kwargs are expressions to be evaluated in context of the caller 32 | values of args become names to be displayed 33 | names of kwargs become names to be displayed 34 | """ 35 | 36 | import miscutils.insp as insp 37 | from peepshow.core import show as core 38 | from peepshow.utils import python as utils 39 | 40 | env = utils.caller_gloloc() 41 | 42 | if args or kwargs: 43 | # show specified variables 44 | names = [*args] + [*kwargs.keys()] 45 | exprs = [*args] + [*kwargs.values()] 46 | 47 | for expr in exprs: 48 | if not isinstance(expr, str): 49 | raise TypeError("Each expression must be a string.") 50 | 51 | values = [eval(expr, env.glo, env.loc) for expr in exprs] 52 | else: 53 | # show all the user variables in scope of the caller 54 | is_user_var = lambda item: not insp.isaccess(item[0]).special 55 | user_vars = filter(is_user_var, env.initial.items()) 56 | names, values = zip(*user_vars) 57 | 58 | core.show(names, values) 59 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Best reference regarding this config file is: 2 | # https://github.com/squidfunk/mkdocs-material/blob/master/mkdocs.yml 3 | 4 | site_name: "PeepShow" 5 | site_url: "https://gergelyk.github.io/peepshow" 6 | 7 | theme: 8 | name: "material" 9 | feature: 10 | tabs: false # Appear in the title bar 11 | 12 | # Don't include MkDocs' JavaScript 13 | include_search_page: false 14 | search_index_only: true 15 | 16 | # Appears in the title bar 17 | repo_name: peepshow 18 | repo_url: https://github.com/gergelyk/peepshow 19 | 20 | extra: 21 | # Appears in the footer bar 22 | social: 23 | - type: gitlab 24 | link: https://github.com/gergelyk/peepshow 25 | - type: envelope-o 26 | link: mailto:grzegorz.krason@gmail.com 27 | 28 | copyright: "Copyright © 2020 Grzegorz Krasoń" 29 | 30 | markdown_extensions: 31 | - abbr 32 | - footnotes 33 | - codehilite: 34 | guess_lang: false 35 | linenums: false 36 | noclasses: false 37 | - def_list 38 | - toc: 39 | permalink: true 40 | - meta 41 | - md_in_html 42 | - mdx_include: 43 | base_path: docs 44 | recurs_remote: null 45 | - sane_lists 46 | - pymdownx.betterem: 47 | smart_enable: all 48 | - pymdownx.caret 49 | - pymdownx.critic 50 | - pymdownx.details 51 | - pymdownx.emoji: 52 | emoji_generator: !!python/name:pymdownx.emoji.to_svg 53 | - pymdownx.escapeall 54 | - pymdownx.inlinehilite 55 | - pymdownx.superfences: 56 | custom_fences: 57 | - name: mermaid 58 | class: mermaid 59 | format: !!python/name:pymdownx.superfences.fence_div_format 60 | - pymdownx.keys 61 | - pymdownx.mark 62 | - pymdownx.smartsymbols 63 | - pymdownx.tasklist: 64 | custom_checkbox: true 65 | - pymdownx.tilde 66 | 67 | plugins: 68 | - search 69 | # - minify: 70 | # minify_html: true # this doesn't work well with mermaid 71 | - macros: 72 | module_name: docs.macros 73 | - mkdocstrings: 74 | watch: 75 | - peepshow 76 | - markdownmermaid 77 | - progress 78 | # - htmlproofer # this only shows broken links in the build console 79 | 80 | extra_javascript: 81 | - https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.4.8/mermaid.core.min.js 82 | 83 | extra_css: 84 | - custom.css 85 | 86 | # Appears on the left side 87 | nav: 88 | - Overview: index.md 89 | - Peep: peep.md 90 | - Show: show.md 91 | -------------------------------------------------------------------------------- /peepshow/utils/traceback.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | import warnings 5 | import inspect 6 | from peepshow.peep import peep 7 | 8 | 9 | class ExceptionSummary: 10 | def __init__(self, exc_type, exc_value, tb): 11 | self.exc_type = exc_type 12 | self.exc_value = exc_value 13 | self.tb = tb 14 | frames = [t[0] for t in traceback.walk_tb(tb)] 15 | self.frames = list(map(FrameSummary, frames)) 16 | 17 | def __str__(self): 18 | return str(self.exc_value) 19 | 20 | def __repr__(self): 21 | return f'' 22 | 23 | def __iter__(self): 24 | yield from self.frames 25 | 26 | class FrameSummary: 27 | def __init__(self, frame): 28 | self.frame = frame 29 | self.line_no = frame.f_lineno 30 | self.file_name = frame.f_code.co_filename 31 | self.callable_name = frame.f_code.co_name 32 | try: 33 | with open(self.file_name) as fh: 34 | lines = fh.readlines() 35 | self.line = lines[self.line_no - 1].strip() 36 | except Exception: 37 | self.line = "" 38 | 39 | self.gloloc = {**self.frame.f_globals, **self.frame.f_locals} 40 | 41 | def __repr__(self): 42 | return f"{self.file_name}:{self.line_no} [{self.callable_name}] {self.line}" 43 | 44 | def __getitem__(self, name): 45 | return self.gloloc[name] 46 | 47 | def keys(self): 48 | return self.gloloc.keys() 49 | 50 | def values(self): 51 | return self.gloloc.values() 52 | 53 | 54 | def peep_except_hook(exc_type, exc_value, traceback): 55 | 56 | exc_stack = [] 57 | while True: 58 | exc_stack.append(ExceptionSummary(exc_type, exc_value, traceback)) 59 | if exc_value.__context__ is not None: 60 | exc_value = exc_value.__context__ 61 | exc_type = type(exc_value) 62 | traceback = exc_value.__traceback__ 63 | continue 64 | break 65 | 66 | if len(exc_stack) == 1: 67 | peep(exc_stack[0]) 68 | else: 69 | peep(exc_stack) 70 | 71 | def enable_except_hook(consider_env=False): 72 | try: 73 | if consider_env: 74 | enable = bool(int(os.getenv('PYTHON_PEEP_EXCEPTIONS', 0))) 75 | else: 76 | enable = true 77 | except (ValueError, TypeError): 78 | warnings.warn("Invalid value of PYTHON_PEEP_EXCEPTIONS", RuntimeWarning) 79 | else: 80 | if enable: 81 | sys.excepthook = peep_except_hook 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /peepshow/pager/pager.py: -------------------------------------------------------------------------------- 1 | from getch import getch # py-getch package 2 | import sys 3 | import colorama 4 | import re 5 | from peepshow.utils import terminal 6 | import shutil 7 | 8 | class Line: 9 | ansi_escape = re.compile(r'\x1b[^m]*m') 10 | 11 | def __init__(self, text): 12 | self.text = text 13 | 14 | def no_colors(self): 15 | return self.ansi_escape.sub('', self.text) 16 | 17 | def __len__(self): 18 | return len(self.no_colors()) 19 | 20 | def __str__(self): 21 | return self.text 22 | 23 | def trim(self, length): 24 | in_buf = self.text 25 | out_buf = '' 26 | out_buf_len = 0 27 | while in_buf and out_buf_len < length: 28 | 29 | m = self.ansi_escape.match(in_buf) 30 | if m: 31 | out_buf += m.group() 32 | in_buf = in_buf[m.end():] 33 | continue 34 | else: 35 | out_buf += in_buf[0] 36 | out_buf_len += 1 37 | in_buf = in_buf[1:] 38 | 39 | return Line(out_buf + colorama.Style.RESET_ALL) 40 | 41 | def __add__(self, other): 42 | return Line(self.text + str(other)) 43 | 44 | class Pager: 45 | 46 | def __init__(self, start_page_callback=((lambda: None),), numeric=False): 47 | self.page_width, self.page_height = shutil.get_terminal_size() 48 | self.start_page_callback = start_page_callback 49 | self.numeric = numeric 50 | 51 | def trim_line(self, line): 52 | elip = Line(colorama.Style.DIM + '...' + colorama.Style.RESET_ALL) 53 | if len(line) > self.page_width: 54 | line = line.trim(self.page_width - len(elip)) + elip 55 | line = line.trim(self.page_width) 56 | return line 57 | 58 | def print_line(self, line): 59 | line = self.trim_line(line) 60 | sys.stdout.write(str(line)) 61 | line_len = len(line) 62 | last_row_len = line_len % self.page_width 63 | if not line_len or last_row_len: 64 | # add extra CR/CRLF if line doesn't cover entire width 65 | # this does matter under Window and is meaningless under Linus 66 | print() 67 | 68 | def prompt(self): 69 | hint = f"Press Q/ESC{['', '/NUMBER'][self.numeric]} to stop or any other key to continue..." 70 | line = str(self.trim_line(Line(hint))) 71 | terminal.print_help(line, end='') 72 | sys.stdout.flush() 73 | try: 74 | key = getch() 75 | ESC = '\x1b' 76 | CTRL_C = '\x03' 77 | terminating_keys = [ESC, CTRL_C, 'q', 'Q'] 78 | numbers = [str(x) for x in range(10)] 79 | if self.numeric: 80 | terminating_keys += numbers 81 | if key in numbers: 82 | terminal.prefill_input(key) 83 | interrupted = key in terminating_keys 84 | except KeyboardInterrupt: 85 | interrupted = True 86 | sys.stdout.write('\r' + ' '*(len(line)) + '\r') 87 | sys.stdout.flush() 88 | return interrupted 89 | 90 | def execute_start_page_callback(self): 91 | self.start_page_callback[0](*self.start_page_callback[1:]) 92 | 93 | def page(self, lines): 94 | footer_height = 1 # reserve one line for the prompt 95 | usable_height = (self.page_height - footer_height) 96 | self.execute_start_page_callback() 97 | 98 | for line_idx, line in enumerate(lines): 99 | end_page = (line_idx + 1) % usable_height == 0 100 | 101 | self.print_line(Line(line)) 102 | 103 | if end_page: 104 | if self.prompt(): 105 | break 106 | else: 107 | self.execute_start_page_callback() 108 | terminal.clear() 109 | 110 | sys.stdout.flush() 111 | 112 | def page(content): 113 | p = Pager() 114 | p.page(content) 115 | -------------------------------------------------------------------------------- /peepshow/core/explorer.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import partial 3 | from peepshow.pager import cache as paged_cache 4 | from peepshow.pager.cache import PagedCache 5 | from peepshow.core.probes import is_subscribable, is_iterable, is_callable 6 | from peepshow.utils.terminal import style, Style, Fore, Back 7 | from peepshow.core.probes import Text, is_of_builtin_type 8 | from peepshow.core.exceptions import CommandError 9 | from peepshow.utils.traceback import FrameSummary 10 | 11 | def get_signature(obj): 12 | try: 13 | return str(inspect.signature(obj)) 14 | except: 15 | return '(?)' 16 | 17 | def str_func(mode, item): 18 | name, transformation = item 19 | attr = transformation.result 20 | available = transformation.available 21 | 22 | base = None 23 | if available: 24 | braces = '' 25 | if is_callable(attr): 26 | try: 27 | builtin_name = attr.__name__ 28 | except AttributeError: 29 | pass 30 | else: 31 | if builtin_name != name: 32 | braces += builtin_name 33 | 34 | braces += get_signature(attr) 35 | if is_subscribable(attr): 36 | braces += '[]' 37 | if is_iterable(attr): 38 | braces += '*' 39 | 40 | if name or braces: 41 | name_color = fr'{style(Style.BRIGHT, name)}' if name else '' 42 | sep = ':' if mode == 'dict' else '' 43 | braces_color = fr'{style(Fore.LIGHTGREEN_EX, braces)}' if braces else '' 44 | base = ' '.join(x for x in (name_color, sep, braces_color) if x) 45 | 46 | if attr is None: 47 | type_name = None 48 | value = repr(attr) 49 | elif is_callable(attr): 50 | if inspect.isclass(attr): 51 | try: 52 | type_name = type(attr).__name__ 53 | except: 54 | type_name = '?' 55 | type_name = f'<{type_name}>' 56 | type_name = style(Fore.LIGHTRED_EX, type_name) 57 | value = repr(str(attr)) 58 | else: 59 | type_name = None 60 | value = None 61 | elif is_of_builtin_type(attr)[0]: 62 | type_name = None 63 | value = repr(attr).splitlines()[0] # what if multiline 64 | elif isinstance(attr, FrameSummary): 65 | type_name = None 66 | location = style(Fore.LIGHTRED_EX, f'{attr.file_name}:{attr.line_no}') 67 | callable_name = style(Fore.LIGHTBLUE_EX, f'[{attr.callable_name}]') 68 | code_line = attr.line 69 | value = f"{location} {callable_name} {code_line}" 70 | else: 71 | try: 72 | type_name = type(attr).__name__ 73 | except: 74 | type_name = '?' 75 | type_name = f'<{type_name}>' 76 | type_name = style(Fore.LIGHTRED_EX, type_name) 77 | value = repr(str(attr)) 78 | 79 | if value is not None: 80 | value = style(Fore.LIGHTBLUE_EX, value) 81 | 82 | info = [base, type_name, value] 83 | else: 84 | if name: 85 | base = fr'{style(Style.BRIGHT, name)}' 86 | info = [base, 'N/A'] 87 | 88 | return ' '.join( (x for x in info if x) ) 89 | 90 | 91 | class Explorer: 92 | def __init__(self, ctx): 93 | self.ctx = ctx 94 | self.cache = PagedCache([]) 95 | self.cached_target = None 96 | 97 | def fill(self, content, style, offset=0): 98 | """style: 'attr', 'list', 'dict' 99 | """ 100 | str_func_styled = partial(str_func, style) 101 | try: 102 | self.cache = paged_cache.page(content, str_func_styled, offset) 103 | except paged_cache.TooManyInitialIterations as ex: 104 | raise CommandError(ex) from ex 105 | self.cached_target = self.ctx.target 106 | 107 | def recall(self): 108 | if self.ctx.target is not self.cached_target: 109 | raise CommandError('No cache for this target') 110 | self.cache.recall_cache() 111 | 112 | def get_transformation(self, index): 113 | if self.ctx.target is not self.cached_target: 114 | raise CommandError('No cache for this target') 115 | 116 | try: 117 | key, transformation = self.cache[index] 118 | except Exception as ex: 119 | raise CommandError(ex) from ex 120 | 121 | if not transformation.available: 122 | raise CommandError('Item not available') 123 | 124 | return transformation 125 | -------------------------------------------------------------------------------- /peepshow/core/peep.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import sys 3 | from peepshow.cmds.cmds import Commands, CommandError 4 | from peepshow.utils import terminal 5 | from peepshow.utils.terminal import print_error, print_help, style, Fore 6 | from peepshow.utils.python import CheckInvocation, InvocationError 7 | from peepshow.utils.system import dev_mode_enabled 8 | from peepshow.core import goto 9 | from peepshow.core.context import Context 10 | from peepshow.core.probes import get_default_action, is_of_builtin_type 11 | from peepshow.utils.python import exc_to_str 12 | 13 | def read_command(ctx): 14 | try: 15 | _, _, type_name = is_of_builtin_type(ctx.target) 16 | if ctx.only_info_visible: 17 | action = get_default_action(ctx.target) 18 | else: 19 | action = 'i' 20 | 21 | type_name_color = style(Fore.LIGHTRED_EX, type_name, for_readline=True) 22 | type_name_color += ' ' if type_name else '' 23 | action_color = style(Fore.CYAN, action, for_readline=True) 24 | action_color += ' ' if action else '' 25 | 26 | cmd_raw = input(type_name_color + action_color + '> ').strip() 27 | terminal.prefill_input() 28 | if not cmd_raw: 29 | # execute suggested action: 30 | cmd_raw = action 31 | # just do nothing and print next prompt: 32 | #raise goto.NextCommand 33 | try: 34 | user_inp = shlex.split(cmd_raw, posix=True) 35 | except Exception as ex: 36 | print_error(f"Invalid syntax. {exc_to_str(ex)}.") 37 | raise goto.NextCommand 38 | 39 | except EOFError: # CTRL+D 40 | raise goto.Stop(exit=False) 41 | except KeyboardInterrupt: # CTRL+C 42 | raise goto.Stop(exit=True) 43 | 44 | cmd_alias, *cmd_usr_args = user_inp 45 | return cmd_raw, cmd_alias, tuple(cmd_usr_args) 46 | 47 | 48 | def prepare_command(cmds, cmd_raw, cmd_alias, cmd_usr_args): 49 | try: 50 | try: 51 | cmd_hdlr, qualification = cmds[cmd_raw] 52 | cmd_usr_args = () 53 | except KeyError: 54 | cmd_hdlr, qualification = cmds[cmd_alias] 55 | 56 | if qualification is not None: 57 | cmd_usr_args = (qualification, *cmd_usr_args) 58 | except KeyError: 59 | print_error(f"Unknown command '{cmd_alias}'.") 60 | cmd_hdlr = Commands.cmd_help 61 | cmd_usr_args = () 62 | 63 | cmd_args = (cmds, *cmd_usr_args) 64 | return cmd_hdlr, cmd_args 65 | 66 | 67 | def execute_command(ctx, cmd_hdlr, cmd_args): 68 | while True: 69 | try: 70 | ctx.only_info_visible = False 71 | 72 | try: 73 | with CheckInvocation(): 74 | new_cmd = cmd_hdlr(*cmd_args) 75 | except goto.GoToLabel: 76 | raise 77 | except InvocationError as ex: 78 | raise CommandError('Wrong number of arguments.') from ex 79 | except Exception as ex: 80 | raise CommandError(ex) from ex 81 | 82 | if new_cmd: 83 | cmd_hdlr, cmd_args = new_cmd 84 | else: 85 | break 86 | except goto.Stop: 87 | raise 88 | except CommandError as ex: 89 | if dev_mode_enabled: 90 | raise 91 | print_error("Error while executing command '{}'.".format(cmd_hdlr.cmd_descr.name)) 92 | print_error(str(ex)) 93 | cmds = cmd_args[0] 94 | cmd_args = (cmds, cmd_hdlr.cmd_descr.name) 95 | cmd_hdlr = Commands.cmd_help 96 | 97 | 98 | def clean_up(): 99 | terminal.clear() 100 | 101 | 102 | def stop(try_exit): 103 | def test_exit(): 104 | try: 105 | exit_func = exit 106 | return True 107 | except NameError: 108 | # e.g. in ipython exit() is not available 109 | return False 110 | 111 | cancel_stop = False 112 | if try_exit: 113 | if sys.gettrace(): 114 | print_error('Underlying debugger cannot be terminated.') 115 | # in fact it can be, but in case of pdb/ipdb it results in an ugly exception 116 | print_help("You can use 'Continue' command to close peepshow.") 117 | cancel_stop = True 118 | elif test_exit(): 119 | clean_up() 120 | exit(0) 121 | else: 122 | print_error('Underlying Python interpreter cannot be terminated.') 123 | print_help("You can use 'Continue' command to close peepshow.") 124 | cancel_stop = True 125 | else: 126 | clean_up() 127 | 128 | return cancel_stop 129 | 130 | 131 | def peep(target, env): 132 | ctx = Context(target, env) 133 | terminal.init(ctx.env.current) 134 | cmds = Commands(ctx) 135 | execute_command(ctx, Commands.cmd_info, (cmds,)) 136 | while True: 137 | try: 138 | cmd_raw, cmd_alias, cmd_usr_args = read_command(ctx) 139 | cmd_hdlr, cmd_args = prepare_command(cmds, cmd_raw, cmd_alias, cmd_usr_args) 140 | execute_command(ctx, cmd_hdlr, cmd_args) 141 | except goto.NextCommand: 142 | continue 143 | except goto.Stop as ex: 144 | if not stop(ex.exit): 145 | break 146 | 147 | return ctx.target 148 | -------------------------------------------------------------------------------- /peepshow/utils/python.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import ast 3 | import astunparse 4 | import astor 5 | import pprintpp 6 | from peepshow.utils.system import OS_BITS 7 | from textwrap import dedent 8 | from peepshow.core.env import Env 9 | from functools import wraps 10 | from itertools import islice 11 | from pygments import highlight 12 | from pygments.lexers import PythonLexer 13 | from pygments.formatters import TerminalFormatter 14 | 15 | def always_assert(condition): 16 | """Assert which works also when code optimization is enabled""" 17 | if not condition: 18 | raise AssertionError 19 | 20 | 21 | def caller_gloloc(level=2): 22 | """Return globals & locals of the caller of the caller. 23 | 24 | level: 0 is current frame, 1 is the caller, 2 is caller of the caller 25 | """ 26 | caller_frame_info = inspect.stack()[level] 27 | caller_frame = caller_frame_info.frame 28 | glo = caller_frame.f_globals 29 | loc = caller_frame.f_locals 30 | return Env(glo, loc) 31 | 32 | 33 | def arg_names(level=2): 34 | """Try to determine names of the variables given as arguments to the caller 35 | of the caller. This works only for trivial function invocations. Otherwise 36 | either results may be corrupted or exception will be raised. 37 | 38 | level: 0 is current frame, 1 is the caller, 2 is caller of the caller 39 | """ 40 | try: 41 | caller_frame_info = inspect.stack()[level] 42 | caller_context = caller_frame_info.code_context 43 | code = dedent(''.join(caller_context)) 44 | tree = ast.parse(code, '', 'eval') 45 | always_assert(isinstance(tree.body, ast.Call)) 46 | args = tree.body.args 47 | names = [astunparse.unparse(arg).strip() for arg in args] 48 | return names 49 | except Exception as ex: 50 | raise Exception('Cannot determine arg names') from None 51 | 52 | 53 | def id_to_str(id_): 54 | """Return id as fixed-length hex.""" 55 | if id_ < 0: 56 | id_ += 2 ** OS_BITS 57 | return f'0x{id_:0{OS_BITS//4}x}' 58 | 59 | def hash_to_str(hash_): 60 | """Return hash as fixed-length hex.""" 61 | return id_to_str(hash_) 62 | 63 | 64 | class InvocationError(RuntimeError): 65 | pass 66 | 67 | class CheckInvocation: 68 | """Check if exception comes from the function body or maybe from the fact 69 | that it is invoked incorrectly. 70 | """ 71 | 72 | def __init__(self, caller_stack_depth=1): 73 | self.caller_stack_depth = caller_stack_depth 74 | 75 | def __enter__(self): 76 | pass 77 | 78 | def __exit__(self, ex_type, ex_value, tb): 79 | 80 | def get_stack_depth(tb=None): 81 | depth = 0 82 | while True: 83 | try: 84 | tb = tb.tb_next 85 | except AttributeError: 86 | break 87 | depth += 1 88 | return depth 89 | 90 | if ex_value: 91 | exc_stack_depth = get_stack_depth(tb) 92 | if exc_stack_depth == self.caller_stack_depth: 93 | raise InvocationError(ex_value) from None 94 | 95 | class NoException(RuntimeError): 96 | def __init__(self, return_value): 97 | super().__init__("no exception raised") 98 | self.return_value = return_value 99 | 100 | def catch(func): 101 | """Return wrapper that executes target function and return exception raised 102 | by this target function. If no exception is raised by the target function, 103 | wrapper raises NoException.""" 104 | 105 | @wraps(func) 106 | def wrapper(*args, **kwargs): 107 | try: 108 | return_value = func(*args, **kwargs) 109 | except Exception as ex: 110 | return ex 111 | else: 112 | raise NoException(return_value) from None 113 | return wrapper 114 | 115 | def nth(iterable, index): 116 | """Get `index`-nth element from iterable.""" 117 | return [*islice(iterable, index, index+1)][0] 118 | 119 | 120 | def prettify_expr(expr): 121 | 122 | def pretty_string(s, *args, **kwargs): 123 | return repr(s) 124 | 125 | def pretty_source(source): 126 | return ''.join(source) 127 | 128 | tree = ast.parse(expr) 129 | pretty = astor.code_gen.to_source(tree, pretty_string=pretty_string, pretty_source=pretty_source).strip() 130 | 131 | # alternatively do: 132 | # pretty = black.format_str(expr, line_length=math.inf).strip() 133 | 134 | return pretty 135 | 136 | def crayon_expr(expr): 137 | return highlight(expr, PythonLexer(), TerminalFormatter()).strip() 138 | 139 | def exc_to_str(exc, show_type=False): 140 | 141 | def try1(): 142 | return exc.msg 143 | 144 | def try2(): 145 | return str(exc) 146 | 147 | tries = [try1, try2] 148 | 149 | ret = "" 150 | for t in tries: 151 | try: 152 | ret = t() 153 | if ret: 154 | break 155 | except Exception: 156 | pass 157 | 158 | type_name = type(exc).__name__ 159 | 160 | if ret: 161 | if show_type: 162 | return f"{type_name}: {ret}" 163 | else: 164 | return ret 165 | else: 166 | return type_name 167 | 168 | def pformat(expr): 169 | # alternatively: pprint.pformat 170 | return pprintpp.pformat(expr) 171 | -------------------------------------------------------------------------------- /tests/test_utils_python.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from peepshow.utils.python import caller_gloloc 3 | from peepshow.utils.python import arg_names 4 | from peepshow.utils.python import CheckInvocation, InvocationError 5 | from peepshow.utils.python import catch, NoException 6 | from peepshow.utils.python import nth, prettify_expr, exc_to_str 7 | 8 | class TestCallerGloloc: 9 | def test_locals(self): 10 | def foo(): 11 | gloloc = caller_gloloc() 12 | loc = gloloc.loc 13 | assert loc['x'] == 123 14 | assert loc['y'] == 234 15 | assert loc['foo'] == foo 16 | 17 | def bar(): 18 | x = 123 19 | y = 234 20 | foo() 21 | 22 | bar() 23 | 24 | def test_globals(self): 25 | def foo(): 26 | gloloc = caller_gloloc() 27 | glo = gloloc.glo 28 | assert glo['caller_gloloc'] == caller_gloloc 29 | assert glo['TestCallerGloloc'] == TestCallerGloloc 30 | 31 | def bar(): 32 | x = 123 33 | y = 234 34 | foo() 35 | 36 | bar() 37 | 38 | 39 | class TestArgNames: 40 | def test_locals(self): 41 | def foo(a, b, c=321, d=432): 42 | anames = arg_names() 43 | assert anames == ['x', 'y'] 44 | 45 | def bar(): 46 | x = 123 47 | y = 234 48 | z = 345 49 | foo(x, y, d=z) 50 | 51 | bar() 52 | 53 | 54 | class TestCheckInvocation: 55 | 56 | def test_incorrect_invocation(self): 57 | def foo(x, y, z): 58 | pass 59 | 60 | with pytest.raises(InvocationError): 61 | with CheckInvocation(): 62 | foo() 63 | 64 | def test_function_raises(self): 65 | def foo(x, y, z): 66 | raise RuntimeError 67 | 68 | with pytest.raises(RuntimeError): 69 | with CheckInvocation(): 70 | foo(1, 2, 3) 71 | 72 | def test_successfull_call(self): 73 | def foo(x, y, z): 74 | return x + y + z 75 | 76 | with CheckInvocation(): 77 | ret = foo(1, 2, 3) 78 | 79 | assert ret == 6 80 | 81 | 82 | class TestCatch: 83 | 84 | def test_target_raises(self): 85 | def foo(x, y): 86 | return x / y 87 | 88 | exc = catch(foo)(1, 0) 89 | assert isinstance(exc, ZeroDivisionError) 90 | 91 | def test_target_doesnt_raise(self): 92 | def foo(x, y): 93 | return x / y 94 | 95 | with pytest.raises(NoException) as exc_inf: 96 | catch(foo)(6, 2) 97 | 98 | assert exc_inf.value.return_value == 3 99 | 100 | 101 | class TestNth: 102 | 103 | def test_tuple(self): 104 | t = (11, 22, 33, 44) 105 | assert nth(t, 2) == 33 106 | 107 | def test_gen_expr(self): 108 | g = (x**2 for x in range(10)) 109 | assert nth(g, 3) == 9 110 | 111 | def test_gen_func(self): 112 | def f(): 113 | yield from [11, 22, 33, 44] 114 | g = f() 115 | assert nth(g, 2) == 33 116 | 117 | def test_index_out_of_range(self): 118 | t = (11, 22, 33, 44) 119 | with pytest.raises(IndexError): 120 | nth(t, 5) 121 | 122 | 123 | class TestPrettifyExpr: 124 | 125 | def test_arithmetic(self): 126 | assert prettify_expr('123+234//5') == '123 + 234 // 5' 127 | assert prettify_expr('123+-234%5 ') == '123 + -234 % 5' 128 | assert prettify_expr('123 *3 #qq') == '123 * 3' 129 | assert prettify_expr('qwe = asd = 234 == None') == 'qwe = asd = 234 == None' 130 | 131 | def test_parentheses(self): 132 | assert prettify_expr('( 12*23)+ 34') == '12 * 23 + 34' 133 | assert prettify_expr('34 + (( 12* (23+23)))') == '34 + 12 * (23 + 23)' 134 | 135 | def test_list(self): 136 | assert prettify_expr('x = [ ] ') == 'x = []' 137 | assert prettify_expr('[1] +[ ] / [1,2,] + [3,4] ') == '[1] + [] / [1, 2] + [3, 4]' 138 | assert prettify_expr('[ [],[[],[]]]') == '[[], [[], []]]' 139 | 140 | def test_tuple(self): 141 | assert prettify_expr('func ( (1,2,3,) )') == 'func((1, 2, 3))' 142 | assert prettify_expr('func( *(x, y ,z) )') == 'func(*(x, y, z))' 143 | assert prettify_expr('foo[(1,2,3)]') == 'foo[1, 2, 3]' 144 | 145 | def test_dict(self): 146 | assert prettify_expr('{"x" : 123, \'yy\': 234, 567:...}') == "{'x': 123, 'yy': 234, (567): ...}" 147 | # note: parentheses could be skipped ^ ^ 148 | assert prettify_expr('{"x" : {}, **{"y":set()}, **abc}') == "{'x': {}, **{'y': set()}, **abc}" 149 | assert prettify_expr('dict ( abc= 123,qwe=x)') == 'dict(abc=123, qwe=x)' 150 | 151 | 152 | class TestExcToStr: 153 | 154 | def test_regular(self): 155 | try: 156 | 123/0 157 | except Exception as ex: 158 | exc = ex 159 | 160 | assert exc_to_str(exc) == 'division by zero' 161 | assert exc_to_str(exc, True) == 'ZeroDivisionError: division by zero' 162 | 163 | def test_no_msg(self): 164 | try: 165 | raise RuntimeError 166 | except Exception as ex: 167 | exc = ex 168 | 169 | assert exc_to_str(exc) == 'RuntimeError' 170 | assert exc_to_str(exc, True) == 'RuntimeError' 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peepshow 2 | ![PyPI - Version](https://img.shields.io/pypi/v/peepshow) 3 | ![PyPI - License](https://img.shields.io/pypi/l/peepshow) 4 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/peepshow) 5 | 6 | Provides following utilities for debugging Python applications: 7 | 8 | * show - lightweight function that prints name and value of your variable(s) to the console. 9 | * peep - featured, interactive interface for data inspection. 10 | 11 | ![](https://raw.githubusercontent.com/gergelyk/peepshow/master/docs/demo.gif) 12 | 13 | ## Resources 14 | 15 | * Documentation: 16 | * Repository: 17 | * Package: 18 | * Author: [Grzegorz Krasoń](mailto:grzegorz.krason@gmail.com) 19 | * License: [MIT](LICENSE) 20 | 21 | ## Installation 22 | 23 | Install `peepshow` package: 24 | 25 | ```sh 26 | pip install peepshow 27 | ``` 28 | 29 | PeepShow uses `clear`, `vim`, `man` commands which are available in most of Linux distributions. Users of other operating systems need to install them on their own. 30 | 31 | ### Built-Ins 32 | 33 | If you expect to use peepshow often, consider adding `peep` and `show` commands to Python's built-ins and enabling except hook. Edit either `{site-packages}/sitecustomize.py` or `{user-site-packages}/usercustomize.py` and append the following: 34 | 35 | ```python 36 | import peepshow 37 | import builtins 38 | builtins.peep = peepshow.peep 39 | builtins.show = peepshow.show 40 | builtins.peep_ = peepshow.peep_ 41 | builtins.show_ = peepshow.show_ 42 | peepshow.enable_except_hook(consider_env=True) 43 | ``` 44 | 45 | ### Breakpoint 46 | 47 | It is also possible to invoke `peep()` as a result of calling built-in function `breakpoint()`. To enable such behavior use `PYTHONBREAKPOINT` system variable: 48 | 49 | ```sh 50 | export PYTHONBREAKPOINT=peepshow.peep 51 | ``` 52 | 53 | ## Compatibility 54 | 55 | * This software is expected to work with Python 3.6, 3.7, 3.8 and compatible. 56 | * It has never been tested under operating systems other than Linux. 57 | * It works fine when started in a plain Python script, in ipython or ptipython. 58 | * In these environments like interactive python console, in pdb and ipdb, peep and show cannot infer names of the variables in the user context, so they need to be provided explicitly (e.g. use `peep_` and `show_`). 59 | 60 | ## Usage 61 | 62 | ### `show` 63 | 64 | Running this script: 65 | 66 | ```python 67 | x = 123 68 | y = {'name': 'John', 'age': 123} 69 | z = "Hello World!" 70 | 71 | # show all the variables in the scope 72 | show() 73 | 74 | # or only variables of your choice 75 | show(x, y) 76 | 77 | # you can also rename them 78 | show(my_var=x) 79 | 80 | # use 'show_' to specify variable names as a string 81 | show_('x') 82 | 83 | # expressions and renaming are also allowed 84 | show_('x + 321', zet='z') 85 | ``` 86 | 87 | will result in following output: 88 | 89 | ``` 90 | x = 123 91 | y = {'age': 123, 'name': 'John'} 92 | z = 'Hello World!' 93 | x = 123 94 | y = {'age': 123, 'name': 'John'} 95 | my_var = 123 96 | x = 123 97 | x + 321 = 444 98 | zet = 'Hello World!' 99 | ``` 100 | 101 | ### `peep` 102 | 103 | Try running the following script: 104 | 105 | ```python 106 | x = 123 107 | y = {'name': 'John', 'age': 123} 108 | z = "Hello World!" 109 | 110 | # inspect dictionary that consists of all the variables in the scope 111 | peep() 112 | 113 | # or inspect variable of your choice directly 114 | peep(x) 115 | 116 | # use 'peep_' to specify variable name as a string 117 | peep_('x') 118 | ``` 119 | 120 | When interactive interface pops up: 121 | 122 | * Hit ENTER to see list of available variables. 123 | * Type `10` and hit ENTER to select `y`. 124 | * Hit ENTER again to see items of your dictionary. 125 | * Type `dir` and hit ENTER to list attributes of `y` (excluding built-ins). 126 | * Type `continue` and hit ENTER to proceed or type `quit` and hit ENTER to terminate your script. 127 | 128 | Note that all the commands have their short aliases. E.g. `quit` and `q` is the same. 129 | 130 | For more help: 131 | 132 | * Type `help` and hit ENTER to see list of available commands. 133 | * Type `man` and hit ENTER to read the manual, hit `q` when you are done. 134 | 135 | ### excepthook 136 | 137 | Before running your script, set environment variable `PYTHON_PEEP_EXCEPTIONS` to `1`. Now run the script and see what happens when an exception is raised. 138 | 139 | ## Development 140 | 141 | ```sh 142 | # Preparing environment 143 | pip install --user poetry # unless already installed 144 | poetry install 145 | 146 | # Testing with coverage 147 | poetry run pytest --cov peepshow --cov tests 148 | 149 | # Rendering documentation 150 | poetry run mkdocs serve 151 | 152 | # Building package 153 | poetry build 154 | 155 | # Releasing 156 | poetry version minor # increment selected component 157 | git commit -am "bump version" 158 | git push 159 | git tag ${$(poetry version)[2]} 160 | git push --tags 161 | poetry build 162 | poetry publish 163 | poetry run mkdocs build 164 | poetry run mkdocs gh-deploy -b gh-pages 165 | ``` 166 | 167 | ## Donations 168 | 169 | If you find this software useful and you would like to repay author's efforts you are welcome to use following button: 170 | 171 | [![Donate](https://www.paypalobjects.com/en_US/PL/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D9KUJD9LTKJY8&source=url) 172 | 173 | -------------------------------------------------------------------------------- /peepshow/core/probes.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from collections import OrderedDict 3 | from peepshow.utils.python import id_to_str, hash_to_str 4 | from peepshow.utils.traceback import FrameSummary 5 | 6 | def is_bound(obj): 7 | try: 8 | return hasattr(obj, '__self__') 9 | except Exception: 10 | return False 11 | 12 | def is_subscribable(obj): 13 | try: 14 | return is_bound(obj.__getitem__) 15 | except Exception: 16 | return False 17 | 18 | def is_iterable(obj): 19 | try: 20 | return is_bound(obj.__iter__) 21 | except Exception: 22 | return False 23 | 24 | is_callable = lambda obj: callable(obj) 25 | 26 | builtin_types = ( 27 | type, 28 | type(Ellipsis), 29 | type(None), 30 | bool, 31 | str, 32 | int, float, complex, 33 | list, tuple, set, dict, frozenset, 34 | range, slice, 35 | bytes, bytearray, memoryview) 36 | 37 | def is_of_builtin_type(obj): 38 | for t in builtin_types: 39 | if isinstance(obj, t): 40 | return True, t, t.__name__ 41 | return False, None, '' 42 | 43 | def can_be_called_wo_args(func): 44 | if not is_callable(func): 45 | return False 46 | try: 47 | spec = inspect.getfullargspec(func) 48 | except: 49 | return False 50 | 51 | len_ = lambda x: 0 if x is None else len(x) 52 | 53 | return len(spec.args) <= len_(spec.defaults) + int(is_bound(func)) and \ 54 | len(spec.kwonlyargs) == len_(spec.kwonlydefaults) 55 | 56 | class Feature: 57 | def __init__(self, value, exists): 58 | self.value = value 59 | self.exists = exists 60 | 61 | def __str__(self): 62 | if self.exists: 63 | return str(self.value) 64 | else: 65 | return 'N/A' 66 | 67 | def get_default_action(obj): 68 | type_is_builtin, type_, _ = is_of_builtin_type(obj) 69 | 70 | if type_is_builtin: 71 | if issubclass(type_, dict): 72 | return '**' 73 | if issubclass(type_, (list, tuple, set, dict, frozenset)): 74 | return '*' 75 | if issubclass(type_, type): 76 | return 'd' 77 | return 'pp' 78 | if isinstance(obj, FrameSummary): 79 | return '??' 80 | if is_iterable(obj): 81 | return '*' 82 | if can_be_called_wo_args(obj): 83 | return '()' 84 | return 'd' 85 | 86 | 87 | def read_features(obj, feat_list): 88 | def try_read(feat): 89 | try: 90 | value = feat(obj) 91 | exists = True 92 | except Exception: 93 | value = None 94 | exists = False 95 | return Feature(value, exists) 96 | 97 | return OrderedDict( (feat[0], try_read(feat[1])) for feat in feat_list ) 98 | 99 | 100 | class Text: 101 | def __init__(self, text, *, hlight): 102 | self.text = text 103 | self.hlight = hlight 104 | 105 | def __str__(self): 106 | return self.text 107 | 108 | def __bool__(self): 109 | return self.hlight 110 | 111 | def str_vs_repr(obj): 112 | str_obj = str(obj) 113 | if obj.__repr__ is obj.__str__: 114 | return Text('is REPR', hlight=False) 115 | elif str_obj == repr(obj): 116 | return Text('REPR', hlight=False) 117 | else: 118 | return Text(repr(str_obj), hlight=True) 119 | 120 | def qualname_vs_name(obj): 121 | if obj.__qualname__ == obj.__name__: 122 | return Text('NAME', hlight=False) 123 | else: 124 | return Text(repr(obj.__qualname__), hlight=True) 125 | 126 | 127 | class HexInt: 128 | def __init__(self, x): 129 | self.x = int(x) 130 | 131 | def __str__(self): 132 | return str(self.x) + ' = ' + hex(self.x) 133 | 134 | 135 | MAJOR_FEATURES = [ 136 | ('TYPE', type), 137 | ('REPR', lambda obj: repr(repr(obj)) ), 138 | ('STR', str_vs_repr), 139 | ('NAME', lambda obj: repr(obj.__name__)), 140 | ('QUALNAME', qualname_vs_name), 141 | ('SIGNATURE', inspect.signature), 142 | ('CALLABLE', is_callable), 143 | ('ITERABLE', is_iterable), 144 | ('SUBSCRIBABLE', is_subscribable), 145 | ('LEN', len), 146 | ('BOOL', bool), 147 | ('INT', HexInt), 148 | ('ID', lambda obj: Text(id_to_str(id(obj)), hlight=False)), 149 | ('HASH', lambda obj: Text(hash_to_str(hash(obj)), hlight=False)), 150 | ('BINDING', lambda obj: obj.__self__), 151 | ('BASES', lambda obj: Text(str(obj.__bases__), hlight=False)), 152 | ('MRO', lambda obj: Text(str(obj.mro()), hlight=False)), 153 | ] 154 | 155 | MINOR_FEATURES = [ 156 | ('IsModule', inspect.ismodule), 157 | ('IsClass', inspect.isclass), 158 | ('IsMethod', inspect.ismethod), 159 | ('IsFunction', inspect.isfunction), 160 | ('IsGeneratorFunction', inspect.isgeneratorfunction), 161 | ('IsGenerator', inspect.isgenerator), 162 | ('IsCoroutineFunction', inspect.iscoroutinefunction), 163 | ('IsCoroutine', inspect.iscoroutine), 164 | ('IsAwaitable', inspect.isawaitable), 165 | ('IsAsyncGenFunction', inspect.isasyncgenfunction), 166 | ('IsAsyncGen', inspect.isasyncgen), 167 | ('IsTraceback', inspect.istraceback), 168 | ('IsFrame', inspect.isframe), 169 | ('IsCode', inspect.iscode), 170 | ('IsBuiltIn', inspect.isbuiltin), 171 | ('IsRoutine', inspect.isroutine), 172 | ('IsAbstract', inspect.isabstract), 173 | ('IsMethodDescriptor', inspect.ismethoddescriptor), 174 | ('IsDataDescriptor', inspect.isdatadescriptor), 175 | ('IsGetSetDescriptor', inspect.isgetsetdescriptor), 176 | ('IsMemberDescriptor', inspect.ismemberdescriptor), 177 | ] 178 | -------------------------------------------------------------------------------- /docs/peep.md: -------------------------------------------------------------------------------- 1 | # Peep 2 | 3 | `peep` function provides an interactive interface to access properties of the target object given as an argument. When called without arguments, `peep` starts from inspecting a dictionary that consists of all the variables in scope of the caller. There is also `peep_` function which expects an expression instead of variable to be provided as an argument. Such expression is evaluated in context of the caller and the result becomes a target object. 4 | 5 | ```python 6 | >>> x = [1, 2, 3] # target object 7 | >>> peep(x) # target provided explicitly 8 | >>> peep() # all the variables in this context a assembly to a target 9 | >>> peep_('x') # target provided as an expression 10 | ``` 11 | 12 | ## Navigating 13 | 14 | After invoking `peep` an interactive prompt appears on the screen. User can call one of the commands using either its full name or and alias. Alias consists only of capital letters form the full name. Use *Help* command to get full list of commands or invoke *help * to get help on specific command . 15 | 16 | ``` 17 | > help # provides list of commands 18 | > h # the same as above 19 | > help manual # provides help on 'manual' command 20 | > h man # the same as above 21 | ``` 22 | 23 | It is also possible to execute suggested command that is displayed in the prompt simply by hitting Enter key. 24 | 25 | Prompt also provides type name of the target. This applies only to selected Python built-in types. 26 | 27 | 28 | ## Basic Inspection 29 | 30 | Regardless of the target type, command *Info* (executed at startup) can be used for displaying basic summary. More detailed information are provided by *Features* command. 31 | 32 | *PrettyPrint* command attempts to show target recursively in a recursive manner. 33 | 34 | Commans *?* and *??* display target's docstring and source code respectively. 35 | 36 | Whenever there is too much information on the screen, *CLear* command or CTRL+L can be used. 37 | 38 | Typically there is a need of inspecting of target's attributes. This can be done by using *Dir* command. Optionally one can enter an integer number to select corresponding attribute. Such attribute becomes a new target. Beside *Dir*, there are also *DirAll* and *DirPublic* that help to specify if private or built-in attributes should be considered. Note that peepshow distinguish between public/private/built-in attributes by looking at the underscores in the name. 39 | 40 | While *Dir* commands inspect by using `dir()` function, there are also *Vars* and *VarsPublic* commands that use `vars()` function instead. 41 | 42 | *.* is a command that can be used to access attributes without listing them. 43 | 44 | Commands *Bases*, *Mro*, *Self*, *Type* can be invoked to understand relations between the type of the target and other types. For more details use *help*. 45 | 46 | Example: 47 | 48 | ``` 49 | > t # take type of the target 50 | > .__name__ # take name of the type above 51 | > d # list attributes of the string above 52 | > 5 # select 5-th attribute 53 | ``` 54 | 55 | ## History 56 | 57 | Peepshow keeps track of the targets that are examined. One can display history and traverse through it by using following commands: 58 | 59 | ``` 60 | > . # display history 61 | > .. # go one step backward 62 | > - # go one step forward 63 | > / # go to the very beginning 64 | ``` 65 | 66 | ## Expressions 67 | 68 | For clarity, in the title bar target is described in a functional language that is not necessarily a valid Python expression. This language extends Python by: 69 | 70 | * `->` to denote passing target to a function. E.g. `x -> func` corresponds to `func(x)` in pure Python. 71 | * `=>` to denote passing target to an expression. E.g. `x => _ + 5` corresponds to `(lambda _: _ + 5)(x)` in pure Python. 72 | * `<>` to denote taking n-th element of iterable. E.g. `range(30, 40)<5>` could be replaced by `[*range(30, 40)][5]` in pure Python. 73 | * `<*>` to denote all the variables in context of the caller. Note that peepshow have access to locals, globals and built-ins, but not to enclosed variables. 74 | 75 | *eXpression* prints a valid Python expression which evaluates to the target. The same expression can be passed to underlying IPython by *Export* command. 76 | 77 | It is possible to evaluate an expression and consider result to be a new target. For instance: 78 | 79 | ``` 80 | > $range(100) # take generator object as a new target 81 | > $list(_) # converts target to a list and takes it as a new target 82 | ``` 83 | 84 | Following predefined symbols are available: 85 | 86 | * `_` - current target 87 | * `nth` - function that returns n-th element from an iterable 88 | * `catch` - wrapper that returns exception raised by wrapped function. For instance: 89 | 90 | ``` 91 | > catch(divmod)(1, 0) # returns ZeroDivisionError 92 | ``` 93 | 94 | Next to *$* (dollar) there is also *!* (exclamation mark) which evaluates expression and prints the result without replacing current target. For example: 95 | 96 | ``` 97 | > !import re # import 're' module 98 | > !m = re.match('\d*', '123abc') # assign 'm' 99 | > $m.group() # execute 'group' method and take result as a target 100 | ``` 101 | 102 | ## Iterables 103 | 104 | Elements of iterables can be listed by *\** (asterisk). Optional offset can be provided to skip given number of initial elements. Items from cache (last items visible on the screen) can be selected from by providing an integer numner. Cache can be recalled any time by *ShowCache*. Example: 105 | 106 | ``` 107 | > $range(1000) 108 | > *100 # show items starting from 100-th 109 | > 105 # pick 105-th item 110 | ``` 111 | 112 | It is important to know that generator objects are drained by listing their elements. In many cases this can influence execution of underlying application. 113 | 114 | In case of dictionaries, only keys are iterated by invoking *\**. Alternatively *\*\** can be used to display keys and corresponding values. Optional offset can be specified. 115 | 116 | 117 | ## Subscribables 118 | 119 | Target can be subscribed by using square brackets. Slicing an other expressions are allowed. Examples: 120 | 121 | ``` 122 | > $[1, 22, 333] 123 | > .. 124 | > [2] # select 333 125 | > .. 126 | > [1:] # select [22, 333] 127 | > .. 128 | > [_[0]] # select [22] 129 | ``` 130 | 131 | 132 | ## Callables 133 | 134 | Targets can be called by using round brackets. All kinds of Python-compatible expressions are allowed inside. Examples: 135 | 136 | ``` 137 | > !x = (123,) 138 | > $str 139 | > () # empty string 140 | > .. 141 | > (*x) # '123' 142 | > .. 143 | > (b'abc', 'utf8') # 'abc' 144 | ``` 145 | 146 | Command *Pass* can be used for passing target to a function and executing this function: 147 | 148 | ``` 149 | > $"abcdef" 150 | > pass len # equivalent of $len(_) 151 | ``` 152 | 153 | ## Exiting 154 | 155 | Use *Quit* or CTRL+C to quit peepshow and terminate underlying application. Use *Continue* or CTRL+D to return to underlying application. Additionally *Export* command continues underlying IPython session and provides it with an expression that evaluates to current target. 156 | -------------------------------------------------------------------------------- /peepshow/core/trans.py: -------------------------------------------------------------------------------- 1 | from peepshow.core.exceptions import CommandError 2 | from peepshow.utils.python import CheckInvocation, InvocationError 3 | from peepshow.utils.python import prettify_expr, exc_to_str 4 | 5 | class Transformation: 6 | def __init__(self): 7 | self.result = None 8 | self._prev = None 9 | self._next = None 10 | self.available = False 11 | 12 | def link(self, prev, *, forward=True): 13 | self._prev = prev 14 | if forward: 15 | prev._next = self 16 | 17 | def evaluate(self, prev, ctx): 18 | raise NotImplementedError 19 | 20 | def assign(self, result): 21 | self.result = result 22 | self.available = True 23 | 24 | def execute(self, prev, ctx, *, safe=False): 25 | try: 26 | result = self.evaluate(prev, ctx) 27 | except: 28 | if not safe: 29 | raise 30 | else: 31 | self.assign(result) 32 | 33 | def get_prev(self): 34 | if self._prev: 35 | return self._prev 36 | else: 37 | raise IndexError 38 | 39 | def get_next(self): 40 | if self._next: 41 | return self._next 42 | else: 43 | raise IndexError 44 | 45 | def get_first(self): 46 | if self._prev: 47 | return self._prev.get_first() 48 | else: 49 | return self 50 | 51 | def stringify(self, style): 52 | prev_str = self._prev.stringify(style) 53 | curr_str = style.apply(self, prev_str) 54 | return curr_str 55 | 56 | class Initial(Transformation): 57 | def __init__(self, target): 58 | super().__init__() 59 | self.result = target 60 | 61 | class GloLoc(Initial): 62 | def stringify(self, style): 63 | return style.apply(self) 64 | 65 | class Given(Initial): 66 | def __init__(self, target, expr): 67 | super().__init__(target) 68 | self.expr = prettify_expr(expr) 69 | 70 | def stringify(self, style): 71 | return style.apply(self, self.expr) 72 | 73 | class Attrib(Transformation): 74 | def __init__(self, attr_name): 75 | super().__init__() 76 | self.attr_name = attr_name 77 | 78 | def evaluate(self, prev, env): 79 | return getattr(prev.result, self.attr_name) 80 | 81 | class Pass(Transformation): 82 | def __init__(self, func): 83 | super().__init__() 84 | self.func = func 85 | self.func_name = func.__name__ 86 | 87 | def evaluate(self, prev, ctx): 88 | return self.func(prev.result) 89 | 90 | class PassExpr(Transformation): 91 | def __init__(self, func_name): 92 | super().__init__() 93 | self.func_name = func_name 94 | 95 | def evaluate(self, prev, ctx): 96 | try: 97 | value = ctx.eval_(f"{self.func_name}(_)") 98 | except Exception as ex: 99 | raise CommandError(f"Invalid invocation: {exc_to_str(ex)}") from ex 100 | return value 101 | 102 | class Call(Transformation): 103 | def __init__(self, args_kwargs): 104 | super().__init__() 105 | self.args_kwargs = args_kwargs 106 | self.catched = False 107 | 108 | def evaluate(self, prev, ctx): 109 | call = f'_({self.args_kwargs})' 110 | try: 111 | with CheckInvocation(caller_stack_depth=3): 112 | return ctx.eval_(call) 113 | except InvocationError as ex: 114 | raise CommandError(ex) from ex 115 | except Exception as ex: 116 | self.catched = True 117 | return ex 118 | 119 | class Subscr(Transformation): 120 | def __init__(self, index): 121 | super().__init__() 122 | self.index = index 123 | 124 | def evaluate(self, prev, ctx): 125 | return prev.result[self.index] 126 | 127 | class SubscrExpr(Transformation): 128 | def __init__(self, expr): 129 | super().__init__() 130 | self.expr = expr 131 | 132 | def evaluate(self, prev, ctx): 133 | try: 134 | value = ctx.eval_(f"_[{self.expr}]") 135 | except Exception as ex: 136 | raise CommandError(exc_to_str(ex, show_type=True)) from ex 137 | return value 138 | 139 | class Iter(Transformation): 140 | def __init__(self, index): 141 | super().__init__() 142 | self.index = index 143 | 144 | def evaluate(self, prev, ctx): 145 | return iget(prev.result, self.index) 146 | 147 | class Eval(Transformation): 148 | def __init__(self, expr): 149 | super().__init__() 150 | self.expr = expr 151 | 152 | def evaluate(self, prev, ctx): 153 | try: 154 | value = ctx.eval_(self.expr) 155 | except SyntaxError as ex: 156 | raise CommandError(exc_to_str(ex, show_type=True)) from ex 157 | return value 158 | 159 | class TransformationMgr: 160 | def __init__(self, target, ctx): 161 | self.selected = None 162 | self._ctx = ctx 163 | self.reset(target) 164 | 165 | def reset(self, transformation): 166 | self.selected = transformation 167 | 168 | def transform(self, transformation): 169 | transformation.execute(self.selected, self._ctx) 170 | self.accept(transformation) 171 | 172 | def accept(self, transformation): 173 | transformation.link(self.selected) 174 | self.selected = transformation 175 | 176 | def propose_iter(self, offset): 177 | for index, item in enumerate(self.selected.result): 178 | transformation = Iter(index) 179 | transformation.link(self.selected, forward=False) 180 | transformation.assign(item) 181 | yield None, transformation 182 | 183 | def propose_attr(self, attr_names, values=None): 184 | if values is None: 185 | for attr_name in attr_names: 186 | transformation = Attrib(attr_name) 187 | transformation.link(self.selected, forward=False) 188 | transformation.execute(self.selected, self._ctx, safe=True) 189 | yield attr_name, transformation 190 | else: 191 | for attr_name, value in zip(attr_names, values): 192 | transformation = Attrib(attr_name) 193 | transformation.link(self.selected, forward=False) 194 | transformation.assign(value) 195 | yield attr_name, transformation 196 | 197 | def propose_subscr(self, indices, values=None): 198 | if values is None: 199 | for index in indices: 200 | transformation = Subscr(index) 201 | transformation.link(self.selected, forward=False) 202 | transformation.execute(self.selected, self._ctx, safe=True) 203 | yield repr(index), transformation 204 | else: 205 | for index, value in zip(indices, values): 206 | transformation = Subscr(index) 207 | transformation.link(self.selected, forward=False) 208 | transformation.assign(value) 209 | yield repr(index), transformation 210 | 211 | def select_next(self): 212 | self.selected = self.selected.get_next() 213 | 214 | def select_prev(self): 215 | self.selected = self.selected.get_prev() 216 | 217 | def select_first(self): 218 | self.selected = self.selected.get_first() 219 | 220 | def get_history(self, style): 221 | for item in self: 222 | yield style.stringify(item), item is self.selected 223 | 224 | def __iter__(self): 225 | item = self.selected.get_first() 226 | while True: 227 | yield item 228 | try: 229 | item = item.get_next() 230 | except IndexError: 231 | break 232 | -------------------------------------------------------------------------------- /peepshow/core/dialect.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import peepshow.core.trans as trans 3 | from peepshow.utils.python import prettify_expr 4 | 5 | def _find_var_offsets(obj, var_name): 6 | ret = () 7 | if isinstance(obj, ast.Name): 8 | if obj.id == var_name: 9 | ret = ((obj.col_offset,),) 10 | elif isinstance(obj, ast.AST): 11 | ret = tuple(_find_var_offsets(getattr(obj, field), var_name) for field in obj._fields) 12 | elif isinstance(obj, list): 13 | ret = tuple(_find_var_offsets(item, var_name) for item in obj) 14 | return sum(ret, ()) 15 | 16 | 17 | def replace_var_name(expr, var_name, replacement): 18 | tree = ast.parse(expr) 19 | offsets = _find_var_offsets(tree.body[0], var_name) 20 | 21 | bias = 0 22 | for start in offsets: 23 | start_biased = start + bias 24 | stop_biased = start_biased + len(var_name) 25 | expr = expr[:start_biased] + replacement + expr[stop_biased:] 26 | bias += len(replacement) - len(var_name) 27 | 28 | return expr 29 | 30 | def is_dependent_expr(expr): 31 | return replace_var_name(expr, '_', '_') != replace_var_name(expr, '_', '') 32 | 33 | def expr_low_prio(expr): 34 | try: 35 | kind = ast.parse(expr).body[0].value 36 | except: 37 | return True 38 | 39 | high_prio_objects = ( 40 | ast.Num, 41 | ast.Str, 42 | ast.Name, 43 | ast.Dict, 44 | ast.Set, 45 | ast.Tuple, 46 | ast.List, 47 | ast.Call, 48 | ast.Subscript, 49 | ast.Attribute) 50 | 51 | # if anything unexpected comes, priority is assumed low 52 | return not isinstance(kind, high_prio_objects) 53 | 54 | def expr_num_literal(expr): 55 | try: 56 | kind = ast.parse(expr).body[0].value 57 | except: 58 | return False 59 | 60 | return isinstance(kind, ast.Num) 61 | 62 | def add_panth(expr): 63 | return f"({expr})" 64 | 65 | class Dialect: 66 | 67 | def stringify(self, obj): 68 | return obj.stringify(self) 69 | 70 | def is_prev_low_prio(self, transformation): 71 | raise NotImplementedError 72 | 73 | def is_prev_num_literal(self, transformation): 74 | prev_trans = transformation.get_prev() 75 | if isinstance(prev_trans, (trans.Given, trans.Eval)): 76 | return expr_num_literal(prev_trans.expr) 77 | return False 78 | 79 | def apply(self, transformation, prev_str=None): 80 | cls = type(transformation) 81 | hndl_name = 'fmt_' + cls.__name__.lower() 82 | hndl = getattr(self, hndl_name) 83 | return hndl(x=prev_str, t=transformation) 84 | 85 | # Depreciated 86 | class EvaluableCustom(Dialect): 87 | 88 | def is_prev_low_prio(self, transformation): 89 | prev_trans = transformation.get_prev() 90 | if prev_trans is None: 91 | return False 92 | elif isinstance(prev_trans, (trans.GloLoc, 93 | trans.Iter, 94 | trans.Attrib, 95 | trans.Pass, 96 | trans.PassExpr, 97 | trans.Call, 98 | trans.Subscr, 99 | trans.SubscrExpr, 100 | trans.Iter)): 101 | return False 102 | elif isinstance(prev_trans, (trans.Given, trans.Eval)): 103 | return expr_low_prio(prev_trans.expr) 104 | else: 105 | return True 106 | 107 | def add_panth(self, x, t): 108 | if self.is_prev_low_prio(t) or self.is_prev_num_literal(t): 109 | return add_panth(x) 110 | else: 111 | return x 112 | 113 | def fmt_gloloc(self, x, t): 114 | return '{**globals(), **locals()}' 115 | 116 | def fmt_given(self, x, t): 117 | return t.expr 118 | 119 | def fmt_attrib(self, x, t): 120 | x = self.add_panth(x, t) 121 | return f'{x}.{t.attr_name}' 122 | 123 | def fmt_pass(self, x, t): 124 | return f'{t.func_name}({x})' 125 | 126 | def fmt_passexpr(self, x, t): 127 | return f'{t.func_name}({x})' 128 | 129 | def fmt_call(self, x, t): 130 | if t.catched: 131 | return f'catch({x})({t.args_kwargs})' 132 | else: 133 | x = self.add_panth(x, t) 134 | return f'{x}({t.args_kwargs})' 135 | 136 | def fmt_subscr(self, x, t): 137 | if isinstance(t.get_prev(), trans.GloLoc): 138 | return f'{t.index}' 139 | else: 140 | x = self.add_panth(x, t) 141 | return f'{x}[{t.index!r}]' 142 | 143 | def fmt_subscrexpr(self, x, t): 144 | x = self.add_panth(x, t) 145 | return f'{x}[{t.expr}]' 146 | 147 | def fmt_iter(self, x, t): 148 | return f'nth({x}, {t.index!r})' 149 | 150 | def fmt_eval(self, x, t): 151 | if is_dependent_expr(t.expr): 152 | x = self.add_panth(x, t) 153 | return replace_var_name(t.expr, '_', x) 154 | else: 155 | return t.expr 156 | 157 | 158 | class EvaluableAuto(Dialect): 159 | 160 | def stringify(self, obj): 161 | return prettify_expr(obj.stringify(self)) 162 | 163 | def fmt_gloloc(self, x, t): 164 | return '{**globals(), **locals()}' 165 | 166 | def fmt_given(self, x, t): 167 | return t.expr 168 | 169 | def fmt_attrib(self, x, t): 170 | x = add_panth(x) 171 | return f'{x}.{t.attr_name}' 172 | 173 | def fmt_pass(self, x, t): 174 | return f'{t.func_name}({x})' 175 | 176 | def fmt_passexpr(self, x, t): 177 | return f'{t.func_name}({x})' 178 | 179 | def fmt_call(self, x, t): 180 | if t.catched: 181 | return f'catch({x})({t.args_kwargs})' 182 | else: 183 | x = add_panth(x) 184 | return f'{x}({t.args_kwargs})' 185 | 186 | def fmt_subscr(self, x, t): 187 | if isinstance(t.get_prev(), trans.GloLoc): 188 | return f'{t.index}' 189 | else: 190 | x = add_panth(x) 191 | return f'{x}[{t.index!r}]' 192 | 193 | def fmt_subscrexpr(self, x, t): 194 | x = add_panth(x) 195 | return f'{x}[{t.expr}]' 196 | 197 | def fmt_iter(self, x, t): 198 | return f'nth({x}, {t.index!r})' 199 | 200 | def fmt_eval(self, x, t): 201 | if is_dependent_expr(t.expr): 202 | x = add_panth(x) 203 | return replace_var_name(t.expr, '_', x) 204 | else: 205 | return t.expr 206 | 207 | Evaluable = EvaluableAuto 208 | 209 | class Readable(Dialect): 210 | 211 | def is_prev_low_prio(self, transformation): 212 | prev_trans = transformation.get_prev() 213 | if prev_trans is None: 214 | return False 215 | elif isinstance(prev_trans, (trans.GloLoc, 216 | trans.Iter, 217 | trans.Attrib, 218 | trans.Call, 219 | trans.Subscr, 220 | trans.SubscrExpr)): 221 | return False 222 | elif isinstance(prev_trans, (trans.Given, trans.Eval)): 223 | return expr_low_prio(prev_trans.expr) 224 | else: 225 | return True 226 | 227 | def add_panth(self, x, t): 228 | if self.is_prev_low_prio(t) or self.is_prev_num_literal(t): 229 | return add_panth(x) 230 | else: 231 | return x 232 | 233 | def fmt_gloloc(self, x, t): 234 | return '<*>' 235 | 236 | def fmt_given(self, x, t): 237 | return t.expr 238 | 239 | def fmt_attrib(self, x, t): 240 | x = self.add_panth(x, t) 241 | return f'{x}.{t.attr_name}' 242 | 243 | def fmt_pass(self, x, t): 244 | return f'{x} -> {t.func_name}' 245 | 246 | def fmt_passexpr(self, x, t): 247 | return f'{x} -> {t.func_name}' 248 | 249 | def fmt_call(self, x, t): 250 | if t.catched: 251 | return f'catch({x})({t.args_kwargs})' 252 | else: 253 | x = self.add_panth(x, t) 254 | return f'{x}({t.args_kwargs})' 255 | 256 | def fmt_subscr(self, x, t): 257 | if isinstance(t.get_prev(), trans.GloLoc): 258 | return f'{t.index}' 259 | else: 260 | x = self.add_panth(x, t) 261 | return f'{x}[{t.index!r}]' 262 | 263 | def fmt_subscrexpr(self, x, t): 264 | x = self.add_panth(x, t) 265 | return f'{x}[{t.expr}]' 266 | 267 | def fmt_iter(self, x, t): 268 | x = self.add_panth(x, t) 269 | return f'{x}<{t.index}>' 270 | 271 | def fmt_eval(self, x, t): 272 | expr = prettify_expr(t.expr) 273 | if is_dependent_expr(t.expr): 274 | return f'{x} => {expr}' 275 | else: 276 | return expr 277 | -------------------------------------------------------------------------------- /peepshow/peepshow.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH "PEEPSHOW" "1" "Nov 19, 2018" "" "peepshow" 4 | .SH NAME 5 | peepshow \- Data Explorer for Python 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .sp 34 | \fIpeepshow\fP package consists of two utilities, \fIpeep\fP and \fIshow\fP dedicated to Python developers. 35 | .SH PEEP 36 | .sp 37 | \fBpeep\fP function provides an interactive interface to access properties of the target object given as an argument. When called without arguments, \fBpeep\fP starts from inspecting a dictionary that consists of all the variables in scope of the caller. There is also \fBpeep_\fP function which expects an expression instead of variable to be provided as an argument. Such expression is evaluated in context of the caller and the result becomes a target object. 38 | .INDENT 0.0 39 | .INDENT 3.5 40 | .sp 41 | .nf 42 | .ft C 43 | >>> x = [1, 2, 3] # target object 44 | >>> peep(x) # target provided explicitly 45 | >>> peep() # all the variables in this context a assembly to a target 46 | >>> peep_(\(aqx\(aq) # target provided as an expression 47 | .ft P 48 | .fi 49 | .UNINDENT 50 | .UNINDENT 51 | .SS Navigating 52 | .sp 53 | After invoking \fBpeep\fP an interactive prompt appears on the screen. User can call one of the commands using either its full name or and alias. Alias consists only of capital letters form the full name. Use \fIHelp\fP command to get full list of commands or invoke \fIhelp \fP to get help on specific command . 54 | .INDENT 0.0 55 | .INDENT 3.5 56 | .sp 57 | .nf 58 | .ft C 59 | > help # provides list of commands 60 | > h # the same as above 61 | > help manual # provides help on \(aqmanual\(aq command 62 | > h man # the same as above 63 | .ft P 64 | .fi 65 | .UNINDENT 66 | .UNINDENT 67 | .sp 68 | It is also possible to execute suggested command that is displayed in the prompt simply by hitting Enter key. 69 | .sp 70 | Prompt also provides type name of the target. This applies only to selected Python built\-in types. 71 | .SS Basic Inspection 72 | .sp 73 | Regardless of the target type, command \fIInfo\fP (executed at startup) can be used for displaying basic summary. More detailed information are provided by \fIFeatures\fP command. 74 | .sp 75 | \fIPrettyPrint\fP command attempts to show target recursively in a recursive manner. 76 | .sp 77 | Commans \fI?\fP and \fI??\fP display target\(aqs docstring and source code respectively. 78 | .sp 79 | Whenever there is too much information on the screen, \fICLear\fP command can be used. 80 | .sp 81 | Typically there is a need of inspecting of target\(aqs attributes. This can be done by using \fIDir\fP command. Optionally one can enter an integer number to select corresponding attribute. Such attribute becomes a new target. Beside \fIDir\fP, there are also \fIDirAll\fP and \fIDirPublic\fP that help to specify if private or built\-in attributes should be considered. Note that peepshow distinguish between public/private/built\-in attributes by looking at the underscores in the name. 82 | .sp 83 | While \fIDir\fP commands inspect by using \fBdir()\fP function, there are also \fIVars\fP and \fIVarsPublic\fP commands that use \fBvars()\fP function instead. 84 | .sp 85 | \fI\&.\fP is a command that can be used to access attributes without listing them. 86 | .sp 87 | Commands \fIBases\fP, \fIMro\fP, \fISelf\fP, \fIType\fP can be invoked to understand relations between the type of the target and other types. For more details use \fIhelp\fP\&. 88 | .sp 89 | Example: 90 | .INDENT 0.0 91 | .INDENT 3.5 92 | .sp 93 | .nf 94 | .ft C 95 | > t # take type of the target 96 | > .__name__ # take name of the type above 97 | > d # list attributes of the string above 98 | > 5 # select 5\-th attribute 99 | .ft P 100 | .fi 101 | .UNINDENT 102 | .UNINDENT 103 | .SS History 104 | .sp 105 | Peepshow keeps track of the targets that are examined. One can display history and traverse through it by using following commands: 106 | .INDENT 0.0 107 | .INDENT 3.5 108 | .sp 109 | .nf 110 | .ft C 111 | > . # display history 112 | > .. # go one step backward 113 | > \- # go one step forward 114 | > / # go to the very beginning 115 | .ft P 116 | .fi 117 | .UNINDENT 118 | .UNINDENT 119 | .SS Expressions 120 | .sp 121 | For clarity, in the title bar target is described in a functional language that is not necessarily a valid Python expression. This language extends Python by: 122 | .INDENT 0.0 123 | .IP \(bu 2 124 | \fB\->\fP to denote passing target to a function. E.g. \fBx \-> func\fP corresponds to \fBfunc(x)\fP in pure Python. 125 | .IP \(bu 2 126 | \fB=>\fP to denote passing target to an expression. E.g. \fBx => _ + 5\fP corresponds to \fB(lambda _: _ + 5)(x)\fP in pure Python. 127 | .IP \(bu 2 128 | \fB<>\fP to denote taking n\-th element of iterable. E.g. \fBrange(30, 40)<5>\fP could be replaced by \fB[*range(30, 40)][5]\fP in pure Python. 129 | .IP \(bu 2 130 | \fB<*>\fP to denote all the variables in context of the caller. Note that peepshow have access to locals, globals and built\-ins, but not to enclosed variables. 131 | .UNINDENT 132 | .sp 133 | \fIeXpression\fP prints a valid Python expression which evaluates to the target. The same expression can be passed to underlying IPython by \fIExport\fP command. 134 | .sp 135 | It is possible to evaluate an expression and consider result to be a new target. For instance: 136 | .INDENT 0.0 137 | .INDENT 3.5 138 | .sp 139 | .nf 140 | .ft C 141 | > $range(100) # take generator object as a new target 142 | > $list(_) # converts target to a list and takes it as a new target 143 | .ft P 144 | .fi 145 | .UNINDENT 146 | .UNINDENT 147 | .sp 148 | Following predefined symbols are available: 149 | .INDENT 0.0 150 | .IP \(bu 2 151 | \fB_\fP \- current target 152 | .IP \(bu 2 153 | \fBnth\fP \- function that returns n\-th element from an iterable 154 | .IP \(bu 2 155 | \fBcatch\fP \- wrapper that returns exception raised by wrapped function. For instance: 156 | .UNINDENT 157 | .INDENT 0.0 158 | .INDENT 3.5 159 | .sp 160 | .nf 161 | .ft C 162 | > catch(divmod)(1, 0) # returns ZeroDivisionError 163 | .ft P 164 | .fi 165 | .UNINDENT 166 | .UNINDENT 167 | .sp 168 | Next to \fI$\fP (dollar) there is also \fI!\fP (exclamation mark) which evaluates expression and prints the result without replacing current target. For example: 169 | .INDENT 0.0 170 | .INDENT 3.5 171 | .sp 172 | .nf 173 | .ft C 174 | > !import re # import \(aqre\(aq module 175 | > !m = re.match(\(aq\ed*\(aq, \(aq123abc\(aq) # assign \(aqm\(aq 176 | > $m.group() # execute \(aqgroup\(aq method and take result as a target 177 | .ft P 178 | .fi 179 | .UNINDENT 180 | .UNINDENT 181 | .SS Iterables 182 | .sp 183 | Elements of iterables can be listed by \fI*\fP (asterisk). Optional offset can be provided to skip given number of initial elements. Items from cache (last items visible on the screen) can be selected from by providing an integer numner. Cache can be recalled any time by \fIShowCache\fP\&. Example: 184 | .INDENT 0.0 185 | .INDENT 3.5 186 | .sp 187 | .nf 188 | .ft C 189 | > $range(1000) 190 | > *100 # show items starting from 100\-th 191 | > 105 # pick 105\-th item 192 | .ft P 193 | .fi 194 | .UNINDENT 195 | .UNINDENT 196 | .sp 197 | It is important to know that generator objects are drained by listing their elements. In many cases this can influence execution of underlying application. 198 | .sp 199 | In case of dictionaries, only keys are iterated by invoking \fI*\fP\&. Alternatively \fI**\fP can be used to display keys and corresponding values. Optional offset can be specified. 200 | .SS Subscribables 201 | .sp 202 | Target can be subscribed by using square brackets. Slicing an other expressions are allowed. Examples: 203 | .INDENT 0.0 204 | .INDENT 3.5 205 | .sp 206 | .nf 207 | .ft C 208 | > $[1, 22, 333] 209 | > .. 210 | > [2] # select 333 211 | > .. 212 | > [1:] # select [22, 333] 213 | > .. 214 | > [_[0]] # select [22] 215 | .ft P 216 | .fi 217 | .UNINDENT 218 | .UNINDENT 219 | .SS Callables 220 | .sp 221 | Targets can be called by using round brackets. All kinds of Python\-compatible expressions are allowed inside. Examples: 222 | .INDENT 0.0 223 | .INDENT 3.5 224 | .sp 225 | .nf 226 | .ft C 227 | > !x = (123,) 228 | > $str 229 | > () # empty string 230 | > .. 231 | > (*x) # \(aq123\(aq 232 | > .. 233 | > (b\(aqabc\(aq, \(aqutf8\(aq) # \(aqabc\(aq 234 | .ft P 235 | .fi 236 | .UNINDENT 237 | .UNINDENT 238 | .sp 239 | Command \fIPass\fP can be used for passing target to a function and executing this function: 240 | .INDENT 0.0 241 | .INDENT 3.5 242 | .sp 243 | .nf 244 | .ft C 245 | > $"abcdef" 246 | > pass len # equivalent of $len(_) 247 | .ft P 248 | .fi 249 | .UNINDENT 250 | .UNINDENT 251 | .SS Exiting 252 | .sp 253 | Use \fIQuit\fP or CTRL+C to quit peepshow and terminate underlying application. Use \fIContinue\fP or CTRL+D to return to underlying application. Additionally \fIExport\fP command continues underlying IPython session and provides it with an expression that evaluates to current target. 254 | .SH SHOW 255 | .sp 256 | \fBshow\fP is a function that displays variables given as arguments. Names of the variables that are provided as positional arguments are determined based on Python reflection. 257 | .INDENT 0.0 258 | .INDENT 3.5 259 | .sp 260 | .nf 261 | .ft C 262 | >>> x = 123 263 | >>> y = [1, 2, 3] 264 | >>> show(x, y, x*2+1) 265 | x = 123 266 | y = [1, 2, 3] 267 | x * 2 + 1 = 247 268 | .ft P 269 | .fi 270 | .UNINDENT 271 | .UNINDENT 272 | .sp 273 | Variables that are provided as keyword arguments inherit names from corresponding arguments. 274 | .INDENT 0.0 275 | .INDENT 3.5 276 | .sp 277 | .nf 278 | .ft C 279 | >>> x = 123 280 | >>> y = 234 281 | >>> show(foo=x+y) 282 | foo = 357 283 | .ft P 284 | .fi 285 | .UNINDENT 286 | .UNINDENT 287 | .sp 288 | There is also \fBshow_\fP function that expects their arguments to be expressions that should be evaluated in context of the caller. 289 | .INDENT 0.0 290 | .INDENT 3.5 291 | .sp 292 | .nf 293 | .ft C 294 | >>> x = 123 295 | >>> y = 234 296 | >>> show_(\(aqx+y\(aq, py_ver=\(aqsys.version.split()[0]\(aq) 297 | x + y = 357 298 | py_ver = \(aq3.6.2\(aq 299 | .ft P 300 | .fi 301 | .UNINDENT 302 | .UNINDENT 303 | .sp 304 | \fBshow\fP and \fBshow_\fP functions can be also called without arguments to display all the variables in context of the caller. 305 | .SH AUTHOR 306 | Grzegorz Krasoń 307 | .SH COPYRIGHT 308 | 2018, Grzegorz Krasoń 309 | .\" Generated by docutils manpage writer. 310 | . 311 | -------------------------------------------------------------------------------- /peepshow/cmds/cmds.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import shutil 4 | import inspect 5 | import subprocess 6 | from pathlib import Path 7 | from miscutils.insp import isaccess 8 | import peepshow 9 | import peepshow.core.dialect as dialect 10 | import peepshow.core.trans as trans 11 | from peepshow.cmds.base import CommandsBase, command 12 | from peepshow.core import goto, probes 13 | from peepshow.core.probes import read_features, MAJOR_FEATURES, MINOR_FEATURES 14 | from peepshow.core.probes import is_subscribable 15 | from peepshow.core.probes import Text, is_of_builtin_type 16 | from peepshow.utils.terminal import print_help, print_error 17 | from peepshow.utils.terminal import style, Style, Fore, Back 18 | from peepshow.utils import terminal 19 | from peepshow.pager import pager 20 | from peepshow.core.exceptions import CommandError 21 | from peepshow.utils.python import exc_to_str, crayon_expr, pformat 22 | from peepshow.utils.traceback import FrameSummary 23 | 24 | def table_width(table): 25 | return max(map(len, table)) + 1 26 | 27 | def print_target_features(obj, features, *, hlight=False, header=None): 28 | table = read_features(obj, features) 29 | width = table_width(table) 30 | def get_lines(): 31 | if header is not None: 32 | yield header 33 | for name, feat in table.items(): 34 | line = f'{name:<{width}}: {feat}' 35 | if hlight and feat.value: 36 | line = style(Style.BRIGHT, line) 37 | yield line 38 | pager.page(get_lines()) 39 | 40 | class Commands(CommandsBase): 41 | 42 | def __init__(self, context): 43 | self.ctx = context 44 | 45 | def qualifier_cmd_eval(alias): 46 | regexp = re.compile('!(.*)$') 47 | m = regexp.match(alias) 48 | return m.groups()[0] 49 | 50 | @command('!', qualifier_cmd_eval) 51 | def cmd_eval(self, expr): 52 | """Evaluate and print the result. 53 | Empty will paste expression of current target and let you edit it. 54 | See also '$' command. 55 | """ 56 | 57 | expr = expr.strip() 58 | if expr: 59 | try: 60 | try: 61 | result = self.ctx.eval_(expr) 62 | print(repr(result)) 63 | except SyntaxError: 64 | self.ctx.exec_(expr) 65 | except Exception as ex: 66 | raise CommandError(exc_to_str(ex, show_type=True)) from ex 67 | terminal.update_suggestions(self.ctx.env.current) 68 | else: 69 | expr = dialect.Evaluable().stringify(self.ctx.mgr.selected) 70 | terminal.prefill_input('!' + expr) 71 | 72 | def qualifier_cmd_eval_get(alias): 73 | regexp = re.compile(r'\$(.*)$') 74 | m = regexp.match(alias) 75 | return m.groups()[0] 76 | 77 | @command('$', qualifier_cmd_eval_get) 78 | def cmd_eval_get(self, expr): 79 | """Evaluate and consider result as a new target. 80 | Empty will paste expression of current target and let you edit it. 81 | See also '!' command. 82 | """ 83 | 84 | expr = expr.strip() 85 | 86 | if expr: 87 | self.ctx.mgr.transform(trans.Eval(expr)) 88 | return Commands.cmd_info, (self,) 89 | else: 90 | expr = dialect.Evaluable().stringify(self.ctx.mgr.selected) 91 | terminal.prefill_input('$' + expr) 92 | 93 | def qualifier_cmd_index(alias): 94 | regexp = re.compile('\[(.+)\]$') 95 | m = regexp.match(alias) 96 | return m.groups()[0] 97 | 98 | @command('[]', qualifier_cmd_index) 99 | def cmd_index(self, expr): 100 | """Subscribe target with .""" 101 | 102 | if not probes.is_subscribable(self.ctx.target): 103 | raise CommandError("Item not subscribable.") 104 | 105 | self.ctx.mgr.transform(trans.SubscrExpr(expr)) 106 | return Commands.cmd_info, (self,) 107 | 108 | def qualifier_cmd_call(alias): 109 | regexp = re.compile('\((.*)\)$') 110 | m = regexp.match(alias) 111 | return m.groups()[0] 112 | 113 | @command('()', qualifier_cmd_call) 114 | def cmd_call(self, args_kwargs_str): 115 | """Call target with .""" 116 | 117 | if not probes.is_callable(self.ctx.target): 118 | raise CommandError("Item not callable.") 119 | 120 | try: 121 | self.ctx.mgr.transform(trans.Call(args_kwargs_str)) 122 | except Exception as ex: 123 | raise CommandError(f'Invalid invocation: {exc_to_str(ex)}') from ex 124 | 125 | return Commands.cmd_info, (self,) 126 | 127 | @command('ShowCache') 128 | def cmd_buffer(self): 129 | """Show content of cache which can be accessed by command. 130 | See also '' command. 131 | """ 132 | self.ctx.explorer.recall() 133 | 134 | @command('DirAll') 135 | def cmd_dir_all(self): 136 | """Call dir() and show all the results. 137 | See also 'Dir' and 'DirPublic' commands. 138 | """ 139 | target = self.ctx.target 140 | content = self.ctx.mgr.propose_attr(dir(target)) 141 | self.ctx.explorer.fill(content, 'attr') 142 | 143 | @command('Dir') 144 | def cmd_dir(self): 145 | """Call dir() and show results other than of __XYZ__ format. 146 | See also 'DirAll' and 'DirPublic' commands. 147 | """ 148 | target = self.ctx.target 149 | cond = lambda k: not isaccess(k).special 150 | attr_names = filter(cond, dir(target)) 151 | content = self.ctx.mgr.propose_attr(attr_names) 152 | self.ctx.explorer.fill(content, 'attr') 153 | 154 | @command('DirPublic') 155 | def cmd_dir_public(self): 156 | """Call dir() and show results other than starting with '_'. 157 | See also 'Dir' and 'DirAll' commands. 158 | """ 159 | target = self.ctx.target 160 | cond = lambda k: isaccess(k).public 161 | attr_names = filter(cond, dir(target)) 162 | content = self.ctx.mgr.propose_attr(attr_names) 163 | self.ctx.explorer.fill(content, 'attr') 164 | 165 | @command('Vars') 166 | def cmd_vars(self): 167 | """Call vars() and show all the results. 168 | See also 'VarsPublic' command. 169 | """ 170 | target = self.ctx.target 171 | try: 172 | v = vars(target) 173 | except Exception as ex: 174 | raise CommandError(exc_to_str(ex)) 175 | 176 | content = self.ctx.mgr.propose_attr(v.keys(), v.values()) 177 | self.ctx.explorer.fill(content, 'attr') 178 | 179 | @command('VarsPublic') 180 | def cmd_vars_public(self): 181 | """Call vars() and show results other than starting with '_'. 182 | See also 'Vars' command. 183 | """ 184 | target = self.ctx.target 185 | try: 186 | v = vars(target) 187 | except Exception as ex: 188 | raise CommandError(exc_to_str(ex)) 189 | 190 | items = ((k, v) for k,v in vars(target).items() if isaccess(k).public) 191 | try: 192 | keys, values = zip(*items) 193 | except ValueError: 194 | keys, values = (), () 195 | content = self.ctx.mgr.propose_attr(keys, values) 196 | self.ctx.explorer.fill(content, 'attr') 197 | 198 | @command('', int) 199 | def cmd_int(self, x): 200 | """Select -th item from cache. 201 | See also 'ShowCache' command. 202 | """ 203 | 204 | transformation = self.ctx.explorer.get_transformation(x) 205 | self.ctx.mgr.accept(transformation) 206 | return Commands.cmd_info, (self,) 207 | 208 | @command('..') 209 | def cmd_back(self): 210 | """Make one step backward in history. 211 | See also '-', '/', '.' commands. 212 | """ 213 | try: 214 | self.ctx.mgr.select_prev() 215 | return Commands.cmd_info, (self,) 216 | except IndexError: 217 | raise CommandError(f"This is the first item in history. Cannot go back any more.") 218 | 219 | @command('-') 220 | def cmd_forward(self): 221 | """Make one step forward in history. 222 | See also '..', '/', '.' commands. 223 | """ 224 | try: 225 | self.ctx.mgr.select_next() 226 | return Commands.cmd_info, (self,) 227 | except IndexError: 228 | raise CommandError("This is the last item in history. Cannot go forward any more.") 229 | 230 | @command('/') 231 | def cmd_root(self): 232 | """Go to the begining in history. 233 | See also '..', '-', '.' commands. 234 | """ 235 | self.ctx.mgr.select_first() 236 | return Commands.cmd_info, (self,) 237 | 238 | @command('.') 239 | def cmd_history(self): 240 | """Display history. 241 | See also '..', '-', '/' commands. 242 | """ 243 | for entry, current in self.ctx.mgr.get_history(dialect.Readable()): 244 | if current: 245 | print(style(Style.BRIGHT, entry)) 246 | else: 247 | print(entry) 248 | 249 | def qualifier_cmd_iterate(alias): 250 | regexp = re.compile('\*(\d*)$') 251 | m = regexp.match(alias) 252 | return int('0' + m.groups()[0]) 253 | 254 | @command('*', qualifier_cmd_iterate) 255 | def cmd_iterate(self, offset): 256 | """Iterate target. 257 | This can be used for listing values of iterable. 258 | Optional can be used for skipping certain number of entires. 259 | """ 260 | target = self.ctx.target 261 | try: 262 | content = self.ctx.mgr.propose_iter(offset) # TODO offset 263 | except TypeError: 264 | raise CommandError("Item is not iterable.") 265 | self.ctx.explorer.fill(content, 'list', offset) 266 | 267 | def qualifier_cmd_items(alias): 268 | regexp = re.compile('\*\*(\d*)$') 269 | m = regexp.match(alias) 270 | return int('0' + m.groups()[0]) 271 | 272 | @command('**', qualifier_cmd_items) 273 | def cmd_items(self, offset): 274 | """Call .items() and iterate through results. 275 | This can be used for listing keys and values of the dictionary. 276 | Optional can be used for skipping certain number of entires. 277 | """ 278 | target = self.ctx.target 279 | try: 280 | keys = target.keys() 281 | except Exception as ex: 282 | raise CommandError(exc_to_str(ex)) 283 | 284 | try: 285 | content = self.ctx.mgr.propose_subscr(keys) 286 | except: 287 | raise CommandError("Error while obtaining items to iterate.") 288 | self.ctx.explorer.fill(content, 'dict', offset) 289 | 290 | def qualifier_cmd_attrib(alias): 291 | regexp = re.compile('\.([A-Za-z_][A-Za-z0-9_]*)$') 292 | m = regexp.match(alias) 293 | return m.groups()[0] 294 | 295 | @command('.', qualifier_cmd_attrib) 296 | def cmd_attrib(self, attr_name): 297 | """Return attribute of the target. 298 | """ 299 | try: 300 | self.ctx.mgr.transform(trans.Attrib(attr_name)) 301 | except: 302 | raise CommandError(f"Attribute cannot be obtained.") 303 | return Commands.cmd_info, (self,) 304 | 305 | @command('Continue') 306 | def cmd_continue(self): 307 | """Exit peepshow and continue execution of underlying application. 308 | Alternatively enter EOF (under Linux press: CTRL+D) 309 | See also 'Quit' and 'Export' commands. 310 | """ 311 | raise goto.Stop(exit=False) 312 | 313 | @command('Quit') 314 | def cmd_quit(self): 315 | """Exit peepshow and terminate underlying Python interpreter. 316 | Alternatively send SIGINT (under Linux press: CTRL+C) 317 | See also 'Continue' and 'Export' commands. 318 | """ 319 | raise goto.Stop(exit=True) 320 | 321 | @command('Export') 322 | def cmd_export(self): 323 | """Export target expression to IPython. 324 | This terminates peepshow and initiates next command in underlying IPython 325 | with the expression describing current target. 326 | See also 'Continue' and 'Quit' commands. 327 | """ 328 | try: 329 | import IPython 330 | ip = IPython.get_ipython() 331 | expr = dialect.Evaluable().stringify(self.ctx.mgr.selected) 332 | ip.set_next_input(expr) 333 | except: 334 | raise CommandError("IPython is not available.") 335 | raise goto.Stop(exit=False) 336 | 337 | @command('?') 338 | def cmd_docstring(self): 339 | """Show docstring of the target.""" 340 | try: 341 | print(inspect.cleandoc(self.ctx.target.__doc__)) 342 | except Exception as ex: 343 | raise CommandError('Cannot read docstring.') 344 | 345 | @command('??') 346 | def cmd_source(self): 347 | """Show source code of the target.""" 348 | try: 349 | target = self.ctx.target 350 | if isinstance(target, FrameSummary): 351 | file_name = target.file_name 352 | first_line_no = target.line_no 353 | first_line_no = max(first_line_no, 1) 354 | variable_list = '\n'.join(f'{name}={value}' for name, value in target.gloloc.items()) 355 | cmd = ('vim', '-', '--not-a-term', '-c', ':set syntax=config', '-c', f'vsplit {file_name}', '-c', 'map q :qa', '-c', 'map :qa', '-c', ':set number', '-RM', f'+{first_line_no}', '-c', 'normal zt') 356 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 357 | p.stdin.write(variable_list.encode()) 358 | p.communicate() 359 | else: 360 | file_name = inspect.getsourcefile(target) 361 | _, first_line_no = inspect.getsourcelines(target) 362 | first_line_no = max(first_line_no, 1) 363 | shell_cmd = f'vim --not-a-term -c "map q :qa" -c "map :qa" -c ":set number" -RM +{first_line_no} -c "normal zt" {file_name}' 364 | os.system(shell_cmd) 365 | except Exception as ex: 366 | raise CommandError('Cannot find source code.') 367 | 368 | @command('Help', syntax='') 369 | def cmd_help(self, alias=None): 370 | """Show list of available commands or context help on given command. 371 | is optional and can be either full command name or just a nick 372 | (capital leters from the name). 373 | See also 'MANual' command. 374 | """ 375 | if alias is None: 376 | print_help('Available commands:') 377 | print_help() 378 | 379 | def get_summary(cmd): 380 | try: 381 | docstr = inspect.cleandoc(cmd.__doc__) 382 | except: 383 | return '' 384 | return (docstr + '\n').splitlines()[0] 385 | 386 | 387 | def get_header(cmd): 388 | return ' '.join( (p for p in (cmd.cmd_descr.name, cmd.cmd_descr.syntax) if p)) 389 | 390 | entires = [(get_header(cmd), get_summary(cmd)) for cmd in self] 391 | headers, summaries = zip(*entires) 392 | left_margin = ' ' 393 | col_sep = ' ' 394 | left_col_width = max(map(len, headers)) 395 | lines = (f"{left_margin}{header:<{left_col_width}}{col_sep}{summary}" for header, summary in entires) 396 | print_help('\n'.join(sorted(lines, key=lambda x: x.lower()))) 397 | print_help() 398 | print_help('Use either full command name or just capital leters from it (nick).') 399 | print_help('Whatever you type, it is case-insensitive.') 400 | print_help('E.g. you can invoke help by typing: h, H, help, Help, HeLp') 401 | print_help() 402 | print_help('Some of the commands have also keyboard bindings assigned.') 403 | print_help('Invoke "help CMD" for more help on specific command.') 404 | else: 405 | try: 406 | cmd_hdlr, _ = self[alias] 407 | except KeyError: 408 | cmds_custom = {cmd.cmd_descr.name.lower(): cmd for cmd in self if callable(cmd.cmd_descr.qualifier)} 409 | try: 410 | cmd_hdlr = cmds_custom[alias.lower()] 411 | except KeyError: 412 | raise CommandError(f'No such command: {alias}.') 413 | 414 | if cmd_hdlr.__doc__: 415 | parts = ('Syntax:', cmd_hdlr.cmd_descr.name, cmd_hdlr.cmd_descr.syntax) 416 | print_help(' '.join( (p for p in parts if p) )) 417 | print_help(inspect.cleandoc(cmd_hdlr.__doc__)) 418 | else: 419 | raise CommandError(f"No help on '{cmd_hdlr.cmd_descr.name}'.") 420 | 421 | @command('MANual') 422 | def cmd_man(self): 423 | """Show manual of peepshow. 424 | See also 'Help' command. 425 | """ 426 | man_path = Path(peepshow.__path__[0]) / 'peepshow.1' 427 | os.system(f'man -l {man_path}') 428 | 429 | @command('Info') 430 | def cmd_info(self): 431 | """Show basic information about the target object. 432 | See also 'Features' command. 433 | """ 434 | expr = f'{self.ctx.readable:^{shutil.get_terminal_size().columns}}' 435 | header = style(Back.LIGHTBLUE_EX + Fore.WHITE + Style.BRIGHT, expr) 436 | terminal.clear() 437 | print_target_features(self.ctx.target, MAJOR_FEATURES, hlight=True, header=header) 438 | self.ctx.only_info_visible = True 439 | 440 | @command('Features') 441 | def cmd_features(self): 442 | """Show detailed features of the target. 443 | Results are obtained by 'inspect' module. 444 | See also 'Info' command. 445 | """ 446 | print_target_features(self.ctx.target, MINOR_FEATURES, hlight=True) 447 | 448 | @command('CLear') 449 | def cmd_clear(self): 450 | """Clear the terminal. 451 | Alternatively (under Linux) press: CTRL+L 452 | """ 453 | terminal.clear() 454 | 455 | @command('Type') 456 | def cmd_type(self): 457 | """Type of the target becomes a new target.""" 458 | self.ctx.mgr.transform(trans.Pass(type)) 459 | return Commands.cmd_info, (self,) 460 | 461 | @command('Pass', syntax='') 462 | def cmd_pass(self, func_name): 463 | """Pass target as an argument to a function.""" 464 | self.ctx.mgr.transform(trans.PassExpr(func_name)) 465 | return Commands.cmd_info, (self,) 466 | 467 | @command('Self') 468 | def cmd_self(self): 469 | """Bound object becomes a new target.""" 470 | try: 471 | self.ctx.mgr.transform(trans.Attrib('__self__')) 472 | except: 473 | raise CommandError(f"This target doesn't have bound object.") 474 | return Commands.cmd_info, (self,) 475 | 476 | @command('Bases') 477 | def cmd_bases(self): 478 | """Tuple of base classes of the target becomes a new target. 479 | See also 'Mro' command. 480 | """ 481 | try: 482 | self.ctx.mgr.transform(trans.Attrib('__bases__')) 483 | except: 484 | raise CommandError(f"This target doesn't have bases.") 485 | return Commands.cmd_info, (self,) 486 | 487 | @command('Mro') 488 | def cmd_mro(self): 489 | """MRO list of the target becomes a new target. 490 | See also 'Bases' command. 491 | """ 492 | try: 493 | self.ctx.mgr.transform(trans.Attrib('__mro__')) 494 | except: 495 | raise CommandError(f'There is no MRO list corresponding to this target.') 496 | return Commands.cmd_info, (self,) 497 | 498 | @command('eXpression') 499 | def cmd_expression(self): 500 | """Print expression that evaluates to examined target. 501 | See also 'PrettyPrint' command. 502 | """ 503 | expr = dialect.Evaluable().stringify(self.ctx.mgr.selected) 504 | print(crayon_expr(expr)) 505 | 506 | @command('PrettyPrint') 507 | def cmd_pretty_print(self): 508 | """Print target recursively. 509 | See also 'eXpression' command. 510 | """ 511 | print(pformat(self.ctx.target)) 512 | -------------------------------------------------------------------------------- /docs/demo.cast: -------------------------------------------------------------------------------- 1 | {"version":2,"width":88,"height":22,"timestamp":1712850505,"env":{"SHELL":"/usr/bin/elvish","TERM":"screen-256color"}} 2 | [0.044672, "o", "Deprecation: \u001b[31;1mthe \"eawk\" command is deprecated; use \"re:awk\" instead\u001b[m\r\n /home/gkrason/.local/share/elvish/lib/github.com/zzamboni/elvish-completions/git.elv:106:35-38: -run-git help -a --no-verbose | \u001b[1;4meawk\u001b[m {|line @f| if (re:match '^ [a-z]' $line) { put $@f } } | each {|c|\r\n"] 3 | [0.048626, "o", "Deprecation: \u001b[31;1mthe \"eawk\" command is deprecated; use \"re:awk\" instead\u001b[m\r\n /home/gkrason/.local/share/elvish/lib/github.com/muesli/elvish-libs/git.elv:56:35-38: var is-ok = ?($git-status-cmd | \u001b[1;4meawk\u001b[m {|line @f|\r\n"] 4 | [0.092433, "o", "\u001b[H\u001b[J"] 5 | [0.110858, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 6 | [0.111015, "o", "\u001b[?25l\r???> ???> \r\u001b[5C\u001b[?25h"] 7 | [0.111195, "o", "\u001b[?25l\r\u001b[83C\u001b[K\u001b[0;7m.venv\u001b[0;m\r\u001b[5C\u001b[?25h"] 8 | [0.112585, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 9 | [0.113809, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 10 | [0.118839, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 11 | [0.120615, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 12 | [0.120755, "o", "\u001b[?25l\r\r\u001b[5C\u001b[?25h"] 13 | [0.123181, "o", "\u001b[?25l\r\u001b[K\u001b[0;36m[~/git/peepshow]\u001b[0;m─\u001b[0;34m[⎇ master]\u001b[0;m─[\u001b[0;31m+\u001b[0;33m●\u001b[0;m]─\u001b[0;32m> \u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[35C\u001b[?25h"] 14 | [0.520801, "o", "\u001b[?25l\r\u001b[35C\u001b[K\u001b[0;32mb\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[36C\u001b[?25h"] 15 | [0.521378, "o", "\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 16 | [0.534674, "o", "\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 17 | [0.534776, "o", "\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 18 | [0.539028, "o", "\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 19 | [0.583581, "o", "\u001b[?25l\r\u001b[35C\u001b[K\u001b[0;31mba\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[37C\u001b[?25h"] 20 | [0.745, "o", "\u001b[?25l\r\u001b[35C\u001b[K\u001b[0;32mbat\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[38C\u001b[?25h\u001b[?25l\r\r\u001b[38C\u001b[?25h"] 21 | [0.881215, "o", "\u001b[?25l\r\r\u001b[39C\u001b[?25h"] 22 | [0.881256, "o", "\u001b[?25l\r\r\u001b[39C\u001b[?25h"] 23 | [1.167688, "o", "\u001b[?25l\r\u001b[39C\u001b[Kd \u001b[0;7m.venv\u001b[0;m\r\u001b[40C\u001b[?25h"] 24 | [1.167695, "o", "\u001b[?25l\r\r\u001b[40C\u001b[?25h"] 25 | [1.32899, "o", "\u001b[?25l\r\u001b[40C\u001b[Ke \u001b[0;7m.venv\u001b[0;m\r\u001b[41C\u001b[?25h\u001b[?25l\r\r\u001b[41C\u001b[?25h"] 26 | [1.447042, "o", "\u001b[?25l\r\u001b[41C\u001b[Km \u001b[0;7m.venv\u001b[0;m\r\u001b[42C\u001b[?25h"] 27 | [1.447079, "o", "\u001b[?25l\r\r\u001b[42C\u001b[?25h"] 28 | [1.487037, "o", "\u001b[?25l\r\u001b[42C\u001b[Ko \u001b[0;7m.venv\u001b[0;m\r\u001b[43C\u001b[?25h"] 29 | [1.487068, "o", "\u001b[?25l\r\r\u001b[43C\u001b[?25h"] 30 | [1.61397, "o", "\u001b[?25l\r\u001b[43C\u001b[K. \u001b[0;7m.venv\u001b[0;m\r\u001b[44C\u001b[?25h"] 31 | [2.008453, "o", "\u001b[?25l\r\u001b[44C\u001b[Kp \u001b[0;7m.venv\u001b[0;m\r\u001b[45C\u001b[?25h\u001b[?25l\r\r\u001b[45C\u001b[?25h"] 32 | [2.107839, "o", "\u001b[?25l\r\u001b[45C\u001b[Ky \u001b[0;7m.venv\u001b[0;m\r\u001b[47C\u001b[?25h"] 33 | [2.360495, "o", "\u001b[?25l\r\u001b[47C\u001b[K\r\n\r\u001b[?25h"] 34 | [2.360636, "o", "\u001b[?7h\u001b[?2004l\r"] 35 | [2.39699, "o", "\r\u001b[35mfrom\u001b[0m\u001b[37m \u001b[0m\u001b[37mpeepshow\u001b[0m\u001b[37m \u001b[0m\u001b[35mimport\u001b[0m\u001b[37m \u001b[0m\u001b[37mpeep\u001b[0m\u001b[m\r\n\u001b[m\r\n\u001b[37mx\u001b[0m\u001b[37m \u001b[0m\u001b[37m=\u001b[0m\u001b[37m \u001b[0m\u001b[38;5;9m123\u001b[0m\u001b[m\r\n\u001b[37my\u001b[0m\u001b[37m \u001b[0m\u001b[37m=\u001b[0m\u001b[37m \u001b[0m\u001b[37m{\u001b[0m\u001b[37m'\u001b[0m\u001b[32mname\u001b[0m\u001b[37m'\u001b[0m\u001b[37m:\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32mJohn\u001b[0m\u001b[37m'\u001b[0m\u001b[37m,\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32mage\u001b[0m\u001b[37m'\u001b[0m\u001b[37m:\u001b[0m\u001b[37m \u001b[0m\u001b[38;5;9m33\u001b[0m\u001b[37m,\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32mskills\u001b[0m\u001b[37m'\u001b[0m\u001b[37m:\u001b[0m\u001b[37m \u001b[0m\u001b[37m[\u001b[0m\u001b[37m'\u001b[0m\u001b[32mpython\u001b[0m\u001b[37m'\u001b[0m\u001b[37m,\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32mdjango\u001b[0m\u001b[37m'\u001b[0m\u001b[37m,\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32mrest\u001b[0m\u001b[37m'\u001b[0m\u001b[37m,\u001b[0m\u001b[37m \u001b[0m\u001b[37m'\u001b[0m\u001b[32msql\u001b[0m\u001b[37m'\u001b[0m\u001b[37m]\u001b[0m\u001b[37m}\u001b[0m\u001b[m\r\n\u001b[37mz\u001b[0m\u001b[37m \u001b[0m\u001b[37m=\u001b[0m\u001b[37m \u001b[0m\u001b[37m\"\u001b[0m\u001b[32mHello World!\u001b[0m\u001b[37m\"\u001b[0m\u001b[m\r\n\u001b[m\r\n\u001b[37mpeep\u001b[0m\u001b[37m(\u001b[0m\u001b[37m)\u001b[0m\u001b[m\r\n\r\u001b[K"] 36 | [2.39821, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 37 | [2.398665, "o", "\u001b[?25l\r\u001b[0;36m[~/git/peepshow]\u001b[0;m─\u001b[0;34m[⎇ master]\u001b[0;m─[\u001b[0;31m+\u001b[0;33m●\u001b[0;m]─\u001b[0;32m> \u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[35C\u001b[?25h\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 38 | [2.398925, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 39 | [2.400435, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 40 | [2.40213, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 41 | [2.402391, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 42 | [2.402756, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 43 | [2.403505, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 44 | [2.403528, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 45 | [2.409541, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 46 | [4.737935, "o", "\u001b[?25l\r\u001b[35C\u001b[K\u001b[0;31mp\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[36C\u001b[?25h\u001b[?25l\r\r\u001b[36C\u001b[?25h"] 47 | [5.033192, "o", "\u001b[?25l\r\u001b[36C\u001b[K\u001b[0;31my\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[37C\u001b[?25h"] 48 | [5.033278, "o", "\u001b[?25l\r\r\u001b[37C\u001b[?25h"] 49 | [5.105178, "o", "\u001b[?25l\r\u001b[37C\u001b[K\u001b[0;31mt\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[38C\u001b[?25h"] 50 | [5.105218, "o", "\u001b[?25l\r\r\u001b[38C\u001b[?25h"] 51 | [5.217827, "o", "\u001b[?25l\r\u001b[38C\u001b[K\u001b[0;31mh\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[39C\u001b[?25h"] 52 | [5.217918, "o", "\u001b[?25l\r\r\u001b[39C\u001b[?25h"] 53 | [5.309059, "o", "\u001b[?25l\r\u001b[35C\u001b[K\u001b[0;32mpython\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[41C\u001b[?25h"] 54 | [5.945565, "o", "\u001b[?25l\r\u001b[41C\u001b[K\u001b[0;32m3\u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[42C\u001b[?25h\u001b[?25l\r\r\u001b[42C\u001b[?25h"] 55 | [6.176637, "o", "\u001b[?25l\r\r\u001b[43C\u001b[?25h\u001b[?25l\r\r\u001b[43C\u001b[?25h"] 56 | [6.519643, "o", "\u001b[?25l\r\u001b[43C\u001b[Kd \u001b[0;7m.venv\u001b[0;m\r\u001b[44C\u001b[?25h\u001b[?25l\r\r\u001b[44C\u001b[?25h"] 57 | [6.655714, "o", "\u001b[?25l\r\u001b[44C\u001b[Ke \u001b[0;7m.venv\u001b[0;m\r\u001b[45C\u001b[?25h\u001b[?25l\r\r\u001b[45C\u001b[?25h"] 58 | [6.753631, "o", "\u001b[?25l\r\u001b[45C\u001b[Kmo. \u001b[0;7m.venv\u001b[0;m\r\u001b[48C\u001b[?25h"] 59 | [7.400969, "o", "\u001b[?25l\r\u001b[48C\u001b[Kp \u001b[0;7m.venv\u001b[0;m\r\u001b[49C\u001b[?25h\u001b[?25l\r\r\u001b[49C\u001b[?25h"] 60 | [7.47351, "o", "\u001b[?25l\r\u001b[49C\u001b[Ky \u001b[0;7m.venv\u001b[0;m\r\u001b[51C\u001b[?25h"] 61 | [8.463304, "o", "\u001b[?25l\r\u001b[51C\u001b[K\r\n\r\u001b[?25h"] 62 | [8.463343, "o", "\u001b[?7h\u001b[?2004l\r"] 63 | [8.561464, "o", "\u001b[H\u001b[J"] 64 | [8.564322, "o", "\u001b[H\u001b[J"] 65 | [8.564664, "o", "\u001b[104m\u001b[37m\u001b[1m <*> \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n"] 66 | [8.564776, "o", "\u001b[1mREPR : \"{'__name__': '__main__', '__doc__': None, '__package__': None, '__loa\u001b[0m\u001b[2m...\u001b[0mSTR : REPR\r\nNAME : N/A\r\nQUALNAME : N/A\r\nSIGNATURE : N/A\r\nCALLABLE : False\r\n\u001b[1mITERABLE : True\u001b[0m\r\n\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n\u001b[1mLEN : 13\u001b[0m\r\n\u001b[1mBOOL : True\u001b[0m\r\nINT : N/A\r\nID : 0x00007f186c729680\r\nHASH : N/A\r\nBINDING : N/A\r\n"] 67 | [8.564779, "o", "BASES : N/A\r\n"] 68 | [8.564789, "o", "MRO : N/A\r\n"] 69 | [8.564846, "o", "\u001b[91mdict\u001b[0m \u001b[36m**\u001b[0m > "] 70 | [12.952605, "o", "\r\n"] 71 | [12.955119, "o", "[ 0] \u001b[1m'__name__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'__main__'\u001b[0m\r\n"] 72 | [12.955214, "o", "[ 1] \u001b[1m'__doc__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n[ 2] \u001b[1m'__package__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 73 | [12.955481, "o", "[ 3] \u001b[1m'__loader__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m'<_frozen_importlib_external.SourceFileLoad\u001b[0m\u001b[2m...\u001b[0m[ 4] \u001b[1m'__spec__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 74 | [12.955534, "o", "[ 5] \u001b[1m'__annotations__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{}\u001b[0m\r\n"] 75 | [12.955593, "o", "[ 6] \u001b[1m'__builtins__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m\"\"\u001b[0m\r\n"] 76 | [12.955647, "o", "[ 7] \u001b[1m'__file__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'demo.py'\u001b[0m\r\n"] 77 | [12.955694, "o", "[ 8] \u001b[1m'__cached__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 78 | [12.955906, "o", "[ 9] \u001b[1m'peep'\u001b[0m : \u001b[92mpeep(*args)\u001b[0m\r\n"] 79 | [12.955959, "o", "[ 10] \u001b[1m'x'\u001b[0m : \u001b[94m123\u001b[0m\r\n"] 80 | [12.956165, "o", "[ 11] \u001b[1m'y'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{'name': 'John', 'age': 33, 'skills': ['python', 'django', 'rest', \u001b[0m\u001b[2m...\u001b[0m[ 12] \u001b[1m'z'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'Hello World!'\u001b[0m\r\n"] 81 | [12.956266, "o", "\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 82 | [16.51093, "o", "1"] 83 | [16.72036, "o", "1"] 84 | [17.920619, "o", "\r\n"] 85 | [17.927495, "o", "\u001b[H\u001b[J"] 86 | [17.928277, "o", "\u001b[104m\u001b[37m\u001b[1m y \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n"] 87 | [17.928315, "o", "\u001b[1mREPR : \"{'name': 'John', 'age': 33, 'skills': ['python', 'django', 'rest', 's\u001b[0m\u001b[2m...\u001b[0mSTR : REPR\r\n"] 88 | [17.928346, "o", "NAME : N/A\r\nQUALNAME : N/A\r\n"] 89 | [17.928363, "o", "SIGNATURE : N/A\r\n"] 90 | [17.928383, "o", "CALLABLE : False\r\n"] 91 | [17.928423, "o", "\u001b[1mITERABLE : True\u001b[0m\r\n"] 92 | [17.928464, "o", "\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n\u001b[1mLEN : 3\u001b[0m\r\n"] 93 | [17.928486, "o", "\u001b[1mBOOL : True\u001b[0m\r\nINT : N/A\r\n"] 94 | [17.928506, "o", "ID : 0x00007f186cc21540\r\n"] 95 | [17.92853, "o", "HASH : N/A\r\n"] 96 | [17.928535, "o", "BINDING : N/A\r\n"] 97 | [17.928557, "o", "BASES : N/A\r\n"] 98 | [17.928577, "o", "MRO : N/A\r\n"] 99 | [17.928911, "o", "\u001b[91mdict\u001b[0m \u001b[36m**\u001b[0m > "] 100 | [18.878684, "o", "\r\n"] 101 | [18.879201, "o", "[ 0] \u001b[1m'name'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'John'\u001b[0m\r\n[ 1] \u001b[1m'age'\u001b[0m : \u001b[94m33\u001b[0m\r\n"] 102 | [18.879236, "o", "[ 2] \u001b[1m'skills'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m['python', 'django', 'rest', 'sql']\u001b[0m\r\n"] 103 | [18.879324, "o", "\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 104 | [22.23277, "o", "2"] 105 | [24.150676, "o", "\r\n"] 106 | [24.153391, "o", "\u001b[H\u001b[J"] 107 | [24.153823, "o", "\u001b[104m\u001b[37m\u001b[1m y['skills'] \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n"] 108 | [24.153827, "o", "\u001b[1mREPR : \"['python', 'django', 'rest', 'sql']\"\u001b[0m\r\n"] 109 | [24.153839, "o", "STR : REPR\r\n"] 110 | [24.153841, "o", "NAME : N/A\r\n"] 111 | [24.153851, "o", "QUALNAME : N/A\r\n"] 112 | [24.153853, "o", "SIGNATURE : N/A\r\n"] 113 | [24.153863, "o", "CALLABLE : False\r\n"] 114 | [24.153871, "o", "\u001b[1mITERABLE : True\u001b[0m\r\n"] 115 | [24.153879, "o", "\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n"] 116 | [24.153897, "o", "\u001b[1mLEN : 4\u001b[0m\r\n"] 117 | [24.153899, "o", "\u001b[1mBOOL : True\u001b[0m\r\n"] 118 | [24.153908, "o", "INT : N/A\r\n"] 119 | [24.153914, "o", "ID : 0x00007f186c85a280\r\n"] 120 | [24.153922, "o", "HASH : N/A\r\n"] 121 | [24.153925, "o", "BINDING : N/A\r\n"] 122 | [24.153932, "o", "BASES : N/A\r\n"] 123 | [24.15394, "o", "MRO : N/A\r\n"] 124 | [24.154021, "o", "\u001b[91mlist\u001b[0m \u001b[36m*\u001b[0m > "] 125 | [24.703248, "o", "\r\n"] 126 | [24.703512, "o", "[ 0] \u001b[92m[]*\u001b[0m \u001b[94m'python'\u001b[0m\r\n"] 127 | [24.703533, "o", "[ 1] \u001b[92m[]*\u001b[0m \u001b[94m'django'\u001b[0m\r\n"] 128 | [24.703559, "o", "[ 2] \u001b[92m[]*\u001b[0m \u001b[94m'rest'\u001b[0m\r\n"] 129 | [24.703575, "o", "[ 3] \u001b[92m[]*\u001b[0m \u001b[94m'sql'\u001b[0m\r\n"] 130 | [24.703637, "o", "\u001b[91mlist\u001b[0m \u001b[36mi\u001b[0m > "] 131 | [29.903821, "o", "2"] 132 | [31.359905, "o", "\r\n"] 133 | [31.365678, "o", "\u001b[H\u001b[J"] 134 | [31.366316, "o", "\u001b[104m\u001b[37m\u001b[1m y['skills']<2> \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n\u001b[1mREPR : \"'rest'\"\u001b[0m\r\n\u001b[1mSTR : 'rest'\u001b[0m\r\nNAME : N/A\r\nQUALNAME : N/A\r\nSIGNATURE : N/A\r\nCALLABLE : False\r\n\u001b[1mITERABLE : True\u001b[0m\r\n\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n\u001b[1mLEN : 4\u001b[0m\r\n\u001b[1mBOOL : True\u001b[0m\r\n"] 135 | [31.366345, "o", "INT : N/A\r\nID : 0x00007f186cca5ef0\r\n"] 136 | [31.36636, "o", "HASH : 0x4a97cc87ec6188fe\r\n"] 137 | [31.366363, "o", "BINDING : N/A\r\n"] 138 | [31.366377, "o", "BASES : N/A\r\n"] 139 | [31.366395, "o", "MRO : N/A\r\n"] 140 | [31.366571, "o", "\u001b[91mstr\u001b[0m \u001b[36mpp\u001b[0m > "] 141 | [32.041221, "o", "\r\n"] 142 | [32.041684, "o", "'rest'\r\n"] 143 | [32.0417, "o", "\u001b[91mstr\u001b[0m \u001b[36mi\u001b[0m > "] 144 | [35.670423, "o", "*"] 145 | [36.600626, "o", "\r\n"] 146 | [36.601272, "o", "[ 0] \u001b[92m[]*\u001b[0m \u001b[94m'r'\u001b[0m\r\n[ 1] \u001b[92m[]*\u001b[0m \u001b[94m'e'\u001b[0m\r\n[ 2] \u001b[92m[]*\u001b[0m \u001b[94m's'\u001b[0m\r\n[ 3] \u001b[92m[]*\u001b[0m \u001b[94m't'\u001b[0m\r\n"] 147 | [36.601413, "o", "\u001b[91mstr\u001b[0m \u001b[36mi\u001b[0m > "] 148 | [39.368982, "o", "2"] 149 | [40.120779, "o", "\r\n"] 150 | [40.12714, "o", "\u001b[H\u001b[J"] 151 | [40.12797, "o", "\u001b[104m\u001b[37m\u001b[1m y['skills']<2><2> \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n\u001b[1mREPR : \"'s'\"\u001b[0m\r\n\u001b[1mSTR : 's'\u001b[0m\r\nNAME : N/A\r\n"] 152 | [40.127996, "o", "QUALNAME : N/A\r\nSIGNATURE : N/A\r\nCALLABLE : False\r\n"] 153 | [40.128029, "o", "\u001b[1mITERABLE : True\u001b[0m\r\n"] 154 | [40.128033, "o", "\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n"] 155 | [40.128057, "o", "\u001b[1mLEN : 1\u001b[0m\r\n"] 156 | [40.1281, "o", "\u001b[1mBOOL : True\u001b[0m\r\nINT : N/A\r\n"] 157 | [40.128124, "o", "ID : 0x00007f186cca0830\r\n"] 158 | [40.128128, "o", "HASH : 0x87c7e26f195cf798\r\n"] 159 | [40.12815, "o", "BINDING : N/A\r\n"] 160 | [40.128167, "o", "BASES : N/A\r\n"] 161 | [40.128182, "o", "MRO : N/A\r\n"] 162 | [40.128332, "o", "\u001b[91mstr\u001b[0m \u001b[36mpp\u001b[0m > "] 163 | [41.152138, "o", "\r\n"] 164 | [41.152377, "o", "'s'\r\n"] 165 | [41.152431, "o", "\u001b[91mstr\u001b[0m \u001b[36mi\u001b[0m > "] 166 | [43.50455, "o", "."] 167 | [44.744237, "o", "\r\n"] 168 | [44.744609, "o", "<*>\r\ny\r\ny['skills']\r\ny['skills']<2>\r\n"] 169 | [44.744656, "o", "\u001b[1my['skills']<2><2>\u001b[0m\r\n"] 170 | [44.744775, "o", "\u001b[91mstr\u001b[0m \u001b[36mi\u001b[0m > "] 171 | [49.18486, "o", "/"] 172 | [49.736699, "o", "\r\n"] 173 | [49.742318, "o", "\u001b[H\u001b[J"] 174 | [49.74371, "o", "\u001b[104m\u001b[37m\u001b[1m <*> \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n"] 175 | [49.744197, "o", "\u001b[1mREPR : \"{'__name__': '__main__', '__doc__': None, '__package__': None, '__loa\u001b[0m\u001b[2m...\u001b[0mSTR : REPR\r\n"] 176 | [49.744208, "o", "NAME : N/A\r\n"] 177 | [49.744245, "o", "QUALNAME : N/A\r\nSIGNATURE : N/A\r\n"] 178 | [49.744273, "o", "CALLABLE : False\r\n"] 179 | [49.744367, "o", "\u001b[1mITERABLE : True\u001b[0m\r\n\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n\u001b[1mLEN : 13\u001b[0m\r\n"] 180 | [49.744389, "o", "\u001b[1mBOOL : True\u001b[0m\r\n"] 181 | [49.744443, "o", "INT : N/A\r\nID : 0x00007f186c729680\r\n"] 182 | [49.744498, "o", "HASH : N/A\r\nBINDING : N/A\r\n"] 183 | [49.744558, "o", "BASES : N/A\r\nMRO : N/A\r\n"] 184 | [49.744794, "o", "\u001b[91mdict\u001b[0m \u001b[36m**\u001b[0m > "] 185 | [50.536646, "o", "\r\n"] 186 | [50.537193, "o", "[ 0] \u001b[1m'__name__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'__main__'\u001b[0m\r\n"] 187 | [50.537232, "o", "[ 1] \u001b[1m'__doc__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n[ 2] \u001b[1m'__package__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 188 | [50.537806, "o", "[ 3] \u001b[1m'__loader__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m'<_frozen_importlib_external.SourceFileLoad\u001b[0m\u001b[2m...\u001b[0m[ 4] \u001b[1m'__spec__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n[ 5] \u001b[1m'__annotations__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{}\u001b[0m\r\n[ 6] \u001b[1m'__builtins__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m\"\"\u001b[0m\r\n"] 189 | [50.537821, "o", "[ 7] \u001b[1m'__file__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'demo.py'\u001b[0m\r\n"] 190 | [50.537855, "o", "[ 8] \u001b[1m'__cached__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 191 | [50.537991, "o", "[ 9] \u001b[1m'peep'\u001b[0m : \u001b[92mpeep(*args)\u001b[0m\r\n"] 192 | [50.538055, "o", "[ 10] \u001b[1m'x'\u001b[0m : \u001b[94m123\u001b[0m\r\n"] 193 | [50.538378, "o", "[ 11] \u001b[1m'y'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{'name': 'John', 'age': 33, 'skills': ['python', 'django', 'rest', \u001b[0m\u001b[2m...\u001b[0m[ 12] \u001b[1m'z'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'Hello World!'\u001b[0m\r\n\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 194 | [53.576294, "o", "!"] 195 | [54.184868, "o", "y"] 196 | [54.440566, "o", "["] 197 | [55.128617, "o", "'"] 198 | [55.247582, "o", "a"] 199 | [55.544149, "o", "g"] 200 | [55.624919, "o", "e"] 201 | [57.016391, "o", "'"] 202 | [57.527895, "o", "]"] 203 | [58.151959, "o", " "] 204 | [58.320618, "o", "="] 205 | [58.376659, "o", " "] 206 | [59.83118, "o", "4"] 207 | [60.048331, "o", "3"] 208 | [61.280048, "o", "\r\n"] 209 | [61.280911, "o", "\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 210 | [65.200623, "o", "\r\n"] 211 | [65.206629, "o", "\u001b[H\u001b[J"] 212 | [65.207468, "o", "\u001b[104m\u001b[37m\u001b[1m <*> \u001b[0m\u001b[1mTYPE : \u001b[0m\r\n"] 213 | [65.207743, "o", "\u001b[1mREPR : \"{'__name__': '__main__', '__doc__': None, '__package__': None, '__loa\u001b[0m\u001b[2m...\u001b[0mSTR : REPR\r\nNAME : N/A\r\nQUALNAME : N/A\r\n"] 214 | [65.207774, "o", "SIGNATURE : N/A\r\n"] 215 | [65.207829, "o", "CALLABLE : False\r\n"] 216 | [65.20798, "o", "\u001b[1mITERABLE : True\u001b[0m\r\n\u001b[1mSUBSCRIBABLE : True\u001b[0m\r\n\u001b[1mLEN : 13\u001b[0m\r\n"] 217 | [65.20805, "o", "\u001b[1mBOOL : True\u001b[0m\r\nINT : N/A\r\nID : 0x00007f186c729680\r\nHASH : N/A\r\n"] 218 | [65.208062, "o", "BINDING : N/A\r\n"] 219 | [65.208232, "o", "BASES : N/A\r\nMRO : N/A\r\n"] 220 | [65.208314, "o", "\u001b[91mdict\u001b[0m \u001b[36m**\u001b[0m > "] 221 | [66.088748, "o", "\r\n"] 222 | [66.0894, "o", "[ 0] \u001b[1m'__name__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'__main__'\u001b[0m\r\n[ 1] \u001b[1m'__doc__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 223 | [66.089496, "o", "[ 2] \u001b[1m'__package__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 224 | [66.089792, "o", "[ 3] \u001b[1m'__loader__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m'<_frozen_importlib_external.SourceFileLoad\u001b[0m\u001b[2m...\u001b[0m[ 4] \u001b[1m'__spec__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 225 | [66.089869, "o", "[ 5] \u001b[1m'__annotations__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{}\u001b[0m\r\n"] 226 | [66.089979, "o", "[ 6] \u001b[1m'__builtins__'\u001b[0m : \u001b[91m\u001b[0m \u001b[94m\"\"\u001b[0m\r\n"] 227 | [66.090374, "o", "[ 7] \u001b[1m'__file__'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'demo.py'\u001b[0m\r\n"] 228 | [66.090461, "o", "[ 8] \u001b[1m'__cached__'\u001b[0m : \u001b[94mNone\u001b[0m\r\n"] 229 | [66.09062, "o", "[ 9] \u001b[1m'peep'\u001b[0m : \u001b[92mpeep(*args)\u001b[0m\r\n"] 230 | [66.090675, "o", "[ 10] \u001b[1m'x'\u001b[0m : \u001b[94m123\u001b[0m\r\n"] 231 | [66.090909, "o", "[ 11] \u001b[1m'y'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m{'name': 'John', 'age': 43, 'skills': ['python', 'django', 'rest', \u001b[0m\u001b[2m...\u001b[0m[ 12] \u001b[1m'z'\u001b[0m : \u001b[92m[]*\u001b[0m \u001b[94m'Hello World!'\u001b[0m\r\n"] 232 | [66.091043, "o", "\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 233 | [70.479417, "o", "h"] 234 | [70.808553, "o", "\r\n"] 235 | [70.808839, "o", "\u001b[93mAvailable commands:\u001b[0m\r\n\u001b[93m\u001b[0m\r\n"] 236 | [70.809607, "o", "\u001b[93m ! Evaluate and print the result.\r\n $ Evaluate and consider result as a new target.\r\n () Call target with .\r\n ** Call .items() and iterate through results.\r\n * Iterate target.\r\n - Make one step forward in history.\r\n . Display history.\r\n .. Make one step backward in history.\r\n . Return attribute of the target.\r\n / Go to the begining in history.\r\n Select -th item from cache.\r\n ? Show docstring of the target.\r\n ?? Show source code of the target.\r\n [] Subscribe target with .\r\n Bases Tuple of base classes of the target becomes a new target.\r\n CLear Clear the terminal.\r\n Continue Exit peepshow and continue execution of underlying application.\r\n Dir Call dir() and show results other than of __XYZ__ format.\r\n DirAll Call dir() and show all the results.\r\n DirPublic Call dir() and show results other than starting with '_'.\r\n Export Export target expression to IPython.\r\n eXpression Print expression that evaluates to examined target.\r\n Features Show detailed features of the target.\r\n Help Show list of available commands or context help on given command.\r\n Info Show basic information about the target object.\r\n MANual Show manual of peepshow.\r\n Mro MRO list of the target becomes a new target.\r\n Pass Pass target as an argument to a function.\r\n PrettyPrint Print target recursively.\r\n Quit Exit peepshow and terminate underlying Python interpreter.\r\n Self Bound object becomes a new target.\r\n ShowCache Show content of cache which can be accessed by command.\r\n Type Type of the target becomes a new target.\r\n Vars Call vars() and show all the results.\r\n VarsPublic Call vars() and show results other than starting with '_'.\u001b[0m\r\n\u001b[93m\u001b[0m\r\n\u001b[93mUse either full command name or just capital leters from it (nick).\u001b[0m\r\n\u001b[93mWhatever you type, it is case-insensitive.\u001b[0m\r\n\u001b[93mE.g. you can invoke help by typing: h, H, help, Help, HeLp\u001b[0m\r\n\u001b[93m\u001b[0m\r\n\u001b[93mSome of the commands have also keyboard bindings assigned.\u001b[0m\r\n\u001b[93mInvoke \"help CMD\" for more help on specific command.\u001b[0m\r\n\u001b[91mdict\u001b[0m \u001b[36mi\u001b[0m > "] 237 | [74.624619, "o", "^D"] 238 | [74.630667, "o", "\u001b[H\u001b[J"] 239 | [74.63125, "o", "\u001b[0m"] 240 | [74.643446, "o", "\u001b[?7h\u001b[7m⏎\u001b[m \r \r\u001b[?7l\u001b[?2004h"] 241 | [74.643623, "o", "\u001b[?25l\r\u001b[0;36m[~/git/peepshow]\u001b[0;m─\u001b[0;34m[⎇ master]\u001b[0;m─[\u001b[0;31m+\u001b[0;33m●\u001b[0;m]─\u001b[0;32m> \u001b[0;m \u001b[0;7m.venv\u001b[0;m\r\u001b[35C\u001b[?25h"] 242 | [74.643635, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 243 | [74.643894, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 244 | [74.64899, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 245 | [74.649003, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 246 | [74.652708, "o", "\u001b[?25l\r\r\u001b[35C\u001b[?25h"] 247 | [75.600785, "o", "\u001b[?25l\r\u001b[35C\u001b[K\r\n\r\u001b[?25h\u001b[?7h\u001b[?2004l\r"] 248 | --------------------------------------------------------------------------------