├── .coveragerc ├── .gitignore ├── .style.yapf ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── setup.py └── wdb │ ├── __init__.py │ ├── __main__.py │ ├── _compat.py │ ├── breakpoint.py │ ├── ext.py │ ├── state.py │ ├── ui.py │ └── utils.py ├── flask_wdb_hook ├── .gitignore ├── README.md ├── demo.gif ├── flask-wdb.pth └── setup.py ├── importtest.py ├── pytest_wdb ├── .gitignore ├── CHANGELOG.md ├── pytest_wdb.py ├── setup.py └── test_wdb.py ├── release.sh ├── server ├── .gitignore ├── Dockerfile ├── Gruntfile.coffee ├── bower.json ├── coffees │ ├── _base.coffee │ ├── _compat.coffee │ ├── _help.coffee │ ├── _history.coffee │ ├── _interpreter.coffee │ ├── _prompt.coffee │ ├── _source.coffee │ ├── _switch.coffee │ ├── _traceback.coffee │ ├── _watchers.coffee │ ├── _websocket.coffee │ ├── home.coffee │ └── wdb.coffee ├── package.json ├── sass │ ├── _interpreter.sass │ ├── _prompt.sass │ ├── _source.sass │ ├── _switch.sass │ ├── _traceback.sass │ ├── _watchers.sass │ ├── home.sass │ └── wdb.sass ├── setup.py ├── wdb.server.py ├── wdb.server.service ├── wdb.server.socket ├── wdb_server │ ├── __init__.py │ ├── state.py │ ├── static │ │ ├── hipster.jpg │ │ ├── img │ │ │ ├── bg-left.png │ │ │ └── bg-right.png │ │ ├── javascripts │ │ │ ├── home.js │ │ │ ├── wdb.js │ │ │ └── wdb │ │ │ │ ├── deps.min.js │ │ │ │ ├── home.min.js │ │ │ │ └── wdb.min.js │ │ ├── libs │ │ │ └── material-design-lite │ │ │ │ ├── material.indigo-red.min.css │ │ │ │ ├── material.min.js │ │ │ │ ├── material.red-indigo.min.css │ │ │ │ └── material.teal-orange.min.css │ │ └── stylesheets │ │ │ ├── deps.min.css │ │ │ ├── home.css │ │ │ └── wdb.css │ ├── streams.py │ ├── templates │ │ ├── _layout.html │ │ ├── home.html │ │ └── wdb.html │ └── utils.py └── yarn.lock ├── setup.cfg ├── test ├── __init__.py ├── conftest.py ├── scripts │ ├── __init__.py │ ├── bad_script.py │ ├── error_ignored_in_script.py │ ├── error_in_script.py │ ├── error_in_with.py │ ├── error_in_with_advanced.py │ ├── error_in_with_below.py │ ├── error_in_with_below_under.py │ ├── error_in_with_under.py │ ├── error_not_ignored_in_script.py │ ├── forks.py │ ├── latin-1.py │ ├── movement.py │ ├── objects.py │ ├── ok_script.py │ ├── osfork.py │ ├── recursive_object.py │ ├── threads.py │ ├── tornado_server.py │ ├── trace_in_script.py │ └── wsgi.py ├── test_breaks.py ├── test_error.py ├── test_forks.py ├── test_main.py ├── test_misc.py ├── test_movement.py ├── test_no_error.py ├── test_objects.py ├── test_osforks.py ├── test_prompt.py ├── test_threads.py ├── test_trace.py └── test_utils.py ├── tox.ini ├── wdb-lg.png ├── wdb.png └── wdb_over_pdb ├── pdb.py └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = test/scripts/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | *.map 4 | *.src.coffee 5 | client/build/ 6 | server/build/ 7 | client/dist/ 8 | server/dist/ 9 | junit-py* 10 | coverage-py* 11 | .cov-py* 12 | .cache 13 | .coverage 14 | .pytest_cache 15 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | 4 | # █████ ██ ██ 5 | # ██ ██ ██ ██ 6 | # ███████ ██ ██ 7 | # ██ ██ ██ ██ 8 | # ██ ██ ███████ ███████ 9 | 10 | 11 | # Align closing bracket with visual indentation. 12 | # align_closing_bracket_with_visual_indent=True 13 | 14 | # Allow dictionary keys to exist on multiple lines. For example: 15 | # 16 | # x = { 17 | # ('this is the first element of a tuple', 18 | # 'this is the second element of a tuple'): 19 | # value, 20 | # } 21 | # allow_multiline_dictionary_keys=False 22 | 23 | # Allow lambdas to be formatted on more than one line. 24 | # allow_multiline_lambdas=False 25 | 26 | # Insert a blank line before a class-level docstring. 27 | # blank_line_before_class_docstring=False 28 | 29 | # Insert a blank line before a 'def' or 'class' immediately nested 30 | # within another 'def' or 'class'. For example: 31 | # 32 | # class Foo: 33 | # # <------ this blank line 34 | # def method(): 35 | # ... 36 | # blank_line_before_nested_class_or_def=False 37 | 38 | # Do not split consecutive brackets. Only relevant when 39 | # dedent_closing_brackets is set. For example: 40 | # 41 | # call_func_that_takes_a_dict( 42 | # { 43 | # 'key1': 'value1', 44 | # 'key2': 'value2', 45 | # } 46 | # ) 47 | # 48 | # would reformat to: 49 | # 50 | # call_func_that_takes_a_dict({ 51 | # 'key1': 'value1', 52 | # 'key2': 'value2', 53 | # }) 54 | coalesce_brackets=True 55 | 56 | # The column limit. 57 | # column_limit=79 58 | 59 | # Indent width used for line continuations. 60 | # continuation_indent_width=4 61 | 62 | # Put closing brackets on a separate line, dedented, if the bracketed 63 | # expression can't fit in a single line. Applies to all kinds of brackets, 64 | # including function definitions and calls. For example: 65 | # 66 | # config = { 67 | # 'key1': 'value1', 68 | # 'key2': 'value2', 69 | # } # <--- this bracket is dedented and on a separate line 70 | # 71 | # time_series = self.remote_client.query_entity_counters( 72 | # entity='dev3246.region1', 73 | # key='dns.query_latency_tcp', 74 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 75 | # start_ts=now()-timedelta(days=3), 76 | # end_ts=now(), 77 | # ) # <--- this bracket is dedented and on a separate line 78 | dedent_closing_brackets=True 79 | 80 | # Place each dictionary entry onto its own line. 81 | # each_dict_entry_on_separate_line=True 82 | 83 | # The regex for an i18n comment. The presence of this comment stops 84 | # reformatting of that line, because the comments are required to be 85 | # next to the string they translate. 86 | # i18n_comment= 87 | 88 | # The i18n function call names. The presence of this function stops 89 | # reformattting on that line, because the string it has cannot be moved 90 | # away from the i18n comment. 91 | # i18n_function_call= 92 | 93 | # Indent the dictionary value if it cannot fit on the same line as the 94 | # dictionary key. For example: 95 | # 96 | # config = { 97 | # 'key1': 98 | # 'value1', 99 | # 'key2': value1 + 100 | # value2, 101 | # } 102 | indent_dictionary_value=True 103 | 104 | # The number of columns to use for indentation. 105 | # indent_width=4 106 | 107 | # Join short lines into one line. E.g., single line 'if' statements. 108 | join_multiple_lines=False 109 | 110 | # Do not include spaces around selected binary operators. For example: 111 | # 112 | # 1 + 2 * 3 - 4 / 5 113 | # 114 | # will be formatted as follows when configured with a value "*,/": 115 | # 116 | # 1 + 2*3 - 4/5 117 | # 118 | # no_spaces_around_selected_binary_operators=set() 119 | 120 | # Use spaces around default or named assigns. 121 | # spaces_around_default_or_named_assign=False 122 | 123 | # Use spaces around the power operator. 124 | # spaces_around_power_operator=False 125 | 126 | # The number of spaces required before a trailing comment. 127 | # spaces_before_comment=2 128 | 129 | # Insert a space between the ending comma and closing bracket of a list, 130 | # etc. 131 | # space_between_ending_comma_and_closing_bracket=True 132 | 133 | # Split before arguments if the argument list is terminated by a 134 | # comma. 135 | # split_arguments_when_comma_terminated=False 136 | 137 | # Set to True to prefer splitting before '&', '|' or '^' rather than 138 | # after. 139 | # split_before_bitwise_operator=True 140 | 141 | # Split before a dictionary or set generator (comp_for). For example, note 142 | # the split before the 'for': 143 | # 144 | # foo = { 145 | # variable: 'Hello world, have a nice day!' 146 | # for variable in bar if variable != 42 147 | # } 148 | # split_before_dict_set_generator=True 149 | 150 | # If an argument / parameter list is going to be split, then split before 151 | # the first argument. 152 | split_before_first_argument=True 153 | 154 | # Set to True to prefer splitting before 'and' or 'or' rather than 155 | # after. 156 | # split_before_logical_operator=True 157 | 158 | # Split named assignments onto individual lines. 159 | # split_before_named_assigns=True 160 | 161 | # The penalty for splitting right after the opening bracket. 162 | # split_penalty_after_opening_bracket=30 163 | 164 | # The penalty for splitting the line after a unary operator. 165 | # split_penalty_after_unary_operator=10000 166 | 167 | # The penalty for splitting right before an if expression. 168 | # split_penalty_before_if_expr=0 169 | 170 | # The penalty of splitting the line around the '&', '|', and '^' 171 | # operators. 172 | # split_penalty_bitwise_operator=300 173 | 174 | # The penalty for characters over the column limit. 175 | # split_penalty_excess_character=4500 176 | 177 | # The penalty incurred by adding a line split to the unwrapped line. The 178 | # more line splits added the higher the penalty. 179 | # split_penalty_for_added_line_split=30 180 | 181 | # The penalty of splitting a list of "import as" names. For example: 182 | # 183 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 184 | # long_argument_2, 185 | # long_argument_3) 186 | # 187 | # would reformat to something like: 188 | # 189 | # from a_very_long_or_indented_module_name_yada_yad import ( 190 | # long_argument_1, long_argument_2, long_argument_3) 191 | # split_penalty_import_names=0 192 | 193 | # The penalty of splitting the line around the 'and' and 'or' 194 | # operators. 195 | # split_penalty_logical_operator=300 196 | 197 | # Use the Tab character for indentation. 198 | # use_tabs=False 199 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | python: 5 | - 2.7 6 | - 3.6 7 | - 3.7 8 | - nightly 9 | # - pypy 10 | 11 | install: 12 | - pip install --upgrade setuptools 13 | - pip install --upgrade pip pytest pytest-flake8 pytest-cov coveralls 14 | - pip install -e client 15 | - pip install -e server 16 | - pip install -e pytest_wdb 17 | 18 | script: py.test server/wdb_server/ client/wdb test pytest_wdb --flake8 --cov-report= --cov=client/wdb --cov=test --cov-config=.coveragerc 19 | 20 | after_success: coveralls 21 | 22 | sudo: true 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.3.0 2 | 3 | - Fix crash on startup 4 | - Upgrade node dependencies 5 | - Pin tornado version to 5.x.x 6 | 7 | # 3.2.5 8 | 9 | - Fix bad horizontal scrolling in case of long strings and on firefox. 10 | 11 | # 3.2.4 12 | 13 | - Merged #117 (thanks @akalipetis): 14 | - Fix crashes when websockets closed prematurely 15 | - Add a base class to handle new connection verification 16 | - Fix protocol interpolation in CoffeeScript 17 | 18 | # 3.2.3 19 | 20 | - Make websockets work over HTTPS #113 (thanks @akalipetis) 21 | - Document logging configuration (thanks @ptim) 22 | 23 | # 3.2.2 24 | 25 | - Don't crash on version request failure and lower the timeouts. Fix #112 26 | 27 | # 3.2.1 28 | 29 | - Fix some unicode handling in exception display. Fix #106 30 | 31 | # 3.2.0 32 | 33 | - Tornado 5 compatibility 34 | 35 | # 3.1.10 36 | 37 | - Force tornado < 5. This is the last release with pre tornado 5 compat. 38 | 39 | # 3.1.9 40 | 41 | - Replace filemagic with python-magic and fix dependencies 42 | 43 | # 3.1.8 44 | 45 | - Prevent uncompyle crash on unknown python version 46 | 47 | # 3.1.7 48 | 49 | - Minor dependency upgrade (importmagic -> importmagic3) 50 | 51 | # 3.1.6 52 | 53 | - Resolve NameError problem. Fix #101 54 | - Fix prompt scroll. Fix #103 55 | - Code style with yapf 56 | 57 | # 3.1.5 58 | 59 | - Fix websocket send method. . Make it work with tornado 4.5. Fix #97 60 | 61 | # 3.1.4 62 | 63 | - Use setuptools to set the **version**. Fix #96 64 | 65 | # 3.1.3 66 | 67 | - Don't execute suggestions when timeout is not available (windows/threads) and when it's not asked manually. Should fix #94 or at least work around it. 68 | 69 | # 3.1.2 70 | 71 | - Remove wheel to prevent pyinotify install by accident. #91 #78 72 | 73 | # 3.1.0 74 | 75 | - Finally quiet uncompyle6 76 | - Change default behavior for wdb script/**main** now doesn't trace script by default but implement sys excepthook instead. Use --trace for old behavior. 77 | - Dont fail out when ran under say a C-based WSGIHandler #87, #88 and Atomically set importmagic to avoid stampede #89 thanks @akatrevorjay 78 | - Add patch_werkzeug method for use with hook 79 | 80 | # 3.0.7 81 | 82 | - Add prompt ctrl+up/down command to include surrounding history 83 | - Prevent prompt autofocus when there is a selection 84 | - Avoid crash on empty code filename 85 | - Fix python 3 kwonly keyword args 86 | - Escape and linkify call/return args 87 | 88 | # 3.0.6 89 | 90 | - Greyscale while working (The spinner might be too subtle) 91 | 92 | # 3.0.5 93 | 94 | - Add --show-filename option to show filename in session list (thanks @wong2) 95 | - Support msys2 (thanks @manuelnaranjo) 96 | 97 | # 3.0.4 98 | 99 | - Fix long subtitle style (thanks @wong2) 100 | - Fix wdb-lite setup.py 101 | - Use release script 102 | 103 | # 3.0.1 104 | 105 | - Fix double evaluation 106 | - Don't send cursor position to external editor open when holding shift key 107 | 108 | # 3.0.0 109 | 110 | - A whole new material design lite interface (a bit responsive) 111 | - Clickable icons instead of hotkeys only 112 | - Visual distinction between stepping, post_mortem and shell 113 | - An actually readable help (you can get by clicking on the help icons) 114 | - A code mirror prompt with syntaxic coloration and more classic completion 115 | - New commands (open current file in external editor, respawn current process...) 116 | - A far better post mortem interaction when using ext and wdb is disabled. 117 | - `importmagic` suggestions on NameError 118 | - Use of `uncompyle6` to get python source when only byte-code is available 119 | 120 | # 2.1.0 121 | 122 | - New completion mechanism, should complete a lot better 123 | - Experimental object tree search (`.f` command) to look for a key or a value condition in an object 124 | - Media display in debugger (`.i` command) 125 | - New diff function to print differences between string / obj / files (`.x` command) 126 | - Timing for expression evaluation (remote and local) 127 | - Prompt exception inspection (click on it) 128 | - A new shell mode and an executable `wdb` 129 | - A new multiline prompt mode triggered by `[ctrl]` + `[enter]` 130 | - And a lot of bug fixes 131 | 132 | # 2.0.0 133 | 134 | - Source syntax highlighting 135 | - Visual breakpoints 136 | - Interactive code completion using [jedi](http://jedi.jedidjah.ch/) 137 | - Persistent breakpoints 138 | - Deep objects inspection using mouse 139 | - Multithreading / Multiprocessing support 140 | - Remote debugging 141 | - Watch expressions 142 | - In debugger code edition 143 | - Popular web servers integration to break on error 144 | - In exception breaking during trace (not post-mortem) in contrary to the werkzeug debugger for instance 145 | - Breaking in currently running programs through code injection (on supported systems) 146 | -------------------------------------------------------------------------------- /client/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | wdb 5 | """ 6 | import sys 7 | 8 | from setuptools import setup 9 | 10 | __version__ = '3.3.0' 11 | 12 | requires = [ 13 | "log_colorizer>=1.8.3", 14 | "jedi>=0.9.0", 15 | 'uncompyle6', 16 | 'python-magic>=0.4.15', 17 | ] 18 | 19 | if sys.version_info[:2] <= (2, 6): 20 | requires.append('argparse') 21 | requires.append('ordereddict') 22 | else: 23 | requires.append('importmagic3') 24 | 25 | options = dict( 26 | name="wdb", 27 | version=__version__, 28 | description="An improbable web debugger through WebSockets (client only)", 29 | long_description="See http://github.com/Kozea/wdb", 30 | author="Florian Mounier @ kozea", 31 | author_email="florian.mounier@kozea.fr", 32 | url="http://github.com/Kozea/wdb", 33 | license="GPLv3", 34 | platforms="Any", 35 | packages=['wdb'], 36 | install_requires=requires, 37 | entry_points={ 38 | 'console_scripts': [ 39 | 'wdb=wdb.__main__:main', 40 | 'wdb-%s=wdb.__main__:main' % sys.version[:3], 41 | ] 42 | }, 43 | classifiers=[ 44 | "Development Status :: 4 - Beta", 45 | "Intended Audience :: Developers", 46 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 47 | "Operating System :: OS Independent", 48 | "Programming Language :: Python :: 2", 49 | "Programming Language :: Python :: 3", 50 | "Programming Language :: Python :: Implementation :: CPython", 51 | "Programming Language :: Python :: Implementation :: PyPy", 52 | "Topic :: Software Development :: Debuggers", 53 | ], 54 | ) 55 | 56 | setup(**options) 57 | -------------------------------------------------------------------------------- /client/wdb/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from wdb import Wdb 6 | from wdb._compat import execute 7 | 8 | parser = argparse.ArgumentParser(description='Wdb, the web python debugger.') 9 | parser.add_argument( 10 | '--source', 11 | dest='source', 12 | help='Source the specified file before openning the shell', 13 | ) 14 | 15 | parser.add_argument( 16 | '--trace', 17 | dest='trace', 18 | action='store_true', 19 | help='Activate trace (otherwise just inspect tracebacks).', 20 | ) 21 | parser.add_argument('file', nargs='?', help='the path to the file to debug.') 22 | parser.add_argument('args', nargs='*', help='arguments to the debugged file.') 23 | 24 | 25 | def main(): 26 | """Wdb entry point""" 27 | sys.path.insert(0, os.getcwd()) 28 | args, extrargs = parser.parse_known_args() 29 | sys.argv = ['wdb'] + args.args + extrargs 30 | 31 | if args.file: 32 | file = os.path.join(os.getcwd(), args.file) 33 | if args.source: 34 | print('The source argument cannot be used with file.') 35 | sys.exit(1) 36 | 37 | if not os.path.exists(file): 38 | print('Error:', file, 'does not exist') 39 | sys.exit(1) 40 | if args.trace: 41 | Wdb.get().run_file(file) 42 | else: 43 | 44 | def wdb_pm(xtype, value, traceback): 45 | sys.__excepthook__(xtype, value, traceback) 46 | wdb = Wdb.get() 47 | wdb.reset() 48 | wdb.interaction(None, traceback, post_mortem=True) 49 | 50 | sys.excepthook = wdb_pm 51 | 52 | with open(file) as f: 53 | code = compile(f.read(), file, 'exec') 54 | execute(code, globals(), globals()) 55 | 56 | else: 57 | source = None 58 | if args.source: 59 | source = os.path.join(os.getcwd(), args.source) 60 | if not os.path.exists(source): 61 | print('Error:', source, 'does not exist') 62 | sys.exit(1) 63 | 64 | Wdb.get().shell(source) 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /client/wdb/breakpoint.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from hashlib import sha1 3 | from ._compat import import_module, logger 4 | 5 | log = logger('wdb.bp') 6 | 7 | 8 | def canonic(filename): 9 | if filename == "<" + filename[1:-1] + ">": 10 | return filename 11 | canonic = os.path.abspath(filename) 12 | canonic = os.path.normcase(canonic) 13 | if canonic.endswith(('.pyc', '.pyo')): 14 | canonic = canonic[:-1] 15 | return canonic 16 | 17 | 18 | def file_from_import(filename, function=None): 19 | try: 20 | module = import_module(filename) 21 | except ImportError: 22 | return filename 23 | if function is None: 24 | return module.__file__ 25 | fun = getattr(module, function, None) 26 | if not fun or not hasattr(fun, '__code__'): 27 | return filename 28 | return fun.__code__.co_filename 29 | 30 | 31 | class Breakpoint(object): 32 | """Simple breakpoint that breaks if in file""" 33 | 34 | def __init__(self, file, temporary=False): 35 | self.fn = file 36 | if not file.endswith(('.py', '.pyc', '.pyo')): 37 | file = file_from_import(file) 38 | self.file = canonic(file) 39 | self.temporary = temporary 40 | 41 | def on_file(self, filename): 42 | return canonic(filename) == self.file 43 | 44 | def breaks(self, frame): 45 | return self.on_file(frame.f_code.co_filename) 46 | 47 | def __repr__(self): 48 | s = 'Temporary ' if self.temporary else '' 49 | s += self.__class__.__name__ 50 | s += ' on file %s' % self.file 51 | return s 52 | 53 | def __eq__(self, other): 54 | return self.file == other.file and self.temporary == other.temporary 55 | 56 | def __hash__(self): 57 | s = sha1() 58 | s.update(repr(self).encode('utf-8')) 59 | return int(s.hexdigest(), 16) 60 | 61 | def to_dict(self): 62 | return { 63 | 'fn': self.file, 64 | 'lno': getattr(self, 'line', None), 65 | 'cond': getattr(self, 'condition', None), 66 | 'fun': getattr(self, 'function', None), 67 | 'temporary': self.temporary, 68 | } 69 | 70 | 71 | class LineBreakpoint(Breakpoint): 72 | """Simple breakpoint that breaks if in file at line""" 73 | 74 | def __init__(self, file, line, temporary=False): 75 | self.line = line 76 | super(LineBreakpoint, self).__init__(file, temporary) 77 | 78 | def breaks(self, frame): 79 | return ( 80 | super(LineBreakpoint, self).breaks(frame) 81 | and frame.f_lineno == self.line 82 | ) 83 | 84 | def __repr__(self): 85 | return ( 86 | super(LineBreakpoint, self).__repr__() + ' on line %d' % self.line 87 | ) 88 | 89 | def __eq__(self, other): 90 | return ( 91 | super(LineBreakpoint, self).__eq__(other) 92 | and self.line == other.line 93 | ) 94 | 95 | def __hash__(self): 96 | return super(LineBreakpoint, self).__hash__() 97 | 98 | 99 | class ConditionalBreakpoint(Breakpoint): 100 | """Breakpoint that breaks if condition is True at line in file""" 101 | 102 | def __init__(self, file, line, condition, temporary=False): 103 | self.line = line 104 | self.condition = condition 105 | super(ConditionalBreakpoint, self).__init__(file, temporary) 106 | 107 | def breaks(self, frame): 108 | try: 109 | return ( 110 | super(ConditionalBreakpoint, self).breaks(frame) 111 | and (self.line is None or frame.f_lineno == self.line) 112 | and eval(self.condition, frame.f_globals, frame.f_locals) 113 | ) 114 | except Exception: 115 | # Break in case of 116 | log.warning('Error in conditional break', exc_info=True) 117 | return True 118 | 119 | def __repr__(self): 120 | return ( 121 | super(ConditionalBreakpoint, self).__repr__() 122 | + ' under the condition %s' % self.condition 123 | ) 124 | 125 | def __eq__(self, other): 126 | return ( 127 | super(ConditionalBreakpoint, self).__eq__(other) 128 | and self.condition == other.condition 129 | ) 130 | 131 | def __hash__(self): 132 | return super(ConditionalBreakpoint, self).__hash__() 133 | 134 | 135 | class FunctionBreakpoint(Breakpoint): 136 | """Breakpoint that breaks if in file in function""" 137 | 138 | def __init__(self, file, function, temporary=False): 139 | self.function = function 140 | if not file.endswith(('.py', '.pyc', '.pyo')): 141 | file = file_from_import(file, function) 142 | self.file = canonic(file) 143 | self.temporary = temporary 144 | 145 | def breaks(self, frame): 146 | return ( 147 | super(FunctionBreakpoint, self).breaks(frame) 148 | and frame.f_code.co_name == self.function 149 | ) 150 | 151 | def __repr__(self): 152 | return ( 153 | super(FunctionBreakpoint, self).__repr__() 154 | + ' in function %s' % self.function 155 | ) 156 | 157 | def __eq__(self, other): 158 | return ( 159 | super(FunctionBreakpoint, self).__eq__(other) 160 | and self.function == other.function 161 | ) 162 | 163 | def __hash__(self): 164 | return super(FunctionBreakpoint, self).__hash__() 165 | -------------------------------------------------------------------------------- /client/wdb/state.py: -------------------------------------------------------------------------------- 1 | from .utils import pretty_frame 2 | 3 | 4 | class State(object): 5 | def __init__(self, frame): 6 | self.frame = frame 7 | 8 | def up(self): 9 | """Go up in stack and return True if top frame""" 10 | if self.frame: 11 | self.frame = self.frame.f_back 12 | return self.frame is None 13 | 14 | def __repr__(self): 15 | return '' % ( 16 | self.__class__.__name__, 17 | pretty_frame(self.frame), 18 | ) 19 | 20 | 21 | class Running(State): 22 | """Running state: never stopping""" 23 | 24 | def stops(self, frame, event): 25 | return False 26 | 27 | 28 | class Step(State): 29 | """Stepping state: always stopping""" 30 | 31 | def stops(self, frame, event): 32 | return True 33 | 34 | 35 | class Next(State): 36 | """Nexting state: stop if same frame""" 37 | 38 | def stops(self, frame, event): 39 | return self.frame == frame 40 | 41 | 42 | class Until(State): 43 | """Nexting until state: stop if same frame and is next line""" 44 | 45 | def __init__(self, frame, lno): 46 | self.frame = frame 47 | self.lno = lno + 1 48 | 49 | def stops(self, frame, event): 50 | return self.frame == frame and frame.f_lineno >= self.lno 51 | 52 | 53 | class Return(Next): 54 | """Returning state: Stop on return event if same frame""" 55 | 56 | def stops(self, frame, event): 57 | return self.frame == frame and event == 'return' 58 | -------------------------------------------------------------------------------- /flask_wdb_hook/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /flask_wdb_hook/README.md: -------------------------------------------------------------------------------- 1 | # Flask WDB Hook 2 | ## Replace flask werkzeug debugger with wdb 3 | 4 | [![](https://raw.github.com/Kozea/wdb/master/flask_wdb_hook/demo.gif)](https://raw.github.com/Kozea/wdb/master/flask_wdb_hook/demo.gif) 5 | 6 | 7 | ### Installation 8 | 9 | ```bash 10 | $ sudo pip install flask-wdb-hook 11 | $ export FLASK_WDB=1 12 | ``` 13 | 14 | ### How does it work 15 | 16 | This package only install a pth file in the site-packages directory which calls: 17 | ```python 18 | import wdb; \ 19 | from wdb.ext import WdbMiddleware; \ 20 | from werkzeug import debug; \ 21 | debug.DebuggedApplication = WdbMiddleware # This is so much a hack 22 | ``` 23 | 24 | As pth files contain either import path or python import statement, we abuse the import evaluation to patch the werkzeug debugger in the same statement. 25 | 26 | Et voilà. 27 | -------------------------------------------------------------------------------- /flask_wdb_hook/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/flask_wdb_hook/demo.gif -------------------------------------------------------------------------------- /flask_wdb_hook/flask-wdb.pth: -------------------------------------------------------------------------------- 1 | import os; exec("try:\n if 'FLASK_WDB' in os.environ:import wdb.ext;wdb.ext.patch_werkzeug()\nexcept:print('An error occured while automatically hooking wdb into flask.');") 2 | -------------------------------------------------------------------------------- /flask_wdb_hook/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from distutils.sysconfig import get_python_lib 4 | 5 | from setuptools import setup 6 | 7 | site_packages_path = get_python_lib().replace(sys.prefix + os.path.sep, '') 8 | 9 | setup( 10 | name="flask-wdb-hook", 11 | version='0.2.1', 12 | author="Florian Mounier @ kozea", 13 | author_email="florian.mounier@kozea.fr", 14 | url="http://github.com/Kozea/wdb", 15 | license='GPLv3', 16 | packages=[], 17 | install_requires=['wdb >= 3.3.0'], 18 | data_files=[(site_packages_path, ['flask-wdb.pth'])], 19 | description="Hook to replace flask werkzeug debugger with wdb.", 20 | ) 21 | -------------------------------------------------------------------------------- /importtest.py: -------------------------------------------------------------------------------- 1 | 1 / 0 2 | -------------------------------------------------------------------------------- /pytest_wdb/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /pytest_wdb/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | ===== 3 | 4 | * wdb seems like a good install requirement! 5 | -------------------------------------------------------------------------------- /pytest_wdb/pytest_wdb.py: -------------------------------------------------------------------------------- 1 | """Wdb plugin for pytest.""" 2 | import wdb 3 | 4 | 5 | def pytest_addoption(parser): 6 | parser.addoption( 7 | "--wdb", 8 | action="store_true", 9 | help="Trace tests with wdb to halt on error.", 10 | ) 11 | 12 | 13 | def pytest_configure(config): 14 | if config.option.wdb: 15 | config.pluginmanager.register(Trace(), '_wdb') 16 | config.pluginmanager.unregister(name='pdb') 17 | 18 | 19 | class Trace(object): 20 | def pytest_collection_modifyitems(config, items): 21 | for item in items: 22 | item.obj = wdb.with_trace(item.obj) 23 | -------------------------------------------------------------------------------- /pytest_wdb/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="pytest_wdb", 5 | version='0.4.0', 6 | author="Florian Mounier @ kozea", 7 | author_email="florian.mounier@kozea.fr", 8 | url="http://github.com/Kozea/wdb", 9 | license='GPLv3', 10 | py_modules=['pytest_wdb'], 11 | install_requires=['wdb'], 12 | description="Trace pytest tests with wdb to halt on error with --wdb.", 13 | entry_points={'pytest11': ['pytest_wdb = pytest_wdb']}, 14 | ) 15 | -------------------------------------------------------------------------------- /pytest_wdb/test_wdb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from multiprocessing import Process, Lock 3 | from multiprocessing.connection import Listener 4 | import wdb 5 | 6 | pytest_plugins = ('pytester',) 7 | 8 | 9 | class FakeWdbServer(Process): 10 | def __init__(self, stops=False): 11 | wdb.SOCKET_SERVER = 'localhost' 12 | wdb.SOCKET_PORT = 18273 13 | wdb.WDB_NO_BROWSER_AUTO_OPEN = True 14 | self.stops = stops 15 | self.lock = Lock() 16 | super(FakeWdbServer, self).__init__() 17 | 18 | def __enter__(self): 19 | self.start() 20 | self.lock.acquire() 21 | 22 | def __exit__(self, *args): 23 | self.lock.release() 24 | self.join() 25 | wdb.Wdb.pop() 26 | 27 | def run(self): 28 | listener = Listener(('localhost', 18273)) 29 | try: 30 | listener._listener._socket.settimeout(10) 31 | except Exception: 32 | pass 33 | connection = listener.accept() 34 | # uuid 35 | connection.recv_bytes().decode('utf-8') 36 | # ServerBreaks 37 | connection.recv_bytes().decode('utf-8') 38 | # Empty breaks 39 | connection.send_bytes(b'{}') 40 | # Continuing 41 | if self.stops: 42 | connection.recv_bytes().decode('utf-8') 43 | connection.send_bytes(b'Continue') 44 | 45 | self.lock.acquire() 46 | connection.close() 47 | listener.close() 48 | self.lock.release() 49 | 50 | 51 | def test_ok(testdir): 52 | p = testdir.makepyfile( 53 | ''' 54 | def test_run(): 55 | print('Test has been run') 56 | ''' 57 | ) 58 | with FakeWdbServer(): 59 | result = testdir.runpytest_inprocess('--wdb', p) 60 | result.stdout.fnmatch_lines(['plugins:*wdb*']) 61 | assert result.ret == 0 62 | 63 | 64 | def test_ok_run_once(testdir): 65 | p = testdir.makepyfile( 66 | ''' 67 | def test_run(): 68 | print('Test has been run') 69 | ''' 70 | ) 71 | 72 | with FakeWdbServer(): 73 | result = testdir.runpytest_inprocess('--wdb', '-s', p) 74 | 75 | assert ( 76 | len( 77 | [ 78 | line 79 | for line in result.stdout.lines 80 | if line == 'test_ok_run_once.py Test has been run' 81 | ] 82 | ) 83 | == 1 84 | ) 85 | assert result.ret == 0 86 | 87 | 88 | # Todo implement fake wdb server 89 | 90 | 91 | def test_fail_run_once(testdir): 92 | p = testdir.makepyfile( 93 | ''' 94 | def test_run(): 95 | print('Test has been run') 96 | assert 0 97 | ''' 98 | ) 99 | with FakeWdbServer(stops=True): 100 | result = testdir.runpytest_inprocess('--wdb', '-s', p) 101 | assert ( 102 | len( 103 | [ 104 | line 105 | for line in result.stdout.lines 106 | if line == 'test_fail_run_once.py Test has been run' 107 | ] 108 | ) 109 | == 1 110 | ) 111 | assert result.ret == 1 112 | 113 | 114 | def test_error_run_once(testdir): 115 | p = testdir.makepyfile( 116 | ''' 117 | def test_run(): 118 | print('Test has been run') 119 | 1/0 120 | ''' 121 | ) 122 | with FakeWdbServer(stops=True): 123 | result = testdir.runpytest_inprocess('--wdb', '-s', p) 124 | assert ( 125 | len( 126 | [ 127 | line 128 | for line in result.stdout.lines 129 | if line == 'test_error_run_once.py Test has been run' 130 | ] 131 | ) 132 | == 1 133 | ) 134 | assert result.ret == 1 135 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | OLD_VERSION=$1 5 | OLD_VERSION_RE=$(echo $OLD_VERSION | sed "s/\./\\\./g") 6 | NEW_VERSION=$2 7 | echo "$OLD_VERSION -> $NEW_VERSION" 8 | 9 | git ls-files | grep -E ".+\.coffee|.+\.json|.+\.py" | xargs sed -i -e "s/$OLD_VERSION_RE/$NEW_VERSION/g" 10 | pushd server 11 | grunt 12 | # python setup.py sdist bdist_wheel --universal --plat-name=linux-x86_64 upload 13 | python setup.py sdist 14 | twine upload dist/* 15 | popd 16 | 17 | pushd client 18 | # python setup.py sdist bdist_wheel --universal --plat-name=linux-x86_64 upload 19 | python setup.py sdist 20 | twine upload dist/* 21 | popd 22 | 23 | sed -i "s/$OLD_VERSION/$NEW_VERSION/g" server/Dockerfile 24 | 25 | git commit -am "Bump Version $OLD_VERSION -> $NEW_VERSION" 26 | git tag $NEW_VERSION 27 | git push 28 | git push --tags 29 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | wdb_server/static/stylesheets/deps.embed.css 2 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | ARG WDB_VERSION="3.3.0" 4 | 5 | RUN pip install wdb.server==$WDB_VERSION 6 | EXPOSE 19840 7 | EXPOSE 1984 8 | CMD ["wdb.server.py", "--detached_session"] 9 | -------------------------------------------------------------------------------- /server/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | require('time-grunt') grunt 3 | require('load-grunt-tasks')(grunt) 4 | jsdeps = [ 5 | 'bower_components/jquery/dist/jquery.min.js' 6 | 'bower_components/codemirror/lib/codemirror.js' 7 | 'bower_components/codemirror/addon/runmode/runmode.js' 8 | 'bower_components/codemirror/addon/dialog/dialog.js' 9 | 'bower_components/codemirror/addon/search/searchcursor.js' 10 | 'bower_components/codemirror/addon/search/jump-to-line.js' 11 | 'bower_components/codemirror/addon/search/search.js' 12 | 'bower_components/codemirror/addon/hint/show-hint.js' 13 | 'bower_components/codemirror/addon/edit/matchbrackets.js' 14 | 'bower_components/codemirror/mode/python/python.js' 15 | 'bower_components/codemirror/mode/jinja2/jinja2.js' 16 | 'bower_components/codemirror/mode/diff/diff.js' 17 | ] 18 | cssdeps = [ 19 | 'bower_components/codemirror/lib/codemirror.css' 20 | 'bower_components/codemirror/theme/material.css' 21 | 'bower_components/codemirror/addon/dialog/dialog.css' 22 | 'bower_components/codemirror/addon/hint/show-hint.css' 23 | ] 24 | 25 | grunt.initConfig 26 | pkg: grunt.file.readJSON('package.json') 27 | fileExists: 28 | jsdeps: jsdeps 29 | cssdeps: cssdeps 30 | 31 | uglify: 32 | options: 33 | banner: '/*! <%= pkg.name %> 34 | <%= grunt.template.today("yyyy-mm-dd") %> */\n' 35 | 36 | wdb: 37 | expand: true 38 | cwd: 'wdb_server/static/javascripts' 39 | src: '*.js' 40 | dest: 'wdb_server/static/javascripts/wdb/' 41 | ext: '.min.js' 42 | 43 | deps: 44 | files: 45 | 'wdb_server/static/javascripts/wdb/deps.min.js': jsdeps 46 | 47 | sass: 48 | options: 49 | implementation: require('dart-sass') 50 | wdb: 51 | expand: true 52 | cwd: 'sass/' 53 | src: '*.sass' 54 | dest: 'wdb_server/static/stylesheets/' 55 | ext: '.css' 56 | 57 | cssmin: 58 | codemirror: 59 | files: 60 | 'wdb_server/static/stylesheets/deps.min.css': cssdeps 61 | 62 | coffee: 63 | options: 64 | bare: true 65 | join: true 66 | wdb: 67 | files: 68 | 'wdb_server/static/javascripts/wdb.js': [ 69 | 'coffees/_compat.coffee' 70 | 'coffees/_base.coffee' 71 | 'coffees/_websocket.coffee' 72 | 'coffees/_source.coffee' 73 | 'coffees/_history.coffee' 74 | 'coffees/_traceback.coffee' 75 | 'coffees/_interpreter.coffee' 76 | 'coffees/_prompt.coffee' 77 | 'coffees/_watchers.coffee' 78 | 'coffees/_switch.coffee' 79 | 'coffees/_help.coffee' 80 | 'coffees/wdb.coffee' 81 | ] 82 | 83 | status: 84 | files: 85 | 'wdb_server/static/javascripts/home.js': [ 86 | 'coffees/_base.coffee' 87 | 'coffees/home.coffee' 88 | ] 89 | 90 | 91 | coffeelint: 92 | wdb: 93 | 'coffees/*.coffee' 94 | 95 | bower: 96 | options: 97 | copy: false 98 | 99 | install: {} 100 | 101 | watch: 102 | options: 103 | livereload: true 104 | 105 | coffee: 106 | files: [ 107 | 'coffees/*.coffee' 108 | 'Gruntfile.coffee' 109 | ] 110 | tasks: ['coffeelint', 'coffee'] 111 | 112 | sass: 113 | files: [ 114 | 'sass/*.sass' 115 | ] 116 | tasks: ['sass'] 117 | 118 | grunt.registerTask 'dev', ['coffeelint', 'coffee', 'watch'] 119 | grunt.registerTask 'css', ['sass'] 120 | grunt.registerTask 'default', [ 121 | 'coffeelint', 'coffee', 122 | 'sass', 123 | 'bower', 'fileExists', 124 | 'uglify', 'cssmin'] 125 | -------------------------------------------------------------------------------- /server/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdb", 3 | "version": "3.3.0", 4 | "homepage": "https://github.com/Kozea/wdb", 5 | "authors": [ 6 | "Florian Mounier " 7 | ], 8 | "description": "An improbable python web debugger through WebSockets", 9 | "license": "GPLv3", 10 | "private": true, 11 | "dependencies": { 12 | "jquery": "~3.2.1", 13 | "codemirror": "~5.30.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/coffees/_base.coffee: -------------------------------------------------------------------------------- 1 | # This file is part of wdb 2 | # 3 | # wdb Copyright (c) 2012-2016 Florian Mounier, Kozea 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | class Log 18 | constructor: -> 19 | @debug = $('body').attr('data-debug') or false 20 | 21 | time: -> 22 | date = new Date() 23 | "#{date.getHours()}:#{date.getMinutes()}:" + 24 | "#{date.getSeconds()}.#{date.getMilliseconds()}" 25 | 26 | log: -> 27 | if @debug 28 | name = "[#{@constructor.name}] (#{@time()})" 29 | log_args = [name].concat Array.prototype.slice.call(arguments, 0) 30 | console.log.apply console, log_args 31 | 32 | dbg: -> 33 | if @debug 34 | name = "[#{@constructor.name}] (#{@time()})" 35 | log_args = [name].concat Array.prototype.slice.call(arguments, 0) 36 | console.debug.apply console, log_args 37 | 38 | fail: -> 39 | name = @constructor.name 40 | log_args = [name].concat Array.prototype.slice.call(arguments, 0) 41 | console.error.apply console, log_args 42 | -------------------------------------------------------------------------------- /server/coffees/_compat.coffee: -------------------------------------------------------------------------------- 1 | if !String::startsWith 2 | String::startsWith = (searchString, position) -> 3 | position = position or 0 4 | @substr(position, searchString.length) == searchString 5 | 6 | unless document.createElement('dialog').showModal 7 | $ -> 8 | $('head').append $(' 20 | 21 | 22 | 23 | {% from wdb_server import StyleHandler %} 24 | {% block css %} 25 | {% end %} 26 | 27 | 28 |
29 | {% block main %} 30 | {% end %} 31 |
32 | {% block script %}{% end %} 33 | 34 | 35 | -------------------------------------------------------------------------------- /server/wdb_server/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends _layout.html %} 2 | 3 | {% block script %} 4 | 5 | {% end %} 6 | 7 | {% block css %} 8 | 9 | {% end %} 10 | 11 | {% block main %} 12 | 13 |
14 |
15 |
16 | 17 | wdb 18 |
19 | 20 |
21 | Active sessions 22 | Breakpoints 23 | Processes 24 | Trace file 25 | 26 | {% if StyleHandler.themes %} 27 | Styles 28 | {% end %} 29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | {% if options.show_filename %} 38 | 39 | {% end %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
FileSession idSocket openWebsocket openAction
49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
FileLine numberConditionFunctionAction
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
PidUserCommand LineTimeMemoryCpuThread IDAction
86 | 96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 |
105 | 108 |
109 |
110 |
111 | 112 | {% if StyleHandler.themes %} 113 |
114 |
115 |
116 | 121 | 122 |
123 |
124 |
125 | {% end %} 126 |
127 | 130 |
131 | 132 | 133 | 134 | {% end %} 135 | -------------------------------------------------------------------------------- /server/wdb_server/templates/wdb.html: -------------------------------------------------------------------------------- 1 | {% extends _layout.html %} 2 | 3 | {% block script %} 4 | 5 | {% end %} 6 | 7 | {% block css %} 8 | 9 | {% end %} 10 | 11 | {% block main %} 12 |
13 |
14 |
15 | 16 | Wdb is tracing your code, this may take some time. 17 | 18 |
19 | 20 | {% if type_ != 'pm' %} 21 | 24 |
Step into
25 | 26 | 29 |
Next
30 | 31 | 34 |
Until
35 | 36 | 39 |
Return
40 | 41 | 44 |
Continue
45 | 46 |
47 | {% end %} 48 | 49 | 50 | home 51 | 52 |
Open wdb home in a new window
53 | 54 | 57 |
Toggle code view
58 | 59 | 62 |
Toggle prompt view
63 | 64 | 67 |
Help
68 | 69 | {% if type_ == 'pm' %} 70 | 73 |
Refresh request with tracing enabled
74 | {% else %} 75 | 78 |
Stop tracing
79 | {% end %} 80 |
81 |
82 | 83 |
84 | 85 | Tracing 86 | 87 | 89 |
90 | 91 |
92 |
93 | 95 | 96 | 107 |
108 | 127 |
128 |
129 | 130 |
131 | {% end %} 132 | -------------------------------------------------------------------------------- /server/wdb_server/utils.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | # This file is part of wdb 3 | # 4 | # wdb Copyright (c) 2012-2016 Florian Mounier, Kozea 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import fnmatch 19 | import os 20 | from glob import glob 21 | from logging import getLogger 22 | 23 | import psutil 24 | from tornado.ioloop import IOLoop 25 | from tornado.options import options 26 | from wdb_server.state import syncwebsockets 27 | 28 | log = getLogger('wdb_server') 29 | log.setLevel(10 if options.debug else 30) 30 | 31 | ioloop = IOLoop.current() 32 | 33 | try: 34 | import pyinotify 35 | except ImportError: 36 | LibPythonWatcher = None 37 | else: 38 | 39 | class LibPythonWatcher(object): 40 | def __init__(self, extra_search_path=None): 41 | inotify = pyinotify.WatchManager() 42 | self.files = glob('/usr/lib/libpython*') 43 | if not self.files: 44 | self.files = glob('/lib/libpython*') 45 | 46 | if extra_search_path is not None: 47 | # Handle custom installation paths 48 | for root, dirnames, filenames in os.walk(extra_search_path): 49 | for filename in fnmatch.filter(filenames, 'libpython*'): 50 | self.files.append(os.path.join(root, filename)) 51 | 52 | log.debug('Watching for %s' % self.files) 53 | self.notifier = pyinotify.TornadoAsyncNotifier( 54 | inotify, ioloop, self.notified, pyinotify.ProcessEvent() 55 | ) 56 | inotify.add_watch( 57 | self.files, 58 | pyinotify.EventsCodes.ALL_FLAGS['IN_OPEN'] 59 | | pyinotify.EventsCodes.ALL_FLAGS['IN_CLOSE_NOWRITE'], 60 | ) 61 | 62 | def notified(self, notifier): 63 | log.debug('Got notified for %s' % self.files) 64 | refresh_process() 65 | log.debug('Process refreshed') 66 | 67 | def close(self): 68 | log.debug('Closing for %s' % self.files) 69 | self.notifier.stop() 70 | 71 | 72 | def refresh_process(uuid=None): 73 | if uuid is not None: 74 | send = lambda cmd, data: syncwebsockets.send(uuid, cmd, data) 75 | else: 76 | send = syncwebsockets.broadcast 77 | 78 | remaining_pids = [] 79 | remaining_tids = [] 80 | for proc in psutil.process_iter(): 81 | try: 82 | cl = proc.cmdline() 83 | except ( 84 | psutil.ZombieProcess, 85 | psutil.AccessDenied, 86 | psutil.NoSuchProcess, 87 | ): 88 | continue 89 | else: 90 | if len(cl) == 0: 91 | continue 92 | 93 | binary = cl[0].split('/')[-1] 94 | if ( 95 | ('python' in binary or 'pypy' in binary) 96 | and proc.is_running() 97 | and proc.status() != psutil.STATUS_ZOMBIE 98 | ): 99 | try: 100 | try: 101 | cpu = proc.cpu_percent(interval=0.01) 102 | send( 103 | 'AddProcess', 104 | { 105 | 'pid': proc.pid, 106 | 'user': proc.username(), 107 | 'cmd': ' '.join(proc.cmdline()), 108 | 'threads': proc.num_threads(), 109 | 'time': proc.create_time(), 110 | 'mem': proc.memory_percent(), 111 | 'cpu': cpu, 112 | }, 113 | ) 114 | remaining_pids.append(proc.pid) 115 | for thread in proc.threads(): 116 | send('AddThread', {'id': thread.id, 'of': proc.pid}) 117 | remaining_tids.append(thread.id) 118 | except psutil.NoSuchProcess: 119 | pass 120 | except Exception: 121 | log.warn('', exc_info=True) 122 | continue 123 | 124 | send('KeepProcess', remaining_pids) 125 | send('KeepThreads', remaining_tids) 126 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | norecursedirs = 3 | server/node_modules 4 | server/build 5 | flake8-ignore = 6 | *.py E731 E402 W503 E203 7 | client/wdb/_compat.py F401 F821 8 | test/scripts/*.py ALL 9 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/test/__init__.py -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process 2 | from multiprocessing.connection import Listener 3 | from log_colorizer import get_color_logger 4 | from pytest import fixture 5 | from pytest import hookimpl 6 | import pickle 7 | import logging 8 | import signal 9 | import json 10 | import os 11 | import sys 12 | 13 | 14 | def u(s): 15 | if sys.version_info[0] == 2: 16 | return s.decode('utf-8') 17 | return s 18 | 19 | 20 | log = get_color_logger('wdb.test') 21 | log.info('Conftest') 22 | log.setLevel(getattr(logging, os.getenv('WDB_TEST_LOG', 'WARNING'))) 23 | GLOBALS = globals() 24 | LOCALS = locals() 25 | 26 | 27 | class Slave(Process): 28 | def __init__(self, use, host='localhost', port=19999): 29 | self.argv = None 30 | self.file = os.path.join( 31 | os.path.dirname(__file__), 'scripts', use.file 32 | ) 33 | 34 | if use.with_main: 35 | self.argv = ['', '--trace', self.file] 36 | self.file = os.path.join( 37 | os.path.dirname(__file__), '..', 'client', 'wdb', '__main__.py' 38 | ) 39 | 40 | self.host = host 41 | self.port = port 42 | super(Slave, self).__init__() 43 | 44 | def run(self): 45 | import wdb 46 | 47 | wdb.SOCKET_SERVER = self.host 48 | wdb.SOCKET_PORT = self.port 49 | wdb.WDB_NO_BROWSER_AUTO_OPEN = True 50 | sys.argv = self.argv 51 | 52 | with open(self.file, 'rb') as file: 53 | LOCALS['__name__'] = '__main__' 54 | exec(compile(file.read(), self.file, 'exec'), GLOBALS, LOCALS) 55 | 56 | 57 | class AttrDict(dict): 58 | def __init__(self, *args, **kwargs): 59 | super(AttrDict, self).__init__(*args, **kwargs) 60 | self.__dict__ = self 61 | 62 | 63 | class Message(object): 64 | def __init__(self, message): 65 | log.info('Received %s' % message) 66 | pickled = False 67 | if message.startswith('Server|'): 68 | message = message.replace('Server|', '') 69 | pickled = True 70 | if '|' in message: 71 | pipe = message.index('|') 72 | self.command, self.data = message[:pipe], message[pipe + 1 :] 73 | if pickled and self.data: 74 | self.data = pickle.loads(self.data.encode('utf-8'), protocol=2) 75 | else: 76 | self.data = json.loads(self.data, object_hook=AttrDict) 77 | else: 78 | self.command, self.data = message, '' 79 | 80 | 81 | class Socket(object): 82 | def __init__(self, testfile, host='localhost', port=19999): 83 | self.slave = Slave(testfile, host, port) 84 | self.slave.start() 85 | self.started = False 86 | self.host = host 87 | self.port = port 88 | self.connections = {} 89 | self.listener = None 90 | 91 | def connection(self, uuid): 92 | if uuid is None and len(self.connections) == 1: 93 | return list(self.connections.values())[0] 94 | else: 95 | return self.connections[uuid] 96 | 97 | def start(self): 98 | log.info('Accepting') 99 | if not self.listener: 100 | self.listener = Listener((self.host, self.port)) 101 | try: 102 | connection = self.listener.accept() 103 | except Exception: 104 | self.listener.close() 105 | raise 106 | self.started = True 107 | 108 | log.info('Connection get') 109 | uuid = connection.recv_bytes().decode('utf-8') 110 | self.connections[uuid] = connection 111 | msg = self.receive(uuid) 112 | assert msg.command == 'ServerBreaks' 113 | self.send('[]', uuid=uuid) 114 | self.send('Start', uuid=uuid) 115 | return uuid 116 | 117 | def assert_init(self): 118 | assert self.receive().command == 'Init' 119 | assert self.receive().command == 'Title' 120 | assert self.receive().command == 'Trace' 121 | assert self.receive().command == 'SelectCheck' 122 | echo_watched = self.receive().command 123 | if echo_watched == 'Echo': 124 | echo_watched = self.receive().command 125 | assert echo_watched == 'Watched' 126 | 127 | def assert_position( 128 | self, 129 | title=None, 130 | subtitle=None, 131 | file=None, 132 | code=None, 133 | function=None, 134 | line=None, 135 | breaks=None, 136 | call=None, 137 | return_=None, 138 | exception=None, 139 | bottom_code=None, 140 | bottom_line=None, 141 | ): 142 | titlemsg = self.receive() 143 | assert titlemsg.command == 'Title' 144 | if title is not None: 145 | assert titlemsg.data.title == title 146 | if subtitle is not None: 147 | assert titlemsg.data.subtitle == subtitle 148 | tracemsg = self.receive() 149 | 150 | assert tracemsg.command == 'Trace' 151 | current = tracemsg.data.trace[-1] 152 | 153 | if file is not None: 154 | assert current.file == file 155 | if code is not None: 156 | assert current.code == code 157 | if function is not None: 158 | assert current.function == function 159 | if line is not None: 160 | assert current.lno == line 161 | 162 | for frame in tracemsg.data.trace: 163 | if frame.file == self.slave.file: 164 | break 165 | 166 | if bottom_code is not None: 167 | assert frame.code == bottom_code 168 | 169 | if bottom_line is not None: 170 | assert frame.lno == bottom_line 171 | 172 | selectmsg = self.receive() 173 | assert selectmsg.command == 'SelectCheck' 174 | 175 | if any((exception, call, return_)): 176 | echomsg = self.receive() 177 | assert echomsg.command == 'Echo' 178 | 179 | if exception: 180 | assert echomsg.data['for'] == '__exception__' 181 | assert exception in echomsg.data.val 182 | 183 | if call: 184 | assert echomsg.data['for'] == '__call__' 185 | assert call in echomsg.data.val 186 | 187 | if return_: 188 | assert echomsg.data['for'] == '__return__' 189 | assert return_ in echomsg.data.val 190 | 191 | watchedmsg = self.receive() 192 | assert watchedmsg.command == 'Watched' 193 | 194 | def receive(self, uuid=None): 195 | got = self.connection(uuid).recv_bytes().decode('utf-8') 196 | if got == 'PING' or got.startswith('UPDATE_FILENAME'): 197 | return self.receive(uuid) 198 | return Message(got) 199 | 200 | def send(self, command, data=None, uuid=None): 201 | message = '%s|%s' % (command, data) if data else command 202 | log.info('Sending %s' % message) 203 | self.connection(uuid).send_bytes(message.encode('utf-8')) 204 | 205 | def join(self): 206 | self.slave.join() 207 | 208 | def close(self, failed=False): 209 | slave_was_alive = False 210 | if self.slave.is_alive(): 211 | self.slave.terminate() 212 | slave_was_alive = True 213 | 214 | if self.started: 215 | for connection in self.connections.values(): 216 | connection.close() 217 | self.listener.close() 218 | 219 | if slave_was_alive and not failed: 220 | raise Exception('Tests must join the subprocess') 221 | 222 | 223 | class use(object): 224 | def __init__(self, file, with_main=False): 225 | self.file = file 226 | self.with_main = with_main 227 | 228 | def __call__(self, fun): 229 | fun._wdb_use = self 230 | return fun 231 | 232 | 233 | def timeout_handler(signum, frame): 234 | raise Exception('Timeout') 235 | 236 | 237 | signal.signal(signal.SIGALRM, timeout_handler) 238 | 239 | 240 | @fixture(scope="function") 241 | def socket(request): 242 | log.info('Fixture') 243 | socket = Socket( 244 | request.function._wdb_use, 245 | port=sys.hexversion % 60000 246 | + 1024 247 | + (1 if hasattr(sys, 'pypy_version_info') else 0), 248 | ) 249 | 250 | # If it takes more than 5 seconds, it must be an error 251 | if not os.getenv('NO_WDB_TIMEOUT'): 252 | signal.alarm(5) 253 | 254 | def end_socket(): 255 | failed = False 256 | if hasattr(request.node, 'rep_call'): 257 | failed = request.node.rep_call.failed 258 | socket.close(failed) 259 | signal.alarm(0) 260 | 261 | request.addfinalizer(end_socket) 262 | return socket 263 | 264 | 265 | @hookimpl(hookwrapper=True) 266 | def pytest_runtest_makereport(item, call): 267 | """Give test status information to finalizer""" 268 | outcome = yield 269 | rep = outcome.get_result() 270 | setattr(item, "rep_" + rep.when, rep) 271 | -------------------------------------------------------------------------------- /test/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/test/scripts/__init__.py -------------------------------------------------------------------------------- /test/scripts/bad_script.py: -------------------------------------------------------------------------------- 1 | def broken(a): 2 | a / 0 3 | 4 | a = 3 5 | b = 5 6 | c = a + b 7 | 8 | broken(c) 9 | -------------------------------------------------------------------------------- /test/scripts/error_ignored_in_script.py: -------------------------------------------------------------------------------- 1 | import wdb 2 | 3 | 4 | def divide_by_zero(z): 5 | return z / 0 6 | 7 | 8 | def with_trace_fun(): 9 | a = 2 10 | b = 4 11 | c = a + b 12 | print(c) 13 | try: 14 | d = divide_by_zero(c) 15 | except ZeroDivisionError: 16 | d = -1 17 | 18 | print(d) 19 | print('The end') 20 | 21 | wdb.start_trace() 22 | with_trace_fun() 23 | wdb.stop_trace() 24 | -------------------------------------------------------------------------------- /test/scripts/error_in_script.py: -------------------------------------------------------------------------------- 1 | import wdb 2 | 3 | 4 | def divide_by_zero(z): 5 | return z / 0 6 | 7 | 8 | def with_trace_fun(): 9 | a = 2 10 | b = 4 11 | c = a + b 12 | print(c) 13 | d = divide_by_zero(c) 14 | print(d) 15 | print('The end') 16 | 17 | wdb.start_trace() 18 | try: 19 | with_trace_fun() 20 | finally: 21 | wdb.stop_trace() 22 | -------------------------------------------------------------------------------- /test/scripts/error_in_with.py: -------------------------------------------------------------------------------- 1 | from wdb import trace 2 | 3 | 4 | def make_error(i): 5 | try: 6 | return i / 0 7 | except ZeroDivisionError: 8 | return 2 9 | 10 | with trace(): 11 | a = 2 12 | b = 4 13 | c = a + b 14 | print(c) 15 | d = make_error(c) 16 | print(d) 17 | 18 | with trace(full=True): 19 | a = 2 20 | b = 4 21 | c = a + b 22 | print(c) 23 | d = make_error(c) 24 | print(d + a) 25 | 26 | 27 | print('The end') 28 | -------------------------------------------------------------------------------- /test/scripts/error_in_with_advanced.py: -------------------------------------------------------------------------------- 1 | from wdb import trace 2 | 3 | 4 | def make_error(i): 5 | return i / 0 6 | 7 | 8 | def parent(): 9 | a = 1 10 | try: 11 | b = make_error(a) 12 | except ZeroDivisionError: 13 | b = 1 14 | c = 3 * b 15 | return c 16 | 17 | 18 | def grandparent(): 19 | a = 2 20 | b = parent() 21 | c = a * b 22 | return c 23 | 24 | 25 | with trace(): 26 | parent() 27 | 28 | with trace(full=True): 29 | parent() 30 | 31 | with trace(): 32 | grandparent() 33 | 34 | with trace(full=True): 35 | grandparent() 36 | 37 | print('The end') 38 | -------------------------------------------------------------------------------- /test/scripts/error_in_with_below.py: -------------------------------------------------------------------------------- 1 | from wdb import trace 2 | 3 | 4 | def catched_exception(below): 5 | try: 6 | return below / 0 7 | except ZeroDivisionError: 8 | return 2 9 | 10 | 11 | def uncatched_exception(below): 12 | return below / 0 13 | 14 | 15 | def uninteresting_function(below): 16 | b = catched_exception(below) 17 | return b 18 | 19 | 20 | def uninteresting_function_not_catching(below): 21 | b = uncatched_exception(below) 22 | return b 23 | 24 | 25 | def uninteresting_function_catching(below): 26 | try: 27 | b = uncatched_exception(below) 28 | except ZeroDivisionError: 29 | b = 2 30 | return b 31 | 32 | 33 | def one_more_step(fun, below): 34 | return fun(below) 35 | 36 | 37 | # This should not stop 38 | # below = 1 so in trace exception should be ignored 39 | with trace(below=1): 40 | try: 41 | raise Exception('Catched Exception') 42 | except Exception: 43 | pass 44 | 45 | # This should not stop 46 | # below = 1 so catched function 2 layer under are ignored 47 | with trace(below=1): 48 | uninteresting_function(1) 49 | 50 | # This should stop 51 | # below = 1 the exception in catched exception should stop trace 52 | with trace(below=1): 53 | try: 54 | uninteresting_function_not_catching(1) 55 | except: 56 | pass 57 | 58 | # This should stop 59 | # below = 1 the function 2 layer under raised an exception 60 | with trace(below=1): 61 | uninteresting_function_catching(1) 62 | 63 | 64 | # This should not stop neither 65 | with trace(below=2): 66 | try: 67 | raise Exception('Catched Exception') 68 | except Exception: 69 | pass 70 | 71 | # This should not stop 72 | with trace(below=2): 73 | one_more_step(uninteresting_function, 2) 74 | 75 | # This should stop 76 | with trace(below=2): 77 | try: 78 | one_more_step(uninteresting_function_not_catching, 2) 79 | except: 80 | pass 81 | 82 | # This should stop 83 | with trace(below=2): 84 | one_more_step(uninteresting_function_catching, 2) 85 | -------------------------------------------------------------------------------- /test/scripts/error_in_with_below_under.py: -------------------------------------------------------------------------------- 1 | from wdb import trace 2 | 3 | 4 | def uncatched_exception(below): 5 | return below / 0 6 | 7 | 8 | def uninteresting_exception(below): 9 | return below.what 10 | 11 | 12 | def uninteresting_function_catching(below): 13 | # Uninteresting exception 14 | try: 15 | uninteresting_exception(below) 16 | except AttributeError: 17 | pass 18 | 19 | try: 20 | b = uncatched_exception(below) 21 | except ZeroDivisionError: 22 | b = 2 23 | 24 | return b 25 | 26 | 27 | def the_step_more(below): 28 | try: 29 | below.what 30 | except AttributeError: 31 | pass 32 | 33 | try: 34 | return uncatched_exception(below) 35 | except ZeroDivisionError: 36 | return 2 37 | 38 | 39 | def uninteresting_function_catching_with_a_step_more(below): 40 | # Uninteresting exception 41 | try: 42 | uninteresting_exception(below) 43 | except AttributeError: 44 | pass 45 | 46 | b = the_step_more(below) 47 | return b 48 | 49 | 50 | def one_more_step(fun, below): 51 | try: 52 | uninteresting_exception(below) 53 | except AttributeError: 54 | pass 55 | 56 | return fun(below) 57 | 58 | 59 | # This should stop for both 60 | with trace(under=uninteresting_function_catching): 61 | uninteresting_function_catching(0) 62 | 63 | # This should stop only for the latter in uncatched 64 | with trace(under=uncatched_exception): 65 | uninteresting_function_catching(0) 66 | 67 | # This should not stop 68 | with trace(under=the_step_more): 69 | uninteresting_function_catching(0) 70 | 71 | # This should not stop 72 | with trace(under=uncatched_exception, below=1): 73 | uninteresting_function_catching(0) 74 | 75 | # This should stop in uncatched_exception 76 | with trace(under=the_step_more, below=1): 77 | uninteresting_function_catching_with_a_step_more(1) 78 | 79 | # This should stop in uncatched_exception 80 | with trace(under=the_step_more, below=1): 81 | one_more_step(uninteresting_function_catching_with_a_step_more, 2) 82 | -------------------------------------------------------------------------------- /test/scripts/error_in_with_under.py: -------------------------------------------------------------------------------- 1 | from wdb import trace 2 | 3 | 4 | def catched_exception(below): 5 | try: 6 | return below / 0 7 | except ZeroDivisionError: 8 | return 2 9 | 10 | 11 | def uncatched_exception(below): 12 | return below / 0 13 | 14 | 15 | def uninteresting_function(below): 16 | b = catched_exception(below) 17 | return b 18 | 19 | 20 | def uninteresting_function_not_catching(below): 21 | b = uncatched_exception(below) 22 | return b 23 | 24 | 25 | def uninteresting_function_catching(below): 26 | try: 27 | b = uncatched_exception(below) 28 | except ZeroDivisionError: 29 | b = 2 30 | return b 31 | 32 | 33 | def one_more_step(fun, below): 34 | return fun(below) 35 | 36 | 37 | # This should not stop 38 | with trace(under=uninteresting_function): 39 | try: 40 | raise Exception('Catched Exception') 41 | except Exception: 42 | pass 43 | 44 | # This should not stop 45 | with trace(under=uninteresting_function): 46 | uninteresting_function(1) 47 | 48 | # This should stop 49 | # below = 1 the exception in catched exception should stop trace 50 | with trace(under=uninteresting_function_not_catching): 51 | try: 52 | uninteresting_function_not_catching(1) 53 | except: 54 | pass 55 | 56 | # This should stop 57 | # below = 1 the function 2 layer under raised an exception 58 | with trace(under=uninteresting_function_catching): 59 | uninteresting_function_catching(1) 60 | 61 | 62 | # This should not stop 63 | with trace(under=uninteresting_function): 64 | one_more_step(uninteresting_function, 2) 65 | 66 | # This should stop 67 | with trace(under=uninteresting_function_not_catching): 68 | try: 69 | one_more_step(uninteresting_function_not_catching, 2) 70 | except: 71 | pass 72 | 73 | # This should stop 74 | with trace(under=uninteresting_function_catching): 75 | one_more_step(uninteresting_function_catching, 2) 76 | -------------------------------------------------------------------------------- /test/scripts/error_not_ignored_in_script.py: -------------------------------------------------------------------------------- 1 | import wdb 2 | 3 | 4 | def divide_by_zero(z): 5 | return z / 0 6 | 7 | 8 | def with_trace_fun(): 9 | a = 2 10 | b = 4 11 | c = a + b 12 | print(c) 13 | try: 14 | d = divide_by_zero(c) 15 | except ZeroDivisionError: 16 | d = -1 17 | 18 | print(d) 19 | print('The end') 20 | 21 | wdb.start_trace(full=True) 22 | with_trace_fun() 23 | wdb.stop_trace() 24 | -------------------------------------------------------------------------------- /test/scripts/forks.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process 2 | from wdb import set_trace as wtf 3 | 4 | 5 | class Process1(Process): 6 | def run(self): 7 | print('Process 1 start') 8 | wtf() 9 | print('Process 1 end') 10 | 11 | 12 | class Process2(Process): 13 | def run(self): 14 | print('Process 2 start') 15 | wtf() 16 | print('Process 2 end') 17 | 18 | t1 = Process1() 19 | t2 = Process2() 20 | t1.daemon = t2.daemon = True 21 | print('Forking process') 22 | t1.start() 23 | t2.start() 24 | 25 | print('Joining') 26 | t1.join() 27 | t2.join() 28 | 29 | wtf() 30 | print('The End') 31 | -------------------------------------------------------------------------------- /test/scripts/latin-1.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/test/scripts/latin-1.py -------------------------------------------------------------------------------- /test/scripts/movement.py: -------------------------------------------------------------------------------- 1 | # This file does some random operation on a list 2 | import wdb 3 | 4 | 5 | def modify_list(ll): 6 | ll[1] = 7 7 | ll.insert(0, 3) 8 | return ll 9 | 10 | wdb.set_trace() 11 | l = [] 12 | l.append(3) 13 | l += [8, 12] 14 | l = modify_list(l) 15 | 16 | for i, e in enumerate(l[:]): 17 | if i > 2: 18 | l[i] = i 19 | else: 20 | l[i] = e * i 21 | 22 | print(l, sum(l)) 23 | -------------------------------------------------------------------------------- /test/scripts/objects.py: -------------------------------------------------------------------------------- 1 | import wdb 2 | 3 | 4 | class A(object): 5 | def __init__(self, n): 6 | self.n = n 7 | 8 | def __repr__(self): 9 | return '' % self.n 10 | 11 | 12 | def create_a(n): 13 | a = A(n) 14 | return a 15 | 16 | 17 | def combine(a, b): 18 | return [a, b, A(a.n + b.n)] 19 | 20 | 21 | def display(a, b=None, *c, **d): 22 | print(locals()) 23 | 24 | 25 | def work(): 26 | wdb.set_trace() 27 | a = create_a(5) 28 | b = create_a(2) 29 | a, b, c = combine(a, b) 30 | display(a, b, wdb, c=c, cls=A, obj=object) 31 | 32 | work() 33 | -------------------------------------------------------------------------------- /test/scripts/ok_script.py: -------------------------------------------------------------------------------- 1 | a = 3 2 | b = 5 3 | c = a + b 4 | print(c) 5 | -------------------------------------------------------------------------------- /test/scripts/osfork.py: -------------------------------------------------------------------------------- 1 | import os 2 | from wdb import set_trace as wtf 3 | 4 | 5 | print('Forking') 6 | 7 | pid = os.fork() 8 | 9 | if pid == 0: 10 | print('In children') 11 | wtf() 12 | print('Children dead') 13 | else: 14 | print('In parent') 15 | wtf() 16 | print('Parent dead') 17 | 18 | print('The End') 19 | -------------------------------------------------------------------------------- /test/scripts/recursive_object.py: -------------------------------------------------------------------------------- 1 | a = { 2 | 'a': 3, 3 | } 4 | b = { 5 | 'b': 4, 6 | 'a': a 7 | } 8 | a['b'] = b 9 | 10 | import wdb 11 | wdb.set_trace() 12 | -------------------------------------------------------------------------------- /test/scripts/threads.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from wdb import set_trace as wtf 3 | 4 | 5 | class Thread1(Thread): 6 | def run(self): 7 | print('Thread 1 start') 8 | wtf() 9 | print('Thread 1 end') 10 | 11 | 12 | class Thread2(Thread): 13 | def run(self): 14 | print('Thread 2 start') 15 | wtf() 16 | print('Thread 2 end') 17 | 18 | t1 = Thread1() 19 | t2 = Thread2() 20 | t1.daemon = t2.daemon = True 21 | print('Starting threads') 22 | t1.start() 23 | t2.start() 24 | 25 | print('Joining') 26 | t1.join() 27 | t2.join() 28 | 29 | wtf() 30 | print('The End') 31 | -------------------------------------------------------------------------------- /test/scripts/tornado_server.py: -------------------------------------------------------------------------------- 1 | from wdb.ext import wdb_tornado 2 | 3 | import tornado.ioloop 4 | import tornado.web 5 | 6 | 7 | class MainHandler(tornado.web.RequestHandler): 8 | def get(self): 9 | a = 2 10 | b = -2 11 | c = 1 / (a + b) < 0 # Err œ 12 | print(c a) 13 | relay_error() 14 | self.write("Hello, world") 15 | 16 | 17 | class OkHandler(tornado.web.RequestHandler): 18 | def get(self): 19 | self.write("Ok") 20 | 21 | 22 | def make_app(): 23 | return tornado.web.Application([ 24 | (r"/", MainHandler), 25 | ]) 26 | 27 | if __name__ == "__main__": 28 | app = make_app() 29 | wdb_tornado(app, start_disabled=True) 30 | app.listen(8888) 31 | tornado.ioloop.IOLoop.current().start() 32 | -------------------------------------------------------------------------------- /test/scripts/trace_in_script.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def fun1(a): 4 | b = 4 5 | c = a + b 6 | for i in range(10): 7 | c += b 8 | return c + 1 9 | 10 | 11 | def fun2(l): 12 | import wdb 13 | wdb.set_trace() 14 | a = 2 15 | e = fun1(a) 16 | return e 17 | 18 | 19 | def main(): 20 | fun2(0) 21 | 22 | 23 | main() 24 | import wdb 25 | wdb.set_trace() 26 | print('The end') 27 | -------------------------------------------------------------------------------- /test/scripts/wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import Flask, request 3 | import logging 4 | from wdb.ext import WdbMiddleware 5 | app = Flask(__name__) 6 | 7 | 8 | def make_error(): 9 | import whatever 10 | 11 | 12 | def relay_error(): 13 | make_error() 14 | 15 | 16 | def bad_recur(n): 17 | 1 / n 18 | return bad_recur(n - 1) 19 | 20 | 21 | @app.route("/ok") 22 | def good_function(): 23 | a = 2 24 | return "It's working" * a 25 | 26 | 27 | @app.route("/") 28 | def bad_function(): 29 | app.logger.warn('It will try to divide by zero') 30 | a = 2 31 | b = -2 32 | c = 1 / (a + b) < 0 # Err œ 33 | print(c a) 34 | relay_error() 35 | return "Hello World!" 36 | 37 | 38 | @app.route("/cascade") 39 | def cascaded_exception(): 40 | try: 41 | bad_function() 42 | except ZeroDivisionError: 43 | raise ValueError 44 | finally: 45 | raise KeyError 46 | 47 | 48 | @app.route("/wtf/error") 49 | def wtf_error(): 50 | import wdb 51 | wdb.set_trace() 52 | a = 2 53 | a / 0 54 | return 12 55 | 56 | 57 | @app.route("/post") 58 | def post(): 59 | return ('
' 60 | ' ' 61 | ' ' 62 | ' ' 63 | ' ' 64 | ' ' 65 | '
') 66 | 67 | 68 | @app.route("/multipart/post") 69 | def multipart_post(): 70 | return ('
' 72 | ' ' 73 | ' ' 74 | ' ' 75 | ' ' 76 | ' ' 77 | '
') 78 | 79 | 80 | @app.route("/post/test", methods=('POST',)) 81 | def post_test(): 82 | a = 2 83 | import wdb 84 | wdb.set_trace() 85 | return 'POST RETURN %r' % request.values 86 | 87 | 88 | @app.route("/wtf") 89 | def wtf(): 90 | a = 12 91 | b = 21 92 | c = a / b 93 | import wdb 94 | wdb.set_trace() 95 | d = a - 2 96 | e = b + a - c + d 97 | for i in range(5): 98 | e += i 99 | # Test breaking on /usr/lib/python2.7/logging/__init__.py:1254 100 | app.logger.info('I was here') 101 | return 'OK! %d' % e 102 | 103 | 104 | @app.route("/long") 105 | def long_trace(): 106 | return bad_recur(10) 107 | 108 | 109 | @app.route("/slow") 110 | def slow(): 111 | from time import sleep 112 | sleep(10) 113 | return 'Finally' 114 | 115 | 116 | @app.route("/gen") 117 | def generator(): 118 | 119 | def bad_gen(n): 120 | for i in reversed(range(n)): 121 | yield 1 / i 122 | 123 | return ''.join(bad_gen(10)) 124 | 125 | 126 | @app.route("/gen2") 127 | def comprehension_generator(): 128 | return ''.join((1 / i for i in reversed(range(10)))) 129 | 130 | 131 | @app.route("/lambda") 132 | def lambda_(): 133 | return ''.join(map(lambda x: 1 / x, reversed(range(10)))) 134 | 135 | 136 | @app.route("/import") 137 | def import_(): 138 | import importtest 139 | 140 | 141 | # @app.route("/favicon.ico") 142 | # def fav(): 143 | # 1/0 144 | 145 | 146 | def init(): 147 | from log_colorizer import make_colored_stream_handler 148 | handler = make_colored_stream_handler() 149 | app.logger.handlers = [] 150 | app.logger.addHandler(handler) 151 | logging.getLogger('werkzeug').handlers = [] 152 | logging.getLogger('werkzeug').addHandler(handler) 153 | handler.setLevel(logging.DEBUG) 154 | app.logger.setLevel(logging.DEBUG) 155 | logging.getLogger('werkzeug').setLevel(logging.DEBUG) 156 | try: 157 | # This is an independant tool for reloading chrome pages 158 | # through websockets 159 | # See https://github.com/paradoxxxzero/wsreload 160 | import wsreload.client 161 | except ImportError: 162 | app.logger.debug('wsreload not found') 163 | else: 164 | url = "http://l:1985/*" 165 | 166 | def log(httpserver): 167 | app.logger.debug('WSReloaded after server restart') 168 | wsreload.client.monkey_patch_http_server({'url': url}, callback=log) 169 | app.logger.debug('HTTPServer monkey patched for url %s' % url) 170 | 171 | 172 | def run(): 173 | init() 174 | app.wsgi_app = WdbMiddleware(app.wsgi_app, start_disabled=True) 175 | app.run( 176 | debug=True, host='0.0.0.0', port=1985, use_debugger=False, 177 | use_reloader=True) 178 | 179 | if __name__ == '__main__': 180 | run() 181 | -------------------------------------------------------------------------------- /test/test_breaks.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | import json 4 | import os 5 | 6 | 7 | def make_break( 8 | fn='movement.py', lno=None, cond=None, fun=None, temporary=False 9 | ): 10 | return json.dumps( 11 | { 12 | 'fn': os.path.join(os.path.dirname(__file__), 'scripts', fn), 13 | 'lno': lno, 14 | 'cond': cond, 15 | 'fun': fun, 16 | 'temporary': temporary, 17 | } 18 | ) 19 | 20 | 21 | @use('movement.py') 22 | def test_simple_break(socket): 23 | socket.start() 24 | socket.assert_init() 25 | socket.send('Break', make_break('movement.py', 7)) 26 | msg = socket.receive() 27 | assert msg.command == 'BreakSet' 28 | socket.send('Next') 29 | socket.assert_position(line=12, breaks=[7]) 30 | socket.send('Continue') 31 | socket.assert_position(line=7) 32 | socket.send('Next') 33 | socket.assert_position(line=8) 34 | 35 | socket.send('Continue') 36 | socket.join() 37 | 38 | 39 | @use('movement.py') 40 | def test_function_break(socket): 41 | socket.start() 42 | socket.assert_init() 43 | 44 | socket.send('Break', make_break(fun='modify_list')) 45 | msg = socket.receive() 46 | assert msg.command == 'BreakSet' 47 | socket.send('Next') 48 | socket.assert_position(line=12) 49 | socket.send('Continue') 50 | socket.assert_position(line=6) 51 | socket.send('Next') 52 | socket.assert_position(line=7) 53 | socket.send('Next') 54 | socket.assert_position(line=8) 55 | socket.send('Unbreak', make_break(fun='modify_list')) 56 | msg = socket.receive() 57 | assert msg.command == 'BreakUnset' 58 | 59 | socket.send('Continue') 60 | socket.join() 61 | 62 | 63 | @use('movement.py') 64 | def test_conditional_break(socket): 65 | socket.start() 66 | socket.assert_init() 67 | 68 | socket.send('Break', make_break(cond='sum(l) > 28')) 69 | msg = socket.receive() 70 | assert msg.command == 'BreakSet' 71 | socket.send('Next') 72 | socket.assert_position(line=12) 73 | socket.send('Continue') 74 | 75 | socket.assert_position(line=16) 76 | socket.send('Unbreak', make_break(cond='sum(l) > 28')) 77 | msg = socket.receive() 78 | assert msg.command == 'BreakUnset' 79 | 80 | socket.send('Continue') 81 | socket.join() 82 | -------------------------------------------------------------------------------- /test/test_error.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('error_in_script.py') 6 | def test_with_error(socket): 7 | socket.start() 8 | msg = socket.receive() 9 | assert msg.command == 'Init' 10 | assert 'cwd' in msg.data 11 | 12 | msg = socket.receive() 13 | assert msg.command == 'Title' 14 | assert msg.data.title == 'ZeroDivisionError' 15 | assert 'division' in msg.data.subtitle 16 | 17 | msg = socket.receive() 18 | assert msg.command == 'Trace' 19 | current_trace = msg.data.trace[-1] 20 | assert current_trace.code == 'return z / 0' 21 | assert current_trace.current is False 22 | assert 'scripts/error_in_script.py' in current_trace.file 23 | assert current_trace.flno == 4 24 | assert current_trace.function == 'divide_by_zero' 25 | assert current_trace.llno == 5 26 | assert current_trace.lno == 5 27 | 28 | msg = socket.receive() 29 | assert msg.command == 'SelectCheck' 30 | assert msg.data.frame.function == 'divide_by_zero' 31 | file = msg.data.name 32 | 33 | msg = socket.receive() 34 | assert msg.command == 'Echo' 35 | assert msg.data['for'] == '__exception__' 36 | assert 'ZeroDivisionError:' in msg.data.val 37 | 38 | msg = socket.receive() 39 | assert msg.command == 'Watched' 40 | assert msg.data == {} 41 | 42 | socket.send('File', file) 43 | msg = socket.receive() 44 | assert msg.command == 'Select' 45 | assert msg.data.name == file 46 | assert len(msg.data.file) == 259 47 | 48 | socket.send('Continue') 49 | socket.join() 50 | -------------------------------------------------------------------------------- /test/test_forks.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('forks.py') 6 | def test_with_forks(socket): 7 | uuid1 = socket.start() 8 | uuid2 = socket.start() 9 | 10 | for uuid in (uuid1, uuid2): 11 | msg = socket.receive(uuid) 12 | assert msg.command == 'Init' 13 | assert 'cwd' in msg.data 14 | 15 | msg = socket.receive(uuid) 16 | assert msg.command == 'Title' 17 | assert msg.data.title == 'Wdb' 18 | assert msg.data.subtitle == 'Stepping' 19 | 20 | msg = socket.receive(uuid1) 21 | assert msg.command == 'Trace' 22 | current_trace = msg.data.trace[-1] 23 | if current_trace.code == "print('Process 1 end')": 24 | assert current_trace.current is True 25 | assert 'scripts/forks.py' in current_trace.file 26 | assert current_trace.flno == 6 27 | assert current_trace.function == 'run' 28 | assert current_trace.llno == 9 29 | assert current_trace.lno == 9 30 | uuid1_fork1 = True 31 | else: 32 | assert current_trace.code == "print('Process 2 end')" 33 | assert current_trace.current is True 34 | assert 'scripts/forks.py' in current_trace.file 35 | assert current_trace.flno == 13 36 | assert current_trace.function == 'run' 37 | assert current_trace.llno == 16 38 | assert current_trace.lno == 16 39 | uuid1_fork1 = False 40 | 41 | msg = socket.receive(uuid2) 42 | assert msg.command == 'Trace' 43 | current_trace = msg.data.trace[-1] 44 | if uuid1_fork1: 45 | assert current_trace.code == "print('Process 2 end')" 46 | assert current_trace.current is True 47 | assert 'scripts/forks.py' in current_trace.file 48 | assert current_trace.flno == 13 49 | assert current_trace.function == 'run' 50 | assert current_trace.llno == 16 51 | assert current_trace.lno == 16 52 | else: 53 | assert current_trace.code == "print('Process 1 end')" 54 | assert current_trace.current is True 55 | assert 'scripts/forks.py' in current_trace.file 56 | assert current_trace.flno == 6 57 | assert current_trace.function == 'run' 58 | assert current_trace.llno == 9 59 | assert current_trace.lno == 9 60 | 61 | for uuid in (uuid1, uuid2): 62 | msg = socket.receive(uuid) 63 | assert msg.command == 'SelectCheck' 64 | assert msg.data.frame.function == 'run' 65 | 66 | msg = socket.receive(uuid) 67 | assert msg.command == 'Watched' 68 | assert msg.data == {} 69 | 70 | socket.send('Continue', uuid=uuid) 71 | 72 | last_uuid = socket.start() 73 | msg = socket.receive(last_uuid) 74 | assert msg.command == 'Init' 75 | assert 'cwd' in msg.data 76 | 77 | msg = socket.receive(last_uuid) 78 | assert msg.command == 'Title' 79 | assert msg.data.title == 'Wdb' 80 | assert msg.data.subtitle == 'Stepping' 81 | 82 | msg = socket.receive(last_uuid) 83 | assert msg.command == 'Trace' 84 | current_trace = msg.data.trace[-1] 85 | assert current_trace.code == "print('The End')" 86 | assert current_trace.current is True 87 | assert 'scripts/forks.py' in current_trace.file 88 | assert current_trace.flno == 1 89 | assert current_trace.function == '' 90 | assert current_trace.llno == 30 91 | assert current_trace.lno == 30 92 | 93 | socket.send('Continue', uuid=last_uuid) 94 | socket.join() 95 | -------------------------------------------------------------------------------- /test/test_main.py: -------------------------------------------------------------------------------- 1 | from .conftest import use 2 | 3 | 4 | @use('ok_script.py', with_main=True) 5 | def test_main_on_running_script(socket): 6 | socket.start() 7 | msg = socket.receive() 8 | assert msg.command == 'Init' 9 | assert 'cwd' in msg.data 10 | 11 | msg = socket.receive() 12 | assert msg.command == 'Title' 13 | assert msg.data.title == 'Wdb' 14 | assert msg.data.subtitle == 'Stepping' 15 | 16 | msg = socket.receive() 17 | assert msg.command == 'Trace' 18 | current_trace = msg.data.trace[-1] 19 | assert current_trace.code == 'a = 3' 20 | assert current_trace.current is True 21 | assert 'scripts/ok_script.py' in current_trace.file 22 | assert current_trace.flno == 1 23 | assert current_trace.function == '' 24 | assert current_trace.llno == 4 25 | assert current_trace.lno == 1 26 | 27 | msg = socket.receive() 28 | assert msg.command == 'SelectCheck' 29 | assert msg.data.frame.function == '' 30 | 31 | msg = socket.receive() 32 | assert msg.command == 'Watched' 33 | assert msg.data == {} 34 | 35 | socket.send('Next') 36 | socket.assert_position(code='b = 5') 37 | socket.send('Next') 38 | socket.assert_position(code='c = a + b') 39 | socket.send('Continue') 40 | socket.join() 41 | 42 | 43 | @use('404.py', with_main=True) 44 | def test_main_on_unexisting_script(socket): 45 | # If it doesn't timeout this is good 46 | socket.join() 47 | -------------------------------------------------------------------------------- /test/test_misc.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('latin-1.py') 6 | def test_latin_1(socket): 7 | socket.start() 8 | socket.assert_init() 9 | socket.send('Continue') 10 | socket.join() 11 | -------------------------------------------------------------------------------- /test/test_movement.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('movement.py') 6 | def test_next(socket): 7 | socket.start() 8 | socket.assert_init() 9 | 10 | def next(code): 11 | socket.send('Next') 12 | socket.assert_position(code=code) 13 | 14 | next('l.append(3)') 15 | next('l += [8, 12]') 16 | next('l = modify_list(l)') 17 | for i in range(3): 18 | next('for i, e in enumerate(l[:]):') 19 | next('if i > 2:') 20 | next('l[i] = e * i') 21 | 22 | next('for i, e in enumerate(l[:]):') 23 | next('if i > 2:') 24 | next('l[i] = i') 25 | 26 | next('for i, e in enumerate(l[:]):') 27 | next('print(l, sum(l))') 28 | 29 | socket.send('Continue') 30 | socket.join() 31 | 32 | 33 | @use('movement.py') 34 | def test_until(socket): 35 | socket.start() 36 | socket.assert_init() 37 | 38 | def until(code): 39 | socket.send('Until') 40 | socket.assert_position(code=code) 41 | 42 | until('l.append(3)') 43 | until('l += [8, 12]') 44 | until('l = modify_list(l)') 45 | until('for i, e in enumerate(l[:]):') 46 | until('if i > 2:') 47 | until('l[i] = e * i') 48 | until('print(l, sum(l))') 49 | 50 | socket.send('Continue') 51 | socket.join() 52 | 53 | 54 | @use('movement.py') 55 | def test_step(socket): 56 | socket.start() 57 | socket.assert_init() 58 | 59 | def step(code, **kwargs): 60 | socket.send('Step') 61 | socket.assert_position(code=code, **kwargs) 62 | 63 | step('l.append(3)') 64 | step('l += [8, 12]') 65 | step('l = modify_list(l)') 66 | step('def modify_list(ll):', call="modify_list(ll=[\n
%r' % (id(var), var) 44 | 45 | step('def create_a(n):', call='create_a(n=%s)' % link(5)) 46 | next('a = A(n)') 47 | next('return a') 48 | next('return a', return_='<A object with n=5>') 49 | next('b = create_a(2)') 50 | next('a, b, c = combine(a, b)') 51 | step('def combine(a, b):', call='<A object with n=5>') 52 | 53 | next('return [a, b, A(a.n + b.n)]') 54 | next('return [a, b, A(a.n + b.n)]', return_='<A object with n=7>') 55 | next('display(a, b, wdb, c=c, cls=A, obj=object)') 56 | step( 57 | 'def display(a, b=None, *c, **d):', call=' class="inspect"><class ' 58 | ) 59 | next('print(locals())') 60 | next('print(locals())', return_='None') 61 | 62 | socket.send('Continue') 63 | socket.join() 64 | -------------------------------------------------------------------------------- /test/test_osforks.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('osfork.py') 6 | def test_with_fork_from_os(socket): 7 | uuid1 = socket.start() 8 | uuid2 = socket.start() 9 | 10 | for uuid in (uuid1, uuid2): 11 | msg = socket.receive(uuid) 12 | assert msg.command == 'Init' 13 | assert 'cwd' in msg.data 14 | 15 | msg = socket.receive(uuid) 16 | assert msg.command == 'Title' 17 | assert msg.data.title == 'Wdb' 18 | assert msg.data.subtitle == 'Stepping' 19 | 20 | msg = socket.receive(uuid1) 21 | assert msg.command == 'Trace' 22 | current_trace = msg.data.trace[-1] 23 | if current_trace.code == "print('Children dead')": 24 | assert current_trace.current is True 25 | assert 'scripts/osfork.py' in current_trace.file 26 | assert current_trace.flno == 1 27 | assert current_trace.function == '' 28 | assert current_trace.llno == 18 29 | assert current_trace.lno == 12 30 | uuid1_fork1 = True 31 | else: 32 | assert current_trace.code == "print('Parent dead')" 33 | assert current_trace.current is True 34 | assert 'scripts/osfork.py' in current_trace.file 35 | assert current_trace.flno == 1 36 | assert current_trace.function == '' 37 | assert current_trace.llno == 18 38 | assert current_trace.lno == 16 39 | uuid1_fork1 = False 40 | 41 | msg = socket.receive(uuid2) 42 | assert msg.command == 'Trace' 43 | current_trace = msg.data.trace[-1] 44 | if uuid1_fork1: 45 | assert current_trace.code == "print('Parent dead')" 46 | assert current_trace.current is True 47 | assert 'scripts/osfork.py' in current_trace.file 48 | assert current_trace.flno == 1 49 | assert current_trace.function == '' 50 | assert current_trace.llno == 18 51 | assert current_trace.lno == 16 52 | else: 53 | assert current_trace.code == "print('Children dead')" 54 | assert current_trace.current is True 55 | assert 'scripts/osfork.py' in current_trace.file 56 | assert current_trace.flno == 1 57 | assert current_trace.function == '' 58 | assert current_trace.llno == 18 59 | assert current_trace.lno == 12 60 | 61 | for uuid in (uuid1, uuid2): 62 | msg = socket.receive(uuid) 63 | assert msg.command == 'SelectCheck' 64 | assert msg.data.frame.function == '' 65 | 66 | msg = socket.receive(uuid) 67 | assert msg.command == 'Watched' 68 | assert msg.data == {} 69 | 70 | socket.send('Continue', uuid=uuid) 71 | 72 | socket.join() 73 | -------------------------------------------------------------------------------- /test/test_prompt.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use, u 3 | 4 | 5 | @use('movement.py') 6 | def test_eval(socket): 7 | socket.start() 8 | socket.assert_init() 9 | socket.send('Next') 10 | socket.assert_position(line=12) 11 | socket.send('Eval', 'l') 12 | print_msg = socket.receive() 13 | assert print_msg.command == 'Print' 14 | assert print_msg.data['for'] == 'l' 15 | assert print_msg.data.result == '[]' 16 | watched_msg = socket.receive() 17 | assert watched_msg.command == 'Watched' 18 | 19 | socket.send('Next') 20 | socket.assert_position(line=13) 21 | 22 | socket.send('Eval', 'l') 23 | print_msg = socket.receive() 24 | assert print_msg.command == 'Print' 25 | assert print_msg.data['for'] == 'l' 26 | assert 'class="inspect">3]' in print_msg.data.result 27 | 28 | watched_msg = socket.receive() 29 | assert watched_msg.command == 'Watched' 30 | 31 | socket.send('Eval', 'l = None') 32 | print_msg = socket.receive() 33 | assert print_msg.command == 'Print' 34 | 35 | assert print_msg.data['for'] == u('l = None') 36 | assert print_msg.data.result == '' 37 | 38 | watched_msg = socket.receive() 39 | assert watched_msg.command == 'Watched' 40 | 41 | socket.send('Continue') 42 | socket.join() 43 | 44 | 45 | @use('movement.py') 46 | def test_eval_dump(socket): 47 | socket.start() 48 | socket.assert_init() 49 | socket.send('Next') 50 | socket.assert_position(line=12) 51 | socket.send('Next') 52 | socket.assert_position(line=13) 53 | 54 | socket.send('Dump', 'l') 55 | print_msg = socket.receive() 56 | assert print_msg.command == 'Dump' 57 | assert print_msg.data['for'] == u('l ⟶ [3] ') 58 | assert print_msg.data.val 59 | 60 | socket.send('Continue') 61 | socket.join() 62 | 63 | 64 | @use('movement.py') 65 | def test_eval_new_line(socket): 66 | socket.start() 67 | socket.assert_init() 68 | socket.send('Next') 69 | socket.assert_position(line=12) 70 | socket.send('Next') 71 | socket.assert_position(line=13) 72 | 73 | socket.send('Eval', 'if l:') 74 | print_msg = socket.receive() 75 | assert print_msg.command == 'NewLine' 76 | 77 | watched_msg = socket.receive() 78 | assert watched_msg.command == 'Watched' 79 | 80 | socket.send('Eval', 'if l:\n l') 81 | print_msg = socket.receive() 82 | assert print_msg.command == 'Print' 83 | assert print_msg.data['for'] == u('if l:\n      l') 84 | assert 'class="inspect">3]' in print_msg.data.result 85 | 86 | socket.send('Continue') 87 | socket.join() 88 | -------------------------------------------------------------------------------- /test/test_threads.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('threads.py') 6 | def test_with_threads(socket): 7 | uuid1 = socket.start() 8 | uuid2 = socket.start() 9 | 10 | for uuid in (uuid1, uuid2): 11 | msg = socket.receive(uuid) 12 | assert msg.command == 'Init' 13 | assert 'cwd' in msg.data 14 | 15 | msg = socket.receive(uuid) 16 | assert msg.command == 'Title' 17 | assert msg.data.title == 'Wdb' 18 | assert msg.data.subtitle == 'Stepping' 19 | 20 | msg = socket.receive(uuid1) 21 | assert msg.command == 'Trace' 22 | current_trace = msg.data.trace[-1] 23 | if current_trace.code == "print('Thread 1 end')": 24 | assert current_trace.current is True 25 | assert 'scripts/threads.py' in current_trace.file 26 | assert current_trace.flno == 6 27 | assert current_trace.function == 'run' 28 | assert current_trace.llno == 9 29 | assert current_trace.lno == 9 30 | uuid1_thread1 = True 31 | else: 32 | assert current_trace.code == "print('Thread 2 end')" 33 | assert current_trace.current is True 34 | assert 'scripts/threads.py' in current_trace.file 35 | assert current_trace.flno == 13 36 | assert current_trace.function == 'run' 37 | assert current_trace.llno == 16 38 | assert current_trace.lno == 16 39 | uuid1_thread1 = False 40 | 41 | msg = socket.receive(uuid2) 42 | assert msg.command == 'Trace' 43 | current_trace = msg.data.trace[-1] 44 | if uuid1_thread1: 45 | assert current_trace.code == "print('Thread 2 end')" 46 | assert current_trace.current is True 47 | assert 'scripts/threads.py' in current_trace.file 48 | assert current_trace.flno == 13 49 | assert current_trace.function == 'run' 50 | assert current_trace.llno == 16 51 | assert current_trace.lno == 16 52 | else: 53 | assert current_trace.code == "print('Thread 1 end')" 54 | assert current_trace.current is True 55 | assert 'scripts/threads.py' in current_trace.file 56 | assert current_trace.flno == 6 57 | assert current_trace.function == 'run' 58 | assert current_trace.llno == 9 59 | assert current_trace.lno == 9 60 | 61 | for uuid in (uuid1, uuid2): 62 | msg = socket.receive(uuid) 63 | assert msg.command == 'SelectCheck' 64 | assert msg.data.frame.function == 'run' 65 | 66 | msg = socket.receive(uuid) 67 | assert msg.command == 'Watched' 68 | assert msg.data == {} 69 | 70 | socket.send('Continue', uuid=uuid) 71 | 72 | last_uuid = socket.start() 73 | msg = socket.receive(last_uuid) 74 | assert msg.command == 'Init' 75 | assert 'cwd' in msg.data 76 | 77 | msg = socket.receive(last_uuid) 78 | assert msg.command == 'Title' 79 | assert msg.data.title == 'Wdb' 80 | assert msg.data.subtitle == 'Stepping' 81 | 82 | msg = socket.receive(last_uuid) 83 | assert msg.command == 'Trace' 84 | current_trace = msg.data.trace[-1] 85 | assert current_trace.code == "print('The End')" 86 | assert current_trace.current is True 87 | assert 'scripts/threads.py' in current_trace.file 88 | assert current_trace.flno == 1 89 | assert current_trace.function == '' 90 | assert current_trace.llno == 30 91 | assert current_trace.lno == 30 92 | 93 | socket.send('Continue', uuid=last_uuid) 94 | socket.join() 95 | -------------------------------------------------------------------------------- /test/test_trace.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | from .conftest import use 3 | 4 | 5 | @use('trace_in_script.py') 6 | def test_with_trace(socket): 7 | socket.start() 8 | 9 | msg = socket.receive() 10 | assert msg.command == 'Init' 11 | assert 'cwd' in msg.data 12 | 13 | msg = socket.receive() 14 | assert msg.command == 'Title' 15 | assert msg.data.title == 'Wdb' 16 | assert msg.data.subtitle == 'Stepping' 17 | 18 | msg = socket.receive() 19 | assert msg.command == 'Trace' 20 | current_trace = msg.data.trace[-1] 21 | assert current_trace.code == 'a = 2' 22 | assert current_trace.current is True 23 | assert 'scripts/trace_in_script.py' in current_trace.file 24 | assert current_trace.flno == 11 25 | assert current_trace.function == 'fun2' 26 | assert current_trace.llno == 16 27 | assert current_trace.lno == 14 28 | 29 | msg = socket.receive() 30 | assert msg.command == 'SelectCheck' 31 | assert msg.data.frame.function == 'fun2' 32 | file = msg.data.name 33 | 34 | msg = socket.receive() 35 | assert msg.command == 'Watched' 36 | assert msg.data == {} 37 | 38 | socket.send('File', file) 39 | msg = socket.receive() 40 | assert msg.command == 'Select' 41 | assert msg.data.name == file 42 | assert len(msg.data.file) == 263 43 | 44 | socket.send('Continue') 45 | 46 | # Tracing 47 | msg = None 48 | while not msg: 49 | try: 50 | msg = socket.receive() 51 | except Exception as e: 52 | msg = None 53 | assert type(e) == EOFError 54 | 55 | assert msg.command == 'Title' 56 | assert msg.data.title == 'Wdb' 57 | assert msg.data.subtitle == 'Stepping' 58 | 59 | msg = socket.receive() 60 | assert msg.command == 'Trace' 61 | current_trace = msg.data.trace[-1] 62 | assert current_trace.code == "print('The end')" 63 | assert current_trace.current is True 64 | assert 'scripts/trace_in_script.py' in current_trace.file 65 | assert current_trace.flno == 3 66 | assert current_trace.function == '' 67 | assert current_trace.llno == 26 68 | assert current_trace.lno == 26 69 | 70 | msg = socket.receive() 71 | assert msg.command == 'SelectCheck' 72 | assert msg.data.frame.function == '' 73 | file = msg.data.name 74 | 75 | msg = socket.receive() 76 | assert msg.command == 'Watched' 77 | assert msg.data == {} 78 | 79 | socket.send('File', file) 80 | msg = socket.receive() 81 | assert msg.command == 'Select' 82 | assert msg.data.name == file 83 | assert len(msg.data.file) == 263 84 | 85 | socket.send('Continue') 86 | socket.join() 87 | 88 | 89 | @use('error_in_with.py') 90 | def test_with_error_in_trace(socket): 91 | socket.start() 92 | # The first to stop must be the one with the full trace 93 | msg = socket.receive() 94 | assert msg.command == 'Init' 95 | assert 'cwd' in msg.data 96 | 97 | socket.assert_position( 98 | title='ZeroDivisionError', 99 | code='return i / 0', 100 | exception="ZeroDivisionError", 101 | ) 102 | socket.send('Return') 103 | socket.assert_position(code='return 2', return_="2") 104 | socket.send('Next') 105 | socket.assert_position(code='print(d + a)', line=24) 106 | 107 | socket.send('Continue') 108 | socket.join() 109 | 110 | 111 | @use('error_in_with_advanced.py') 112 | def test_with_error_in_trace_advanced(socket): 113 | socket.start() 114 | # The first to stop must be the one with the full trace on parent 115 | msg = socket.receive() 116 | assert msg.command == 'Init' 117 | assert 'cwd' in msg.data 118 | 119 | for i in range(2): 120 | socket.assert_position( 121 | title='ZeroDivisionError', 122 | code='return i / 0', 123 | exception="ZeroDivisionError", 124 | ) 125 | socket.send('Next') 126 | socket.assert_position( 127 | code='return i / 0', 128 | return_='None', 129 | subtitle='Returning from make_error with value None', 130 | ) 131 | # Full trace catch exception at everly traced level 132 | socket.send('Next') 133 | socket.assert_position( 134 | title='ZeroDivisionError', 135 | code='return i / 0', 136 | bottom_code='parent()' if not i else 'grandparent()', 137 | exception="ZeroDivisionError", 138 | ) 139 | 140 | socket.send('Next') 141 | socket.assert_position(code='except ZeroDivisionError:') 142 | socket.send('Continue') 143 | socket.join() 144 | 145 | 146 | @use('error_in_with_below.py') 147 | def test_with_error_in_trace_below(socket): 148 | socket.start() 149 | # The first to stop must be the one with the full trace on parent 150 | msg = socket.receive() 151 | assert msg.command == 'Init' 152 | assert 'cwd' in msg.data 153 | 154 | socket.assert_position( 155 | title='ZeroDivisionError', 156 | code='return below / 0', 157 | exception="ZeroDivisionError", 158 | bottom_code='uninteresting_function_not_catching(1)', 159 | bottom_line=54, 160 | ) 161 | socket.send('Continue') 162 | 163 | socket.assert_position( 164 | title='ZeroDivisionError', 165 | code='return below / 0', 166 | exception="ZeroDivisionError", 167 | bottom_code='uninteresting_function_catching(1)', 168 | bottom_line=61, 169 | ) 170 | 171 | socket.send('Continue') 172 | 173 | socket.assert_position( 174 | title='ZeroDivisionError', 175 | code='return below / 0', 176 | exception="ZeroDivisionError", 177 | bottom_code='one_more_step(uninteresting_function_not_catching, 2)', 178 | bottom_line=78, 179 | ) 180 | socket.send('Continue') 181 | 182 | socket.assert_position( 183 | title='ZeroDivisionError', 184 | code='return below / 0', 185 | exception="ZeroDivisionError", 186 | bottom_code='one_more_step(uninteresting_function_catching, 2)', 187 | bottom_line=84, 188 | ) 189 | socket.send('Continue') 190 | socket.join() 191 | 192 | 193 | @use('error_in_with_under.py') 194 | def test_with_error_in_trace_under(socket): 195 | socket.start() 196 | # The first to stop must be the one with the full trace on parent 197 | msg = socket.receive() 198 | assert msg.command == 'Init' 199 | assert 'cwd' in msg.data 200 | 201 | socket.assert_position( 202 | title='ZeroDivisionError', 203 | code='return below / 0', 204 | exception="ZeroDivisionError", 205 | bottom_code='uninteresting_function_not_catching(1)', 206 | bottom_line=52, 207 | ) 208 | socket.send('Continue') 209 | 210 | socket.assert_position( 211 | title='ZeroDivisionError', 212 | code='return below / 0', 213 | exception="ZeroDivisionError", 214 | bottom_code='uninteresting_function_catching(1)', 215 | bottom_line=59, 216 | ) 217 | 218 | socket.send('Continue') 219 | 220 | socket.assert_position( 221 | title='ZeroDivisionError', 222 | code='return below / 0', 223 | exception="ZeroDivisionError", 224 | bottom_code='one_more_step(uninteresting_function_not_catching, 2)', 225 | bottom_line=69, 226 | ) 227 | socket.send('Continue') 228 | 229 | socket.assert_position( 230 | title='ZeroDivisionError', 231 | code='return below / 0', 232 | exception="ZeroDivisionError", 233 | bottom_code='one_more_step(uninteresting_function_catching, 2)', 234 | bottom_line=75, 235 | ) 236 | socket.send('Continue') 237 | socket.join() 238 | 239 | 240 | @use('error_in_with_below_under.py') 241 | def test_with_error_in_trace_below_under(socket): 242 | socket.start() 243 | # The first to stop must be the one with the full trace on parent 244 | msg = socket.receive() 245 | assert msg.command == 'Init' 246 | assert 'cwd' in msg.data 247 | 248 | socket.assert_position( 249 | title='AttributeError', 250 | code='return below.what', 251 | exception='AttributeError', 252 | bottom_code='uninteresting_function_catching(0)', 253 | bottom_line=61, 254 | ) 255 | socket.send('Continue') 256 | 257 | socket.assert_position( 258 | title='ZeroDivisionError', 259 | code='return below / 0', 260 | exception="ZeroDivisionError", 261 | bottom_code='uninteresting_function_catching(0)', 262 | bottom_line=61, 263 | ) 264 | socket.send('Continue') 265 | 266 | socket.assert_position( 267 | title='ZeroDivisionError', 268 | code='return below / 0', 269 | exception="ZeroDivisionError", 270 | bottom_code='uninteresting_function_catching(0)', 271 | bottom_line=65, 272 | ) 273 | socket.send('Continue') 274 | 275 | socket.assert_position( 276 | title='ZeroDivisionError', 277 | code='return below / 0', 278 | exception="ZeroDivisionError", 279 | bottom_code='uninteresting_function_catching_with_a_step_more(1)', 280 | bottom_line=77, 281 | ) 282 | socket.send('Continue') 283 | 284 | socket.assert_position( 285 | title='ZeroDivisionError', 286 | code='return below / 0', 287 | exception="ZeroDivisionError", 288 | bottom_code='one_more_step(' 289 | 'uninteresting_function_catching_with_a_step_more, 2)', 290 | bottom_line=81, 291 | ) 292 | socket.send('Continue') 293 | 294 | socket.join() 295 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | import sys 3 | 4 | from wdb._compat import OrderedDict 5 | 6 | 7 | def test_args(): 8 | from wdb.utils import get_args 9 | 10 | def f(i, j): 11 | assert get_args(sys._getframe()) == OrderedDict((('i', i), ('j', j))) 12 | 13 | f(10, 'a') 14 | f(None, 2 + 7j) 15 | 16 | 17 | def test_empty(): 18 | from wdb.utils import get_args 19 | 20 | def f(): 21 | assert get_args(sys._getframe()) == OrderedDict() 22 | 23 | f() 24 | 25 | 26 | def test_arg_with_default(): 27 | from wdb.utils import get_args 28 | 29 | def f(x, y=12, z=2193): 30 | assert get_args(sys._getframe()) == OrderedDict( 31 | (('x', x), ('y', y), ('z', z)) 32 | ) 33 | 34 | f(5) 35 | f('a') 36 | f('a', 5, 19) 37 | f('a', z=19) 38 | 39 | 40 | def test_varargs(): 41 | from wdb.utils import get_args 42 | 43 | def f(x, *args): 44 | assert get_args(sys._getframe()) == OrderedDict( 45 | (('x', x), ('*args', args)) 46 | ) 47 | 48 | f(2, 3, 5, 'a') 49 | f(2, *[[1, 2], 3]) 50 | f(2) 51 | 52 | 53 | def test_varargs_only(): 54 | from wdb.utils import get_args 55 | 56 | def f(*a): 57 | assert get_args(sys._getframe()) == OrderedDict((('*a', a),)) 58 | 59 | f(5, 2) 60 | f(10) 61 | f() 62 | 63 | 64 | def test_kwargs(): 65 | from wdb.utils import get_args 66 | 67 | def f(x, **kwargs): 68 | assert get_args(sys._getframe()) == OrderedDict( 69 | (('x', x), ('**kwargs', kwargs)) 70 | ) 71 | 72 | f(5) 73 | f(9, i=4, j=53) 74 | f(1, i=4, **{'d': 5, 'c': 'c'}) 75 | 76 | 77 | def test_kwargs_only(): 78 | from wdb.utils import get_args 79 | 80 | def f(**kw): 81 | assert get_args(sys._getframe()) == OrderedDict((('**kw', kw),)) 82 | 83 | f() 84 | f(a='i', b=5) 85 | f(d={'i': 3}) 86 | f(**{'d': 5, 'c': 'c'}) 87 | 88 | 89 | def test_method(): 90 | from wdb.utils import get_args 91 | 92 | class cls(object): 93 | def f(self, a, b=2, *args, **kwargs): 94 | assert get_args(sys._getframe()) == OrderedDict( 95 | ( 96 | ('self', self), 97 | ('a', a), 98 | ('b', b), 99 | ('*args', args), 100 | ('**kwargs', kwargs), 101 | ) 102 | ) 103 | 104 | obj = cls() 105 | obj.f(8, 'p', 10, z=2, o=9) 106 | obj.f(None) 107 | 108 | 109 | if sys.version_info[0] >= 3: 110 | # ... 111 | exec( 112 | ''' 113 | def test_method_reverse(): 114 | from wdb.utils import get_args 115 | 116 | class cls(object): 117 | def f(self, a, *args, b=2, **kwargs): 118 | assert get_args(sys._getframe()) == OrderedDict(( 119 | ('self', self), 120 | ('a', a), 121 | ('*args', args), 122 | ('b', b), 123 | ('**kwargs', kwargs) 124 | )) 125 | 126 | obj = cls() 127 | obj.f(8, 'p', 10, z=2, o=9) 128 | obj.f(8, 10, z=2, o=9, b='p') 129 | obj.f(None) 130 | 131 | 132 | def test_complicated(): 133 | from wdb.utils import get_args 134 | 135 | def f(a, b, c, d=5, e=12, *args, h=1, i=8, j=None, **kwargs): 136 | assert get_args(sys._getframe()) == OrderedDict(( 137 | ('a', a), 138 | ('b', b), 139 | ('c', c), 140 | ('d', d), 141 | ('e', e), 142 | ('*args', args), 143 | ('h', h), 144 | ('i', i), 145 | ('j', j), 146 | ('**kwargs', kwargs) 147 | )) 148 | 149 | def g(a, b, c, *args, h=1, i=8, j=None, **kwargs): 150 | assert get_args(sys._getframe()) == OrderedDict(( 151 | ('a', a), 152 | ('b', b), 153 | ('c', c), 154 | ('*args', args), 155 | ('h', h), 156 | ('i', i), 157 | ('j', j), 158 | ('**kwargs', kwargs) 159 | )) 160 | 161 | def h(a, b, c, h=1, i=8, j=None, **kwargs): 162 | assert get_args(sys._getframe()) == OrderedDict(( 163 | ('a', a), 164 | ('b', b), 165 | ('c', c), 166 | ('h', h), 167 | ('i', i), 168 | ('j', j), 169 | ('**kwargs', kwargs) 170 | )) 171 | 172 | def i(a, b, c, *args, h=1, i=8, j=None): 173 | assert get_args(sys._getframe()) == OrderedDict(( 174 | ('a', a), 175 | ('b', b), 176 | ('c', c), 177 | ('*args', args), 178 | ('h', h), 179 | ('i', i), 180 | ('j', j), 181 | )) 182 | 183 | def j(a, b, c, h=1, i=8, j=None): 184 | assert get_args(sys._getframe()) == OrderedDict(( 185 | ('a', a), 186 | ('b', b), 187 | ('c', c), 188 | ('h', h), 189 | ('i', i), 190 | ('j', j), 191 | )) 192 | 193 | def k(a, b, c, d=5, e=12, *args, **kwargs): 194 | assert get_args(sys._getframe()) == OrderedDict(( 195 | ('a', a), 196 | ('b', b), 197 | ('c', c), 198 | ('d', d), 199 | ('e', e), 200 | ('*args', args), 201 | ('**kwargs', kwargs) 202 | )) 203 | 204 | def l(a, b, c, d=5, e=12, *args, h=1, i=8, j=None): 205 | assert get_args(sys._getframe()) == OrderedDict(( 206 | ('a', a), 207 | ('b', b), 208 | ('c', c), 209 | ('d', d), 210 | ('e', e), 211 | ('*args', args), 212 | ('h', h), 213 | ('i', i), 214 | ('j', j), 215 | )) 216 | 217 | def m(a, b, c, d=5, e=12, h=1, i=8, j=None): 218 | assert get_args(sys._getframe()) == OrderedDict(( 219 | ('a', a), 220 | ('b', b), 221 | ('c', c), 222 | ('d', d), 223 | ('e', e), 224 | ('h', h), 225 | ('i', i), 226 | ('j', j), 227 | )) 228 | 229 | f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, m=13, n=14, o=15) 230 | f(1, 2, 3, 4, 5, 6, 7, 8, 9, h=10, i=11, j=12, m=13, n=14, o=15) 231 | g(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, m=13, n=14, o=15) 232 | g(1, 2, 3, 4, 5, 6, 7, 8, 9, h=10, i=11, j=12, m=13, n=14, o=15) 233 | h(1, 2, 3, 10, 11, 12, m=13, n=14, o=15) 234 | h(1, 2, 3, h=10, i=11, j=12, m=13, n=14, o=15) 235 | i(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) 236 | i(1, 2, 3, 4, 5, 6, 7, 8, 9, h=10, i=11, j=12) 237 | j(1, 2, 3, 10, 11, 12) 238 | j(1, 2, 3, h=10, i=11, j=12) 239 | k(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, m=13, n=14, o=15) 240 | k(1, 2, 3, 4, 5, 6, 7, 8, 9, h=10, i=11, j=12, m=13, n=14, o=15) 241 | l(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) 242 | l(1, 2, 3, 4, 5, 6, 7, 8, 9, h=10, i=11, j=12) 243 | m(1, 2, 3, 4, 5, 10, 11, 12) 244 | m(1, 2, 3, 4, 5, h=10, i=11, j=12) 245 | ''' 246 | ) 247 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32, py33, py34, pypy 3 | setupdir = client 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | coverage 9 | 10 | setenv = 11 | COVERAGE_FILE=.cov-{envname} 12 | 13 | commands= 14 | coverage run --source=client/wdb {envbindir}/py.test test --junitxml=junit-{envname}.xml 15 | coverage xml -o coverage-{envname}.xml 16 | -------------------------------------------------------------------------------- /wdb-lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/wdb-lg.png -------------------------------------------------------------------------------- /wdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kozea/wdb/14d942444ecaeae21487f2423a76193509e0be70/wdb.png -------------------------------------------------------------------------------- /wdb_over_pdb/pdb.py: -------------------------------------------------------------------------------- 1 | from wdb import * 2 | 3 | 4 | class Pdb(Wdb): 5 | pass 6 | 7 | 8 | def import_from_stdlib(name): 9 | """Copied from pdbpp https://bitbucket.org/antocuni/pdb""" 10 | import os 11 | import types 12 | import code # arbitrary module which stays in the same dir as pdb 13 | 14 | stdlibdir, _ = os.path.split(code.__file__) 15 | pyfile = os.path.join(stdlibdir, name + '.py') 16 | result = types.ModuleType(name) 17 | exec(compile(open(pyfile).read(), pyfile, 'exec'), result.__dict__) 18 | return result 19 | 20 | 21 | old = import_from_stdlib('pdb') 22 | -------------------------------------------------------------------------------- /wdb_over_pdb/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | single_version = '--single-version-externally-managed' 5 | if single_version in sys.argv: 6 | sys.argv.remove(single_version) 7 | 8 | setup( 9 | name='wdb_over_pdb', 10 | version='0.1.1', 11 | author="Florian Mounier @ kozea", 12 | author_email="florian.mounier@kozea.fr", 13 | py_modules=['pdb'], 14 | url="http://github.com/Kozea/wdb", 15 | license='GPLv3', 16 | description='Hack to force use of wdb over pdb ' 17 | '(Useful for thing like py.test --pdb)', 18 | install_requires=["wdb"], 19 | ) 20 | --------------------------------------------------------------------------------