├── test_scripts ├── __init__.py ├── traced.py └── gold.py ├── tests ├── future_tests │ ├── __init__.py │ ├── without_future.py │ └── with_future.py ├── __init__.py ├── test_db.py ├── test_tracer.py ├── utils.py ├── test_import_hook.py ├── test_utils.py ├── test_interface.py └── test_birdseye.py ├── MANIFEST.in ├── gulp ├── .gitignore ├── install-deps.sh ├── .eslintrc.json └── gulpfile.js ├── docs ├── _static │ ├── custom.css │ └── img │ │ └── call_to_foo.png ├── index.rst ├── how_it_works.rst ├── limitations.rst ├── quickstart.rst ├── tips.rst ├── configuration.rst ├── conf.py ├── integrations.rst └── contributing.rst ├── birdseye ├── trace_module.py ├── trace_module_deep.py ├── __main__.py ├── static │ ├── favicon.ico │ ├── img │ │ ├── logo.png │ │ ├── handle.png │ │ └── type_icons │ │ │ ├── dict.png │ │ │ ├── int.png │ │ │ ├── list.png │ │ │ ├── set.png │ │ │ ├── str.png │ │ │ ├── float.png │ │ │ ├── object.png │ │ │ ├── tuple.png │ │ │ ├── NoneType.png │ │ │ └── complex.png │ ├── css │ │ ├── proton │ │ │ ├── 30px.png │ │ │ ├── 32px.png │ │ │ ├── throbber.gif │ │ │ └── fonts │ │ │ │ └── titillium │ │ │ │ ├── titilliumweb-bold-webfont.eot │ │ │ │ ├── titilliumweb-bold-webfont.ttf │ │ │ │ ├── titilliumweb-bold-webfont.woff │ │ │ │ ├── titilliumweb-regular-webfont.eot │ │ │ │ ├── titilliumweb-regular-webfont.ttf │ │ │ │ ├── titilliumweb-regular-webfont.woff │ │ │ │ ├── titilliumweb-extralight-webfont.eot │ │ │ │ ├── titilliumweb-extralight-webfont.ttf │ │ │ │ └── titilliumweb-extralight-webfont.woff │ │ ├── hljs.min.css │ │ ├── main.css │ │ └── jquery-ui.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── libs │ │ └── highlight.pack.js │ │ └── call.js ├── clear_db.py ├── templates │ ├── call_base.html │ ├── ipython_call.html │ ├── info_panel.html │ ├── base.html │ ├── call_core.html │ ├── file.html │ ├── index.html │ ├── function.html │ ├── call.html │ └── ipython_iframe.html ├── __init__.py ├── import_hook.py ├── ipython.py ├── utils.py ├── server.py └── db.py ├── setup.py ├── misc ├── type_icons │ ├── dict.png │ ├── int.png │ ├── list.png │ ├── set.png │ ├── str.png │ ├── float.png │ ├── object.png │ ├── tuple.png │ ├── NoneType.png │ └── complex.png ├── test.sh ├── mypy_filter.py └── mypy_ignore.txt ├── tox.ini ├── .gitignore ├── pyproject.toml ├── make_release.sh ├── .github └── workflows │ └── workflow.yml ├── LICENSE.txt ├── setup.cfg └── README.rst /test_scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/future_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | -------------------------------------------------------------------------------- /gulp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | table.field-list { 2 | line-height: 1.5; 3 | } -------------------------------------------------------------------------------- /tests/future_tests/without_future.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return 3 / 2 3 | -------------------------------------------------------------------------------- /birdseye/trace_module.py: -------------------------------------------------------------------------------- 1 | from birdseye import eye 2 | 3 | eye.trace_this_module(1) 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | -------------------------------------------------------------------------------- /birdseye/trace_module_deep.py: -------------------------------------------------------------------------------- 1 | from birdseye import eye 2 | 3 | eye.trace_this_module(1, deep=True) 4 | -------------------------------------------------------------------------------- /misc/type_icons/dict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/dict.png -------------------------------------------------------------------------------- /misc/type_icons/int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/int.png -------------------------------------------------------------------------------- /misc/type_icons/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/list.png -------------------------------------------------------------------------------- /misc/type_icons/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/set.png -------------------------------------------------------------------------------- /misc/type_icons/str.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/str.png -------------------------------------------------------------------------------- /birdseye/__main__.py: -------------------------------------------------------------------------------- 1 | from birdseye.server import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /misc/type_icons/float.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/float.png -------------------------------------------------------------------------------- /misc/type_icons/object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/object.png -------------------------------------------------------------------------------- /misc/type_icons/tuple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/tuple.png -------------------------------------------------------------------------------- /birdseye/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/favicon.ico -------------------------------------------------------------------------------- /birdseye/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/logo.png -------------------------------------------------------------------------------- /misc/type_icons/NoneType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/NoneType.png -------------------------------------------------------------------------------- /misc/type_icons/complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/misc/type_icons/complex.png -------------------------------------------------------------------------------- /birdseye/static/img/handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/handle.png -------------------------------------------------------------------------------- /tests/future_tests/with_future.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | 4 | def foo(): 5 | return 3 / 2 6 | -------------------------------------------------------------------------------- /docs/_static/img/call_to_foo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/docs/_static/img/call_to_foo.png -------------------------------------------------------------------------------- /birdseye/static/css/proton/30px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/30px.png -------------------------------------------------------------------------------- /birdseye/static/css/proton/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/32px.png -------------------------------------------------------------------------------- /birdseye/static/css/proton/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/throbber.gif -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/dict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/dict.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/int.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/list.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/set.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/str.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/str.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/float.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/float.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/object.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/tuple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/tuple.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/NoneType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/NoneType.png -------------------------------------------------------------------------------- /birdseye/static/img/type_icons/complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/img/type_icons/complex.png -------------------------------------------------------------------------------- /birdseye/clear_db.py: -------------------------------------------------------------------------------- 1 | from birdseye.db import Database 2 | 3 | Database(_skip_version_check=True).clear() 4 | 5 | print('Database cleared!') 6 | -------------------------------------------------------------------------------- /birdseye/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /birdseye/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /birdseye/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /birdseye/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /gulp/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | npm install --global gulp-cli 6 | npm install gulp gulp-eslint 7 | 8 | # Now run `gulp` to lint JS continuously 9 | -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.eot -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.ttf -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-bold-webfont.woff -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.eot -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.ttf -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-regular-webfont.woff -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.eot -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.ttf -------------------------------------------------------------------------------- /birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexmojaki/birdseye/HEAD/birdseye/static/css/proton/fonts/titillium/titilliumweb-extralight-webfont.woff -------------------------------------------------------------------------------- /gulp/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "$": false, 7 | "hljs": false, 8 | "_": false 9 | }, 10 | "extends": "eslint:recommended" 11 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38,py39,py310,py311,py312,py313 3 | 4 | [testenv] 5 | deps = 6 | .[tests] 7 | 8 | commands = ./misc/test.sh 9 | allowlist_externals = ./misc/test.sh 10 | 11 | passenv = 12 | FIX_TESTS 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | TODO.txt 3 | __pycache__ 4 | *.egg-info/ 5 | .eggs/ 6 | .tox/ 7 | *.pyc 8 | error_screenshot.png 9 | ghostdriver.log 10 | screen_*.png 11 | .mypy_cache/ 12 | sandbox.py 13 | test.txt 14 | _build/ 15 | build/ 16 | version.py 17 | -------------------------------------------------------------------------------- /birdseye/templates/call_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | 4 | 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :end-before: inclusion-end-marker 3 | 4 | Contents 5 | -------- 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | quickstart 11 | integrations 12 | tips 13 | configuration 14 | limitations 15 | how_it_works 16 | contributing 17 | -------------------------------------------------------------------------------- /birdseye/templates/ipython_call.html: -------------------------------------------------------------------------------- 1 | {% extends "call_base.html" %} 2 | {% block whole_body %} 3 | {% include "call_core.html" %} 4 | 11 | {% endblock %} -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cheap_repr import repr_str, cheap_repr 3 | from birdseye import eye 4 | 5 | path = os.path.join(os.path.expanduser('~'), '.birdseye_test.db') 6 | os.environ.setdefault('BIRDSEYE_DB', 'sqlite:///' + path) 7 | 8 | repr_str.maxparts = 30 9 | cheap_repr.raise_exceptions = True 10 | 11 | eye.num_samples['big']['list'] = 10 12 | -------------------------------------------------------------------------------- /gulp/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | eslint = require('gulp-eslint'); 3 | 4 | gulp.task('lint', function() { 5 | return gulp.src('../birdseye/static/js/call.js') 6 | .pipe(eslint('.eslintrc.json')) 7 | .pipe(eslint.format()) 8 | }); 9 | 10 | gulp.task('default', function () { 11 | return gulp.watch(['../birdseye/static/js/call.js'], ['lint']); 12 | }); 13 | -------------------------------------------------------------------------------- /birdseye/templates/info_panel.html: -------------------------------------------------------------------------------- 1 | {% macro info_panel () -%} 2 |
3 |
4 | 5 |
6 |
7 | {{ caller() }} 8 |
9 |
10 |
11 |
12 | {%- endmacro %} 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | write_to = "birdseye/version.py" 7 | write_to_template = "__version__ = '{version}'\n" 8 | 9 | [tool.pytest.ini_options] 10 | filterwarnings = [ 11 | "ignore::outdated.OutdatedPackageWarning", 12 | "ignore::cheap_repr.ReprSuppressedWarning", 13 | ] 14 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from birdseye.db import Database 4 | 5 | 6 | class TestDatabase(unittest.TestCase): 7 | def test_key_value_store(self): 8 | kv = Database().key_value_store 9 | 10 | self.assertIsNone(kv.thing) 11 | 12 | kv.thing = 'foo' 13 | self.assertEqual(kv.thing, 'foo') 14 | 15 | kv.thing = 'bar' 16 | self.assertEqual(kv.thing, 'bar') 17 | 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /tests/test_tracer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from tests.utils import requires_python_version 5 | 6 | 7 | class TestTreeTrace(unittest.TestCase): 8 | maxDiff = None 9 | 10 | @requires_python_version(3.5) 11 | def test_async_forbidden(self): 12 | from birdseye.tracer import TreeTracerBase 13 | tracer = TreeTracerBase() 14 | with self.assertRaises(ValueError): 15 | exec(""" 16 | @tracer 17 | async def f(): pass""") 18 | 19 | if sys.version_info >= (3, 6): 20 | with self.assertRaises(ValueError): 21 | exec(""" 22 | @tracer 23 | async def f(): yield 1""") 24 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import sys 3 | from unittest import skipUnless 4 | 5 | 6 | def requires_python_version(version): 7 | version_tuple = tuple(map(int, str(version).split('.'))) 8 | return skipUnless(sys.version_info >= version_tuple, 9 | 'Requires python version %s' % version) 10 | 11 | 12 | class SharedCounter(object): 13 | def __init__(self): 14 | self._val = multiprocessing.Value('i', 0) 15 | 16 | def increment(self, n=1): 17 | with self._val.get_lock(): 18 | self._val.value += n 19 | return self._val.value 20 | 21 | @property 22 | def value(self): 23 | return self._val.value 24 | -------------------------------------------------------------------------------- /make_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | # Ensure that there are no uncommitted changes 5 | # which would mess up using the git tag as a version 6 | [ -z "$(git status --porcelain)" ] 7 | 8 | if [ -z "${1+x}" ] 9 | then 10 | set +x 11 | echo Provide a version argument 12 | echo "${0} .." 13 | exit 1 14 | else 15 | if [[ ${1} =~ ^([0-9]+)(\.[0-9]+)?(\.[0-9]+)?$ ]]; then 16 | : 17 | else 18 | echo "Not a valid release tag." 19 | exit 1 20 | fi 21 | fi 22 | 23 | export TAG="v${1}" 24 | git tag "${TAG}" 25 | git push origin master "${TAG}" 26 | rm -rf ./build ./dist 27 | python -m build --sdist --wheel . 28 | twine upload ./dist/*.whl dist/*.tar.gz 29 | -------------------------------------------------------------------------------- /test_scripts/traced.py: -------------------------------------------------------------------------------- 1 | import birdseye.trace_module_deep 2 | 3 | 4 | def deco(f): 5 | return f 6 | 7 | 8 | def m(): 9 | qwe = 9 10 | str(qwe) 11 | 12 | class A: 13 | for i in range(3): 14 | str(i * i) 15 | 16 | class B: 17 | str([[i * 2 for i in range(j)] 18 | for j in range(3)]) 19 | 20 | (lambda *_: 9)(None) 21 | (lambda x: [i * x for i in range(3)])(8) 22 | str({(lambda x: i + x)(7) for i in range(3)}) 23 | 24 | @deco 25 | def foo(self): 26 | x = 9 * 0 27 | str(1 + 2 + x) 28 | return self 29 | 30 | def bar(self): 31 | return 1 + 3 32 | 33 | A().foo().bar() 34 | 35 | 36 | m() 37 | -------------------------------------------------------------------------------- /birdseye/static/css/hljs.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /docs/how_it_works.rst: -------------------------------------------------------------------------------- 1 | How it works 2 | ============ 3 | 4 | The source file of a decorated function is parsed into the standard Python Abstract Syntax Tree. The tree is then modified so that every statement is wrapped in its own ``with`` statement and every expression is wrapped in a function call. The modified tree is compiled and the resulting code object is used to directly construct a brand new function. This is why the ``eye`` decorator must be applied first: it's not a wrapper like most decorators, so other decorators applied first would almost certainly either have no effect or bypass the tracing. The AST modifications notify the tracer both before and after every expression and statement. 5 | 6 | `Here is a talk going into more detail. `_ 7 | 8 | See the :ref:`source_overview` for an even closer look. 9 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: [ 3.8, 3.9, '3.10', 3.11, 3.12, 3.13 ] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | allow-prereleases: true 23 | - name: Set up Node 24 | uses: actions/setup-node@v4 25 | - name: Install chromedriver 26 | uses: nanasess/setup-chromedriver@master 27 | - name: run tests 28 | run: | 29 | pip install --upgrade pip 30 | pip install .[tests] 31 | ./misc/test.sh 32 | -------------------------------------------------------------------------------- /misc/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | export DB=${DB:-sqlite} 6 | 7 | if [ ${DB} = sqlite ]; then 8 | rm ~/.birdseye_test.db || true 9 | export BIRDSEYE_DB=sqlite:///$HOME/.birdseye_test.db 10 | elif [ ${DB} = postgres ]; then 11 | psql -c 'DROP DATABASE IF EXISTS birdseye_test;' -U postgres 12 | psql -c 'CREATE DATABASE birdseye_test;' -U postgres 13 | export BIRDSEYE_DB="postgresql://postgres:@localhost/birdseye_test" 14 | elif [ ${DB} = mysql ]; then 15 | mysql -e 'DROP DATABASE IF EXISTS birdseye_test;' 16 | mysql -e 'CREATE DATABASE birdseye_test;' 17 | export BIRDSEYE_DB="mysql+mysqlconnector://root:@localhost/birdseye_test" 18 | else 19 | echo "Unknown database $DB" 20 | exit 1 21 | fi 22 | 23 | python -m gunicorn -b 127.0.0.1:7777 birdseye.server:app & 24 | 25 | set +e 26 | 27 | python -m pytest -vv 28 | result=$? 29 | kill $(ps aux | grep birdseye.server:app | grep -v grep | awk '{print $2}') 30 | exit ${result} 31 | -------------------------------------------------------------------------------- /birdseye/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}birdseye{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | {% block head %}{% endblock %} 12 | 13 | 14 | {% block whole_body %} 15 |
16 | 17 |

18 | 19 | birdseye 20 |

21 |
22 |
23 | {% block body %}{% endblock %} 24 |
25 | {% block after_container %} 26 | {% endblock %} 27 | {% endblock %} 28 | 29 | -------------------------------------------------------------------------------- /birdseye/templates/call_core.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
{{ func.html_body | safe }}
4 |
5 |
6 |
9 |
10 |
11 |

12 | 
13 | 14 | 15 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alex Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_import_hook.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import unittest 3 | 4 | from tests.utils import requires_python_version 5 | 6 | 7 | class TestImportHook(unittest.TestCase): 8 | @requires_python_version(3.5) 9 | def test_should_trace(self): 10 | from birdseye.import_hook import should_trace 11 | deep, trace_stmt = should_trace('import birdseye.trace_module') 12 | self.assertFalse(deep) 13 | self.assertIsNotNone(trace_stmt) 14 | 15 | deep, trace_stmt = should_trace('import birdseye.trace_module_deep') 16 | self.assertTrue(deep) 17 | self.assertIsNotNone(trace_stmt) 18 | 19 | deep, trace_stmt = should_trace('from birdseye import trace_module_deep, eye') 20 | self.assertTrue(deep) 21 | self.assertIsNotNone(trace_stmt) 22 | 23 | deep, trace_stmt = should_trace('from birdseye import trace_module, eye') 24 | self.assertFalse(deep) 25 | self.assertIsNotNone(trace_stmt) 26 | 27 | deep, trace_stmt = should_trace('from birdseye import eye') 28 | self.assertFalse(deep) 29 | self.assertIsNone(trace_stmt) 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /birdseye/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import import_module 3 | 4 | try: 5 | from .version import __version__ 6 | except ImportError: # pragma: no cover 7 | # version.py is auto-generated with the git tag when building 8 | __version__ = "???" 9 | 10 | 11 | # birdseye has so many dependencies that simply importing them can be quite slow 12 | # Sometimes you just want to leave an import sitting around without actually using it 13 | # These proxies ensure that if you do, program startup won't be slowed down 14 | # In a nutshell: 15 | # from birdseye import eye 16 | # is a lazy version of 17 | # from birdseye.bird import eye 18 | 19 | class _SimpleProxy(object): 20 | def __init__(self, val): 21 | object.__setattr__(self, '_SimpleProxy__val', val) 22 | 23 | def __call__(self, *args, **kwargs): 24 | return self.__val()(*args, **kwargs) 25 | 26 | def __getattr__(self, item): 27 | return getattr(self.__val(), item) 28 | 29 | def __setattr__(self, key, value): 30 | setattr(self.__val(), key, value) 31 | 32 | 33 | eye = _SimpleProxy(lambda: import_module('birdseye.bird').eye) 34 | BirdsEye = _SimpleProxy(lambda: import_module('birdseye.bird').BirdsEye) 35 | 36 | 37 | def load_ipython_extension(ipython_shell): 38 | from birdseye.ipython import BirdsEyeMagics 39 | ipython_shell.register_magics(BirdsEyeMagics) 40 | 41 | 42 | if sys.version_info.major == 3: 43 | from birdseye.import_hook import BirdsEyeFinder 44 | 45 | sys.meta_path.insert(0, BirdsEyeFinder()) 46 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = birdseye 3 | author = Alex Hall 4 | author_email = alex.mojaki@gmail.com 5 | license = MIT 6 | description = Graphical Python debugger which lets you easily view the values of all evaluated expressions 7 | url = http://github.com/alexmojaki/birdseye 8 | long_description = file: README.rst 9 | long_description_content_type = text/x-rst 10 | classifiers = 11 | Intended Audience :: Developers 12 | Programming Language :: Python 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.8 15 | Programming Language :: Python :: 3.9 16 | Programming Language :: Python :: 3.10 17 | Programming Language :: Python :: 3.11 18 | Programming Language :: Python :: 3.12 19 | Programming Language :: Python :: 3.13 20 | License :: OSI Approved :: MIT License 21 | Operating System :: OS Independent 22 | Topic :: Software Development :: Debuggers 23 | 24 | [options] 25 | packages = birdseye 26 | install_requires = 27 | Flask 28 | flask-humanize 29 | sqlalchemy 30 | asttokens 31 | littleutils>=0.2 32 | cheap_repr 33 | outdated 34 | cached_property 35 | 36 | setup_requires = setuptools>=44; wheel; setuptools_scm[toml]>=3.4.3 37 | python_requires = >=3.8 38 | include_package_data = True 39 | 40 | test_suite = tests 41 | 42 | [options.extras_require] 43 | tests = 44 | bs4 45 | selenium 46 | pytest 47 | numpy 48 | pandas 49 | gunicorn 50 | twine 51 | build 52 | django 53 | IPython 54 | 55 | [options.entry_points] 56 | console_scripts = 57 | birdseye = birdseye.server:main 58 | -------------------------------------------------------------------------------- /docs/limitations.rst: -------------------------------------------------------------------------------- 1 | Performance and limitations 2 | -------------------------------------------- 3 | 4 | Every function call is recorded, and every nontrivial expression is 5 | traced. This means that: 6 | 7 | - Programs are greatly slowed down, and you should be wary of tracing 8 | functions that are called many times or that run through many loop 9 | iterations. Note that function calls are not visible in the interface 10 | until they have been completed. 11 | - A large amount of data may be collected for every function call, 12 | especially for functions with many loop iterations and large nested 13 | objects and data structures. This may be a problem for memory both 14 | when running the program and viewing results in your browser. 15 | - To limit the amount of data saved, only a sample is stored. 16 | Specifically: 17 | 18 | - The first and last 3 iterations of loops, except if an expression 19 | or statement is only evaluated at some point in the middle of a 20 | loop, in which case up to two iterations where it was evaluated 21 | will also be included (see :ref:`middle-of-loop`). 22 | - A limited version of the ``repr()`` of values is used, provided by 23 | the `cheap_repr`_ package. 24 | - Nested data structures and objects can only be expanded by up to 3 25 | levels. Inside loops this is decreased, except when all current loops 26 | are in their first iteration. 27 | - Only pieces of objects are recorded - see :ref:`collecting-data`. 28 | 29 | In IPython shells and notebooks, ``shell.ast_transformers`` is ignored 30 | in decorated functions. 31 | 32 | .. _cheap_repr: https://github.com/alexmojaki/cheap_repr 33 | -------------------------------------------------------------------------------- /birdseye/templates/file.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | {% if is_ipython %} 4 |

{{ short_path }}

5 | {% else %} 6 |

File: {{ short_path }}

7 |

Full path: {{ full_path }}

8 | {% endif %} 9 | 10 | {% from 'info_panel.html' import info_panel %} 11 | 12 | {% call info_panel() %} 13 |

Click on a function name to view calls to that function.

14 |

Click on the icon 15 | to view the most recent call to that function.

16 | {% endcall %} 17 | 18 |
19 | 20 | {% if funcs.module %} 21 | {% set func = funcs.module[0] %} 22 | 23 | 24 | 25 | 27 | Execution of module 28 | 29 | {% endif %} 30 | 31 | {% if funcs.function %} 32 |

Functions:

33 | 48 | {% endif %} 49 | {% endblock -%} -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |logo| birdseye 2 | =============== 3 | 4 | |Supports Python versions 3.8+| 5 | 6 | birdseye is a Python debugger which records the values of expressions in a 7 | function call and lets you easily view them after the function exits. 8 | For example: 9 | 10 | .. figure:: https://i.imgur.com/rtZEhHb.gif 11 | :alt: Hovering over expressions 12 | 13 | You can use birdseye no matter how you run or edit your code. Just ``pip install birdseye``, add the ``@eye`` decorator 14 | as seen above, run your function however you like, and view the results in your browser. 15 | It's also `integrated with some common tools `_ for a smoother experience. 16 | 17 | You can try it out **instantly** on `futurecoder `_: enter your code in the editor on the left and click the ``birdseye`` button to run. No imports or decorators required. 18 | 19 | Feature Highlights 20 | ------------------ 21 | 22 | Rather than stepping through lines, move back and forth through loop 23 | iterations and see how the values of selected expressions change: 24 | 25 | .. figure:: https://i.imgur.com/236Gj2E.gif 26 | :alt: Stepping through loop iterations 27 | 28 | See which expressions raise exceptions, even if they’re suppressed: 29 | 30 | .. figure:: http://i.imgur.com/UxqDyIL.png 31 | :alt: Exception highlighting 32 | 33 | Expand concrete data structures and objects to see their contents. 34 | Lengths and depths are limited to avoid an overload of data. 35 | 36 | .. figure:: http://i.imgur.com/PfmqZnT.png 37 | :alt: Exploring data structures and objects 38 | 39 | Calls are organised into functions (which are organised into files) and 40 | ordered by time, letting you see what happens at a glance: 41 | 42 | .. figure:: https://i.imgur.com/5OrB76I.png 43 | :alt: List of function calls 44 | 45 | .. |logo| image:: https://i.imgur.com/i7uaJDO.png 46 | .. |Supports Python versions 3.8+| image:: https://img.shields.io/pypi/pyversions/birdseye.svg 47 | :target: https://pypi.python.org/pypi/birdseye 48 | 49 | .. inclusion-end-marker 50 | 51 | **Read more documentation** `here `_ 52 | -------------------------------------------------------------------------------- /misc/mypy_filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This script parses output from mypy and makes it more manageable, particularly 5 | if lots of warnings are raised that you want to ignore. It's an alternative to 6 | '# type: ignore' comments and other ways of appeasing mypy that doesn't 7 | interfere with your source code. 8 | 9 | Here is how to use it in this project: 10 | 11 | python3 -m mypy -p birdseye --ignore-missing-imports | misc/mypy_filter.py misc/mypy_ignore.txt 12 | 13 | This will output all warning messages not found in mypy_ignore.txt. It will also group them 14 | so that you don't have to read the same message twice. Inspect the output. 15 | If it contains any legitimate errors, or messages that are generic enough to apply to other 16 | situations, fix the code to remove them. Once the output looks safe, run the command again 17 | with 'ok' at the end, i.e: 18 | 19 | python3 -m mypy -p birdseye --ignore-missing-imports | misc/mypy_filter.py misc/mypy_ignore.txt ok 20 | 21 | This will add any remaining warnings to mypy_ignore.txt so that they are ignored in the future. 22 | """ 23 | 24 | import sys 25 | import re 26 | from collections import defaultdict 27 | 28 | 29 | def main(): 30 | ignore_messages = [] 31 | ignore_file = None 32 | if len(sys.argv) > 1: 33 | ignore_file = sys.argv[1] 34 | with open(ignore_file) as f: 35 | ignore_messages = f.readlines() 36 | 37 | messages = defaultdict(lambda: defaultdict(set)) 38 | for line in sys.stdin: 39 | match = re.match(r'^(.+?):(\d+): (.+)$', line) 40 | if not match or any(ignore in line for ignore in ignore_messages): 41 | continue 42 | path, lineno, message = match.groups() 43 | messages[message][path].add(int(lineno)) 44 | 45 | if sys.argv[2:3] == ['ok']: 46 | with open(ignore_file, 'a') as f: 47 | for message in sorted(messages): 48 | f.write(message + '\n') 49 | print('Added', len(messages), 'messages to', ignore_file) 50 | else: 51 | for message, places in sorted(messages.items()): 52 | print(message) 53 | for path, linenos in sorted(places.items()): 54 | print(' ', path, ':', ', '.join(map(str, sorted(linenos)))) 55 | print() 56 | 57 | 58 | main() 59 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick start 2 | =============== 3 | 4 | First, install birdseye using `pip `_:: 5 | 6 | pip install --user birdseye 7 | 8 | To debug a function: 9 | 10 | 1. Decorate it with ``birdseye.eye``, e.g.:: 11 | 12 | from birdseye import eye 13 | 14 | @eye 15 | def foo(): 16 | 17 | **The** ``eye`` **decorator must be applied before any other decorators, 18 | i.e. at the bottom of the list.** 19 | 20 | 2. Call the function [*]_. 21 | 3. Run ``birdseye`` or ``python -m birdseye`` in a terminal to run the 22 | UI server. 23 | 4. Open http://localhost:7777 in your browser. 24 | 5. Note the instructions at the top for navigating through the UI. Usually you will want to jump straight to the most recent call of the function you're debugging by clicking on the play icon: 25 | 26 | |most recent call| 27 | 28 | When viewing a function call, you can: 29 | 30 | - Hover over an expression to view its value at the bottom of the 31 | screen. 32 | - Click on an expression to select it so that it stays in the 33 | inspection panel, allowing you to view several values simultaneously 34 | and expand objects and data structures. Click again to deselect. 35 | - Hover over an item in the inspection panel and it will be highlighted 36 | in the code. 37 | - Drag the bar at the top of the inspection panel to resize it 38 | vertically. 39 | - Click on the arrows next to loops to step back and forth through 40 | iterations. Click on the number in the middle for a dropdown to jump 41 | straight to a particular iteration. 42 | - If the function call you’re viewing includes a function call that was 43 | also traced, the expression where the call happens will have an arrow 44 | (|blue curved arrow|) in the corner which you can click on to go to 45 | that function call. For generator functions, the arrow will appear 46 | where the generator is first iterated over, not just when the function is called, 47 | since that is when execution of the function begins. 48 | 49 | .. |blue curved arrow| image:: https://i.imgur.com/W7DfVeg.png 50 | .. |most recent call| image:: /_static/img/call_to_foo.png 51 | .. [*] You can run the program however you want, as long as the function gets called and completes, whether by a normal return or an exception. The program itself doesn't need to terminate, only the function. 52 | -------------------------------------------------------------------------------- /birdseye/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | 4 | {% from 'info_panel.html' import info_panel %} 5 | 6 | {% call info_panel() %} 7 | {% if files %} 8 |

Click on a file to view all the functions in that file (some of which may not be listed here).

9 |

Click on a function name to view calls to that function.

10 |

Click on the icon 11 | to view the most recent call to that function.

12 | {% else %} 13 |

You haven't traced any functions! Decorate a function with @eye to get started:

14 |
15 | from birdseye import eye
16 | 
17 | @eye
18 | def foo():
19 |

Read more documentation 20 | here. 21 |

22 | 23 | 24 | 27 | {% endif %} 28 | {% endcall %} 29 | 30 | 60 | {% endblock %} -------------------------------------------------------------------------------- /test_scripts/gold.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | 3 | from birdseye import eye 4 | 5 | G = 9 6 | 7 | 8 | @eye 9 | def factorial(n): 10 | if n <= 1: 11 | return 1 12 | return n * factorial(n - 1) 13 | 14 | 15 | def dummy(*args): 16 | pass 17 | 18 | 19 | class SlotClass(object): 20 | __slots__ = ('slot1',) 21 | 22 | def __init__(self): 23 | self.slot1 = 3 24 | 25 | 26 | @eye 27 | def complex_args(pos1, pos2, key1=3, key2=4, *args, **kwargs): 28 | return [pos1, pos2, kwargs] 29 | 30 | 31 | @eye 32 | def gen(): 33 | for i in range(6): 34 | yield i 35 | 36 | 37 | @eye 38 | def use_gen_1(g): 39 | for x in islice(g, 3): 40 | dummy(x) 41 | 42 | 43 | @eye 44 | def use_gen_2(g): 45 | for y in g: 46 | dummy(y) 47 | 48 | 49 | class MyClass(object): 50 | @eye 51 | def __add__(self, other): 52 | return other 53 | 54 | @eye 55 | def __enter__(self): 56 | pass 57 | 58 | @eye 59 | def __exit__(self, exc_type, exc_val, exc_tb): 60 | pass 61 | 62 | 63 | @eye 64 | def main(): 65 | assert factorial(3) == 6 66 | 67 | vals = [] 68 | for i in range(100): 69 | vals.append([]) 70 | for j in range(2 * i): 71 | vals[-1].append(i + j) 72 | dummy(vals) 73 | 74 | for i in range(6): 75 | try: 76 | dummy(1 / (i % 2) + 10) 77 | except ZeroDivisionError: 78 | continue 79 | if i == 3: 80 | break 81 | 82 | c = MyClass() + MyClass() 83 | c.list = [[x + y for x in range(100)] for y in range(100)] 84 | sum (n for n in range(4)) 85 | dummy({n for n in range(4)}) 86 | dummy({n: n for n in range(1)}) 87 | with c: 88 | pass 89 | dummy(c + SlotClass()) 90 | 91 | assert complex_args( 92 | list(range(1000)), 93 | "hello", 94 | key2=8, 95 | kwarg1={'key': 'value'} 96 | ) == [list(range(1000)), 97 | 'hello', 98 | dict(kwarg1={'key': 'value'})] 99 | 100 | assert complex_args(*[1, 2], **{'k': 23}) == [1, 2, {'k': 23}] 101 | 102 | assert eval('%s + %s' % (1, 2)) == 3 103 | 104 | x = 1 105 | x += 5 106 | assert x == 6 107 | del x 108 | 109 | dummy(True, False, None) 110 | 111 | assert [1, 2, 3][1] == 2 112 | assert (1, 2, 3)[:2] == (1, 2) 113 | 114 | try: 115 | raise ValueError() 116 | except AssertionError as e: 117 | pass 118 | except TypeError: 119 | pass 120 | except: 121 | pass 122 | finally: 123 | dummy() 124 | 125 | while True: 126 | break 127 | 128 | assert (lambda x: x * 2)(4) == 8 129 | 130 | global G 131 | G = 4 132 | assert G == 4 133 | 134 | g = gen() 135 | use_gen_1(g) 136 | use_gen_2(g) 137 | 138 | 139 | main() 140 | -------------------------------------------------------------------------------- /birdseye/templates/function.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | 13 | {% endblock %} 14 | {% block body %} 15 | 16 | {% set typ = func.type %} 17 | 18 |

19 | {% if typ == 'module' %} 20 | Executions of module: 21 | {% else %} 22 | Function: 23 | {% endif %} 24 | 25 | 26 | {% if typ == 'module' %} 27 | {{ short_path }} 28 | {% else %} 29 | {{ func.name }} 30 | {% endif %} 31 | 32 |

33 | 34 |

Full file path: {{ func.file }}

35 | 36 | {% if typ == 'function' %} 37 | {# if there are no calls then this may not be the most recent function object #} 38 | {% if calls %} 39 |

Line: {{ func.lineno }}

40 | {% endif %} 41 | 42 |

Calls:

43 | {% endif %} 44 | 45 | {% if calls %} 46 | 47 | 48 | 49 | 50 | 51 | {% if typ == 'function' %} 52 | 53 | {% endif %} 54 | 55 | 56 | 57 | 58 | {% for call in calls %} 59 | 60 | 63 | 66 | 67 | {% if typ == 'function' %} 68 | 81 | {% endif %} 82 | 83 | 86 | 87 | {% endfor %} 88 | 89 | 90 |
Start timeArgumentsResult
61 | {{ call.state_icon }} 62 | 64 | {{ call.pretty_start_time }} 65 | 69 | {% if call.arguments_list %} 70 |
    71 | {% for k, v in call.arguments_list %} 72 |
  • 73 | {{ k }} = {{ v }} 74 |
  • 75 | {% endfor %} 76 |
77 | {% else %} 78 | - 79 | {% endif %} 80 |
84 | {{ call.result }} 85 |
91 | {% else %} 92 |

No calls yet. Is the function still running?

93 | {% endif %} 94 | {% endblock %} -------------------------------------------------------------------------------- /docs/tips.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | Debugging an entire file 5 | ------------------------- 6 | 7 | Instead of decorating individual functions with ``@eye``, you may want to debug *all* the functions in a module, or you may want to debug the top-level execution of the module itself without wrapping it in a function. 8 | 9 | To trace every function in the file, as well as the module execution itself, add the line:: 10 | 11 | import birdseye.trace_module_deep 12 | 13 | To trace only the module execution but none of the functions (to reduce the performance impact), leave out the ``_deep``, i.e.:: 14 | 15 | import birdseye.trace_module 16 | 17 | There are some caveats to note: 18 | 19 | #. These import statements must be unindented, not inside a block such as ``if`` or ``try``. 20 | #. If the module being traced is not the module that is being run directly, i.e. it's being imported by another module, then: 21 | #. The module will not be traced in Python 2. 22 | #. ``birdseye`` must be imported somewhere before importing the traced module. 23 | #. The execution of the entire module will be traced, not just the part after the import statement as when the traced module is run directly. 24 | 25 | Debugging functions without importing 26 | ------------------------------------- 27 | 28 | If you're working on a project with many files and you're tired of writing ``from birdseye import eye`` every time you want to debug a function, add code such as this to the entrypoint of your project:: 29 | 30 | from birdseye import eye 31 | 32 | # If you don't need Python 2/3 compatibility, 33 | # just one of these lines will do 34 | try: 35 | import __builtin__ as builtins # Python 2 36 | except ImportError: 37 | import builtins # Python 3 38 | 39 | builtins.eye = eye 40 | # or builtins. = eye if you want to use a different name 41 | 42 | Now you can decorate a function with ``@eye`` anywhere without importing. 43 | 44 | .. _middle-of-loop: 45 | 46 | Debugging the middle of a loop 47 | ------------------------------ 48 | 49 | birdseye will always save data from the first and last three iterations of a loop, but sometimes you have a loop with many iterations and you want to know about a specific iteration in the middle. If you were using a traditional debugger, you might do something like:: 50 | 51 | for item in long_list_of_items: 52 | if has_specific_property(item): 53 | print(item) # <-- put a breakpoint here 54 | ... 55 | 56 | You can actually use the same technique in birdseye, and you don't even need anything like a breakpoint. For every statement/expression node in a loop block, birdseye will ensure that at least two iterations where that node was evaluated are saved, assuming they exist. That means that if a statement/expression is only evaluated in the middle of the loop, those iterations will still be saved. Use a specific ``if`` or ``try/except`` statement to track down the iterations you need. 57 | 58 | You can also try wrapping the contents of the loop in a function and debugging that function. Then every call to the function will be saved and you can find the call you want by looking at the arguments and return values in the calls table. However this does come with a performance cost. 59 | -------------------------------------------------------------------------------- /birdseye/templates/call.html: -------------------------------------------------------------------------------- 1 | {% extends "call_base.html" %} 2 | {% block body %} 3 | 4 | {% set typ = func.type %} 5 | 6 |

7 | {% if typ == 'module' %} 8 | Execution of module: 9 | {% else %} 10 | Call to function: 11 | {% endif %} 12 | 13 | 15 | {% if typ == 'module' %} 16 | {{ short_path }} 17 | {% else %} 18 | {{ func.name }} 19 | {% endif %} 20 | 21 |

22 | 23 | {% if typ == 'function' %} 24 |

Line: {{ func.lineno }}

25 | {% endif %} 26 | 27 |

Full file path: {{ func.file }}

28 | 29 | {% if typ == 'function' %} 30 | {% if call.arguments_list %} 31 |

Arguments:

32 | 33 | {% for k, v in call.arguments_list %} 34 | 35 | 36 | 37 | 38 | {% endfor %} 39 |
{{ k }}{{ v }}
40 | {% else %} 41 |

No arguments

42 | {% endif %} 43 | {% endif %} 44 | 45 |

46 | Start time: {{ call.pretty_start_time }} 47 |

48 |

Result: 49 | {% if call.success %} 50 | {{ call.result }}

51 | {% else %} 52 |

53 |
{{ call.traceback }}
54 | {% endif %} 55 |

Code:

56 | 57 | {% from 'info_panel.html' import info_panel %} 58 | 59 |
60 | {% call info_panel() %} 61 | 62 | Go to newest call 63 | 64 | {% endcall %} 65 |
66 | 67 | 105 | {% endblock %} 106 | {% block after_container %} 107 | {% include "call_core.html" %} 108 | {% endblock %} -------------------------------------------------------------------------------- /birdseye/import_hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from importlib.machinery import ModuleSpec 5 | from importlib.util import spec_from_loader 6 | import ast 7 | from types import ModuleType 8 | from typing import Sequence, Iterator, cast 9 | 10 | 11 | class BirdsEyeLoader: 12 | def __init__(self, spec, source, deep): 13 | self._spec = spec 14 | self.source = source 15 | self.deep = deep 16 | 17 | def create_module(self, spec): 18 | pass 19 | 20 | def exec_module(self, module): 21 | from birdseye.bird import eye 22 | eye.exec_string( 23 | source=self.source, 24 | filename=self._spec.origin, 25 | globs=module.__dict__, 26 | locs=module.__dict__, 27 | deep=self.deep, 28 | ) 29 | 30 | def get_filename(self, fullname): 31 | return self._spec.loader.get_filename(fullname) 32 | 33 | def is_package(self, fullname): 34 | return self._spec.loader.is_package(fullname) 35 | 36 | 37 | class BirdsEyeFinder: 38 | """Loads a module and looks for tracing inside, only providing a loader 39 | if it finds some. 40 | """ 41 | 42 | def _find_plain_specs( 43 | self, fullname: str, path: Sequence[str] | None, target: ModuleType | None 44 | ) -> Iterator[ModuleSpec]: 45 | """Yield module specs returned by other finders on `sys.meta_path`.""" 46 | for finder in sys.meta_path: 47 | # Skip this finder or any like it to avoid infinite recursion. 48 | if isinstance(finder, BirdsEyeFinder): 49 | continue 50 | 51 | try: 52 | plain_spec = finder.find_spec(fullname, path, target) 53 | except Exception: # pragma: no cover 54 | continue 55 | 56 | if plain_spec: 57 | yield plain_spec 58 | 59 | def find_spec( 60 | self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = None 61 | ) -> ModuleSpec | None: 62 | """This is the method that is called by the import system. 63 | 64 | It uses the other existing meta path finders to do most of the standard work, 65 | particularly finding the module's source code. 66 | If it finds a module spec, it returns a new spec that uses the BirdsEyeLoader. 67 | """ 68 | for plain_spec in self._find_plain_specs(fullname, path, target): 69 | # Not all loaders have get_source, but it's an abstract method of the standard ABC InspectLoader. 70 | # In particular it's implemented by `importlib.machinery.SourceFileLoader` 71 | # which is provided by default. 72 | get_source = getattr(plain_spec.loader, 'get_source', None) 73 | if not callable(get_source): # pragma: no cover 74 | continue 75 | 76 | try: 77 | source = cast(str, get_source(fullname)) 78 | except Exception: # pragma: no cover 79 | continue 80 | 81 | if not source: 82 | continue 83 | 84 | if "birdseye" not in source: 85 | return None 86 | 87 | deep, trace_stmt = should_trace(source) 88 | 89 | if not trace_stmt: 90 | return None 91 | 92 | loader = BirdsEyeLoader(plain_spec, source, deep) 93 | return spec_from_loader(fullname, loader) 94 | 95 | 96 | def should_trace(source): 97 | trace_stmt = None 98 | deep = False 99 | for stmt in ast.parse(source).body: 100 | if isinstance(stmt, ast.Import): 101 | for alias in stmt.names: 102 | if alias.name.startswith('birdseye.trace_module'): 103 | trace_stmt = stmt 104 | if alias.name.endswith('deep'): 105 | deep = True 106 | 107 | if isinstance(stmt, ast.ImportFrom) and stmt.module == 'birdseye': 108 | for alias in stmt.names: 109 | if alias.name.startswith('trace_module'): 110 | trace_stmt = stmt 111 | if alias.name.endswith('deep'): 112 | deep = True 113 | return deep, trace_stmt 114 | -------------------------------------------------------------------------------- /birdseye/ipython.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import socket 3 | import sys 4 | from io import BytesIO, StringIO 5 | from threading import currentThread, Thread 6 | from uuid import uuid4 7 | 8 | from IPython.core.display import HTML, display 9 | from IPython.core.magic import Magics, cell_magic, magics_class 10 | from jinja2 import Environment, PackageLoader, select_autoescape 11 | from traitlets import Unicode, Int, Bool 12 | from werkzeug.local import LocalProxy 13 | 14 | from birdseye.bird import PY2, Database 15 | from birdseye import server, eye 16 | 17 | try: 18 | from werkzeug.serving import ThreadingMixIn 19 | except ImportError: 20 | from socketserver import ThreadingMixIn 21 | 22 | fake_stream = BytesIO if PY2 else StringIO 23 | 24 | thread_proxies = {} 25 | 26 | 27 | def stream_proxy(original): 28 | def p(): 29 | frame = inspect.currentframe() 30 | while frame: 31 | if frame.f_code == ThreadingMixIn.process_request_thread.__code__: 32 | return fake_stream() 33 | frame = frame.f_back 34 | return thread_proxies.get(currentThread().ident, 35 | original) 36 | 37 | return LocalProxy(p) 38 | 39 | 40 | sys.stderr = stream_proxy(sys.stderr) 41 | sys.stdout = stream_proxy(sys.stdout) 42 | 43 | 44 | def run_server(port, bind_host, show_server_output): 45 | if not show_server_output: 46 | thread_proxies[currentThread().ident] = fake_stream() 47 | try: 48 | server.app.run( 49 | debug=True, 50 | port=port, 51 | host=bind_host, 52 | use_reloader=False, 53 | ) 54 | except socket.error: 55 | pass 56 | 57 | 58 | templates_env = Environment( 59 | loader=PackageLoader('birdseye', 'templates'), 60 | autoescape=select_autoescape(['html', 'xml']) 61 | ) 62 | 63 | 64 | @magics_class 65 | class BirdsEyeMagics(Magics): 66 | server_url = Unicode( 67 | u'', config=True, 68 | help='If set, a server will not be automatically started by %%eye. ' 69 | 'The iframe containing birdseye output will use this value as the base ' 70 | 'of its URL.' 71 | ) 72 | 73 | port = Int( 74 | 7777, config=True, 75 | help='Port number for the server started by %%eye.' 76 | ) 77 | 78 | bind_host = Unicode( 79 | '127.0.0.1', config=True, 80 | help='Host that the server started by %%eye listens on. ' 81 | 'Set to 0.0.0.0 to make it accessible anywhere.' 82 | ) 83 | 84 | show_server_output = Bool( 85 | False, config=True, 86 | help='Set to True to show stdout and stderr from the server started by %%eye.' 87 | ) 88 | 89 | db_url = Unicode( 90 | u'', config=True, 91 | help='The database URL that the server started by %%eye reads from. ' 92 | 'Equivalent to the environment variable BIRDSEYE_DB.' 93 | ) 94 | 95 | @cell_magic 96 | def eye(self, _line, cell): 97 | if not self.server_url: 98 | server.db = Database(self.db_url) 99 | server.Function = server.db.Function 100 | server.Call = server.db.Call 101 | server.Session = server.db.Session 102 | Thread( 103 | target=run_server, 104 | args=( 105 | self.port, 106 | self.bind_host, 107 | self.show_server_output, 108 | ), 109 | ).start() 110 | 111 | eye.db = Database(self.db_url) 112 | 113 | def callback(call_id): 114 | """ 115 | Always executes after the cell, whether or not an exception is raised 116 | in the user code. 117 | """ 118 | if call_id is None: # probably means a bug 119 | return 120 | 121 | html = HTML(templates_env.get_template('ipython_iframe.html').render( 122 | call_id=call_id, 123 | url=self.server_url.rstrip('/'), 124 | port=self.port, 125 | container_id=uuid4().hex, 126 | )) 127 | 128 | # noinspection PyTypeChecker 129 | display(html) 130 | 131 | value = eye.exec_ipython_cell(cell, callback) 132 | # Display the value as would happen if the %eye magic wasn't there 133 | return value 134 | -------------------------------------------------------------------------------- /misc/mypy_ignore.txt: -------------------------------------------------------------------------------- 1 | error: "AST" has no attribute "_depth" 2 | error: "AST" has no attribute "_loops" 3 | error: "AST" has no attribute "_tree_index" 4 | error: "AST" has no attribute "first_token" 5 | error: "AST" has no attribute "parent" 6 | error: "FrameInfo" has no attribute "arguments" 7 | error: "FrameInfo" has no attribute "call_id" 8 | error: "FrameInfo" has no attribute "inner_call" 9 | error: "FrameInfo" has no attribute "iteration" 10 | error: "FrameInfo" has no attribute "start_time" 11 | error: "FunctionType" has no attribute "traced_file" 12 | error: "TracedFile" has no attribute "tokens" 13 | error: "Type[Base]" has no attribute "metadata" 14 | error: "comprehension" has no attribute "first_token" 15 | error: "comprehension" has no attribute "parent" 16 | error: "expr" has no attribute "_is_interesting_expression" 17 | error: "expr" has no attribute "_loops" 18 | error: "expr" has no attribute "ctx" 19 | error: "expr" has no attribute "parent" 20 | error: "generic_visit" of "NodeTransformer" does not return a value 21 | error: "stmt" has no attribute "_loops" 22 | error: "stmt" has no attribute "parent" 23 | error: Argument 1 to "partial" has incompatible type "function"; expected "Callable[..., ]" 24 | error: Incompatible types in assignment (expression has type "Type[type]", variable has type "_SpecialForm") 25 | error: Item "For" of "Union[While, For, comprehension]" has no attribute "parent" 26 | error: Item "While" of "Union[While, For, comprehension]" has no attribute "parent" 27 | error: Item "comprehension" of "Union[While, For, comprehension]" has no attribute "parent" 28 | error: Module has no attribute "_depth" 29 | error: Module has no attribute "isasyncgenfunction" 30 | error: Name 'lru_cache' already defined 31 | error: Return type of "__call__" incompatible with supertype "TreeTracerBase" 32 | error: Return type of "generic_visit" incompatible with supertype "NodeTransformer" 33 | error: Return type of "generic_visit" incompatible with supertype "NodeVisitor" 34 | error: Too many arguments for "Function" 35 | error: Too many arguments for "FunctionType" 36 | error: Unexpected keyword argument "arguments" for "Call" 37 | error: Unexpected keyword argument "data" for "Call" 38 | error: Unexpected keyword argument "exception" for "Call" 39 | error: Unexpected keyword argument "function" for "Call" 40 | error: Unexpected keyword argument "id" for "Call" 41 | error: Unexpected keyword argument "return_value" for "Call" 42 | error: Unexpected keyword argument "start_time" for "Call" 43 | error: Unexpected keyword argument "traceback" for "Call" 44 | note: "Call" defined here 45 | error: Incompatible types in assignment (expression has type "Iterator[Any]", variable has type "AbstractSet[Any]") 46 | error: Incompatible types in assignment (expression has type "chain[int]", variable has type "range") 47 | error: Name 'unicode' is not defined 48 | error: Name 'xrange' is not defined 49 | error: Unexpected keyword argument "data" for "Function" 50 | error: Unexpected keyword argument "file" for "Function" 51 | error: Unexpected keyword argument "hash" for "Function" 52 | error: Unexpected keyword argument "html_body" for "Function" 53 | error: Unexpected keyword argument "lineno" for "Function" 54 | error: Unexpected keyword argument "name" for "Function" 55 | note: "Function" defined here 56 | error: Argument 2 to "decorate_methods" has incompatible type "Callable[[FunctionType], FunctionType]"; expected "Callable[[Callable[..., Any]], Callable[..., Any]]" 57 | error: Item "For" of "Union[For, While, comprehension]" has no attribute "_tree_index" 58 | error: Item "While" of "Union[For, While, comprehension]" has no attribute "_tree_index" 59 | error: Item "comprehension" of "Union[For, While, comprehension]" has no attribute "_tree_index" 60 | error: No overload variant of "map" matches argument types [Any, builtins.list[builtins.int*], builtins.list[builtins.bool*], builtins.list[Any], builtins.list[builtins.str*]] 61 | error: "FrameInfo" has no attribute "inner_calls" 62 | error: Module has no attribute "getargs" 63 | error: No overload variant of "map" matches argument types [Any, builtins.list[Any], builtins.list[builtins.bool*], builtins.list[Any], builtins.list[builtins.str*]] 64 | error: Module has no attribute "cache" 65 | error: "FunctionDef" has no attribute "first_token" 66 | error: Module 'lib2to3.pgen2.tokenize' has no attribute 'detect_encoding' 67 | error: Incompatible types in assignment (expression has type "CodeType", variable has type Module) 68 | error: Module 'threading' has no attribute 'currentThread' 69 | error: Module 'werkzeug.serving' has no attribute 'ThreadingMixIn' 70 | error: Need type annotation for variable 71 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ------------- 3 | 4 | Server 5 | ~~~~~~ 6 | 7 | The server provides the user interface which can be accessed in the browser. You can run it using the ``birdseye`` command in a terminal. The command has a couple of options which can be viewed using ``--help``:: 8 | 9 | $ birdseye --help 10 | usage: birdseye [-h] [-p PORT] [--host HOST] 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | -p PORT, --port PORT HTTP port, default is 7777 15 | --host HOST HTTP host, default is 'localhost' 16 | 17 | To run a remote server accessible from anywhere, run 18 | ``birdseye --host 0.0.0.0``. 19 | 20 | The ``birdseye`` command uses the Flask development server, which is fine for local debugging but doesn't scale very well. You may want to use a proper WSGI server, especially if you host it remotely. `Here are some options `_. The WSGI application is named ``app`` in the ``birdseye.server`` module. For example, you could use ``gunicorn`` as follows:: 21 | 22 | gunicorn -b 0.0.0.0:7777 birdseye.server:app 23 | 24 | .. _db_config: 25 | 26 | Database 27 | ~~~~~~~~ 28 | 29 | Data is kept in a SQL database. You can configure this by setting the 30 | environment variable ``BIRDSEYE_DB`` to a `database URL used by 31 | SQLAlchemy`_, or just a path to a file for a simple sqlite database. 32 | The default is ``.birdseye.db`` under the home directory. The variable is checked 33 | by both the server and the tracing by the ``@eye`` decorator. 34 | 35 | If environment variables are inconvenient, you can do this instead: 36 | 37 | .. code:: python 38 | 39 | from birdseye import BirdsEye 40 | 41 | eye = BirdsEye('') 42 | 43 | You can conveniently empty the database by running: 44 | 45 | .. code:: bash 46 | 47 | python -m birdseye.clear_db 48 | 49 | Making tracing optional 50 | ~~~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | Sometimes you may want to only trace certain calls based on a condition, 53 | e.g. to increase performance or reduce database clutter. In this case, 54 | decorate your function with ``@eye(optional=True)`` instead of just 55 | ``@eye``. Then your function will have an additional optional parameter 56 | ``trace_call``, default False. When calling the decorated function, if 57 | ``trace_call`` is false, the underlying untraced function is used. If 58 | true, the traced version is used. 59 | 60 | .. _collecting-data: 61 | 62 | Collecting more or less data 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | Only pieces of objects are recorded, e.g. the first and last 3 items of 66 | a list. The number depends on the type of object and the context, and it 67 | can be configured according to the ``num_samples`` attribute of a 68 | ``BirdsEye`` instance. This can be set directly when constructing the 69 | instance, e.g.: 70 | 71 | .. code:: python 72 | 73 | from birdseye import BirdsEye 74 | 75 | eye = BirdsEye(num_samples=dict(...)) 76 | 77 | or modify the dict of an existing instance: 78 | 79 | .. code:: python 80 | 81 | from birdseye import eye 82 | 83 | eye.num_samples['big']['list'] = 100 84 | 85 | The default value is this: 86 | 87 | .. code:: python 88 | 89 | dict( 90 | big=dict( 91 | attributes=50, 92 | dict=50, 93 | list=30, 94 | set=30, 95 | pandas_rows=20, 96 | pandas_cols=100, 97 | ), 98 | small=dict( 99 | attributes=50, 100 | dict=10, 101 | list=6, 102 | set=6, 103 | pandas_rows=6, 104 | pandas_cols=10, 105 | ), 106 | ) 107 | 108 | Any value of ``num_samples`` must have this structure. 109 | 110 | The values of the ``big`` dict are used when recording an expression 111 | directly (as opposed to recording a piece of an expression, e.g. an item 112 | of a list, which is just part of the tree that is viewed in the UI) 113 | outside of any loop or in the first iteration of all current loops. In 114 | these cases more data is collected because using too much time or space 115 | is less of a concern. Otherwise, the ``small`` values are used. The 116 | inner keys correspond to different types: 117 | 118 | - ``attributes``: (e.g. ``x.y``) collected from the ``__dict__``. This 119 | applies to any type of object. 120 | - ``dict`` (or any instance of ``Mapping``) 121 | - ``list`` (or any ``Sequence``, such as tuples, or numpy arrays) 122 | - ``set`` (or any instance of ``Set``) 123 | - ``pandas_rows``: the number of rows of a ``pandas`` ``DataFrame`` or 124 | ``Series``. 125 | - ``pandas_cols``: the number of columns of a ``pandas`` ``DataFrame``. 126 | 127 | .. _database URL used by SQLAlchemy: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls 128 | -------------------------------------------------------------------------------- /birdseye/templates/ipython_iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 | 42 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /birdseye/static/css/main.css: -------------------------------------------------------------------------------- 1 | .box { 2 | position: relative; 3 | display: inline-block; 4 | padding: 3px 8px; 5 | border: 1px solid lightgrey; 6 | border-radius: 4px; 7 | } 8 | 9 | .box.has-inner { 10 | padding-right: 12px; 11 | } 12 | 13 | .box.has_value { 14 | border-color: grey; 15 | } 16 | 17 | .box.selected { 18 | padding: 1px 6px; 19 | border: 3px solid #ccc; 20 | } 21 | 22 | .box.selected.has_value { 23 | border-color: #2822ab; 24 | } 25 | 26 | .box.has_value.value_none { 27 | border-style: dashed; 28 | } 29 | 30 | .box.has_value.exception_node, .box.has_value.selected.exception_node { 31 | padding: 1px 6px; 32 | border: 3px solid red; 33 | } 34 | 35 | .box.hovering { 36 | background-color: #c1fdff; 37 | } 38 | 39 | #bottom_panel { 40 | position: fixed !important; 41 | bottom: 0 !important; 42 | margin: 0; 43 | width: 100%; 44 | } 45 | 46 | #box_value, #inspector { 47 | border: 1px solid lightgrey; 48 | margin: 0; 49 | padding: 10px; 50 | border-radius: 0; 51 | background: white; 52 | } 53 | 54 | #inspector { 55 | height: 12em; 56 | overflow: auto; 57 | } 58 | 59 | #resize-handle { 60 | background-color: lightgrey; 61 | height: 10px; 62 | width: 100%; 63 | cursor: ns-resize; 64 | background-repeat: no-repeat; 65 | background-position: center; 66 | } 67 | 68 | #box_value { 69 | max-height: 5.7em; 70 | overflow: hidden; 71 | } 72 | 73 | .flex-container { 74 | display: flex; 75 | width: 100%; 76 | } 77 | 78 | #arrows-holder { 79 | width: 100px; 80 | height: 100px; 81 | display: inline-block; 82 | position: relative; 83 | } 84 | 85 | #code { 86 | flex-grow: 1; 87 | display: inline-block; 88 | margin-bottom: 40em; 89 | overflow: visible; 90 | } 91 | 92 | .code-font, #inspector ul li.jstree-node { 93 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 94 | } 95 | 96 | .call-row a { 97 | display: block; 98 | } 99 | 100 | .args-table tr td:first-child { 101 | max-width: 20em; 102 | } 103 | 104 | .loop-label { 105 | pointer-events: none; 106 | } 107 | 108 | .loop-navigator > button, .loop-navigator > .btn-group { 109 | float: none !important; 110 | } 111 | 112 | .jstree-proton .jstree-icon.jstree-themeicon-custom { 113 | margin-right: 8px; 114 | } 115 | 116 | #code span span.stmt_uncovered, 117 | #code span span.stmt_uncovered span[class^="hljs-"] { 118 | color: #b3b3b3; 119 | font-weight: normal; 120 | } 121 | 122 | .inner-call { 123 | position: absolute; 124 | right: 0; 125 | } 126 | 127 | .inner-call .glyphicon { 128 | transform: rotate(45deg); 129 | } 130 | 131 | body > .container { 132 | margin-left: 100px; 133 | } 134 | 135 | .jstree-anchor .inspector-value { 136 | display: inline-block; 137 | white-space: pre; 138 | vertical-align: top; 139 | } 140 | 141 | #inspector .jstree-anchor { 142 | height: auto; 143 | } 144 | 145 | table.dataframe.table { 146 | width: auto; 147 | font-size: 9pt; 148 | margin: 5px; 149 | border: 2px solid black; 150 | } 151 | 152 | table.dataframe td.numeric { 153 | text-align: right; 154 | } 155 | 156 | .table-striped > tbody > tr:nth-of-type(odd) { 157 | background-color: #f5f5f5; 158 | } 159 | 160 | .table-striped.table-hover > tbody > tr:hover { 161 | background-color: rgba(66, 165, 245, 0.2); 162 | } 163 | 164 | /* Based on https://bootsnipp.com/snippets/949K6 */ 165 | .panel.panel-info { 166 | position: relative; 167 | /* 168 | Shrink to fit text: 169 | https://stackoverflow.com/a/3917059/2482744 170 | */ 171 | display: inline-block; 172 | } 173 | 174 | .panel-leftheading { 175 | width: 34px; 176 | text-align: center; 177 | border-right: 1px solid transparent; 178 | border-bottom: 1px solid transparent; 179 | float: left; 180 | height: 100%; 181 | position: absolute; 182 | /* 183 | Center text vertically: 184 | https://stackoverflow.com/a/13515693/2482744 185 | */ 186 | display: flex; 187 | justify-content: center; 188 | align-content: center; 189 | flex-direction: column; 190 | } 191 | 192 | .panel-leftheading > .glyphicon { 193 | font-size: 160%; 194 | } 195 | 196 | .panel-rightbody { 197 | float: left; 198 | margin-left: 45px; 199 | padding: 15px; 200 | } 201 | 202 | .panel-info > .panel-leftheading { 203 | color: #31708f; 204 | background-color: #d9edf7; 205 | border-color: #bce8f1; 206 | } 207 | 208 | .no-bullet { 209 | list-style-type: none; 210 | } 211 | 212 | #new-call { 213 | display: none; 214 | position: fixed; 215 | top: 0; 216 | right: 0; 217 | transform-origin: top right; 218 | transform: scale(1); 219 | } 220 | 221 | #new-call.animate { 222 | animation: size-bounce 0.5s; 223 | } 224 | 225 | @keyframes size-bounce { 226 | 0% { 227 | transform: scale(1); 228 | } 229 | 50% { 230 | transform: scale(1.2); 231 | } 232 | 100% { 233 | transform: scale(1); 234 | } 235 | } -------------------------------------------------------------------------------- /birdseye/utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import json 3 | import linecache 4 | import ntpath 5 | import os 6 | import sys 7 | import token 8 | 9 | from littleutils import strip_required_prefix 10 | 11 | from typing import Union, List, Any, Iterator, Tuple, Iterable 12 | 13 | 14 | PYPY = 'pypy' in sys.version.lower() 15 | IPYTHON_FILE_PATH = 'IPython notebook or shell' 16 | FILE_SENTINEL_NAME = '$$__FILE__$$' 17 | 18 | 19 | def path_leaf(path): 20 | # type: (str) -> str 21 | # http://stackoverflow.com/a/8384788/2482744 22 | head, tail = ntpath.split(path) 23 | return tail or ntpath.basename(head) 24 | 25 | 26 | def common_ancestor(paths): 27 | # type: (List[str]) -> str 28 | """ 29 | Returns a path to a directory that contains all the given absolute paths 30 | """ 31 | prefix = os.path.commonprefix(paths) 32 | 33 | # Ensure that the prefix doesn't end in part of the name of a file/directory 34 | prefix = ntpath.split(prefix)[0] 35 | 36 | # Ensure that it ends with a slash 37 | first_char_after = paths[0][len(prefix)] 38 | if first_char_after in r'\/': 39 | prefix += first_char_after 40 | 41 | return prefix 42 | 43 | 44 | def short_path(path, all_paths): 45 | # type: (str, List[str]) -> str 46 | if path == IPYTHON_FILE_PATH: 47 | return path 48 | 49 | all_paths = [f for f in all_paths 50 | if f != IPYTHON_FILE_PATH] 51 | prefix = common_ancestor(all_paths) 52 | if prefix in r'\/': 53 | prefix = '' 54 | return strip_required_prefix(path, prefix) or path_leaf(path) 55 | 56 | 57 | def fix_abs_path(path): 58 | if path == IPYTHON_FILE_PATH: 59 | return path 60 | if os.path.sep == '/' and not path.startswith('/'): 61 | path = '/' + path 62 | return path 63 | 64 | 65 | def of_type(type_or_tuple, iterable): 66 | # type: (Union[type, Tuple[Union[type, tuple], ...]], Iterable[Any]) -> Iterator[Any] 67 | return (x for x in iterable if isinstance(x, type_or_tuple)) 68 | 69 | 70 | def one_or_none(expression): 71 | """Performs a one_or_none on a sqlalchemy expression.""" 72 | if hasattr(expression, 'one_or_none'): 73 | return expression.one_or_none() 74 | result = expression.all() 75 | if len(result) == 0: 76 | return None 77 | elif len(result) == 1: 78 | return result[0] 79 | else: 80 | raise Exception("There is more than one item returned for the supplied filter") 81 | 82 | 83 | def flatten_list(lst): 84 | result = [] 85 | for x in lst: 86 | if isinstance(x, list): 87 | result.extend(flatten_list(x)) 88 | else: 89 | result.append(x) 90 | return result 91 | 92 | 93 | def is_lambda(f): 94 | try: 95 | code = f.__code__ 96 | except AttributeError: 97 | return False 98 | return code.co_name == (lambda: 0).__code__.co_name 99 | 100 | 101 | class ProtocolEncoder(json.JSONEncoder): 102 | def default(self, o): 103 | try: 104 | method = o.as_json 105 | except AttributeError: 106 | return super(ProtocolEncoder, self).default(o) 107 | else: 108 | return method() 109 | 110 | 111 | def read_source_file(filename): 112 | lines = linecache.getlines(filename) 113 | return ''.join(lines) 114 | 115 | 116 | def source_without_decorators(tokens, function_node): 117 | def_token = tokens.find_token(function_node.first_token, token.NAME, 'def') 118 | startpos = def_token.startpos 119 | source = tokens.text[startpos:function_node.last_token.endpos].rstrip() 120 | assert source.startswith('def') 121 | 122 | return startpos, source 123 | 124 | 125 | def prn(*args): 126 | for arg in args: 127 | print(arg) 128 | if len(args) == 1: 129 | return args[0] 130 | return args 131 | 132 | 133 | def is_ipython_cell(filename): 134 | return filename.startswith('": ">", 159 | "<": "<", 160 | } 161 | 162 | 163 | def html_escape(text): 164 | return "".join(html_escape_table.get(c, c) for c in text) 165 | 166 | 167 | def format_pandas_index(index): 168 | """ 169 | Supports different versions of pandas 170 | """ 171 | try: 172 | return index.astype(str) 173 | except Exception: 174 | try: 175 | return index.format(sparsify=False) 176 | except TypeError: 177 | return index.format() 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'birdseye' 23 | copyright = '2018, Alex Hall' 24 | author = 'Alex Hall' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | # 'sphinxcontrib.fulltoc', 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path . 66 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = 'sphinx' 70 | 71 | # -- Options for HTML output ------------------------------------------------- 72 | 73 | # The theme to use for HTML and HTML Help pages. See the documentation for 74 | # a list of builtin themes. 75 | # 76 | html_theme = 'alabaster' 77 | 78 | # Theme options are theme-specific and customize the look and feel of a theme 79 | # further. For a list of options available for each theme, see the 80 | # documentation. 81 | # 82 | # html_theme_options = {} 83 | 84 | # Add any paths that contain custom static files (such as style sheets) here, 85 | # relative to this directory. They are copied after the builtin static files, 86 | # so a file named "default.css" will overwrite the builtin "default.css". 87 | html_static_path = ['_static'] 88 | 89 | # Custom sidebar templates, must be a dictionary that maps document names 90 | # to template names. 91 | # 92 | # The default sidebars (for documents that don't match any pattern) are 93 | # defined by theme itself. Builtin themes are using these templates by 94 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 95 | # 'searchbox.html']``. 96 | # 97 | 98 | # -- Options for HTMLHelp output --------------------------------------------- 99 | 100 | # Output file base name for HTML help builder. 101 | htmlhelp_basename = 'birdseyedoc' 102 | 103 | # -- Options for LaTeX output ------------------------------------------------ 104 | 105 | latex_elements = { 106 | # The paper size ('letterpaper' or 'a4paper'). 107 | # 108 | # 'papersize': 'letterpaper', 109 | 110 | # The font size ('10pt', '11pt' or '12pt'). 111 | # 112 | # 'pointsize': '10pt', 113 | 114 | # Additional stuff for the LaTeX preamble. 115 | # 116 | # 'preamble': '', 117 | 118 | # Latex figure (float) alignment 119 | # 120 | # 'figure_align': 'htbp', 121 | } 122 | 123 | # Grouping the document tree into LaTeX files. List of tuples 124 | # (source start file, target name, title, 125 | # author, documentclass [howto, manual, or own class]). 126 | latex_documents = [ 127 | (master_doc, 'birdseye.tex', 'birdseye Documentation', 128 | 'Alex Hall', 'manual'), 129 | ] 130 | 131 | # -- Options for manual page output ------------------------------------------ 132 | 133 | # One entry per manual page. List of tuples 134 | # (source start file, name, description, authors, manual section). 135 | man_pages = [ 136 | (master_doc, 'birdseye', 'birdseye Documentation', 137 | [author], 1) 138 | ] 139 | 140 | # -- Options for Texinfo output ---------------------------------------------- 141 | 142 | # Grouping the document tree into Texinfo files. List of tuples 143 | # (source start file, target name, title, author, 144 | # dir menu entry, description, category) 145 | texinfo_documents = [ 146 | (master_doc, 'birdseye', 'birdseye Documentation', 147 | author, 'birdseye', 'One line description of project.', 148 | 'Miscellaneous'), 149 | ] 150 | -------------------------------------------------------------------------------- /docs/integrations.rst: -------------------------------------------------------------------------------- 1 | Integrations with other tools 2 | ============================= 3 | 4 | birdseye can be used no matter how you write or run your code, requiring only a browser for the interface. But it's also integrated with some common tools for a smoother experience. 5 | 6 | snoop 7 | ----- 8 | 9 | `snoop `_ is another fairly similar debugging library by the same author. Typically you decorate a function with ``@snoop`` and it will log the execution and local variables in the function. You can also use the ``@spy`` decorator which is a combination of ``@snoop`` and ``@eye`` from birdseye so that you get the best of both worlds with no extra effort. 10 | 11 | Jupyter/IPython notebooks 12 | ------------------------- 13 | 14 | First, `load the birdseye extension `_, using either ``%load_ext birdseye`` 15 | in a notebook cell or by adding ``'birdseye'`` to the list 16 | ``c.InteractiveShellApp.extensions`` in your IPython configuration file, 17 | e.g. ``~/.ipython/profile_default/ipython_config.py``. 18 | 19 | Use the cell magic ``%%eye`` at the top of a notebook cell to trace that 20 | cell. When you run the cell and it finishes executing, a frame should 21 | appear underneath with the traced code. 22 | 23 | .. figure:: https://i.imgur.com/bYL5U4N.png 24 | :alt: Jupyter notebook screenshot 25 | 26 | Hovering over an expression should show the value at the bottom of the 27 | frame. This requires the bottom of the frame being visible. Sometimes 28 | notebooks fold long output (which would include the birdseye frame) into 29 | a limited space - if that happens, click the space just left of the 30 | output. You can also resize the frame by dragging the bar at the bottom, 31 | or click ‘Open in new tab’ just above the frame. 32 | 33 | For convenience, the cell magic automatically starts a birdseye server 34 | in the background. You can configure this by settings attributes on 35 | ``BirdsEyeMagics``, e.g. with:: 36 | 37 | %config BirdsEyeMagics.port = 7778 38 | 39 | in a cell or:: 40 | 41 | c.BirdsEyeMagics.port = 7778 42 | 43 | in your IPython config file. The available attributes are: 44 | 45 | :``server_url``: 46 | If set, a server will not be automatically started by 47 | ``%%eye``. The iframe containing birdseye output will use this value 48 | as the base of its URL. 49 | 50 | :``port``: 51 | Port number for the background server. 52 | 53 | :``bind_host``: Host that the background server listens on. Set to 54 | 0.0.0.0 to make it accessible anywhere. Note that birdseye is NOT 55 | SECURE and doesn’t require any authentication to access, even if the 56 | notebook server does. Do not expose birdseye on a remote server 57 | unless you have some other form of security preventing HTTP access to 58 | the server, e.g. a VPN, or you don’t care about exposing your code 59 | and data. If you don’t know what any of this means, just leave this 60 | setting alone and you’ll be fine. 61 | 62 | :``show_server_output``: Set to True to show stdout and stderr from 63 | the background server. 64 | 65 | :``db_url``: The database URL that the background server reads from. 66 | Equivalent to the :ref:`environment variable BIRDSEYE_DB `. 67 | 68 | Visual Studio Code extension 69 | ---------------------------- 70 | 71 | - `Visual Studio Marketplace page `_ 72 | - `GitHub repo `_ 73 | 74 | Usage is simple: open the Command Palette (F1 or Cmd+Shift+P) and choose 'Show birdseye'. 75 | This will start the server and show a browser pane with the UI inside VS Code. 76 | 77 | You can also search for birdseye under settings for configuration and possibly 78 | troubleshooting. 79 | 80 | PythonAnywhere 81 | -------------- 82 | 83 | This isn't really an integration, just some instructions. 84 | 85 | The birdseye server needs to run in a web app for you to access it. You can either use a dedicated web app, or if you can't afford to spare one, combine it with an existing app. 86 | 87 | To use a dedicated web app, create a new web app, choose any framework you want (manual configuration will do), and in the WSGI configuration file ``/var/www/your_domain_com_wsgi.py`` put the following code:: 88 | 89 | from birdseye.server import app as application 90 | 91 | To combine with an existing web app, add this code at the end of the WSGI file:: 92 | 93 | import birdseye.server 94 | from werkzeug.wsgi import DispatcherMiddleware 95 | 96 | application = DispatcherMiddleware(application, { 97 | '/birdseye': birdseye.server.app 98 | }) 99 | 100 | Here ``application`` should already be defined higher up as the WSGI object for your original web app. Then your existing web app should be unaffected, except that you can also go to ``your.domain.com/birdseye`` to view the birdseye UI. You can also choose another prefix instead of ``'/birdseye'``. 101 | 102 | Either way, you should also ensure that your web app is secure, as birdseye will expose your code and data. Under the Security section of your web app configuration, enable Force HTTPS and Password protection, choose a username and password, then reload the web app. 103 | 104 | PyCharm plugin 105 | -------------- 106 | 107 | This plugin hasn't worked for a long time and is no longer being maintained. 108 | 109 | - `JetBrains Plugin Repository page `_ 110 | - `GitHub repo `_ 111 | 112 | .. _birdseye: https://github.com/alexmojaki/birdseye 113 | .. _learn how: https://github.com/alexmojaki/birdseye#installation 114 | .. |logo| image:: https://i.imgur.com/i7uaJDO.png 115 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | import ast 4 | import linecache 5 | import sys 6 | import unittest 7 | from tempfile import mkstemp 8 | 9 | import asttokens 10 | import numpy as np 11 | import pandas as pd 12 | from cheap_repr import cheap_repr 13 | 14 | from birdseye.utils import ( 15 | common_ancestor, 16 | short_path, 17 | flatten_list, 18 | is_lambda, 19 | source_without_decorators, 20 | read_source_file, 21 | ) 22 | 23 | 24 | def def_decorator(_): 25 | def actual_decorator(f): 26 | return f 27 | 28 | return actual_decorator 29 | 30 | 31 | def eye(f): 32 | return f 33 | 34 | 35 | @def_decorator('def') 36 | @eye 37 | def define(defx, defy): 38 | """ 39 | def def def 40 | 41 | @eye 42 | def define(defx, defy): 43 | """ 44 | def inner(): 45 | pass 46 | return defx + defy + inner 47 | 48 | 49 | define_source = '''\ 50 | def define(defx, defy): 51 | """ 52 | def def def 53 | 54 | @eye 55 | def define(defx, defy): 56 | """ 57 | def inner(): 58 | pass 59 | return defx + defy + inner''' 60 | 61 | 62 | class TestUtils(unittest.TestCase): 63 | def test_common_ancestor(self): 64 | self.assertEqual( 65 | common_ancestor(['/123/456', '/123/457', '/123/abc/def']), 66 | '/123/' 67 | ) 68 | self.assertEqual( 69 | common_ancestor(['\\123\\456', '\\123\\457', '\\123\\abc\\def']), 70 | '\\123\\' 71 | ) 72 | 73 | def test_short_path(self): 74 | def check(paths, result): 75 | self.assertEqual(result, [short_path(path, paths) for path in paths]) 76 | 77 | check(['/123/456', '/123/457', '/123/abc/def'], ['456', '457', 'abc/def']) 78 | check(['/123/456'], ['456']) 79 | check(['/123'], ['/123']) 80 | check(['/123/456', '/abc/def'], ['/123/456', '/abc/def']) 81 | check(['\\123\\456', '\\123\\457', '\\123\\abc\\def'], ['456', '457', 'abc\\def']) 82 | check(['\\123\\456'], ['456']) 83 | check(['\\123'], ['\\123']) 84 | check(['\\123\\456', '\\abc\\def'], ['\\123\\456', '\\abc\\def']) 85 | 86 | def test_flatten_list(self): 87 | def check(inp, out): 88 | self.assertEqual(flatten_list(inp), out) 89 | 90 | check([], []) 91 | check(['abc'], ['abc']) 92 | check(['ab', 'cd'], ['ab', 'cd']) 93 | check(['ab', ['cd', 'ef']], ['ab', 'cd', 'ef']) 94 | check([['x', 'y'], 'ab', [['0', '1'], [[], 'cd', 'ef', ['ghi', [[['jkl']]]]]]], 95 | ['x', 'y', 'ab', '0', '1', 'cd', 'ef', 'ghi', 'jkl']) 96 | 97 | def test_is_lambda(self): 98 | self.assertTrue(is_lambda(lambda: 0)) 99 | self.assertTrue(is_lambda(lambda x, y: x + y)) 100 | self.assertFalse(is_lambda(min)) 101 | self.assertFalse(is_lambda(flatten_list)) 102 | self.assertFalse(is_lambda(self.test_is_lambda)) 103 | 104 | def test_open_with_encoding_check(self): 105 | filename = mkstemp()[1] 106 | 107 | def write(stuff): 108 | with open(filename, 'wb') as f: 109 | f.write(stuff) 110 | linecache.cache.pop(filename, None) 111 | 112 | def read(): 113 | return read_source_file(filename).strip() 114 | 115 | # Correctly declared encodings 116 | 117 | write(u'# coding=utf8\né'.encode('utf8')) 118 | self.assertEqual(u'# coding=utf8\né', read()) 119 | 120 | write(u'# coding=gbk\né'.encode('gbk')) 121 | self.assertEqual(u'# coding=gbk\né', read()) 122 | 123 | # Wrong encodings 124 | 125 | write(u'# coding=utf8\né'.encode('gbk')) 126 | try: 127 | result = read() 128 | except UnicodeDecodeError: 129 | assert sys.version_info[:2] <= (3, 9) 130 | else: 131 | assert sys.version_info[:2] >= (3, 10) 132 | assert result == '' 133 | 134 | write(u'# coding=gbk\né'.encode('utf8')) 135 | self.assertFalse(u'é' in read()) 136 | 137 | write(u'é'.encode('utf8')) 138 | self.assertEqual(u'é', read()) 139 | 140 | write(u'é'.encode('gbk')) 141 | 142 | try: 143 | result = read() 144 | except SyntaxError: 145 | assert sys.version_info[:2] <= (3, 9) 146 | else: 147 | assert sys.version_info[:2] >= (3, 10) 148 | assert result == '' 149 | 150 | def test_source_without_decorators(self): 151 | source = read_source_file(__file__) 152 | tokens = asttokens.ASTTokens(source, parse=True) 153 | function_def_node = next(n for n in ast.walk(tokens.tree) 154 | if isinstance(n, ast.FunctionDef) and 155 | n.name == 'define') 156 | self.assertEqual(define_source, 157 | source_without_decorators(tokens, function_def_node)[1]) 158 | 159 | def test_cheap_repr(self): 160 | arr = np.arange(10000) 161 | arr = arr.reshape((100, 100)) 162 | df = pd.DataFrame(arr) 163 | series = df[0] 164 | self.assertEqual(cheap_repr(series), "0 = 0; 1 = 100; 2 = 200; ...; 97 = 9700; 98 = 9800; 99 = 9900") 165 | 166 | def test_read_source_file_as_string(self): 167 | import linecache 168 | import birdseye 169 | 170 | source = """ 171 | from birdseye import eye 172 | 173 | @eye 174 | def func(): 175 | return 42 176 | """ 177 | filename = "" 178 | co = compile(source, filename, "exec") 179 | linecache.cache[filename] = ( 180 | len(source), 181 | None, 182 | [line + '\n' for line in source.splitlines()], 183 | filename, 184 | ) 185 | exec(co, {"birdseye": birdseye}) 186 | rv = read_source_file(filename) 187 | self.assertEqual(rv, source) 188 | return 189 | 190 | 191 | if __name__ == '__main__': 192 | unittest.main() 193 | -------------------------------------------------------------------------------- /tests/test_interface.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from time import sleep 3 | 4 | from littleutils import only 5 | from selenium import webdriver 6 | from selenium.webdriver import ActionChains 7 | from selenium.webdriver.chrome.options import Options 8 | from selenium.webdriver.common.by import By 9 | 10 | from birdseye import eye 11 | 12 | 13 | @eye 14 | def foo(): 15 | for i in range(20): 16 | for j in range(3): 17 | int(i * 13 + j * 17) 18 | if i > 0: 19 | try: 20 | assert j 21 | except AssertionError: 22 | pass 23 | str(bar()) 24 | 25 | x = list(range(1, 30, 2)) 26 | list(x) 27 | 28 | 29 | @eye 30 | def bar(): 31 | pass 32 | 33 | 34 | class TestInterface(unittest.TestCase): 35 | maxDiff = None 36 | 37 | def setUp(self): 38 | chrome_options = Options() 39 | chrome_options.add_argument("--headless") 40 | chrome_options.add_argument("--no-sandbox") 41 | self.driver = webdriver.Chrome(options=chrome_options) 42 | self.driver.set_window_size(1600, 1200) 43 | self.driver.implicitly_wait(2) 44 | 45 | def test(self): 46 | try: 47 | self._do_test() 48 | except: 49 | self.driver.save_screenshot('error_screenshot.png') 50 | raise 51 | 52 | def _do_test(self): 53 | foo() 54 | driver = self.driver 55 | 56 | # On the index page, note the links to the function and call 57 | driver.get('http://localhost:7777/') 58 | function_link = driver.find_element(By.LINK_TEXT, 'foo') 59 | function_url = function_link.get_attribute('href') 60 | call_url = function_link.find_element(By.XPATH, '..//i/..').get_attribute('href') 61 | 62 | # On the file page, check that the links still match 63 | driver.find_element(By.PARTIAL_LINK_TEXT, 'test_interface').click() 64 | function_link = driver.find_element(By.LINK_TEXT, 'foo') 65 | self.assertEqual(function_link.get_attribute('href'), function_url) 66 | self.assertEqual(call_url, function_link.find_element(By.XPATH, '..//i/..').get_attribute('href')) 67 | 68 | # Finally navigate to the call and check the original call_url 69 | function_link.click() 70 | driver.find_element(By.CSS_SELECTOR, 'table a').click() 71 | self.assertEqual(driver.current_url, call_url) 72 | 73 | # Test hovering, clicking on expressions, and stepping through loops 74 | 75 | vals = {'i': 0, 'j': 0} 76 | exprs = driver.find_elements(By.CLASS_NAME, 'has_value') 77 | expr_value = driver.find_element(By.ID, 'box_value') 78 | 79 | expr_strings = [ 80 | 'i * 13 + j * 17', 81 | 'j * 17', 82 | 'i * 13', 83 | ] 84 | 85 | def find_by_text(text, elements): 86 | return only(n for n in elements if n.text == text) 87 | 88 | def find_expr(text): 89 | return find_by_text(text, exprs) 90 | 91 | def tree_nodes(root=driver): 92 | return root.find_elements(By.CLASS_NAME, 'jstree-node') 93 | 94 | def select(node, prefix, value_text): 95 | self.assertIn('box', classes(node)) 96 | self.assertIn('has_value', classes(node)) 97 | self.assertNotIn('selected', classes(node)) 98 | node.click() 99 | self.assertIn('selected', classes(node)) 100 | self.assertEqual(expr_value.text, value_text) 101 | tree_node = tree_nodes()[-1] 102 | self.assertEqual(tree_node.text, prefix + value_text) 103 | return tree_node 104 | 105 | def classes(node): 106 | return set(node.get_attribute('class').split()) 107 | 108 | def assert_classes(node, *cls): 109 | self.assertEqual(classes(node), set(cls)) 110 | 111 | for i, expr in enumerate(expr_strings): 112 | find_expr(expr).click() 113 | 114 | def step(loop, increment): 115 | selector = '.loop-navigator > .btn:%s-child' % ('first' if increment == -1 else 'last') 116 | buttons = driver.find_elements(By.CSS_SELECTOR, selector) 117 | self.assertEqual(len(buttons), 2) 118 | buttons[loop].click() 119 | vals['ij'[loop]] += increment 120 | 121 | for expr in expr_strings: 122 | ActionChains(driver).move_to_element(find_expr(expr)).perform() 123 | value = str(eval(expr, {}, vals)) 124 | self.assertEqual(expr_value.text, value) 125 | node = only(n for n in tree_nodes() 126 | if n.text.startswith(expr + ' =')) 127 | self.assertEqual(node.text, '%s = int: %s' % (expr, value)) 128 | 129 | stmt = find_by_text('assert j', driver.find_elements(By.CLASS_NAME, 'stmt')) 130 | assert_classes(stmt, 'stmt', 'stmt_uncovered', 'box') 131 | 132 | step(0, 1) 133 | select(stmt, 'assert j : ', 'AssertionError') 134 | assert_classes(stmt, 'stmt', 'selected', 'box', 'hovering', 'has_value', 'exception_node') 135 | step(1, 1) 136 | self.assertEqual(tree_nodes()[-1].text, 'assert j : fine') 137 | assert_classes(stmt, 'stmt', 'selected', 'box', 'has_value', 'value_none') 138 | step(1, 1) 139 | step(0, -1) 140 | self.assertTrue({'stmt', 'stmt_uncovered', 'selected', 'box'} <= classes(stmt)) 141 | step(1, -1) 142 | 143 | # Expanding values 144 | x_node = find_expr('x') 145 | tree_node = select(x_node, 'x = list: ', '[1, 3, 5, ..., 25, 27, 29]') 146 | tree_node.find_element(By.CLASS_NAME, 'jstree-ocl').click() # expand 147 | sleep(0.2) 148 | self.assertEqual([n.text for n in tree_nodes(tree_node)], 149 | ['len() = 15', 150 | '0 = int: 1', 151 | '1 = int: 3', 152 | '2 = int: 5', 153 | '3 = int: 7', 154 | '4 = int: 9', 155 | '10 = int: 21', 156 | '11 = int: 23', 157 | '12 = int: 25', 158 | '13 = int: 27', 159 | '14 = int: 29']), 160 | 161 | # Click on an inner call 162 | find_expr('bar()').find_element(By.CLASS_NAME, 'inner-call').click() 163 | self.assertEqual(driver.find_element(By.TAG_NAME, 'h2').text, 164 | 'Call to function: bar') 165 | -------------------------------------------------------------------------------- /birdseye/static/js/libs/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)"/,e:/"/,c:[c]},e.ASM,e.QSM]},s={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},i={cN:"params",b:/\(/,e:/\)/,c:["self",b,s,a]};return c.c=[a,s,b],{aliases:["py","gyp"],k:r,i:/(<\/|->|\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}); -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Here’s how you can get started if you want to help: 5 | 6 | 1. Fork the `repository `_, and clone your fork. 7 | 8 | 2. Run :: 9 | 10 | pip install -e '.[tests]' 11 | 12 | in the root of the repo. This will install it 13 | using a symlink such that changes to the code immediately affect the 14 | installed library. In other words, you can edit a ``.py`` file in your copy of birdseye, then debug a 15 | separate program, and the results of your edit will be 16 | visible. This makes development and testing straightforward. 17 | The `[tests]` part installs extra dependencies needed for development. 18 | 19 | If you have one or more other projects that you’re working on where birdseye 20 | might be useful for development and debugging, install birdseye into 21 | the interpreter (so the virtualenv if there is one) used for that 22 | project. 23 | 3. Try using birdseye for a bit, ideally in a real 24 | scenario. Get a feel for what using it is like. Note any 25 | bugs it has or features you’d like added. `Create an issue`_ where 26 | appropriate or `ask questions on the gitter chatroom`_. 27 | 4. Pick an issue that interests you and that you’d like to work on, 28 | either one that you created or an existing one. An issue with the 29 | `easy label`_ might be a good place to start. 30 | 5. Read through the source code overview below to get an idea of how it all 31 | works. 32 | 6. :ref:`Run the tests ` before making any changes just to verify that it all 33 | works on your computer. 34 | 7. Dive in and start coding! I’ve tried to make the code readable and 35 | well documented. Don’t hesitate to ask any questions on `gitter`_. If 36 | you installed correctly, you should find that changes you make to the 37 | code are reflected immediately when you run it. 38 | 8. Once you’re happy with your changes, `make a pull request`_. 39 | 40 | .. _here: https://github.com/alexmojaki/birdseye#usage-and-features 41 | .. _Create an issue: https://github.com/alexmojaki/birdseye/issues/new 42 | .. _ask questions on the gitter chatroom: https://gitter.im/python_birdseye/Lobby 43 | .. _easy label: https://github.com/alexmojaki/birdseye/issues?q=is%3Aissue+is%3Aopen+label%3Aeasy 44 | .. _gitter: https://gitter.im/python_birdseye/Lobby 45 | .. _make a pull request: http://scholarslab.org/research-and-development/forking-fetching-pushing-pulling/ 46 | 47 | .. _source_overview: 48 | 49 | Source code overview 50 | -------------------- 51 | 52 | This is a brief and rough overview of how the core of this library 53 | works, to assist anyone reading the source code for the first time. 54 | 55 | See also ':doc:`how_it_works`' for a higher level view of the concepts 56 | apart from the actual source code. 57 | 58 | Useful background knowledge 59 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 60 | 61 | 1. The ``ast`` module of the Python standard library, for parsing, 62 | traversing, and modifying source code. You don’t need to know the 63 | details of this in advance, but you should know that `this`_ is a 64 | great resource for learning about it if necessary, as the official 65 | documentation is not very helpful. 66 | 2. **Code objects**: every function in Python has a ``__code__`` 67 | attribute pointing to a special internal code object. This contains 68 | the raw instructions for executing the function. A locally defined 69 | function (i.e. a ``def`` inside a ``def``) can have multiple separate 70 | instances, but they all share the same code object, so this is the 71 | key used for storing/finding metadata for functions. 72 | 3. **Frame objects**: sometimes referred to as the frame of execution, 73 | this is another special python object that exists for every function 74 | call that is currently running. It contains local variables, the code 75 | object that is being run, a pointer to the previous frame on the 76 | stack, and more. It’s used as the key for data for the current call. 77 | 78 | .. _this: https://greentreesnakes.readthedocs.io/en/latest/index.html 79 | 80 | When a function is decorated [``BirdsEye.trace_function``] 81 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 82 | 83 | 1. [``TracedFile.__init__``] The entire file is parsed using the 84 | standard ``ast`` module. The tree is modified so that every 85 | expression is wrapped in two function calls 86 | [``_NodeVisitor.visit_expr``] and every statement is wrapped in a 87 | ``with`` block [``_NodeVisitor.visit_stmt``]. 88 | 2. [``BirdsEye.compile``] An ``ASTTokens`` object is created so that the 89 | positions of AST nodes in the source code are known. 90 | 3. The modified tree is compiled into a code object. Inside this we find 91 | the code object corresponding to the function being traced. 92 | 4. The ``__globals__`` of the function are updated to contain references 93 | to the functions that were inserted into the tree in step 1. 94 | 5. A new function object is created that’s a copy of the original 95 | function except with the new code object. 96 | 6. An HTML document is constructed where the expressions and statements 97 | of the source are wrapped in ````\ s. 98 | 7. A ``Function`` row is stored in the database containing the HTML and 99 | other metadata about the function. 100 | 8. A ``CodeInfo`` object is kept in memory to keep track of metadata 101 | about the function. 102 | 103 | When a function runs 104 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 105 | 106 | 1. When the first statement of the function runs, the tracer notices 107 | that it’s the first statement and calls 108 | ``TreeTracerBase._enter_call``. A new ``FrameInfo`` is created and 109 | associated with the current frame. 110 | 2. [``BirdsEye.enter_call``] The arguments to the function are noted and 111 | stored in the ``FrameInfo``. If the parent frame is also being 112 | traced, this is noted as an inner call of the parent call. 113 | 3. A ``_StmtContext`` is created for every statement in the function. 114 | These lead to calling ``BirdsEye.before_stmt`` and 115 | ``BirdsEye.after_stmt``. 116 | 4. For every expression in the function call, ``BirdsEye.before_expr`` 117 | and ``BirdsEye.after_expr`` are called. The values of expressions are 118 | expanded in ``NodeValue.expression`` and stored in an ``Iteration``, 119 | belonging either directly to the current ``FrameInfo`` (if this is at 120 | the top level of the function) or indirectly via an ``IterationList`` 121 | (if this is inside a loop). 122 | 5. When the function call ends, ``BirdsEye.exit_call`` is called. The 123 | data from the current ``FrameInfo`` is gathered and stored in the 124 | database in a new ``Call`` row. 125 | 126 | .. _testing: 127 | 128 | Testing 129 | ------- 130 | 131 | Run ``./misc/test.sh`` to run all 132 | tests with a single Python interpreter. You will need to have 133 | `chromedriver` installed. 134 | 135 | Run `tox`_ (``pip install tox``) to run tests on all supported 136 | versions of Python. You must install the interpreters 137 | separately yourself. 138 | 139 | ``test_against_files`` 140 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | One of the tests involves comparing data produced by the debugger to the 143 | contents of golden JSON files. This produces massive diffs when the 144 | tests fail. To read these I suggest redirecting or copying the output to 145 | a file and then doing a regex search for ``^[+-]`` to find the 146 | actual differences. 147 | 148 | If you’re satisfied that the code is doing the correct thing and the 149 | golden files need to be updated, set the environment variable ``FIX_TESTS=1``, 150 | then rerun the tests. This will write 151 | to the files instead of comparing to them. Since there are files for 152 | each version of python, you will need to run the tests on all supported 153 | interpreters, so tox is recommended. 154 | 155 | Browser screenshots for test failures 156 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 157 | 158 | ``test_interface.py`` runs a test using selenium and headless Chrome. If it 159 | fails, it produces a file ``error_screenshot.png`` which is helpful for 160 | debugging the failure locally. 161 | 162 | .. _tox: https://tox.readthedocs.io/en/latest/ 163 | 164 | 165 | Linting 166 | ------- 167 | 168 | None of this is strictly required, but may help spot errors to improve 169 | the development process. 170 | 171 | Linting Python using mypy (type warnings) 172 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 173 | 174 | The code has type hints so that ``mypy`` can be used on it, but there 175 | are many false warnings for various reasons. To ignore these, use the 176 | ``misc/mypy_filter.py`` script. The docstring explains in more detail. 177 | 178 | Linting JavaScript using gulp and eslint 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | 1. Install ``npm`` 182 | 2. Change to the ``gulp`` directory. 183 | 3. Run ``install-deps.sh``. 184 | 4. Run ``gulp``. This will lint the JavaScript continuously, checking 185 | every time the files change. 186 | -------------------------------------------------------------------------------- /birdseye/server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import sys 5 | from collections import OrderedDict 6 | from functools import partial 7 | from os.path import basename 8 | 9 | import sqlalchemy 10 | from flask import Flask, request, jsonify, url_for 11 | from flask.templating import render_template 12 | from flask_humanize import Humanize 13 | from littleutils import DecentJSONEncoder, withattrs, group_by_attr 14 | from werkzeug.routing import PathConverter 15 | 16 | from birdseye.db import Database 17 | from birdseye.utils import short_path, IPYTHON_FILE_PATH, fix_abs_path, is_ipython_cell 18 | 19 | app = Flask('birdseye') 20 | app.jinja_env.auto_reload = True 21 | 22 | Humanize(app) 23 | 24 | 25 | class FileConverter(PathConverter): 26 | part_isolating = False 27 | regex = '.*?' 28 | 29 | 30 | app.url_map.converters['file'] = FileConverter 31 | 32 | 33 | db = Database() 34 | Session = db.Session 35 | Function = db.Function 36 | Call = db.Call 37 | 38 | 39 | @app.route('/') 40 | @db.provide_session 41 | def index(session): 42 | all_paths = db.all_file_paths() 43 | 44 | recent_calls = (session.query(*(Call.basic_columns + Function.basic_columns)) 45 | .join(Function) 46 | .order_by(Call.start_time.desc())[:100]) 47 | 48 | files = OrderedDict() 49 | 50 | for row in recent_calls: 51 | if is_ipython_cell(row.file): 52 | continue 53 | files.setdefault( 54 | row.file, OrderedDict() 55 | ).setdefault( 56 | row.name, row 57 | ) 58 | 59 | for path in all_paths: 60 | files.setdefault( 61 | path, OrderedDict() 62 | ) 63 | 64 | short = partial(short_path, all_paths=all_paths) 65 | 66 | return render_template('index.html', 67 | short=short, 68 | files=files) 69 | 70 | 71 | @app.route('/file/') 72 | @db.provide_session 73 | def file_view(session, path): 74 | path = fix_abs_path(path) 75 | 76 | # Get all calls and functions in this file 77 | filtered_calls = (session.query(*(Call.basic_columns + Function.basic_columns)) 78 | .join(Function) 79 | .filter_by(file=path) 80 | .subquery('filtered_calls')) 81 | 82 | # Get the latest call *time* for each function in the file 83 | latest_calls = session.query( 84 | filtered_calls.c.name, 85 | sqlalchemy.func.max(filtered_calls.c.start_time).label('maxtime') 86 | ).group_by( 87 | filtered_calls.c.name, 88 | ).subquery('latest_calls') 89 | 90 | # Get the latest call for each function 91 | query = session.query(filtered_calls).join( 92 | latest_calls, 93 | sqlalchemy.and_( 94 | filtered_calls.c.name == latest_calls.c.name, 95 | filtered_calls.c.start_time == latest_calls.c.maxtime, 96 | ) 97 | ).order_by(filtered_calls.c.start_time.desc()) 98 | funcs = group_by_attr(query, 'type') 99 | 100 | # Add any functions which were never called 101 | all_funcs = sorted(session.query(Function.name, Function.type) 102 | .filter_by(file=path) 103 | .distinct()) 104 | func_names = {row.name for row in query} 105 | for func in all_funcs: 106 | if func.name not in func_names: 107 | funcs[func.type].append(func) 108 | 109 | return render_template('file.html', 110 | funcs=funcs, 111 | is_ipython=path == IPYTHON_FILE_PATH, 112 | full_path=path, 113 | short_path=basename(path)) 114 | 115 | 116 | @app.route('/file//__function__/') 117 | @db.provide_session 118 | def func_view(session, path, func_name): 119 | path = fix_abs_path(path) 120 | query = get_calls(session, path, func_name, 200) 121 | if query: 122 | func = query[0] 123 | calls = [withattrs(Call(), **row._asdict()) for row in query] 124 | else: 125 | func = session.query(Function).filter_by(file=path, name=func_name)[0] 126 | calls = None 127 | 128 | return render_template('function.html', 129 | func=func, 130 | short_path=basename(path), 131 | calls=calls) 132 | 133 | 134 | @app.route('/api/file//__function__//latest_call/') 135 | @db.provide_session 136 | def latest_call(session, path, func_name): 137 | path = fix_abs_path(path) 138 | call = get_calls(session, path, func_name, 1)[0] 139 | return jsonify(dict( 140 | id=call.id, 141 | url=url_for(call_view.__name__, 142 | call_id=call.id), 143 | )) 144 | 145 | 146 | def get_calls(session, path, func_name, limit): 147 | return (session.query(*(Call.basic_columns + Function.basic_columns)) 148 | .join(Function) 149 | .filter_by(file=path, name=func_name) 150 | .order_by(Call.start_time.desc())[:limit]) 151 | 152 | 153 | @db.provide_session 154 | def base_call_view(session, call_id, template): 155 | call = session.query(Call).filter_by(id=call_id).one() 156 | func = call.function 157 | return render_template(template, 158 | short_path=basename(func.file), 159 | call=call, 160 | func=func) 161 | 162 | 163 | @app.route('/call/') 164 | def call_view(call_id): 165 | return base_call_view(call_id, 'call.html') 166 | 167 | 168 | @app.route('/ipython_call/') 169 | def ipython_call_view(call_id): 170 | return base_call_view(call_id, 'ipython_call.html') 171 | 172 | 173 | @app.route('/ipython_iframe/') 174 | def ipython_iframe_view(call_id): 175 | """ 176 | This view isn't generally used, it's just an easy way to play with the template 177 | without a notebook. 178 | """ 179 | return render_template('ipython_iframe.html', 180 | container_id='1234', 181 | port=7777, 182 | call_id=call_id) 183 | 184 | 185 | @app.route('/api/call/') 186 | @db.provide_session 187 | def api_call_view(session, call_id): 188 | call = session.query(Call).filter_by(id=call_id).one() 189 | func = call.function 190 | return DecentJSONEncoder().encode(dict( 191 | call=dict(data=call.parsed_data, **Call.basic_dict(call)), 192 | function=dict(data=func.parsed_data, **Function.basic_dict(func)))) 193 | 194 | 195 | @app.route('/api/calls_by_body_hash/') 196 | @db.provide_session 197 | def calls_by_body_hash(session, body_hash): 198 | query = (session.query(*Call.basic_columns + (Function.data,)) 199 | .join(Function) 200 | .filter_by(body_hash=body_hash) 201 | .order_by(Call.start_time.desc())[:200]) 202 | 203 | calls = [Call.basic_dict(withattrs(Call(), **row._asdict())) 204 | for row in query] 205 | 206 | function_data_set = {row.data for row in query} 207 | ranges = set() 208 | loop_ranges = set() 209 | for function_data in function_data_set: 210 | function_data = json.loads(function_data) 211 | 212 | def add(key, ranges_set): 213 | for node in function_data[key]: 214 | ranges_set.add((node['start'], node['end'])) 215 | 216 | add('node_ranges', ranges) 217 | 218 | # All functions are expected to have the same set 219 | # of loop nodes 220 | current_loop_ranges = set() 221 | add('loop_ranges', current_loop_ranges) 222 | assert loop_ranges in (set(), current_loop_ranges) 223 | loop_ranges = current_loop_ranges 224 | 225 | ranges = [dict(start=start, end=end) for start, end in ranges] 226 | loop_ranges = [dict(start=start, end=end) for start, end in loop_ranges] 227 | 228 | return DecentJSONEncoder().encode(dict( 229 | calls=calls, ranges=ranges, loop_ranges=loop_ranges)) 230 | 231 | 232 | @app.route('/api/body_hashes_present/', methods=['POST']) 233 | @db.provide_session 234 | def body_hashes_present(session): 235 | hashes = request.get_json() 236 | query = (session.query(Function.body_hash, sqlalchemy.func.count(Call.id)) 237 | .outerjoin(Call) 238 | .filter(Function.body_hash.in_(hashes)) 239 | .group_by(Function.body_hash)) 240 | return DecentJSONEncoder().encode([ 241 | dict(hash=h, count=count) 242 | for h, count in query 243 | ]) 244 | 245 | 246 | def main(argv=sys.argv[1:]): 247 | # Support legacy CLI where there was just one positional argument: the port 248 | if len(argv) == 1 and argv[0].isdigit(): 249 | argv.insert(0, '--port') 250 | 251 | parser = argparse.ArgumentParser(description="Bird's Eye: A graphical Python debugger") 252 | parser.add_argument('-p', '--port', help='HTTP port, default is 7777', default=7777, type=int) 253 | parser.add_argument('--host', help="HTTP host, default is '127.0.0.1'", default=None) 254 | 255 | args = parser.parse_args(argv) 256 | app.run( 257 | port=args.port, 258 | host=args.host, 259 | use_reloader=os.environ.get('BIRDSEYE_RELOADER') == '1', 260 | ) 261 | 262 | 263 | if __name__ == '__main__': 264 | main() 265 | -------------------------------------------------------------------------------- /birdseye/db.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import os 4 | import sys 5 | from contextlib import contextmanager 6 | from typing import List 7 | 8 | from humanize import naturaltime 9 | from littleutils import select_attrs, retry 10 | from markupsafe import Markup 11 | from sqlalchemy import Sequence, UniqueConstraint, create_engine, Column, Integer, Text, ForeignKey, DateTime, String, \ 12 | Index 13 | from sqlalchemy.dialects.mysql import LONGTEXT 14 | from sqlalchemy.exc import OperationalError, InterfaceError, InternalError, ProgrammingError, ArgumentError 15 | from sqlalchemy.ext.declarative import declared_attr 16 | from sqlalchemy.orm import backref, relationship, sessionmaker 17 | 18 | from birdseye.utils import IPYTHON_FILE_PATH, is_ipython_cell 19 | 20 | try: 21 | from sqlalchemy.dialects.mysql.base import RESERVED_WORDS 22 | except ImportError: 23 | pass 24 | else: 25 | RESERVED_WORDS.add('function') 26 | 27 | try: 28 | from sqlalchemy.orm import declarative_base 29 | except ImportError: 30 | from sqlalchemy.ext.declarative import declarative_base 31 | 32 | 33 | DB_VERSION = 1 34 | 35 | 36 | class Database(object): 37 | def __init__(self, db_uri=None, _skip_version_check=False): 38 | self.db_uri = db_uri = ( 39 | db_uri 40 | or os.environ.get('BIRDSEYE_DB') 41 | or os.path.join(os.path.expanduser('~'), 42 | '.birdseye.db')) 43 | 44 | kwargs = dict( 45 | pool_recycle=280, 46 | echo=False, # for convenience when debugging 47 | ) 48 | 49 | try: 50 | engine = create_engine(db_uri, **kwargs) 51 | except ArgumentError: 52 | db_uri = 'sqlite:///' + db_uri 53 | engine = create_engine(db_uri, **kwargs) 54 | 55 | self.engine = engine 56 | 57 | self.Session = sessionmaker(bind=engine) 58 | 59 | class Base(object): 60 | @declared_attr 61 | def __tablename__(cls): 62 | return cls.__name__.lower() 63 | 64 | Base = declarative_base(cls=Base) # type: ignore 65 | 66 | class KeyValue(Base): 67 | key = Column(String(50), primary_key=True) 68 | value = Column(Text) 69 | 70 | db_self = self 71 | 72 | class KeyValueStore(object): 73 | def __getitem__(self, item): 74 | with db_self.session_scope() as session: 75 | return (session 76 | .query(KeyValue.value) 77 | .filter_by(key=item) 78 | .scalar()) 79 | 80 | def __setitem__(self, key, value): 81 | with db_self.session_scope() as session: 82 | session.query(KeyValue).filter_by(key=key).delete() 83 | session.add(KeyValue(key=key, value=str(value))) 84 | 85 | __getattr__ = __getitem__ 86 | __setattr__ = __setitem__ 87 | 88 | LongText = LONGTEXT if engine.name == 'mysql' else Text 89 | 90 | class Call(Base): 91 | id = Column(String(length=32), primary_key=True) 92 | function_id = Column(Integer, ForeignKey('function.id'), index=True) 93 | function = relationship('Function', backref=backref('calls', lazy='dynamic')) 94 | arguments = Column(Text) 95 | return_value = Column(Text) 96 | exception = Column(Text) 97 | traceback = Column(Text) 98 | data = Column(LongText) 99 | start_time = Column(DateTime, index=True) 100 | 101 | @property 102 | def pretty_start_time(self): 103 | return self._pretty_time(self.start_time) 104 | 105 | @staticmethod 106 | def _pretty_time(dt): 107 | if not dt: 108 | return '' 109 | return Markup('%s (%s)' % ( 110 | dt.strftime('%Y-%m-%d %H:%M:%S'), 111 | naturaltime(dt))) 112 | 113 | @property 114 | def state_icon(self): 115 | return Markup('' % ( 117 | ('ok', 'green') if self.success else 118 | ('remove', 'red'))) 119 | 120 | @property 121 | def success(self): 122 | if self.exception: 123 | assert self.traceback 124 | assert self.return_value == 'None' 125 | return False 126 | else: 127 | assert not self.traceback 128 | return True 129 | 130 | @property 131 | def result(self): 132 | if self.success: 133 | return str(self.return_value) 134 | else: 135 | return str(self.exception) 136 | 137 | @property 138 | def arguments_list(self): 139 | return json.loads(self.arguments) 140 | 141 | @property 142 | def parsed_data(self): 143 | return json.loads(self.data) 144 | 145 | @staticmethod 146 | def basic_dict(call): 147 | return dict(arguments=call.arguments_list, 148 | **select_attrs(call, 'id function_id return_value traceback ' 149 | 'exception start_time')) 150 | 151 | basic_columns = (id, function_id, return_value, 152 | traceback, exception, start_time, arguments) 153 | 154 | class Function(Base): 155 | id = Column(Integer, Sequence('function_id_seq'), primary_key=True) 156 | file = Column(Text) 157 | name = Column(Text) 158 | type = Column(Text) # function or module 159 | html_body = Column(LongText) 160 | lineno = Column(Integer) 161 | data = Column(LongText) 162 | hash = Column(String(length=64), index=True) 163 | body_hash = Column(String(length=64), index=True) 164 | 165 | __table_args__ = ( 166 | UniqueConstraint('hash', 167 | name='everything_unique'), 168 | Index('idx_file', 'file', mysql_length=256), 169 | Index('idx_name', 'name', mysql_length=32), 170 | ) 171 | 172 | @property 173 | def parsed_data(self): 174 | return json.loads(self.data) 175 | 176 | @staticmethod 177 | def basic_dict(func): 178 | return select_attrs(func, 'file name lineno hash body_hash type') 179 | 180 | basic_columns = (file, name, lineno, hash, body_hash, type) 181 | 182 | self.Call = Call 183 | self.Function = Function 184 | self._KeyValue = KeyValue 185 | 186 | self.key_value_store = kv = KeyValueStore() 187 | 188 | if _skip_version_check: 189 | return 190 | 191 | if not self.table_exists(Function): 192 | Base.metadata.create_all(engine) 193 | kv.version = DB_VERSION 194 | elif not self.table_exists(KeyValue) or int(kv.version) < DB_VERSION: 195 | sys.exit('The birdseye database schema is out of date. ' 196 | 'Run "python -m birdseye.clear_db" to delete the existing tables.') 197 | 198 | def table_exists(self, table): 199 | try: 200 | from sqlalchemy import inspect 201 | 202 | return inspect(self.engine).has_table(table.__name__) 203 | except (ImportError, AttributeError): 204 | try: 205 | return self.engine.has_table(table.__name__) 206 | except AttributeError: 207 | return self.engine.dialect.has_table(self.engine, table.__name__) 208 | 209 | def all_file_paths(self): 210 | # type: () -> List[str] 211 | with self.session_scope() as session: 212 | paths = [f[0] for f in session.query(self.Function.file).distinct() 213 | if not is_ipython_cell(f[0])] 214 | paths.sort() 215 | if IPYTHON_FILE_PATH in paths: 216 | paths.remove(IPYTHON_FILE_PATH) 217 | paths.insert(0, IPYTHON_FILE_PATH) 218 | return paths 219 | 220 | def clear(self): 221 | for model in [self.Call, self.Function, self._KeyValue]: 222 | if self.table_exists(model): 223 | model.__table__.drop(self.engine) 224 | 225 | @contextmanager 226 | def session_scope(self): 227 | """Provide a transactional scope around a series of operations.""" 228 | session = self.Session() 229 | try: 230 | yield session 231 | session.commit() 232 | except: 233 | session.rollback() 234 | raise 235 | finally: 236 | session.close() 237 | 238 | def provide_session(self, func): 239 | @functools.wraps(func) 240 | def wrapper(*args, **kwargs): 241 | with self.session_scope() as session: 242 | return func(session, *args, **kwargs) 243 | 244 | return retry_db(wrapper) 245 | 246 | 247 | # Based on https://docs.sqlalchemy.org/en/latest/errors.html#error-dbapi 248 | retry_db = retry(3, (InterfaceError, OperationalError, InternalError, ProgrammingError)) 249 | -------------------------------------------------------------------------------- /birdseye/static/css/jquery-ui.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2017-06-16 2 | * http://jqueryui.com 3 | * Includes: core.css, resizable.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif 5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} -------------------------------------------------------------------------------- /birdseye/static/js/call.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global call_data, func_data */ 4 | 5 | 6 | $(function () { 7 | 8 | _.mixin({ 9 | toggle: function (a, b) { 10 | return _.contains(a, b) ? _.without(a, b) : _.union(a, [b]); 11 | }, 12 | deepContains: function (arr, x) { 13 | return _.any(arr, _.partial(_.isEqual, x)) 14 | } 15 | }); 16 | 17 | var $code = $('#code'); 18 | hljs.highlightBlock($code[0]); 19 | 20 | var node_values = call_data.node_values; 21 | var loop_iterations = call_data.loop_iterations; 22 | var node_loops = func_data.node_loops; 23 | var _current_iteration = {}; 24 | $code.find('.loop').each(function (_, loop_span) { 25 | _current_iteration[loop_span.dataset.index] = 0; 26 | }); 27 | 28 | if (!call_success) { 29 | // Go to the end of all loops to see what 30 | // happened when there was an exception 31 | var fill_last_iterations = function (loops) { 32 | if (!loops) { 33 | return; 34 | } 35 | Object.keys(loops).forEach(function (tree_index) { 36 | var loop = loops[tree_index]; 37 | var iteration = _.last(loop); 38 | _current_iteration[tree_index] = iteration.index; 39 | fill_last_iterations(iteration.loops); 40 | }); 41 | }; 42 | fill_last_iterations(loop_iterations); 43 | } 44 | 45 | var normal_stmt_value = 'fine'; 46 | 47 | var selected_boxes = []; 48 | var index_to_node = {}; 49 | 50 | function make_jstree_nodes(prefix, path, val, $node) { 51 | if (!val) { 52 | return { 53 | text: '' + _.escape(prefix) + '', 54 | state: { 55 | disabled: true, 56 | }, 57 | icon: false, 58 | $node: $node, 59 | } 60 | } 61 | var val_repr = '
' + _.escape(val[0]) + '
'; 62 | var type_index = val[1]; 63 | var type_name = call_data.type_names[type_index]; 64 | var is_special = type_index < call_data.num_special_types; 65 | 66 | function special(name) { 67 | return is_special && type_name === name; 68 | } 69 | 70 | var text = _.escape(prefix) + (type_index < 0 ? ' : ' : ' = '); 71 | if (type_index === -2) { 72 | text += '' + normal_stmt_value + ''; 73 | } else { 74 | if (special('NoneType')) { 75 | text += 'None'; 76 | } else if (type_index === -1) { // exception 77 | text += '' + val_repr + ''; 78 | } else if (val[2].dataframe && val[3] && (val[3][1].length > 4)) { 79 | var table = dataframeTable(val); 80 | text += table[0].outerHTML 81 | } else { 82 | text += '' + _.escape(type_name) + ': ' + val_repr; 83 | } 84 | } 85 | 86 | var icon; 87 | if (special('bool')) { 88 | icon = 'glyphicon glyphicon-' + (val[0] === 'True' ? 'ok' : 'remove'); 89 | } else if (type_index === -1) { 90 | icon = 'glyphicon glyphicon-warning-sign'; 91 | } else if (type_index === -2) { 92 | icon = 'glyphicon glyphicon-ok'; 93 | } else { 94 | icon = static_url + 'img/type_icons/'; 95 | if (is_special) { 96 | if ('str NoneType complex float int list tuple dict set'.indexOf(type_name) > -1) { 97 | icon += type_name; 98 | } else { 99 | icon += { 100 | unicode: 'str', 101 | bytes: 'str', 102 | frozenset: 'set', 103 | long: 'int', 104 | }[type_name]; 105 | } 106 | } else { 107 | icon += 'object'; 108 | } 109 | icon += '.png'; 110 | } 111 | 112 | var result = { 113 | text: text, 114 | icon: icon, 115 | path: path, 116 | $node: $node, 117 | state: { 118 | opened: _.deepContains(open_paths, path), 119 | } 120 | }; 121 | 122 | var children = []; 123 | var len = val[2].len; 124 | if (len !== undefined && !(len === 0 && is_special)) { 125 | children.push({ 126 | icon: false, 127 | text: 'len() = ' + len, 128 | }); 129 | } 130 | 131 | $.merge(children, val.slice(3).map(function (child) { 132 | return make_jstree_nodes(child[0], path.concat([child[0]]), child[1]); 133 | })); 134 | 135 | if (children.length) { 136 | result.children = children; 137 | } 138 | 139 | return result; 140 | } 141 | 142 | function dataframeTable(val) { 143 | var meta = val[2].dataframe; 144 | var numCols = val.length - 3; 145 | var numRows = val[3][1].length - 4; 146 | var i, j, value, column; 147 | var table = $('').addClass('dataframe table table-striped table-hover'); 148 | var header = $(''); 149 | header.append($(''); 156 | for (j = 0; j < numCols + 1 + (meta.col_break ? 1 : 0); j++) { 157 | row.append($(''); 164 | table.append(row); 165 | rows.push(row); 166 | column = val[3]; 167 | var label = column[1][4 + i][0]; 168 | row.append($('
')); 150 | table.append(header); 151 | var rows = []; 152 | for (i = 0; i < numRows; i++) { 153 | var row; 154 | if (i === meta.row_break) { 155 | row = $('
') 158 | .text('...') 159 | .css({'text-align': 'center'})); 160 | } 161 | table.append(row); 162 | } 163 | row = $('
').text(label)); 169 | } 170 | for (i = 0; i < numCols; i++) { 171 | if (i === meta.col_break) { 172 | header.append($('').text('...')); 173 | } 174 | column = val[3 + i]; 175 | header.append($('').text(column[0])); 176 | var values = []; 177 | var isNumeric = true; 178 | var maxDecimals = 1; 179 | for (j = 0; j < numRows; j++) { 180 | value = column[1][4 + j][1][0]; 181 | values.push(value); 182 | isNumeric &= !isNaN(parseFloat(value)) || value.toLowerCase() === 'nan'; 183 | var decimals = value.split(".")[1]; 184 | if (decimals) { 185 | maxDecimals = Math.max(maxDecimals, decimals.length); 186 | } 187 | } 188 | for (j = 0; j < numRows; j++) { 189 | if (i === meta.col_break) { 190 | rows[j].append($('').text('...')); 191 | } 192 | value = values[j]; 193 | if (isNumeric) { 194 | value = parseFloat(value).toFixed(Math.min(maxDecimals, 6)); 195 | } 196 | rows[j].append($('').text(value).toggleClass('numeric', isNumeric)); 197 | } 198 | } 199 | return table; 200 | } 201 | 202 | var open_paths = []; 203 | 204 | $('#inspector').jstree({ 205 | core: { 206 | themes: { 207 | name: 'proton', 208 | responsive: true 209 | } 210 | }, 211 | }).on("hover_node.jstree dehover_node.jstree", function (e, data) { 212 | var $node = data.node.original.$node; 213 | if (!$node) { 214 | return; 215 | } 216 | var hovering = e.type === 'hover_node'; 217 | $node.toggleClass('hovering', hovering); 218 | }).on("open_node.jstree", function (e, data) { 219 | var path = data.node.original.path; 220 | if (!_(open_paths).deepContains(path)) { 221 | open_paths.push(path); 222 | } 223 | }).on("close_node.jstree", function (e, data) { 224 | var path = data.node.original.path; 225 | while (_(open_paths).deepContains(path)) { 226 | var index = _.findIndex(open_paths, _.partial(_.isEqual, path)); 227 | open_paths.splice(index, 1); 228 | } 229 | }); 230 | 231 | $code.find('span[data-index]').each(function () { 232 | var $this = $(this); 233 | var tree_index = this.dataset.index; 234 | var json = JSON.stringify(node_values[tree_index]) || ''; 235 | $this.toggleClass('box', tree_index in node_values && !( 236 | // This is a statement/comprehension node that never encounters an exception, 237 | // or has any metadata, so it never has a 'value' worth checking. 238 | ($this.hasClass('stmt') || $this.hasClass('loop')) && 239 | -1 === json.indexOf('-1') && 240 | -1 === json.indexOf('inner_call'))); 241 | $this.toggleClass( 242 | 'has-inner', 243 | json.indexOf('"inner_calls":["') !== -1); 244 | $this.click(function () { 245 | if ($this.hasClass('hovering')) { 246 | $this.toggleClass('selected'); 247 | selected_boxes = _.toggle(selected_boxes, tree_index); 248 | } 249 | render(); 250 | }); 251 | index_to_node[tree_index] = this; 252 | }); 253 | 254 | function render() { 255 | 256 | $('#inspector, #resize-handle').css({display: selected_boxes.length ? 'block' : 'none'}); 257 | 258 | var loop_indices = {}; 259 | 260 | function findRanges(iters) { 261 | if (!iters) { 262 | return; 263 | } 264 | Object.keys(iters).forEach(function (key) { 265 | var value = iters[key]; 266 | loop_indices[key] = _.pluck(value, 'index'); 267 | findRanges(value[current_iteration(key)].loops); 268 | }); 269 | } 270 | 271 | function current_iteration(i) { 272 | if (!(i in loop_indices)) { 273 | return -1; 274 | } 275 | return Math.min(_current_iteration[i], loop_indices[i].length - 1); 276 | } 277 | 278 | findRanges(loop_iterations); 279 | 280 | function get_value(tree_index) { 281 | var value = node_values[tree_index]; 282 | var loops = node_loops[tree_index] || []; 283 | loops.forEach(function (loopIndex) { 284 | if (value) { 285 | value = value[current_iteration(loopIndex)]; 286 | } 287 | }); 288 | return value; 289 | } 290 | 291 | $code.find('span[data-index]').each( 292 | function () { 293 | var value; 294 | var $this = $(this); 295 | var tree_index = this.dataset.index; 296 | if (tree_index in node_values) { 297 | value = get_value(tree_index); 298 | } 299 | $this.toggleClass('has_value', Boolean(value)); 300 | $this.toggleClass('stmt_uncovered', $this.hasClass('stmt') && !value); 301 | 302 | if (value && $this.hasClass('box')) { 303 | $this.on('mouseover mouseout', function (e) { 304 | var hovering = e.type === 'mouseover'; 305 | $this.toggleClass('hovering', hovering); 306 | if (hovering) { 307 | if (value[1] === -2) { 308 | value[0] = normal_stmt_value; 309 | } 310 | $('#box_value').text(value[0]); 311 | } 312 | e.stopPropagation(); 313 | }); 314 | $this.toggleClass('exception_node', value[1] === -1); 315 | $this.toggleClass('value_none', value[1] === 0 || value[1] === -2); 316 | $this.children('a.inner-call').remove(); 317 | 318 | var inner_calls = value[2].inner_calls || []; 319 | var place_link = function (inner_call, css) { 320 | var link = $('' + 321 | '' + 322 | '') 323 | .css(css); 324 | $this.append(link); 325 | }; 326 | if (inner_calls.length === 1) { 327 | place_link(inner_calls[0], {bottom: '-4px'}); 328 | } else if (inner_calls.length >= 2) { 329 | place_link(inner_calls[0], {top: 0}); 330 | place_link(inner_calls[inner_calls.length - 1], {bottom: '-4px'}); 331 | } 332 | } else { 333 | $this.off('mouseover mouseout'); 334 | } 335 | } 336 | ); 337 | 338 | var inspector = $('#inspector'); 339 | inspector.jstree(true).settings.core.data = selected_boxes.map(function (tree_index) { 340 | var node = index_to_node[tree_index]; 341 | var $node = $(node); 342 | var value = get_value(tree_index); 343 | return make_jstree_nodes($node.text(), [tree_index], value, $node); 344 | }); 345 | inspector.jstree(true).refresh(); 346 | 347 | $('.loop-navigator').remove(); 348 | 349 | $code.find('.loop').each(function (_, loop_span) { 350 | 351 | var loopIndex = loop_span.dataset.index; 352 | 353 | var buttonGroup = $('
', { 354 | class: "btn-group loop-navigator", 355 | role: "group", 356 | }).css({ 357 | position: 'absolute', 358 | top: $(loop_span).offset().top - $code.offset().top, 359 | right: '5px', 360 | }); 361 | 362 | function mkButton(cls, disabled, html, onclick) { 363 | var attrs = { 364 | type: 'button', 365 | class: 'btn btn-default btn-xs ' + cls, 366 | html: html, 367 | }; 368 | if (disabled) { 369 | attrs.disabled = 'disabled'; 370 | } 371 | if (cls === 'dropdown-toggle') { 372 | attrs['data-toggle'] = 'dropdown'; 373 | } 374 | var button = $('