├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pylintrc ├── Changelog.rst ├── LICENSE.txt ├── MANIFEST.in ├── Readme.rst ├── frontend ├── .eslintignore ├── .eslintrc.js ├── Readme.rst ├── package-lock.json ├── package.json ├── src │ ├── button_events.js │ ├── globals.js │ ├── index.js │ ├── key_events.js │ ├── prism-line-numbers.js │ ├── styles.css │ ├── update_ui.js │ └── utils.js └── webpack.config.js ├── pyproject.toml ├── requirements.txt ├── screenshot.png ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── db.py ├── db_i.py ├── db_pm.py ├── db_ps.py └── tests.py ├── tox.ini └── web_pdb ├── __init__.py ├── buffer.py ├── static ├── bundle.min.js ├── bundle.min.js.LICENSE.txt ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── debug.png │ └── debug.svg └── styles.min.css ├── templates └── index.html ├── web_console.py └── wsgi_app.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish library 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | check-javascript: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16.17.0 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "16.17.0" 19 | 20 | - name: Install dependencies 21 | run: | 22 | cd frontend 23 | npm install 24 | 25 | - name: JS lint 26 | run: | 27 | cd frontend 28 | npm run lint 29 | 30 | - name: Build JS Bundle 31 | run: | 32 | cd frontend 33 | npm run build 34 | 35 | pylint: 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | 41 | - name: Set up Python 3.10 42 | uses: actions/setup-python@v2 43 | with: 44 | python-version: "3.10" 45 | 46 | - name: Install dependencies 47 | run: | 48 | python -m pip install -q --upgrade pip 49 | pip install -r requirements.txt 50 | 51 | - name: Check with Pylint 52 | run: | 53 | pylint web_pdb 54 | 55 | test: 56 | runs-on: ubuntu-latest 57 | strategy: 58 | matrix: 59 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | 64 | - name: Set up Python ${{ matrix.python-version }} 65 | uses: actions/setup-python@v2 66 | with: 67 | python-version: ${{ matrix.python-version }} 68 | 69 | - name: Install dependencies 70 | run: | 71 | python -m pip install --upgrade pip 72 | pip install tox tox-gh-actions 73 | - name: Test with tox 74 | run: tox 75 | 76 | publish: 77 | needs: 78 | - check-javascript 79 | - pylint 80 | - test 81 | runs-on: ubuntu-latest 82 | 83 | steps: 84 | - uses: actions/checkout@v3 85 | 86 | - name: Set up Python 3.10 87 | uses: actions/setup-python@v1 88 | with: 89 | python-version: "3.10" 90 | 91 | - name: Build artifacts 92 | run: | 93 | pip install wheel 94 | python setup.py sdist bdist_wheel 95 | 96 | - name: Publish a Python distribution to PyPI 97 | uses: pypa/gh-action-pypi-publish@release/v1 98 | with: 99 | user: __token__ 100 | password: ${{ secrets.PYPI_API_TOKEN }} 101 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | check-javascript: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Node.js 16.17.0 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 16.17.0 22 | 23 | - name: Install dependencies 24 | run: | 25 | cd frontend 26 | npm install 27 | 28 | - name: JS lint 29 | run: | 30 | cd frontend 31 | npm run lint 32 | 33 | - name: Build JS Bundle 34 | run: | 35 | cd frontend 36 | npm run build 37 | 38 | pylint: 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v3 43 | 44 | - name: Set up Python 3.11 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: "3.11" 48 | 49 | - name: Install dependencies 50 | run: | 51 | python -m pip install -q --upgrade pip 52 | pip install -q -r requirements.txt 53 | 54 | - name: Check with Pylint 55 | run: | 56 | pylint web_pdb 57 | 58 | test: 59 | runs-on: ubuntu-latest 60 | strategy: 61 | matrix: 62 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 63 | 64 | steps: 65 | - uses: actions/checkout@v3 66 | 67 | - name: Set up Python ${{ matrix.python-version }} 68 | uses: actions/setup-python@v2 69 | with: 70 | python-version: ${{ matrix.python-version }} 71 | 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install -q --upgrade pip 75 | pip install -q tox tox-gh-actions 76 | 77 | - name: Test with tox 78 | run: tox 79 | 80 | - name: 'Upload Screenshots' 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: screenshots 84 | path: tests/*.png 85 | retention-days: 3 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | .venv/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # PyCharm 95 | .idea/ 96 | 97 | # Custom 98 | *.bat 99 | *.cmd 100 | *.zip 101 | main.py 102 | node_modules/ 103 | .vscode/ 104 | .venv*/ 105 | tests/*.png 106 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold to be exceeded before program exits with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regex patterns to the ignore-list. The 48 | # regex matches against paths and can be in Posix or Windows format. 49 | ignore-paths= 50 | 51 | # Files or directories matching the regex patterns are skipped. The regex 52 | # matches against base names, not paths. The default value ignores Emacs file 53 | # locks 54 | ignore-patterns=^\.# 55 | 56 | # List of module names for which member attributes should not be checked 57 | # (useful for modules/projects where namespaces are manipulated during runtime 58 | # and thus existing member attributes cannot be deduced by static analysis). It 59 | # supports qualified module names, as well as Unix pattern matching. 60 | ignored-modules= 61 | 62 | # Python code to execute, usually for sys.path manipulation such as 63 | # pygtk.require(). 64 | #init-hook= 65 | 66 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 67 | # number of processors available to use, and will cap the count on Windows to 68 | # avoid hangs. 69 | jobs=1 70 | 71 | # Control the amount of potential inferred values when inferring a single 72 | # object. This can help the performance when dealing with large functions or 73 | # complex, nested conditions. 74 | limit-inference-results=100 75 | 76 | # List of plugins (as comma separated values of python module names) to load, 77 | # usually to register additional checkers. 78 | load-plugins= 79 | 80 | # Pickle collected data for later comparisons. 81 | persistent=yes 82 | 83 | # Minimum Python version to use for version dependent checks. Will default to 84 | # the version used to run pylint. 85 | py-version=3.7 86 | 87 | # Discover python modules and packages in the file system subtree. 88 | recursive=no 89 | 90 | # When enabled, pylint would attempt to guess common misconfiguration and emit 91 | # user-friendly hints instead of false-positive error messages. 92 | suggestion-mode=yes 93 | 94 | # Allow loading of arbitrary C extensions. Extensions are imported into the 95 | # active Python interpreter and may run arbitrary code. 96 | unsafe-load-any-extension=no 97 | 98 | # In verbose mode, extra non-checker-related info will be displayed. 99 | #verbose= 100 | 101 | 102 | [REPORTS] 103 | 104 | # Python expression which should return a score less than or equal to 10. You 105 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 106 | # 'convention', and 'info' which contain the number of messages in each 107 | # category, as well as 'statement' which is the total number of statements 108 | # analyzed. This score is used by the global evaluation report (RP0004). 109 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 110 | 111 | # Template used to display messages. This is a python new-style format string 112 | # used to format the message information. See doc for all details. 113 | msg-template= 114 | 115 | # Set the output format. Available formats are text, parseable, colorized, json 116 | # and msvs (visual studio). You can also give a reporter class, e.g. 117 | # mypackage.mymodule.MyReporterClass. 118 | #output-format= 119 | 120 | # Tells whether to display a full report or only the messages. 121 | reports=no 122 | 123 | # Activate the evaluation score. 124 | score=yes 125 | 126 | 127 | [MESSAGES CONTROL] 128 | 129 | # Only show warnings with the listed confidence levels. Leave empty to show 130 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 131 | # UNDEFINED. 132 | confidence=HIGH, 133 | CONTROL_FLOW, 134 | INFERENCE, 135 | INFERENCE_FAILURE, 136 | UNDEFINED 137 | 138 | # Disable the message, report, category or checker with the given id(s). You 139 | # can either give multiple identifiers separated by comma (,) or put this 140 | # option multiple times (only on the command line, not in the configuration 141 | # file where it should appear only once). You can also use "--disable=all" to 142 | # disable everything first and then re-enable specific checks. For example, if 143 | # you want to run only the similarities checker, you can use "--disable=all 144 | # --enable=similarities". If you want to run only the classes checker, but have 145 | # no Warning level messages displayed, use "--disable=all --enable=classes 146 | # --disable=W". 147 | disable=raw-checker-failed, 148 | bad-inline-option, 149 | locally-disabled, 150 | file-ignored, 151 | suppressed-message, 152 | useless-suppression, 153 | deprecated-pragma, 154 | use-symbolic-message-instead, 155 | invalid-name, 156 | missing-docstring 157 | 158 | # Enable the message, report, category or checker with the given id(s). You can 159 | # either give multiple identifier separated by comma (,) or put this option 160 | # multiple time (only on the command line, not in the configuration file where 161 | # it should appear only once). See also the "--disable" option for examples. 162 | enable=c-extension-no-member 163 | 164 | 165 | [SIMILARITIES] 166 | 167 | # Comments are removed from the similarity computation 168 | ignore-comments=yes 169 | 170 | # Docstrings are removed from the similarity computation 171 | ignore-docstrings=yes 172 | 173 | # Imports are removed from the similarity computation 174 | ignore-imports=yes 175 | 176 | # Signatures are removed from the similarity computation 177 | ignore-signatures=yes 178 | 179 | # Minimum lines number of a similarity. 180 | min-similarity-lines=4 181 | 182 | 183 | [BASIC] 184 | 185 | # Naming style matching correct argument names. 186 | argument-naming-style=snake_case 187 | 188 | # Regular expression matching correct argument names. Overrides argument- 189 | # naming-style. If left empty, argument names will be checked with the set 190 | # naming style. 191 | #argument-rgx= 192 | 193 | # Naming style matching correct attribute names. 194 | attr-naming-style=snake_case 195 | 196 | # Regular expression matching correct attribute names. Overrides attr-naming- 197 | # style. If left empty, attribute names will be checked with the set naming 198 | # style. 199 | #attr-rgx= 200 | 201 | # Bad variable names which should always be refused, separated by a comma. 202 | bad-names=foo, 203 | bar, 204 | baz, 205 | toto, 206 | tutu, 207 | tata 208 | 209 | # Bad variable names regexes, separated by a comma. If names match any regex, 210 | # they will always be refused 211 | bad-names-rgxs= 212 | 213 | # Naming style matching correct class attribute names. 214 | class-attribute-naming-style=any 215 | 216 | # Regular expression matching correct class attribute names. Overrides class- 217 | # attribute-naming-style. If left empty, class attribute names will be checked 218 | # with the set naming style. 219 | #class-attribute-rgx= 220 | 221 | # Naming style matching correct class constant names. 222 | class-const-naming-style=UPPER_CASE 223 | 224 | # Regular expression matching correct class constant names. Overrides class- 225 | # const-naming-style. If left empty, class constant names will be checked with 226 | # the set naming style. 227 | #class-const-rgx= 228 | 229 | # Naming style matching correct class names. 230 | class-naming-style=PascalCase 231 | 232 | # Regular expression matching correct class names. Overrides class-naming- 233 | # style. If left empty, class names will be checked with the set naming style. 234 | #class-rgx= 235 | 236 | # Naming style matching correct constant names. 237 | const-naming-style=UPPER_CASE 238 | 239 | # Regular expression matching correct constant names. Overrides const-naming- 240 | # style. If left empty, constant names will be checked with the set naming 241 | # style. 242 | #const-rgx= 243 | 244 | # Minimum line length for functions/classes that require docstrings, shorter 245 | # ones are exempt. 246 | docstring-min-length=-1 247 | 248 | # Naming style matching correct function names. 249 | function-naming-style=snake_case 250 | 251 | # Regular expression matching correct function names. Overrides function- 252 | # naming-style. If left empty, function names will be checked with the set 253 | # naming style. 254 | #function-rgx= 255 | 256 | # Good variable names which should always be accepted, separated by a comma. 257 | good-names=i, 258 | j, 259 | k, 260 | ex, 261 | Run, 262 | _ 263 | 264 | # Good variable names regexes, separated by a comma. If names match any regex, 265 | # they will always be accepted 266 | good-names-rgxs= 267 | 268 | # Include a hint for the correct naming format with invalid-name. 269 | include-naming-hint=no 270 | 271 | # Naming style matching correct inline iteration names. 272 | inlinevar-naming-style=any 273 | 274 | # Regular expression matching correct inline iteration names. Overrides 275 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 276 | # with the set naming style. 277 | #inlinevar-rgx= 278 | 279 | # Naming style matching correct method names. 280 | method-naming-style=snake_case 281 | 282 | # Regular expression matching correct method names. Overrides method-naming- 283 | # style. If left empty, method names will be checked with the set naming style. 284 | #method-rgx= 285 | 286 | # Naming style matching correct module names. 287 | module-naming-style=snake_case 288 | 289 | # Regular expression matching correct module names. Overrides module-naming- 290 | # style. If left empty, module names will be checked with the set naming style. 291 | #module-rgx= 292 | 293 | # Colon-delimited sets of names that determine each other's naming style when 294 | # the name regexes allow several styles. 295 | name-group= 296 | 297 | # Regular expression which should only match function or class names that do 298 | # not require a docstring. 299 | no-docstring-rgx=^_ 300 | 301 | # List of decorators that produce properties, such as abc.abstractproperty. Add 302 | # to this list to register other decorators that produce valid properties. 303 | # These decorators are taken in consideration only for invalid-name. 304 | property-classes=abc.abstractproperty 305 | 306 | # Regular expression matching correct type variable names. If left empty, type 307 | # variable names will be checked with the set naming style. 308 | #typevar-rgx= 309 | 310 | # Naming style matching correct variable names. 311 | variable-naming-style=snake_case 312 | 313 | # Regular expression matching correct variable names. Overrides variable- 314 | # naming-style. If left empty, variable names will be checked with the set 315 | # naming style. 316 | #variable-rgx= 317 | 318 | 319 | [EXCEPTIONS] 320 | 321 | # Exceptions that will emit a warning when caught. 322 | overgeneral-exceptions=BaseException, 323 | Exception 324 | 325 | 326 | [VARIABLES] 327 | 328 | # List of additional names supposed to be defined in builtins. Remember that 329 | # you should avoid defining new builtins when possible. 330 | additional-builtins= 331 | 332 | # Tells whether unused global variables should be treated as a violation. 333 | allow-global-unused-variables=yes 334 | 335 | # List of names allowed to shadow builtins 336 | allowed-redefined-builtins= 337 | 338 | # List of strings which can identify a callback function by name. A callback 339 | # name must start or end with one of those strings. 340 | callbacks=cb_, 341 | _cb 342 | 343 | # A regular expression matching the name of dummy variables (i.e. expected to 344 | # not be used). 345 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 346 | 347 | # Argument names that match this expression will be ignored. Default to name 348 | # with leading underscore. 349 | ignored-argument-names=_.*|^ignored_|^unused_ 350 | 351 | # Tells whether we should check for unused import in __init__ files. 352 | init-import=no 353 | 354 | # List of qualified module names which can have objects that can redefine 355 | # builtins. 356 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 357 | 358 | 359 | [REFACTORING] 360 | 361 | # Maximum number of nested blocks for function / method body 362 | max-nested-blocks=5 363 | 364 | # Complete name of functions that never returns. When checking for 365 | # inconsistent-return-statements if a never returning function is called then 366 | # it will be considered as an explicit return statement and no message will be 367 | # printed. 368 | never-returning-functions=sys.exit,argparse.parse_error 369 | 370 | 371 | [STRING] 372 | 373 | # This flag controls whether inconsistent-quotes generates a warning when the 374 | # character used as a quote delimiter is used inconsistently within a module. 375 | check-quote-consistency=no 376 | 377 | # This flag controls whether the implicit-str-concat should generate a warning 378 | # on implicit string concatenation in sequences defined over several lines. 379 | check-str-concat-over-line-jumps=no 380 | 381 | 382 | [TYPECHECK] 383 | 384 | # List of decorators that produce context managers, such as 385 | # contextlib.contextmanager. Add to this list to register other decorators that 386 | # produce valid context managers. 387 | contextmanager-decorators=contextlib.contextmanager 388 | 389 | # List of members which are set dynamically and missed by pylint inference 390 | # system, and so shouldn't trigger E1101 when accessed. Python regular 391 | # expressions are accepted. 392 | generated-members= 393 | 394 | # Tells whether to warn about missing members when the owner of the attribute 395 | # is inferred to be None. 396 | ignore-none=yes 397 | 398 | # This flag controls whether pylint should warn about no-member and similar 399 | # checks whenever an opaque object is returned when inferring. The inference 400 | # can return multiple potential results while evaluating a Python object, but 401 | # some branches might not be evaluated, which results in partial inference. In 402 | # that case, it might be useful to still emit no-member and other checks for 403 | # the rest of the inferred objects. 404 | ignore-on-opaque-inference=yes 405 | 406 | # List of symbolic message names to ignore for Mixin members. 407 | ignored-checks-for-mixins=no-member, 408 | not-async-context-manager, 409 | not-context-manager, 410 | attribute-defined-outside-init 411 | 412 | # List of class names for which member attributes should not be checked (useful 413 | # for classes with dynamically set attributes). This supports the use of 414 | # qualified names. 415 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 416 | 417 | # Show a hint with possible names when a member name was not found. The aspect 418 | # of finding the hint is based on edit distance. 419 | missing-member-hint=yes 420 | 421 | # The minimum edit distance a name should have in order to be considered a 422 | # similar match for a missing member name. 423 | missing-member-hint-distance=1 424 | 425 | # The total number of similar names that should be taken in consideration when 426 | # showing a hint for a missing member. 427 | missing-member-max-choices=1 428 | 429 | # Regex pattern to define which classes are considered mixins. 430 | mixin-class-rgx=.*[Mm]ixin 431 | 432 | # List of decorators that change the signature of a decorated function. 433 | signature-mutators= 434 | 435 | 436 | [CLASSES] 437 | 438 | # Warn about protected attribute access inside special methods 439 | check-protected-access-in-special-methods=no 440 | 441 | # List of method names used to declare (i.e. assign) instance attributes. 442 | defining-attr-methods=__init__, 443 | __new__, 444 | setUp, 445 | __post_init__ 446 | 447 | # List of member names, which should be excluded from the protected access 448 | # warning. 449 | exclude-protected=_asdict, 450 | _fields, 451 | _replace, 452 | _source, 453 | _make 454 | 455 | # List of valid names for the first argument in a class method. 456 | valid-classmethod-first-arg=cls 457 | 458 | # List of valid names for the first argument in a metaclass class method. 459 | valid-metaclass-classmethod-first-arg=cls 460 | 461 | 462 | [IMPORTS] 463 | 464 | # List of modules that can be imported at any level, not just the top level 465 | # one. 466 | allow-any-import-level= 467 | 468 | # Allow wildcard imports from modules that define __all__. 469 | allow-wildcard-with-all=no 470 | 471 | # Deprecated modules which should not be used, separated by a comma. 472 | deprecated-modules= 473 | 474 | # Output a graph (.gv or any supported image format) of external dependencies 475 | # to the given file (report RP0402 must not be disabled). 476 | ext-import-graph= 477 | 478 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 479 | # external) dependencies to the given file (report RP0402 must not be 480 | # disabled). 481 | import-graph= 482 | 483 | # Output a graph (.gv or any supported image format) of internal dependencies 484 | # to the given file (report RP0402 must not be disabled). 485 | int-import-graph= 486 | 487 | # Force import order to recognize a module as part of the standard 488 | # compatibility libraries. 489 | known-standard-library= 490 | 491 | # Force import order to recognize a module as part of a third party library. 492 | known-third-party=enchant 493 | 494 | # Couples of modules and preferred modules, separated by a comma. 495 | preferred-modules= 496 | 497 | 498 | [FORMAT] 499 | 500 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 501 | expected-line-ending-format= 502 | 503 | # Regexp for a line that is allowed to be longer than the limit. 504 | ignore-long-lines=^\s*(# )??$ 505 | 506 | # Number of spaces of indent required inside a hanging or continued line. 507 | indent-after-paren=4 508 | 509 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 510 | # tab). 511 | indent-string=' ' 512 | 513 | # Maximum number of characters on a single line. 514 | max-line-length=100 515 | 516 | # Maximum number of lines in a module. 517 | max-module-lines=1000 518 | 519 | # Allow the body of a class to be on the same line as the declaration if body 520 | # contains single statement. 521 | single-line-class-stmt=no 522 | 523 | # Allow the body of an if to be on the same line as the test if there is no 524 | # else. 525 | single-line-if-stmt=no 526 | 527 | 528 | [MISCELLANEOUS] 529 | 530 | # List of note tags to take in consideration, separated by a comma. 531 | notes=FIXME, 532 | XXX, 533 | TODO 534 | 535 | # Regular expression of note tags to take in consideration. 536 | notes-rgx= 537 | 538 | 539 | [SPELLING] 540 | 541 | # Limits count of emitted suggestions for spelling mistakes. 542 | max-spelling-suggestions=4 543 | 544 | # Spelling dictionary name. Available dictionaries: none. To make it work, 545 | # install the 'python-enchant' package. 546 | spelling-dict= 547 | 548 | # List of comma separated words that should be considered directives if they 549 | # appear at the beginning of a comment and should not be checked. 550 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 551 | 552 | # List of comma separated words that should not be checked. 553 | spelling-ignore-words= 554 | 555 | # A path to a file that contains the private dictionary; one word per line. 556 | spelling-private-dict-file= 557 | 558 | # Tells whether to store unknown words to the private dictionary (see the 559 | # --spelling-private-dict-file option) instead of raising a message. 560 | spelling-store-unknown-words=no 561 | 562 | 563 | [LOGGING] 564 | 565 | # The type of string formatting that logging methods do. `old` means using % 566 | # formatting, `new` is for `{}` formatting. 567 | logging-format-style=old 568 | 569 | # Logging modules to check that the string format arguments are in logging 570 | # function parameter format. 571 | logging-modules=logging 572 | 573 | 574 | [DESIGN] 575 | 576 | # List of regular expressions of class ancestor names to ignore when counting 577 | # public methods (see R0903) 578 | exclude-too-few-public-methods= 579 | 580 | # List of qualified class names to ignore when counting class parents (see 581 | # R0901) 582 | ignored-parents= 583 | 584 | # Maximum number of arguments for function / method. 585 | max-args=5 586 | 587 | # Maximum number of attributes for a class (see R0902). 588 | max-attributes=7 589 | 590 | # Maximum number of boolean expressions in an if statement (see R0916). 591 | max-bool-expr=5 592 | 593 | # Maximum number of branch for function / method body. 594 | max-branches=12 595 | 596 | # Maximum number of locals for function / method body. 597 | max-locals=15 598 | 599 | # Maximum number of parents for a class (see R0901). 600 | max-parents=7 601 | 602 | # Maximum number of public methods for a class (see R0904). 603 | max-public-methods=20 604 | 605 | # Maximum number of return / yield for function / method body. 606 | max-returns=6 607 | 608 | # Maximum number of statements in function / method body. 609 | max-statements=50 610 | 611 | # Minimum number of public methods for a class (see R0903). 612 | min-public-methods=2 613 | -------------------------------------------------------------------------------- /Changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ######### 3 | 4 | v.1.6.3 5 | ======= 6 | * Fixed incompatibility with latest Bottle versions 7 | 8 | v.1.6.2 9 | ======= 10 | * Rewrite tests with the latest Selenium version. 11 | * Bump asyncore-wsgi version in dependencies. 12 | 13 | v.1.6.0 14 | ======= 15 | 16 | * Removed Python 2 support. 17 | * Moved to the modern Python package layout. 18 | 19 | v.1.5.7 20 | ======= 21 | 22 | * Updated frontend dependencies. 23 | 24 | v.1.5.6 25 | ======= 26 | 27 | * Fixed the issue when a local variable could not be assigned from the debugger console. 28 | * Updated frontend dependencies. 29 | 30 | v.1.5.3 31 | ======= 32 | 33 | * Fixed the issue with closed debugger still being stored in ``active_instance`` 34 | class property that prevented starting a new debugger session (thanks to **maiamcc**). 35 | 36 | v.1.5.2 37 | ======= 38 | 39 | * Updated frontend dependencies. 40 | 41 | v.1.5.1 42 | ======= 43 | 44 | * Fixed using ``inspect`` command for variables with ``None`` values. 45 | 46 | v.1.5.0 47 | ======= 48 | 49 | * Added ``inspect`` command. 50 | 51 | v.1.4.4 52 | ======= 53 | 54 | * Upgrade Prism.js to v.1.15.0 55 | 56 | v.1.4.3 57 | ======= 58 | 59 | * Use gzip compression instead of deflate for web-console endpoints 60 | (`more info `_). 61 | 62 | v.1.4.2 63 | ======= 64 | 65 | * Bump ``asyncore-wsgi`` dependency version. This fixes issues with the web-UI 66 | on some platforms. 67 | * Code cleanup. 68 | 69 | v.1.4.1 70 | ======= 71 | 72 | * Use full path for setting and removing breakpoints. 73 | 74 | v.1.4.0 75 | ======= 76 | 77 | * Replaced a wsgiref-based multi-threaded WSGI server with a single-threaded 78 | asynchronous WSGI/WebSocket server. 79 | * Implemented WebSockets instead of periodic back-end polling for retrieving 80 | updated debugger data. 81 | 82 | v.1.3.5 83 | ======= 84 | 85 | * Fix crash when clearing a breakpoint on Linux. 86 | * Fix autoscrolling on large files. 87 | * Move frontend to modern JavaScript syntax and tooling. 88 | * Optimize Python syntax highlighting performance. 89 | 90 | v.1.3.4 91 | ======= 92 | 93 | * Fix a bug with patched ``cl`` command not working. 94 | 95 | v.1.3.3 96 | ======= 97 | 98 | * Fixed setting ``set_trace()`` at the last line of a Python script. 99 | * Fixed clearing a breakpoint at setups with the current workdir different 100 | from the current module directory. 101 | 102 | v.1.3.2 103 | ======= 104 | 105 | * Internal changes. 106 | 107 | v.1.3.1 108 | ======= 109 | 110 | * Now if web-console data haven't changed 111 | the back-end sends "null" response body instead of a 403 error. 112 | 113 | v.1.3.0 114 | ======= 115 | 116 | * Implemented a multi-threaded WSGI server to increase responsiveness of the web-UI. 117 | 118 | v.1.2.2 119 | ======= 120 | 121 | * Added deflate compression for data sent to a browser. 122 | * Attempt to fix **Current file** box auto-scrolling. 123 | 124 | v.1.2.1 125 | ======= 126 | 127 | * Logger fix. 128 | 129 | v.1.2.0 130 | ======= 131 | 132 | * Minor UI redesign. 133 | * Added a quick action toolbar and hotkeys for common commands. 134 | * Added a quick help dialog. 135 | * Breakpoints can be added/deleted with a click on a line number. 136 | * The **Currrent file** box is not auto-scrolled if the current line hasn't changed. 137 | * Multiple ``set_trace()`` and ``post_mortem()`` calls are processed correctly. 138 | * Added random web-UI port selection with ``port=-1``. 139 | 140 | v.1.1.0 141 | ======= 142 | 143 | * Initial PyPI release 144 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Roman Miroshnychenko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py Readme.rst Changelog.rst LICENSE.txt 2 | recursive-include web_pdb * 3 | recursive-include tests * 4 | exclude main.py 5 | global-exclude *.pyc __pycache__ *.log 6 | -------------------------------------------------------------------------------- /Readme.rst: -------------------------------------------------------------------------------- 1 | Web-PDB 2 | ####### 3 | 4 | .. image:: https://github.com/romanvm/python-web-pdb/actions/workflows/tests.yml/badge.svg 5 | :target: https://github.com/romanvm/python-web-pdb/actions/workflows/tests.yml 6 | :alt: GitHub Action tests 7 | .. image:: https://badge.fury.io/py/web-pdb.svg 8 | :target: https://badge.fury.io/py/web-pdb 9 | :alt: PyPI version 10 | 11 | Web-PDB is a web-interface for Python's built-in `PDB`_ debugger. 12 | It allows to debug Python scripts remotely in a web-browser. 13 | 14 | Important Notice 15 | ================ 16 | 17 | Currently this project is in a minimum maintenance mode. 18 | It means that I will try to maintain compatibility with new Python versions and address 19 | critical issues, if any arise. But at the moment I have no time or resources to develop 20 | new features or properly review external contributions. That is why only pull requests 21 | with bug fixes will be prioritized. Other issues or pull requests may or may not be reviewed or accepted. 22 | 23 | Features 24 | ======== 25 | 26 | - Responsive design based on `Bootstrap`_. 27 | - Python syntax highlighting with `Prism`_ ("Okaida" theme). 28 | - Supports all PDB features. 29 | - Standard input and output can be redirected to the web-console 30 | to interact with Python scripts remotely. 31 | - **Current file** box tracks current position in a file being executed. 32 | Red line numbers indicate breakpoints, if any. 33 | - **Globals** and **Locals** boxes show local and global variables in the current scope. 34 | Special variables that start and end with double underscores ``__`` are excluded 35 | (you can always view them using PDB commands). 36 | - Command history that stores up to 10 last unique PDB commands (accessed by arrow UP/DOWN keys). 37 | 38 | .. figure:: https://raw.githubusercontent.com/romanvm/python-web-pdb/master/screenshot.png 39 | :alt: Web-PDB screenshot 40 | :width: 640px 41 | :height: 490px 42 | 43 | *Web-PDB console in Chrome browser* 44 | 45 | Usage 46 | ===== 47 | 48 | Install Web-PDB into your working Python environment:: 49 | 50 | pip install web-pdb 51 | 52 | Insert the following line into your Python program at the point where you want 53 | to start debugging: 54 | 55 | .. code-block:: python 56 | 57 | import web_pdb; web_pdb.set_trace() 58 | 59 | The ``set_trace()`` call will suspend your program and open a web-UI at the default port ``5555`` 60 | (port value can be changed). Enter in your browser's address bar: 61 | ``http://:5555``, 62 | for example ``http://monty-python:5555``, 63 | and you should see the web-UI like the one on the preceding screenshot. 64 | Now you can use all PDB commands and features. Additional **Current file**, **Globals** 65 | and **Locals** information boxes help you better track your program runtime state. 66 | 67 | **Note**: it is strongly recommended to work with the Web-PDB web-UI only in one browser session. 68 | With more than one browser window accessing the web-UI it may display incorrect data in one or more 69 | browser sessions. 70 | 71 | Subsequent ``set_trace()`` calls can be used as hardcoded breakpoints. 72 | 73 | Web-PDB is compatible with the new `breakpoint()`_ function added in Python 3.7. 74 | Set environment variable ``PYTHONBREAKPOINT="web_pdb.set_trace"`` to launch Web-PDB 75 | with ``breakpoint()``. 76 | 77 | Additionally, Web-PDB provides ``catch_post_mortem`` context manager that can catch 78 | unhandled exceptions raised within its scope and automatically start PDB post-mortem debugging session. 79 | For example: 80 | 81 | .. code-block:: python 82 | 83 | import web_pdb 84 | 85 | with web_pdb.catch_post_mortem(): 86 | # Some error-prone code 87 | assert foo == bar, 'Oops!' 88 | 89 | For more detailed info about the Web-PDB API read docstrings in the ``./web_pdb/__init__.py`` file. 90 | 91 | The ``inspect`` Command 92 | ----------------------- 93 | 94 | Web-PDB provides ``inspect`` or ``i`` command that is not present in the original PDB. 95 | This command outputs the list of object's members along with their values. 96 | Syntax: ``inspect `` or ``i ``. 97 | 98 | Special members with names enclosed in double underscores (``__``) are ignored. 99 | 100 | Considerations for Multithreading and Multiprocessing Programs 101 | ============================================================== 102 | Multithreading 103 | -------------- 104 | 105 | Web-PDB maintains one debugger instance that traces only one thread. You should not call ``set_trace()`` 106 | from different threads to avoid race conditions. Each thread needs to be debugged separately one at a time. 107 | 108 | Multiprocessing 109 | --------------- 110 | 111 | Each process can have its own debugger instance provided you call ``set_trace`` with a different port value 112 | for each process. This way you can debug each process in a separate browser tab/window. 113 | To simplify this you can use ``set_trace(port=-1)`` to select a random port between 32768 and 65536. 114 | 115 | Compatibility 116 | ============= 117 | 118 | - **Python**: 3.6+ 119 | - **Browsers**: Firefox, Chrome (all modern browsers should work) 120 | 121 | License 122 | ======= 123 | 124 | MIT, see ``LICENSE.txt``. 125 | 126 | The debugger icon made by `Freepik`_ from `www.flaticon.com`_ is licensed by `CC 3.0 BY`_. 127 | 128 | .. _PDB: https://docs.python.org/3.6/library/pdb.html 129 | .. _Bootstrap: http://getbootstrap.com 130 | .. _Prism: http://prismjs.com/ 131 | .. _Freepik: http://www.freepik.com 132 | .. _www.flaticon.com: http://www.flaticon.com 133 | .. _CC 3.0 BY: http://creativecommons.org/licenses/by/3.0/ 134 | .. _breakpoint(): https://docs.python.org/3/library/functions.html#breakpoint 135 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | webpack.config.js 3 | node_modules/** 4 | prism-line-numbers.js 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "accessor-pairs": "error", 12 | "array-bracket-newline": "error", 13 | "array-bracket-spacing": "error", 14 | "array-callback-return": "error", 15 | "array-element-newline": "error", 16 | "arrow-body-style": "error", 17 | "arrow-parens": [ 18 | "error", 19 | "always" 20 | ], 21 | "arrow-spacing": [ 22 | "error", 23 | { 24 | "after": true, 25 | "before": true 26 | } 27 | ], 28 | "block-scoped-var": "error", 29 | "block-spacing": [ 30 | "error", 31 | "always" 32 | ], 33 | "brace-style": "off", 34 | "callback-return": "error", 35 | "camelcase": "off", 36 | "capitalized-comments": [ 37 | "error", 38 | "always" 39 | ], 40 | "class-methods-use-this": "error", 41 | "comma-dangle": "off", 42 | "comma-spacing": [ 43 | "error", 44 | { 45 | "after": true, 46 | "before": false 47 | } 48 | ], 49 | "comma-style": [ 50 | "error", 51 | "last" 52 | ], 53 | "complexity": "error", 54 | "computed-property-spacing": [ 55 | "error", 56 | "never" 57 | ], 58 | "consistent-return": "off", 59 | "consistent-this": "error", 60 | "curly": "error", 61 | "default-case": "error", 62 | "dot-location": [ 63 | "error", 64 | "property" 65 | ], 66 | "dot-notation": "error", 67 | "eol-last": "error", 68 | "eqeqeq": "off", 69 | "for-direction": "error", 70 | "func-call-spacing": "error", 71 | "func-name-matching": "error", 72 | "func-names": "error", 73 | "func-style": [ 74 | "error", 75 | "declaration" 76 | ], 77 | "generator-star-spacing": "error", 78 | "global-require": "error", 79 | "guard-for-in": "error", 80 | "handle-callback-err": "error", 81 | "id-blacklist": "error", 82 | "id-length": "off", 83 | "id-match": "error", 84 | "indent": "off", 85 | "indent-legacy": "off", 86 | "init-declarations": "error", 87 | "jsx-quotes": "error", 88 | "key-spacing": "error", 89 | "keyword-spacing": [ 90 | "error", 91 | { 92 | "after": true, 93 | "before": true 94 | } 95 | ], 96 | "line-comment-position": "error", 97 | "linebreak-style": [ 98 | "off", 99 | "unix" 100 | ], 101 | "lines-around-comment": "error", 102 | "lines-around-directive": "error", 103 | "max-depth": "error", 104 | "max-len": "off", 105 | "max-lines": "error", 106 | "max-nested-callbacks": "error", 107 | "max-params": "error", 108 | "max-statements": "off", 109 | "max-statements-per-line": "off", 110 | "multiline-ternary": "error", 111 | "new-cap": "error", 112 | "new-parens": "error", 113 | "newline-after-var": "off", 114 | "newline-before-return": "off", 115 | "newline-per-chained-call": "error", 116 | "no-alert": "error", 117 | "no-array-constructor": "error", 118 | "no-await-in-loop": "error", 119 | "no-bitwise": "error", 120 | "no-buffer-constructor": "error", 121 | "no-caller": "error", 122 | "no-catch-shadow": "error", 123 | "no-confusing-arrow": "error", 124 | "no-continue": "error", 125 | "no-div-regex": "error", 126 | "no-duplicate-imports": "error", 127 | "no-else-return": "error", 128 | "no-empty-function": "error", 129 | "no-eq-null": "error", 130 | "no-eval": "error", 131 | "no-extend-native": "error", 132 | "no-extra-bind": "error", 133 | "no-extra-label": "error", 134 | "no-extra-parens": "error", 135 | "no-floating-decimal": "error", 136 | "no-implicit-coercion": "error", 137 | "no-implicit-globals": "error", 138 | "no-implied-eval": "error", 139 | "no-inline-comments": "error", 140 | "no-invalid-this": "error", 141 | "no-iterator": "error", 142 | "no-label-var": "error", 143 | "no-labels": "error", 144 | "no-lone-blocks": "error", 145 | "no-lonely-if": "error", 146 | "no-loop-func": "error", 147 | "no-magic-numbers": "off", 148 | "no-mixed-operators": "off", 149 | "no-mixed-requires": "error", 150 | "no-multi-assign": "error", 151 | "no-multi-spaces": "error", 152 | "no-multi-str": "error", 153 | "no-multiple-empty-lines": "error", 154 | "no-native-reassign": "error", 155 | "no-negated-condition": "error", 156 | "no-negated-in-lhs": "error", 157 | "no-nested-ternary": "error", 158 | "no-new": "error", 159 | "no-new-func": "error", 160 | "no-new-object": "error", 161 | "no-new-require": "error", 162 | "no-new-wrappers": "error", 163 | "no-octal-escape": "error", 164 | "no-param-reassign": "error", 165 | "no-path-concat": "error", 166 | "no-plusplus": "off", 167 | "no-process-env": "error", 168 | "no-process-exit": "error", 169 | "no-proto": "error", 170 | "no-prototype-builtins": "error", 171 | "no-restricted-globals": "error", 172 | "no-restricted-imports": "error", 173 | "no-restricted-modules": "error", 174 | "no-restricted-properties": "error", 175 | "no-restricted-syntax": "error", 176 | "no-return-assign": "error", 177 | "no-return-await": "error", 178 | "no-script-url": "error", 179 | "no-self-compare": "error", 180 | "no-sequences": "error", 181 | "no-shadow": "error", 182 | "no-shadow-restricted-names": "error", 183 | "no-spaced-func": "error", 184 | "no-sync": "error", 185 | "no-tabs": "error", 186 | "no-template-curly-in-string": "error", 187 | "no-ternary": "error", 188 | "no-throw-literal": "error", 189 | "no-trailing-spaces": "error", 190 | "no-undef-init": "error", 191 | "no-undefined": "error", 192 | "no-underscore-dangle": "error", 193 | "no-unmodified-loop-condition": "error", 194 | "no-unneeded-ternary": "error", 195 | "no-unused-expressions": "error", 196 | "no-use-before-define": "error", 197 | "no-useless-call": "error", 198 | "no-useless-computed-key": "error", 199 | "no-useless-concat": "error", 200 | "no-useless-constructor": "error", 201 | "no-useless-rename": "error", 202 | "no-useless-return": "error", 203 | "no-var": "off", 204 | "no-void": "error", 205 | "no-warning-comments": "error", 206 | "no-whitespace-before-property": "error", 207 | "no-with": "error", 208 | "nonblock-statement-body-position": "error", 209 | "object-curly-newline": "off", 210 | "object-curly-spacing": [ 211 | "error", 212 | "always" 213 | ], 214 | "object-property-newline": "error", 215 | "object-shorthand": "error", 216 | "one-var": "error", 217 | "one-var-declaration-per-line": "error", 218 | "operator-assignment": "error", 219 | "operator-linebreak": "error", 220 | "padded-blocks": "off", 221 | "padding-line-between-statements": "error", 222 | "prefer-arrow-callback": "error", 223 | "prefer-const": "off", 224 | "prefer-numeric-literals": "error", 225 | "prefer-promise-reject-errors": "error", 226 | "prefer-reflect": "error", 227 | "prefer-rest-params": "error", 228 | "prefer-spread": "error", 229 | "prefer-template": "off", 230 | "quote-props": "off", 231 | "quotes": [ 232 | "error", 233 | "single" 234 | ], 235 | "radix": "error", 236 | "require-await": "error", 237 | "require-jsdoc": "off", 238 | "rest-spread-spacing": "error", 239 | "semi": "off", 240 | "semi-spacing": "error", 241 | "semi-style": [ 242 | "error", 243 | "last" 244 | ], 245 | "sort-imports": "off", 246 | "sort-keys": "off", 247 | "sort-vars": "error", 248 | "space-before-blocks": "error", 249 | "space-before-function-paren": "off", 250 | "space-in-parens": [ 251 | "error", 252 | "never" 253 | ], 254 | "space-infix-ops": "error", 255 | "space-unary-ops": "error", 256 | "spaced-comment": [ 257 | "error", 258 | "always" 259 | ], 260 | "strict": "error", 261 | "switch-colon-spacing": "error", 262 | "symbol-description": "error", 263 | "template-curly-spacing": [ 264 | "error", 265 | "never" 266 | ], 267 | "template-tag-spacing": "error", 268 | "unicode-bom": [ 269 | "error", 270 | "never" 271 | ], 272 | "valid-jsdoc": "error", 273 | "vars-on-top": "error", 274 | "wrap-iife": "error", 275 | "wrap-regex": "error", 276 | "yield-star-spacing": "error", 277 | "yoda": [ 278 | "error", 279 | "never" 280 | ] 281 | } 282 | }; -------------------------------------------------------------------------------- /frontend/Readme.rst: -------------------------------------------------------------------------------- 1 | Frontend Code 2 | ============= 3 | 4 | Web-PDB is distributed with pre-bundled and minified JavaScript and CSS files, 5 | and all the necessary static assets. However, if you want to make some changes 6 | to the frontend code and/or to bundle the resulting code yourself, you need 7 | to install the necessary development dependencies. First, install Node.js and NPM 8 | for your platform. Then go to the ``./frontend/`` directory and run 9 | ``npm install`` there. 10 | 11 | Available Commands 12 | ------------------ 13 | 14 | - ``npm run build-dev``: build development bundle. 15 | - ``npm run build``: build production (minified) bundle. 16 | - ``npm run watch``: watch the frontend source files for changes (during development). 17 | - ``npm run lint``: run ESLint with the default set of rules for ES6. 18 | 19 | Bundled static files are automatically placed in ``./web_pdb/static`` directory. 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-pdb", 3 | "version": "1.6.2", 4 | "description": "Front-end for Web-PDB Python debugger", 5 | "scripts": { 6 | "build": "webpack --mode=production", 7 | "build-dev": "webpack --mode=development", 8 | "watch": "webpack watch", 9 | "lint": "eslint --ext .js src/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/romanvm/python-web-pdb.git" 14 | }, 15 | "keywords": [ 16 | "python", 17 | "remote", 18 | "debugger" 19 | ], 20 | "author": "Roman Miroshnychenko", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/romanvm/python-web-pdb/issues" 24 | }, 25 | "homepage": "https://github.com/romanvm/python-web-pdb#readme", 26 | "dependencies": { 27 | "@babel/core": "^7.24.0", 28 | "@babel/preset-env": "^7.24.0", 29 | "babel-loader": "^9.1.3", 30 | "bootstrap": "^3.4.1", 31 | "css-loader": "^6.8.1", 32 | "eslint": "^8.22.0", 33 | "jquery": "^3.6.0", 34 | "mini-css-extract-plugin": "^2.6.1", 35 | "path": "^0.12.7", 36 | "prismjs": "^1.27.0", 37 | "style-loader": "^0.20.3", 38 | "webpack": "^5.76.0", 39 | "webpack-cli": "^4.10.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/button_events.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Roman Miroshnychenko 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | import $ from 'jquery'; 24 | 25 | import { save_command_in_history, send_command } from './utils'; 26 | 27 | function bind_button_events() { 28 | $('#next_btn').click(() => { 29 | send_command('n'); 30 | }); 31 | 32 | $('#step_btn').click(() => { 33 | send_command('s'); 34 | }); 35 | 36 | $('#return_btn').click(() => { 37 | send_command('r'); 38 | }); 39 | 40 | $('#continue_btn').click(() => { 41 | send_command('c'); 42 | }); 43 | 44 | $('#up_btn').click(() => { 45 | send_command('u'); 46 | }); 47 | 48 | $('#down_btn').click(() => { 49 | send_command('d'); 50 | }); 51 | 52 | $('#where_btn').click(() => { 53 | send_command('w'); 54 | }); 55 | 56 | $('#help_btn').click(() => { 57 | $('#help_window').modal(); 58 | }); 59 | 60 | $('#send_btn').click(() => { 61 | const $stdin = $('#stdin'); 62 | let command = $stdin.val(); 63 | if (send_command(command)) { 64 | save_command_in_history(command); 65 | $stdin.val(''); 66 | } 67 | }); 68 | } 69 | 70 | export default bind_button_events; 71 | -------------------------------------------------------------------------------- /frontend/src/globals.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Roman Miroshnychenko 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | const state = { 24 | command_history: [], 25 | history_index: -1, 26 | console_history: '', 27 | dirname: '', 28 | filename: '', 29 | current_line: -1, 30 | breakpoints: [] 31 | }, 32 | websocket = new WebSocket('ws://' + window.location.host + '/ws'); 33 | 34 | export { websocket, state }; 35 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Roman Miroshnychenko 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | import $ from 'jquery'; 24 | import 'bootstrap/dist/js/bootstrap.min.js'; 25 | import 'bootstrap/dist/css/bootstrap.min.css'; 26 | 27 | import bind_button_events from './button_events'; 28 | import bind_key_events from './key_events'; 29 | import { resize_console } from './utils'; 30 | import update_ui from './update_ui'; 31 | 32 | import './styles.css'; 33 | 34 | $(() => { 35 | bind_button_events(); 36 | bind_key_events(); 37 | $(window).resize(resize_console); 38 | $('title').text(`Web-PDB Console on ${window.location.host}`); 39 | $('#host').html(`Web-PDB Console on ${window.location.host}`); 40 | resize_console(); 41 | update_ui(); 42 | }); 43 | -------------------------------------------------------------------------------- /frontend/src/key_events.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Roman Miroshnychenko 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | import $ from 'jquery'; 24 | import { state } from './globals'; 25 | import { send_command } from './utils'; 26 | 27 | function bind_key_events() { 28 | $(document).keydown((event) => { 29 | if (event.keyCode === 121) { 30 | send_command('n'); 31 | return false; 32 | } 33 | else if (event.keyCode === 122 && !event.shiftKey) { 34 | send_command('s'); 35 | return false; 36 | } 37 | else if (event.keyCode === 122 && event.shiftKey) { 38 | send_command('r'); 39 | return false; 40 | } 41 | else if (event.keyCode === 119) { 42 | send_command('c'); 43 | return false; 44 | } 45 | }); 46 | 47 | $('#stdin').keydown((event) => { 48 | if (event.keyCode === 13) { 49 | $('#send_btn').click(); 50 | return false; 51 | } else if (event.keyCode === 38) { 52 | state.history_index++; 53 | if (state.history_index >= state.command_history.length) { 54 | state.history_index = 0; 55 | } 56 | $('#stdin').val(state.command_history[state.history_index]); 57 | return false; 58 | } else if (event.keyCode === 40) { 59 | state.history_index--; 60 | if (state.history_index < 0) { 61 | state.history_index = state.command_history.length - 1; 62 | } else if (state.history_index >= state.command_history.length) { 63 | state.history_index = 0; 64 | } 65 | $('#stdin').val(state.command_history[state.history_index]); 66 | return false; 67 | } 68 | }); 69 | 70 | } 71 | 72 | export default bind_key_events; 73 | -------------------------------------------------------------------------------- /frontend/src/prism-line-numbers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a modified version of Prism.js line numbers plugin that adds 3 | * necessary properties and event listeners to line numbers tags. 4 | */ 5 | 6 | var globals = require('./globals'); 7 | var utils = require('./utils'); 8 | 9 | (function () { 10 | 11 | if (typeof self === 'undefined' || !self.Prism || !self.document) { 12 | return; 13 | } 14 | 15 | /** 16 | * Plugin name which is used as a class name for
 which is activating the plugin
 17 | 	 * @type {String}
 18 | 	 */
 19 | 	var PLUGIN_NAME = 'line-numbers';
 20 | 	
 21 | 	/**
 22 | 	 * Regular expression used for determining line breaks
 23 | 	 * @type {RegExp}
 24 | 	 */
 25 | 	var NEW_LINE_EXP = /\n(?!$)/g;
 26 | 
 27 | 	/**
 28 | 	 * Resizes line numbers spans according to height of line of code
 29 | 	 * @param {Element} element 
 element
 30 | 	 */
 31 | 	var _resizeElement = function (element) {
 32 | 		var codeStyles = getStyles(element);
 33 | 		var whiteSpace = codeStyles['white-space'];
 34 | 
 35 | 		if (whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line') {
 36 | 			var codeElement = element.querySelector('code');
 37 | 			var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
 38 | 			var lineNumberSizer = element.querySelector('.line-numbers-sizer');
 39 | 			var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
 40 | 
 41 | 			if (!lineNumberSizer) {
 42 | 				lineNumberSizer = document.createElement('span');
 43 | 				lineNumberSizer.className = 'line-numbers-sizer';
 44 | 
 45 | 				codeElement.appendChild(lineNumberSizer);
 46 | 			}
 47 | 
 48 | 			lineNumberSizer.style.display = 'block';
 49 | 
 50 | 			codeLines.forEach(function (line, lineNumber) {
 51 | 				lineNumberSizer.textContent = line || '\n';
 52 | 				var lineSize = lineNumberSizer.getBoundingClientRect().height;
 53 | 				lineNumbersWrapper.children[lineNumber].style.height = lineSize + 'px';
 54 | 			});
 55 | 
 56 | 			lineNumberSizer.textContent = '';
 57 | 			lineNumberSizer.style.display = 'none';
 58 | 		}
 59 | 	};
 60 | 
 61 | 	/**
 62 | 	 * Returns style declarations for the element
 63 | 	 * @param {Element} element
 64 | 	 */
 65 | 	var getStyles = function (element) {
 66 | 		if (!element) {
 67 | 			return null;
 68 | 		}
 69 | 
 70 | 		return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
 71 | 	};
 72 | 
 73 | 	window.addEventListener('resize', function () {
 74 | 		Array.prototype.forEach.call(document.querySelectorAll('pre.' + PLUGIN_NAME), _resizeElement);
 75 | 	});
 76 | 
 77 | 	Prism.hooks.add('complete', function (env) {
 78 | 		if (!env.code) {
 79 | 			return;
 80 | 		}
 81 | 
 82 | 		// works only for  wrapped inside 
 (not inline)
 83 | 		var pre = env.element.parentNode;
 84 | 		var clsReg = /\s*\bline-numbers\b\s*/;
 85 | 		if (
 86 | 			!pre || !/pre/i.test(pre.nodeName) ||
 87 | 			// Abort only if nor the 
 nor the  have the class
 88 | 			(!clsReg.test(pre.className) && !clsReg.test(env.element.className))
 89 | 		) {
 90 | 			return;
 91 | 		}
 92 | 
 93 | 		if (env.element.querySelector('.line-numbers-rows')) {
 94 | 			// Abort if line numbers already exists
 95 | 			return;
 96 | 		}
 97 | 
 98 | 		if (clsReg.test(env.element.className)) {
 99 | 			// Remove the class 'line-numbers' from the 
100 | 			env.element.className = env.element.className.replace(clsReg, ' ');
101 | 		}
102 | 		if (!clsReg.test(pre.className)) {
103 | 			// Add the class 'line-numbers' to the 
104 | 			pre.className += ' line-numbers';
105 | 		}
106 | 
107 | 		var match = env.code.match(NEW_LINE_EXP);
108 | 		var linesNum = match ? match.length + 1 : 1;
109 | 		var lineNumbersWrapper;
110 | 
111 | 		// var lines = new Array(linesNum + 1);
112 | 		// lines = lines.join('');
113 | 
114 | 		lineNumbersWrapper = document.createElement('span');
115 | 		lineNumbersWrapper.setAttribute('aria-hidden', 'true');
116 | 		lineNumbersWrapper.className = 'line-numbers-rows';
117 | 		// lineNumbersWrapper.innerHTML = lines;
118 | 
119 | 		// Create  elements for line numbers with all necessary attributes
120 | 		for (var i = 1; i < linesNum + 1; i++) {
121 | 			var span = document.createElement('span');
122 | 			span.id = 'lineno_' + i;
123 | 			span.onclick = function(event) {
124 | 				var line_number = event.currentTarget.id.split('_')[1];
125 | 				if (event.currentTarget.className == 'breakpoint') {
126 | 					utils.send_command('cl ' + globals.state.dirname + globals.state.filename + ':' + line_number);
127 | 				} else {
128 | 					utils.send_command('b ' + globals.state.dirname + globals.state.filename + ':' + line_number);
129 | 				}
130 | 			};
131 | 			if (globals.state.breakpoints.indexOf(i) != -1) {
132 | 				span.className = 'breakpoint';
133 | 			}
134 | 			lineNumbersWrapper.appendChild(span);
135 | 		}
136 | 
137 | 		if (pre.hasAttribute('data-start')) {
138 | 			pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
139 | 		}
140 | 
141 | 		env.element.appendChild(lineNumbersWrapper);
142 | 
143 | 		_resizeElement(pre);
144 | 
145 | 		Prism.hooks.run('line-numbers', env);
146 | 	});
147 | 
148 | 	Prism.hooks.add('line-numbers', function (env) {
149 | 		env.plugins = env.plugins || {};
150 | 		env.plugins.lineNumbers = true;
151 | 	});
152 | 	
153 | 	/**
154 | 	 * Global exports
155 | 	 */
156 | 	Prism.plugins.lineNumbers = {
157 | 		/**
158 | 		 * Get node for provided line number
159 | 		 * @param {Element} element pre element
160 | 		 * @param {Number} number line number
161 | 		 * @return {Element|undefined}
162 | 		 */
163 | 		getLine: function (element, number) {
164 | 			if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
165 | 				return;
166 | 			}
167 | 
168 | 			var lineNumberRows = element.querySelector('.line-numbers-rows');
169 | 			var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
170 | 			var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
171 | 
172 | 			if (number < lineNumberStart) {
173 | 				number = lineNumberStart;
174 | 			}
175 | 			if (number > lineNumberEnd) {
176 | 				number = lineNumberEnd;
177 | 			}
178 | 
179 | 			var lineIndex = number - lineNumberStart;
180 | 
181 | 			return lineNumberRows.children[lineIndex];
182 | 		}
183 | 	};
184 | 
185 | }());


--------------------------------------------------------------------------------
/frontend/src/styles.css:
--------------------------------------------------------------------------------
 1 | /*
 2 | Copyright (c) 2018 Roman Miroshnychenko 
 3 | 
 4 | Permission is hereby granted, free of charge, to any person obtaining a copy
 5 | of this software and associated documentation files (the "Software"), to deal
 6 | in the Software without restriction, including without limitation the rights
 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 8 | copies of the Software, and to permit persons to whom the Software is
 9 | furnished to do so, subject to the following conditions:
10 | 
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 | 
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 | */
22 | 
23 | html, body {
24 |   padding-top: 30px;
25 | }
26 | 
27 | .container {
28 |   padding: 5px;
29 | }
30 | 
31 | .info {
32 |   height: 300px;
33 | }
34 | 
35 | .line-numbers-rows > span:before {
36 |   pointer-events: all;
37 |   cursor: default;
38 | }
39 | 
40 | .line-numbers-rows > span.breakpoint:before {
41 |   color: red;
42 |   font-weight: bold;
43 | }
44 | 
45 | .navbar-text {
46 |   font-weight: bold;
47 | }
48 | 
49 | div.infobox-label {
50 |   position: relative;
51 | }
52 | 
53 | div.infobox-label > div.infobox-label-text {
54 |   color: black;
55 |   background-color: #CFCFCF;
56 |   display: inline-block;
57 |   position: absolute;
58 |   bottom: auto;
59 |   left: auto;
60 |   top: 0;
61 |   right: 0;
62 |   width: auto;
63 |   height: auto;
64 |   font-size: 0.9em;
65 |   border-radius: 0 0 0 5px;
66 |   padding: 0 0.5em;
67 |   text-shadow: none;
68 |   z-index: 1;
69 |   box-shadow: none;
70 |   -webkit-transform: none;
71 |   -moz-transform: none;
72 |   -ms-transform: none;
73 |   -o-transform: none;
74 |   transform: none;
75 | }
76 | 


--------------------------------------------------------------------------------
/frontend/src/update_ui.js:
--------------------------------------------------------------------------------
 1 | /*
 2 | Copyright (c) 2018 Roman Miroshnychenko 
 3 | 
 4 | Permission is hereby granted, free of charge, to any person obtaining a copy
 5 | of this software and associated documentation files (the "Software"), to deal
 6 | in the Software without restriction, including without limitation the rights
 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 8 | copies of the Software, and to permit persons to whom the Software is
 9 | furnished to do so, subject to the following conditions:
10 | 
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 | 
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 | */
22 | 
23 | import $ from 'jquery';
24 | import Prism from 'prismjs';
25 | import 'prismjs/components/prism-python.js';
26 | import 'prismjs/plugins/line-highlight/prism-line-highlight.js';
27 | import './prism-line-numbers.js';
28 | 
29 | import 'prismjs/themes/prism-okaidia.css';
30 | import 'prismjs/plugins/line-highlight/prism-line-highlight.css';
31 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
32 | 
33 | import { websocket, state } from './globals';
34 | 
35 | let wait_buffer = [];
36 | 
37 | function update_ui() {
38 |   $.getJSON('/frame-data')
39 |   .then((frame_data) => {
40 |     state.breakpoints = frame_data.breakpoints;
41 |     state.dirname = frame_data.dirname;
42 |     $('#filename').text(frame_data.filename);
43 |     $('#curr_line').text(frame_data.current_line);
44 |     const $console = $('#console'),
45 |         $curr_file = $('#curr_file'),
46 |         $curr_file_code = $('#curr_file_code'),
47 |         $globals = $('#globals'),
48 |         $locals = $('#locals'),
49 |         $stdout = $('#stdout');
50 |     $globals.text(frame_data.globals);
51 |     $locals.text(frame_data.locals);
52 |     $stdout.text(frame_data.console_history);
53 |     $console.scrollTop($console.prop('scrollHeight'));
54 |     $curr_file_code.text(frame_data.file_listing);
55 |     $curr_file.attr('data-line', frame_data.current_line);
56 |     Prism.highlightAll();
57 |     if (frame_data.current_line !== -1 &&
58 |         (frame_data.filename !== state.filename ||
59 |           frame_data.current_line !== state.current_line)) {
60 |       state.filename = frame_data.filename;
61 |       state.current_line = frame_data.current_line;
62 |       // Modified from here: https://stackoverflow.com/questions/2905867/how-to-scroll-to-specific-item-using-jquery
63 |       $curr_file.scrollTop($(`#lineno_${state.current_line}`).offset().top -
64 |           $curr_file.offset().top + $curr_file.scrollTop() - $curr_file.height() / 2);
65 |     }
66 |   });
67 | }
68 | 
69 | websocket.onmessage = () => {
70 |   // WebSocket receives only data update pings from the back-end so payload does not matter.
71 |   // This method prevents firing bursts of requests to the back-end when it sends a series of pings.
72 |   wait_buffer.push(null);
73 |   setTimeout(() => {
74 |     wait_buffer.pop();
75 |     if (!wait_buffer.length) {
76 |       update_ui();
77 |     }
78 |   }, 1);
79 | };
80 | 
81 | export default update_ui;
82 | 


--------------------------------------------------------------------------------
/frontend/src/utils.js:
--------------------------------------------------------------------------------
 1 | /*
 2 | Copyright (c) 2018 Roman Miroshnychenko 
 3 | 
 4 | Permission is hereby granted, free of charge, to any person obtaining a copy
 5 | of this software and associated documentation files (the "Software"), to deal
 6 | in the Software without restriction, including without limitation the rights
 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 8 | copies of the Software, and to permit persons to whom the Software is
 9 | furnished to do so, subject to the following conditions:
10 | 
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 | 
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 | */
22 | 
23 | import $ from 'jquery';
24 | 
25 | import { websocket, state } from './globals';
26 | 
27 | function save_command_in_history(command) {
28 |   state.history_index = -1;
29 |   if (command !== '' && command !== state.command_history[0]) {
30 |     state.command_history.unshift(command);
31 |     if (state.command_history.length > 10) {
32 |       state.command_history.pop();
33 |     }
34 |   }
35 | }
36 | 
37 | function send_command(command) {
38 |   if (websocket.readyState === websocket.OPEN) {
39 |     websocket.send(command + '\n');
40 |     return true;
41 |   }
42 |   return false;
43 | }
44 | 
45 | function resize_console() {
46 |   let con_height = $(window).height() - 490;
47 |   if (con_height <= 240) {
48 |     con_height = 240;
49 |   }
50 |   $('#console').height(con_height);
51 | }
52 | 
53 | export { save_command_in_history, send_command, resize_console };
54 | 


--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
 1 | const webpack = require('webpack');
 2 | const path = require('path');
 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 4 | 
 5 | const SRC = path.resolve(__dirname, 'src');
 6 | const BUILD = path.resolve(path.dirname(__dirname), 'web_pdb', 'static');
 7 | 
 8 | const config = {
 9 |   entry: SRC + '/index.js',
10 |   output: {
11 |     path: BUILD,
12 |     filename: 'bundle.min.js'
13 |   },
14 |   plugins: [
15 |     new webpack.ProvidePlugin({
16 |       $: 'jquery',
17 |       jQuery: 'jquery',
18 |       'window.jQuery': 'jquery',
19 |       tether: 'tether',
20 |       Tether: 'tether',
21 |       'window.Tether': 'tether',
22 |   }),
23 |     new MiniCssExtractPlugin({
24 |       filename: 'styles.min.css'
25 |     })
26 |   ],
27 |   module: {
28 |     rules: [
29 |       {
30 |         test: /\.js$/,
31 |         exclude: /node_modules/,
32 |         use: {
33 |           loader: 'babel-loader',
34 |           options: {
35 |             presets: ['@babel/preset-env']
36 |           }
37 |         }
38 |       },
39 |       {
40 |         test: /\.(svg|woff2?|ttf|eot)$/,
41 |         type: 'asset/resource',
42 |         generator: {
43 |           filename: './fonts/[name][ext]'
44 |         }
45 |       },
46 |       {
47 |         test: /\.css$/,
48 |         use: [MiniCssExtractPlugin.loader, "css-loader"],
49 |       }
50 |     ]
51 |   }
52 | };
53 | 
54 | module.exports = config;
55 | 


--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | bottle>=0.12.25
2 | asyncore-wsgi>=0.0.11
3 | selenium==4.10.0
4 | Pylint==2.15.0
5 | 


--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/screenshot.png


--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
 1 | [metadata]
 2 | name = web-pdb
 3 | version = 1.6.3
 4 | author = Roman Miroshnychenko
 5 | author_email = roman1972@gmail.com
 6 | url = https://github.com/romanvm/python-web-pdb
 7 | description = Web interface for Python's built-in PDB debugger
 8 | long_description = file: Readme.rst
 9 | long_description_content_type = text/x-rst
10 | keywords = pdb remote web debugger'
11 | license = MIT License
12 | classifiers =
13 |     Development Status :: 5 - Production/Stable
14 |     Environment :: Web Environment
15 |     Framework :: Bottle
16 |     Intended Audience :: Developers
17 |     License :: OSI Approved :: MIT License
18 |     Operating System :: OS Independent
19 |     Programming Language :: Python
20 |     Programming Language :: Python :: 3
21 |     Topic :: Software Development :: Debuggers
22 | platform = any
23 | 
24 | [options]
25 | packages =
26 |     web_pdb
27 | zip_safe = False
28 | include_package_data = True
29 | python_requires = >=3.6
30 | install_requires =
31 |     bottle>=0.12.25
32 |     asyncore-wsgi>=0.0.11
33 | test_suite = tests.tests
34 | tests_require =
35 |     selenium==4.10.0
36 | 


--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
 1 | # Created on:
 2 | # Author: Roman Miroshnychenko aka Roman V.M. (roman1972@gmail.com)
 3 | #
 4 | # Copyright (c) 2016 Roman Miroshnychenko
 5 | #
 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 7 | # of this software and associated documentation files (the "Software"), to deal
 8 | # in the Software without restriction, including without limitation the rights
 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | 
24 | from setuptools import setup
25 | 
26 | setup()
27 | 


--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | # Created on: 03.10.2016
3 | # Author: Roman Miroshnychenko aka Roman V.M. (romanvm@yandex.ua)
4 | 
5 | 
6 | 


--------------------------------------------------------------------------------
/tests/db.py:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | # Created on: 13.09.2016
 3 | # Author: Roman Miroshnychenko aka Roman V.M. (romanvm@yandex.ua)
 4 | 
 5 | import os
 6 | import sys
 7 | 
 8 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 9 | sys.path.append(basedir)
10 | 
11 | ustr = u'Тест'
12 | foo = 'foo'
13 | from web_pdb import set_trace; set_trace()
14 | bar = 'bar'
15 | ham = 'spam'
16 | name = u'Монти'
17 | 
18 | 
19 | def func(spam):
20 |     print(spam)
21 | 
22 | 
23 | func(ham)
24 | 


--------------------------------------------------------------------------------
/tests/db_i.py:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | # Created on: 30.03.2019
 3 | # Author: Roman Miroshnychenko aka Roman V.M. (roman1972@gmail.com)
 4 | #
 5 | # Copyright (c) 2016 Roman Miroshnychenko
 6 | #
 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 8 | # of this software and associated documentation files (the "Software"), to deal
 9 | # in the Software without restriction, including without limitation the rights
10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | # copies of the Software, and to permit persons to whom the Software is
12 | # furnished to do so, subject to the following conditions:
13 | #
14 | # The above copyright notice and this permission notice shall be included in
15 | # all copies or substantial portions of the Software.
16 | #
17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | # THE SOFTWARE.
24 | 
25 | import os
26 | import sys
27 | 
28 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
29 | sys.path.append(basedir)
30 | 
31 | 
32 | class Foo(object):
33 |     foo = 'foo'
34 |     bar = 'bar'
35 | 
36 | 
37 | bar = None
38 | 
39 | import web_pdb; web_pdb.set_trace()
40 | pass
41 | 


--------------------------------------------------------------------------------
/tests/db_pm.py:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | # Created on: 16.09.2016
 3 | # Author: Roman Miroshnychenko aka Roman V.M. (romanvm@yandex.ua)
 4 | 
 5 | 
 6 | import os
 7 | import sys
 8 | 
 9 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | sys.path.append(basedir)
11 | import web_pdb
12 | 
13 | with web_pdb.catch_post_mortem():
14 |     assert False, 'Oops!'
15 | 


--------------------------------------------------------------------------------
/tests/db_ps.py:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | # Author: Roman Miroshnychenko aka Roman V.M.
 3 | 
 4 | import os
 5 | import sys
 6 | 
 7 | basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 8 | sys.path.append(basedir)
 9 | 
10 | from web_pdb import set_trace; set_trace(patch_stdstreams=True)
11 | foo = input('Enter something: ')
12 | print('You have entered: ' + foo)
13 | 


--------------------------------------------------------------------------------
/tests/tests.py:
--------------------------------------------------------------------------------
  1 | # Created on: 13.09.2016
  2 | # Author: Roman Miroshnychenko aka Roman V.M. (romanvm@yandex.ua)
  3 | #
  4 | # Copyright (c) 2016 Roman Miroshnychenko
  5 | #
  6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
  7 | # of this software and associated documentation files (the "Software"), to deal
  8 | # in the Software without restriction, including without limitation the rights
  9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 | # copies of the Software, and to permit persons to whom the Software is
 11 | # furnished to do so, subject to the following conditions:
 12 | #
 13 | # The above copyright notice and this permission notice shall be included in
 14 | # all copies or substantial portions of the Software.
 15 | #
 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 | # THE SOFTWARE.
 23 | 
 24 | import sys
 25 | import time
 26 | from pathlib import Path
 27 | from subprocess import Popen
 28 | from unittest import TestCase, main, skipIf
 29 | 
 30 | from selenium import webdriver
 31 | from selenium.webdriver.common.by import By
 32 | from selenium.webdriver.common.keys import Keys
 33 | 
 34 | CWD = Path(__file__).resolve().parent
 35 | DB_PY = CWD / 'db.py'
 36 | 
 37 | IS_PY_310 = sys.version_info[:2] >= (3, 10)
 38 | IS_PY_313 = sys.version_info[:2] >= (3, 13)
 39 | 
 40 | class SeleniumTestCase(TestCase):
 41 |     @classmethod
 42 |     def setUpClass(cls):
 43 |         if sys.platform == 'win32':
 44 |             cls.browser = webdriver.Firefox()
 45 |         else:
 46 |             options = webdriver.ChromeOptions()
 47 |             options.add_argument('headless')
 48 |             options.add_argument('disable-gpu')
 49 |             cls.browser = webdriver.Chrome(options=options)
 50 |         cls.browser.implicitly_wait(10)
 51 |         cls.browser.set_window_size(1280, 1024)
 52 |         cls.browser.get('http://127.0.0.1:5555')
 53 |         cls.stdin = cls.browser.find_element(By.ID, 'stdin')
 54 |         cls.send_btn = cls.browser.find_element(By.ID, 'send_btn')
 55 |         cls.stdout_tag = cls.browser.find_element(By.ID, 'stdout')
 56 | 
 57 |     @classmethod
 58 |     def tearDownClass(cls):
 59 |         cls.stdin.clear()
 60 |         cls.stdin.send_keys('q')
 61 |         cls.send_btn.click()
 62 |         time.sleep(1)
 63 |         cls.db_proc.kill()
 64 |         cls.browser.quit()
 65 | 
 66 |     def tearDown(self):
 67 |         if hasattr(self, '_outcome'):
 68 |             result = self._outcome.result
 69 |             if result.failures:
 70 |                 failed_tests = [test for test, _ in result.failures]
 71 |                 if self in failed_tests:
 72 |                     self.browser.save_screenshot(f'screenshot-{self}.png')
 73 | 
 74 | class WebPdbTestCase(SeleniumTestCase):
 75 |     """
 76 |     This class provides basic functionality testing for Web-PDB
 77 |     """
 78 |     @classmethod
 79 |     def setUpClass(cls):
 80 |         cls.db_proc = Popen(['python', str(DB_PY)], shell=False)
 81 |         super(WebPdbTestCase, cls).setUpClass()
 82 | 
 83 |     def test_1_set_trace(self):
 84 |         """
 85 |         Test back-end/front-end interaction during debugging
 86 |         """
 87 |         time.sleep(1)
 88 |         filename_tag = self.browser.find_element(By.ID, 'filename')
 89 |         self.assertEqual('db.py', filename_tag.text)
 90 |         curr_line_tag = self.browser.find_element(By.ID, 'curr_line')
 91 |         expected = '13' if IS_PY_313 else '14'
 92 |         self.assertEqual(expected, curr_line_tag.text)
 93 |         curr_file_tag = self.browser.find_element(By.ID, 'curr_file_code')
 94 |         self.assertIn('foo = \'foo\'', curr_file_tag.text)
 95 |         globals_tag = self.browser.find_element(By.ID, 'globals')
 96 |         self.assertIn('foo = \'foo\'', globals_tag.text)
 97 |         if IS_PY_313:
 98 |             self.stdin.send_keys('n')
 99 |             self.send_btn.click()
100 |             time.sleep(1)
101 |         self.assertIn('-> bar = \'bar\'', self.stdout_tag.text)
102 |         # Test if Prismjs syntax coloring actually works
103 |         self.assertIn('foo = \'foo\'',
104 |                       self.browser.page_source)
105 | 
106 |     def test_2_next_command(self):
107 |         """
108 |         Test sending PDB commands
109 |         """
110 |         self.stdin.clear()
111 |         self.stdin.send_keys('n')
112 |         self.send_btn.click()
113 |         time.sleep(1)
114 |         curr_line_tag = self.browser.find_element(By.ID, 'curr_line')
115 |         self.assertEqual(curr_line_tag.text, '15')
116 |         globals_tag = self.browser.find_element(By.ID, 'globals')
117 |         self.assertIn('bar = \'bar\'', globals_tag.text)
118 |         self.assertIn('-> ham = \'spam\'', self.stdout_tag.text)
119 |         self.assertEqual('', self.stdin.get_attribute('value'))
120 | 
121 |     def test_3_history(self):
122 |         """
123 |         Test for the recent commands history
124 |         """
125 |         self.stdin.clear()
126 |         self.stdin.send_keys('h')
127 |         self.send_btn.click()
128 |         time.sleep(1)
129 |         self.stdin.send_keys(Keys.ARROW_UP)
130 |         self.assertEqual('h', self.stdin.get_attribute('value'))
131 |         self.stdin.send_keys(Keys.ARROW_UP)
132 |         self.assertEqual('n', self.stdin.get_attribute('value'))
133 | 
134 |     def test_4_breakpints(self):
135 |         """
136 |         Test for highlighting breakpoints
137 |         """
138 |         self.stdin.clear()
139 |         self.stdin.send_keys('b 20')
140 |         self.send_btn.click()
141 |         time.sleep(1)
142 |         line_numbers_rows = self.browser.find_element(By.CSS_SELECTOR, 'span.line-numbers-rows')
143 |         line_spans = line_numbers_rows.find_elements(By.TAG_NAME, 'span')
144 |         self.assertEqual('breakpoint', line_spans[19].get_attribute('class'))
145 | 
146 |     def test_5_unicode_literal(self):
147 |         """
148 |         Test for displaying unicode literals
149 |         """
150 |         self.stdin.clear()
151 |         self.stdin.send_keys('n')
152 |         self.send_btn.click()
153 |         time.sleep(1)
154 |         self.assertIn('-> name = u\'Монти\'', self.stdout_tag.text)
155 | 
156 |     def test_6_entering_unicode_string(self):
157 |         """
158 |         Test for entering unicode literal via console
159 |         """
160 |         self.stdin.clear()
161 |         self.stdin.send_keys('p u\'python - питон\'')
162 |         self.send_btn.click()
163 |         time.sleep(1)
164 |         stdout_tag = self.browser.find_element(By.ID, 'stdout')
165 |         self.assertIn('u\'python - питон\'', stdout_tag.text)
166 | 
167 |     def test_7_local_vars(self):
168 |         """
169 |         Test for displaying local variables
170 |         """
171 |         self.stdin.clear()
172 |         self.stdin.send_keys('c')
173 |         self.send_btn.click()
174 |         time.sleep(1)
175 |         locals_tag = self.browser.find_element(By.ID, 'locals')
176 |         self.assertIn('spam = \'spam\'', locals_tag.text)
177 |         globals_tag = self.browser.find_element(By.ID, 'globals')
178 |         self.assertNotEqual(globals_tag.text, locals_tag.text)
179 | 
180 | 
181 | class PatchStdStreamsTestCase(SeleniumTestCase):
182 |     """
183 |     This class tests patching sys.std* streams
184 |     """
185 |     @classmethod
186 |     def setUpClass(cls):
187 |         cls.db_proc = Popen(['python', str(CWD / 'db_ps.py')], shell=False)
188 |         super(PatchStdStreamsTestCase, cls).setUpClass()
189 | 
190 |     def test_patching_std_streams(self):
191 |         """
192 |         Test if std streams are correctly redirected to the web-console
193 |         """
194 |         time.sleep(1)
195 |         self.stdin.send_keys('n')
196 |         self.send_btn.click()
197 |         time.sleep(1)
198 |         if IS_PY_313:
199 |             self.stdin.send_keys('n')
200 |             self.send_btn.click()
201 |             time.sleep(1)
202 |         self.assertIn('Enter something:', self.stdout_tag.text)
203 |         self.stdin.send_keys('spam')
204 |         self.send_btn.click()
205 |         time.sleep(1)
206 |         self.stdin.send_keys('n')
207 |         self.send_btn.click()
208 |         time.sleep(1)
209 |         self.assertIn('You have entered: spam', self.stdout_tag.text)
210 | 
211 | 
212 | class CatchPostMortemTestCase(SeleniumTestCase):
213 |     """
214 |     This class for catching exceptions
215 |     """
216 |     @classmethod
217 |     def setUpClass(cls):
218 |         cls.db_proc = Popen(['python', str(CWD / 'db_pm.py')], shell=False)
219 |         super(CatchPostMortemTestCase, cls).setUpClass()
220 | 
221 |     def test_catch_post_mortem(self):
222 |         """
223 |         Test if catch_post_mortem context manager catches exceptions
224 |         """
225 |         time.sleep(1)
226 |         curr_line_tag = self.browser.find_element(By.ID, 'curr_line')
227 |         expected = '13' if IS_PY_310 else '14'
228 |         self.assertEqual(expected, curr_line_tag.text)
229 |         curr_file_tag = self.browser.find_element(By.ID, 'curr_file_code')
230 |         self.assertIn('assert False, \'Oops!\'', curr_file_tag.text)
231 |         stdout_tag = self.browser.find_element(By.ID, 'stdout')
232 |         self.assertIn('AssertionError', stdout_tag.text)
233 | 
234 | 
235 | class InspectCommandTestCase(SeleniumTestCase):
236 |     """
237 |     Test for inspect command
238 |     """
239 |     @classmethod
240 |     def setUpClass(cls):
241 |         cls.db_proc = Popen(['python', str(CWD / 'db_i.py')], shell=False)
242 |         super(InspectCommandTestCase, cls).setUpClass()
243 | 
244 |     def test_inspect_existing_object(self):
245 |         """
246 |         Test inspecting existing object
247 |         """
248 |         time.sleep(1)
249 |         self.stdin.send_keys('i Foo')
250 |         self.send_btn.click()
251 |         time.sleep(1)
252 |         self.assertIn('foo: \'foo\'', self.stdout_tag.text)
253 |         self.assertIn('bar: \'bar\'', self.stdout_tag.text)
254 |         self.stdin.send_keys('i bar')
255 |         self.send_btn.click()
256 |         time.sleep(1)
257 |         self.assertNotIn('NameError: name "bar" is not defined',
258 |                          self.stdout_tag.text)
259 | 
260 |     def test_inspect_non_existing_object(self):
261 |         """
262 |         Test inspecting non-existing object
263 |         """
264 |         self.stdin.send_keys('i spam')
265 |         self.send_btn.click()
266 |         time.sleep(1)
267 |         self.assertIn('NameError: name "spam" is not defined',
268 |                       self.stdout_tag.text)
269 | 
270 | 
271 | if __name__ == '__main__':
272 |     main()
273 | 


--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
 1 | [tox]
 2 | envlist = py{38,39,310,311,312,313}
 3 | 
 4 | [testenv]
 5 | commands=
 6 |   pip install -q -r requirements.txt
 7 |   python tests/tests.py
 8 | 
 9 | [gh-actions]
10 | python =
11 |   3.8: py38
12 |   3.9: py39
13 |   3.10: py310
14 |   3.11: py311
15 |   3.12: py312
16 |   3.13: py313
17 | 


--------------------------------------------------------------------------------
/web_pdb/__init__.py:
--------------------------------------------------------------------------------
  1 | # Author: Roman Miroshnychenko aka Roman V.M.
  2 | # E-mail: roman1972@gmail.com
  3 | #
  4 | # Copyright (c) 2016 Roman Miroshnychenko
  5 | #
  6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
  7 | # of this software and associated documentation files (the "Software"), to deal
  8 | # in the Software without restriction, including without limitation the rights
  9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 | # copies of the Software, and to permit persons to whom the Software is
 11 | # furnished to do so, subject to the following conditions:
 12 | #
 13 | # The above copyright notice and this permission notice shall be included in
 14 | # all copies or substantial portions of the Software.
 15 | #
 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 | # THE SOFTWARE.
 23 | """
 24 | A web-interface for Python's built-in PDB debugger
 25 | """
 26 | 
 27 | import inspect
 28 | import os
 29 | import random
 30 | import sys
 31 | import traceback
 32 | from contextlib import contextmanager
 33 | from pdb import Pdb
 34 | from pprint import pformat
 35 | 
 36 | from .web_console import WebConsole
 37 | 
 38 | __all__ = ['WebPdb', 'set_trace', 'post_mortem', 'catch_post_mortem']
 39 | 
 40 | 
 41 | class WebPdb(Pdb):
 42 |     """
 43 |     The main debugger class
 44 | 
 45 |     It provides a web-interface for Python's built-in PDB debugger
 46 |     with extra convenience features.
 47 |     """
 48 |     active_instance = None
 49 |     null = object()
 50 | 
 51 |     def __init__(self, host='', port=5555, patch_stdstreams=False):
 52 |         """
 53 |         :param host: web-UI hostname or IP-address
 54 |         :type host: str
 55 |         :param port: web-UI port. If ``port=-1``, choose a random port value
 56 |             between 32768 and 65536.
 57 |         :type port: int
 58 |         :param patch_stdstreams: redirect all standard input and output
 59 |             streams to the web-UI.
 60 |         :type patch_stdstreams: bool
 61 |         """
 62 |         if port == -1:
 63 |             random.seed()
 64 |             port = random.randint(32768, 65536)
 65 |         self.console = WebConsole(host, port, self)
 66 |         super().__init__(stdin=self.console, stdout=self.console)
 67 |         # Borrowed from here: https://github.com/ionelmc/python-remote-pdb
 68 |         self._backup = []
 69 |         if patch_stdstreams:
 70 |             for name in (
 71 |                     'stderr',
 72 |                     'stdout',
 73 |                     '__stderr__',
 74 |                     '__stdout__',
 75 |                     'stdin',
 76 |                     '__stdin__',
 77 |             ):
 78 |                 self._backup.append((name, getattr(sys, name)))
 79 |                 setattr(sys, name, self.console)
 80 |         WebPdb.active_instance = self
 81 | 
 82 |     def do_quit(self, arg):
 83 |         """
 84 |         quit || exit || q
 85 |         Stop and quit the current debugging session
 86 |         """
 87 |         for name, fh in self._backup:
 88 |             setattr(sys, name, fh)
 89 |         self.console.writeline('*** Aborting program ***\n')
 90 |         self.console.flush()
 91 |         self.console.close()
 92 |         WebPdb.active_instance = None
 93 |         return super().do_quit(arg)
 94 | 
 95 |     do_q = do_exit = do_quit
 96 | 
 97 |     def do_inspect(self, arg):
 98 |         """
 99 |         i(nspect) object
100 |         Inspect an object
101 |         """
102 |         if arg in self.curframe_locals:
103 |             obj = self.curframe_locals[arg]
104 |         elif arg in self.curframe.f_globals:
105 |             obj = self.curframe.f_globals[arg]
106 |         else:
107 |             obj = WebPdb.null
108 |         if obj is not WebPdb.null:
109 |             self.console.writeline(f'{arg} = {type(obj)}:\n')
110 |             for name, value in inspect.getmembers(obj):
111 |                 if not (name.startswith('__') and (name.endswith('__'))):
112 |                     repr_value = self._get_repr(value, pretty=True, indent=8)
113 |                     self.console.writeline(f'    {name}: {repr_value}\n')
114 |         else:
115 |             self.console.writeline(f'NameError: name "{arg}" is not defined\n')
116 |         self.console.flush()
117 | 
118 |     do_i = do_inspect
119 | 
120 |     @staticmethod
121 |     def _get_repr(obj, pretty=False, indent=1):
122 |         """
123 |         Get string representation of an object
124 | 
125 |         :param obj: object
126 |         :type obj: object
127 |         :param pretty: use pretty formatting
128 |         :type pretty: bool
129 |         :param indent: indentation for pretty formatting
130 |         :type indent: int
131 |         :return: string representation
132 |         :rtype: str
133 |         """
134 |         if pretty:
135 |             repr_value = pformat(obj, indent)
136 |         else:
137 |             repr_value = repr(obj)
138 |         return repr_value
139 | 
140 |     def set_continue(self):
141 |         # We do not detach the debugger
142 |         # for correct multiple set_trace() and post_mortem() calls.
143 |         self._set_stopinfo(self.botframe, None, -1)
144 | 
145 |     def dispatch_return(self, frame, arg):
146 |         # The parent's method needs to be called first.
147 |         ret = super().dispatch_return(frame, arg)
148 |         if frame.f_back is None:
149 |             self.console.writeline('*** Thread finished ***\n')
150 |             if not self.console.closed:
151 |                 self.console.flush()
152 |                 self.console.close()
153 |                 WebPdb.active_instance = None
154 |         return ret
155 | 
156 |     def get_current_frame_data(self):
157 |         """
158 |         Get all date about the current execution frame
159 | 
160 |         :return: current frame data
161 |         :rtype: dict
162 |         :raises AttributeError: if the debugger does hold any execution frame.
163 |         :raises IOError: if source code for the current execution frame is not accessible.
164 |         """
165 |         filename = self.curframe.f_code.co_filename
166 |         lines, _ = inspect.findsource(self.curframe)
167 |         return {
168 |             'dirname': os.path.dirname(os.path.abspath(filename)) + os.path.sep,
169 |             'filename': os.path.basename(filename),
170 |             'file_listing': ''.join(lines),
171 |             'current_line': self.curframe.f_lineno,
172 |             'breakpoints': self.get_file_breaks(filename),
173 |             'globals': self.get_globals(),
174 |             'locals': self.get_locals()
175 |         }
176 | 
177 |     def _format_variables(self, raw_vars):
178 |         """
179 |         :param raw_vars: a `dict` of `var_name: var_object` pairs
180 |         :type raw_vars: dict
181 |         :return: sorted list of variables as a unicode string
182 |         :rtype: unicode
183 |         """
184 |         f_vars = []
185 |         for var, value in raw_vars.items():
186 |             if not (var.startswith('__') and var.endswith('__')):
187 |                 repr_value = self._get_repr(value)
188 |                 f_vars.append(f'{var} = {repr_value}')
189 |         return '\n'.join(sorted(f_vars))
190 | 
191 |     def get_globals(self):
192 |         """
193 |         Get the listing of global variables in the current scope
194 | 
195 |         .. note:: special variables that start and end with
196 |             double underscores ``__`` are not included.
197 | 
198 |         :return: a listing of ``var = value`` pairs sorted alphabetically
199 |         :rtype: unicode
200 |         """
201 |         return self._format_variables(self.curframe.f_globals)
202 | 
203 |     def get_locals(self):
204 |         """
205 |         Get the listing of local variables in the current scope
206 | 
207 |         .. note:: special variables that start and end with
208 |             double underscores ``__`` are not included.
209 |             For module scope globals and locals listings are the same.
210 | 
211 |         :return: a listing of ``var = value`` pairs sorted alphabetically
212 |         :rtype: unicode
213 |         """
214 |         return self._format_variables(self.curframe_locals)
215 | 
216 |     def remove_trace(self, frame=None):
217 |         """
218 |         Detach the debugger from the execution stack
219 | 
220 |         :param frame: the lowest frame to detach the debugger from.
221 |         :type frame: types.FrameType
222 |         """
223 |         sys.settrace(None)
224 |         if frame is None:
225 |             frame = self.curframe
226 |         while frame and frame is not self.botframe:
227 |             del frame.f_trace
228 |             frame = frame.f_back
229 | 
230 | 
231 | def set_trace(host='', port=5555, patch_stdstreams=False):
232 |     """
233 |     Start the debugger
234 | 
235 |     This method suspends execution of the current script
236 |     and starts a PDB debugging session. The web-interface is opened
237 |     on the specified port (default: ``5555``).
238 | 
239 |     Example::
240 | 
241 |         import web_pdb;web_pdb.set_trace()
242 | 
243 |     Subsequent :func:`set_trace` calls can be used as hardcoded breakpoints.
244 | 
245 |     :param host: web-UI hostname or IP-address
246 |     :type host: str
247 |     :param port: web-UI port. If ``port=-1``, choose a random port value
248 |      between 32768 and 65536.
249 |     :type port: int
250 |     :param patch_stdstreams: redirect all standard input and output
251 |         streams to the web-UI.
252 |     :type patch_stdstreams: bool
253 |     """
254 |     pdb = WebPdb.active_instance
255 |     if pdb is None:
256 |         pdb = WebPdb(host, port, patch_stdstreams)
257 |     else:
258 |         # If the debugger is still attached reset trace to a new location
259 |         pdb.remove_trace()
260 |     pdb.set_trace(sys._getframe().f_back)  # pylint: disable=protected-access
261 | 
262 | 
263 | def post_mortem(tb=None, host='', port=5555, patch_stdstreams=False):
264 |     """
265 |     Start post-mortem debugging for the provided traceback object
266 | 
267 |     If no traceback is provided the debugger tries to obtain a traceback
268 |     for the last unhandled exception.
269 | 
270 |     Example::
271 | 
272 |         try:
273 |             # Some error-prone code
274 |             assert ham == spam
275 |         except:
276 |             web_pdb.post_mortem()
277 | 
278 |     :param tb: traceback for post-mortem debugging
279 |     :type tb: types.TracebackType
280 |     :param host: web-UI hostname or IP-address
281 |     :type host: str
282 |     :param port: web-UI port. If ``port=-1``, choose a random port value
283 |         between 32768 and 65536.
284 |     :type port: int
285 |     :param patch_stdstreams: redirect all standard input and output
286 |         streams to the web-UI.
287 |     :type patch_stdstreams: bool
288 |     :raises ValueError: if no valid traceback is provided and the Python
289 |         interpreter is not handling any exception
290 |     """
291 |     # handling the default
292 |     if tb is None:
293 |         # sys.exc_info() returns (type, value, traceback) if an exception is
294 |         # being handled, otherwise it returns (None, None, None)
295 |         t, v, tb = sys.exc_info()
296 |         exc_data = traceback.format_exception(t, v, tb)
297 |     else:
298 |         exc_data = traceback.format_tb(tb)
299 |     if tb is None:
300 |         raise ValueError('A valid traceback must be passed if no '
301 |                          'exception is being handled')
302 |     pdb = WebPdb.active_instance
303 |     if pdb is None:
304 |         pdb = WebPdb(host, port, patch_stdstreams)
305 |     else:
306 |         pdb.remove_trace()
307 |     pdb.console.writeline('*** Web-PDB post-mortem ***\n')
308 |     pdb.console.writeline(''.join(exc_data))
309 |     pdb.reset()
310 |     pdb.interaction(None, tb)
311 | 
312 | 
313 | @contextmanager
314 | def catch_post_mortem(host='', port=5555, patch_stdstreams=False):
315 |     """
316 |     A context manager for tracking potentially error-prone code
317 | 
318 |     If an unhandled exception is raised inside context manager's code block,
319 |     the post-mortem debugger is started automatically.
320 | 
321 |     Example::
322 | 
323 |         with web_pdb.catch_post_mortem()
324 |             # Some error-prone code
325 |             assert ham == spam
326 | 
327 |     :param host: web-UI hostname or IP-address
328 |     :type host: str
329 |     :param port: web-UI port. If ``port=-1``, choose a random port value
330 |         between 32768 and 65536.
331 |     :type port: int
332 |     :param patch_stdstreams: redirect all standard input and output
333 |         streams to the web-UI.
334 |     :type patch_stdstreams: bool
335 |     """
336 |     try:
337 |         yield
338 |     except Exception:  # pylint: disable=broad-except
339 |         post_mortem(None, host, port, patch_stdstreams)
340 | 


--------------------------------------------------------------------------------
/web_pdb/buffer.py:
--------------------------------------------------------------------------------
 1 | # Author: Roman Miroshnychenko aka Roman V.M.
 2 | # E-mail: roman1972@gmail.com
 3 | #
 4 | # Copyright (c) 2016 Roman Miroshnychenko
 5 | #
 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 7 | # of this software and associated documentation files (the "Software"), to deal
 8 | # in the Software without restriction, including without limitation the rights
 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | 
24 | from threading import RLock
25 | 
26 | __all__ = ['ThreadSafeBuffer']
27 | 
28 | 
29 | class ThreadSafeBuffer:
30 |     """
31 |     A buffer for data exchange between threads
32 |     """
33 |     def __init__(self, contents=None):
34 |         self._lock = RLock()
35 |         self._contents = contents
36 |         self._is_dirty = contents is not None
37 | 
38 |     @property
39 |     def is_dirty(self):
40 |         """Indicates whether a buffer contains unread data"""
41 |         with self._lock:
42 |             return self._is_dirty
43 | 
44 |     @property
45 |     def contents(self):
46 |         """Get or set buffer contents"""
47 |         with self._lock:
48 |             self._is_dirty = False
49 |             return self._contents
50 | 
51 |     @contents.setter
52 |     def contents(self, value):
53 |         with self._lock:
54 |             self._contents = value
55 |             self._is_dirty = True
56 | 


--------------------------------------------------------------------------------
/web_pdb/static/bundle.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
 1 | /*!
 2 |  * Bootstrap v3.4.1 (https://getbootstrap.com/)
 3 |  * Copyright 2011-2019 Twitter, Inc.
 4 |  * Licensed under the MIT license
 5 |  */
 6 | 
 7 | /*!
 8 |  * Sizzle CSS Selector Engine v2.3.6
 9 |  * https://sizzlejs.com/
10 |  *
11 |  * Copyright JS Foundation and other contributors
12 |  * Released under the MIT license
13 |  * https://js.foundation/
14 |  *
15 |  * Date: 2021-02-16
16 |  */
17 | 
18 | /*!
19 |  * jQuery JavaScript Library v3.6.0
20 |  * https://jquery.com/
21 |  *
22 |  * Includes Sizzle.js
23 |  * https://sizzlejs.com/
24 |  *
25 |  * Copyright OpenJS Foundation and other contributors
26 |  * Released under the MIT license
27 |  * https://jquery.org/license
28 |  *
29 |  * Date: 2021-03-02T17:08Z
30 |  */
31 | 
32 | /**
33 |  * Prism: Lightweight, robust, elegant syntax highlighting
34 |  *
35 |  * @license MIT 
36 |  * @author Lea Verou 
37 |  * @namespace
38 |  * @public
39 |  */
40 | 


--------------------------------------------------------------------------------
/web_pdb/static/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/web_pdb/static/fonts/glyphicons-halflings-regular.eot


--------------------------------------------------------------------------------
/web_pdb/static/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/web_pdb/static/fonts/glyphicons-halflings-regular.ttf


--------------------------------------------------------------------------------
/web_pdb/static/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/web_pdb/static/fonts/glyphicons-halflings-regular.woff


--------------------------------------------------------------------------------
/web_pdb/static/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/web_pdb/static/fonts/glyphicons-halflings-regular.woff2


--------------------------------------------------------------------------------
/web_pdb/static/img/debug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romanvm/python-web-pdb/a3369ef6b6f97afb15ae5c64f917b4c934062a62/web_pdb/static/img/debug.png


--------------------------------------------------------------------------------
/web_pdb/static/img/debug.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 | 
 7 | 
 8 | 	
39 | 
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 | 
48 | 
49 | 
50 | 
51 | 
52 | 
53 | 
54 | 
55 | 
56 | 
57 | 
58 | 
59 | 
60 | 
61 | 
62 | 
63 | 
64 | 
65 | 
66 | 
67 | 
68 | 
69 | 
70 | 
71 | 


--------------------------------------------------------------------------------
/web_pdb/templates/index.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 | 
  4 |   
  5 |   
  6 |   
  7 |   
  8 |   
  9 |   
 10 |   
 11 | 
 12 | 
 13 |   
 21 |   
22 |
23 |
24 |
25 |
26 | Current file: () 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Globals 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Locals 43 |
44 |
45 |
46 |
47 |
48 |
49 | 53 | 57 | 61 | 65 | 69 | 73 | 77 | 81 |
82 |
83 |
84 |
85 |
86 | PDB Console 87 |
88 |
89 |
90 |
91 |
92 |
93 |
(Pdb)
94 | 95 | 96 | Send 97 | 98 |
99 |
100 |
101 |
102 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /web_pdb/web_console.py: -------------------------------------------------------------------------------- 1 | # Author: Roman Miroshnychenko aka Roman V.M. 2 | # E-mail: roman1972@gmail.com 3 | # 4 | # Copyright (c) 2016 Roman Miroshnychenko 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | """ 24 | File-like web-based input/output console 25 | """ 26 | 27 | import logging 28 | import queue 29 | import time 30 | import weakref 31 | from socket import gethostname 32 | from threading import Thread, Event 33 | 34 | from asyncore_wsgi import make_server, AsyncWebSocketHandler 35 | 36 | from .buffer import ThreadSafeBuffer 37 | from .wsgi_app import app 38 | 39 | __all__ = ['WebConsole'] 40 | 41 | 42 | class WebConsoleSocket(AsyncWebSocketHandler): 43 | """ 44 | WebConsoleSocket receives PDB commands from the front-end and 45 | sends pings to client(s) about console updates 46 | """ 47 | clients = [] 48 | input_queue = queue.Queue() 49 | 50 | @classmethod 51 | def broadcast(cls, msg): 52 | for cl in cls.clients: 53 | if cl.handshaked: 54 | cl.sendMessage(msg) # sendMessage uses deque so it is thread-safe 55 | 56 | def handleConnected(self): 57 | self.clients.append(self) 58 | 59 | def handleMessage(self): 60 | self.input_queue.put(self.data) 61 | 62 | def handleClose(self): 63 | self.clients.remove(self) 64 | 65 | 66 | class WebConsole: 67 | """ 68 | A file-like class for exchanging data between PDB and the web-UI 69 | """ 70 | def __init__(self, host, port, debugger): 71 | self._debugger = weakref.proxy(debugger) 72 | self._console_history = ThreadSafeBuffer('') 73 | self._frame_data = None 74 | self._stop_all = Event() 75 | self._server_thread = Thread(target=self._run_server, args=(host, port)) 76 | self._server_thread.daemon = True 77 | logging.critical( 78 | 'Web-PDB: starting web-server on http://%s:%s', gethostname(), port) 79 | self._server_thread.start() 80 | 81 | @property 82 | def seekable(self): 83 | return False 84 | 85 | @property 86 | def writable(self): 87 | return True 88 | 89 | @property 90 | def encoding(self): 91 | return 'utf-8' 92 | 93 | @property 94 | def closed(self): 95 | return self._stop_all.is_set() 96 | 97 | def _run_server(self, host, port): 98 | self._frame_data = app.frame_data 99 | httpd = make_server(host, port, app, ws_handler_class=WebConsoleSocket) 100 | while not self._stop_all.is_set(): 101 | try: 102 | httpd.handle_request() 103 | except (KeyboardInterrupt, SystemExit): 104 | break 105 | httpd.handle_close() 106 | 107 | def readline(self): 108 | while not self._stop_all.is_set(): 109 | try: 110 | data = WebConsoleSocket.input_queue.get(timeout=0.1) 111 | break 112 | except queue.Empty: 113 | continue 114 | else: 115 | data = '\n' # Empty string causes BdbQuit exception. 116 | self.writeline(data) 117 | return data 118 | 119 | read = readline 120 | 121 | def writeline(self, data): 122 | self._console_history.contents += data 123 | try: 124 | frame_data = self._debugger.get_current_frame_data() 125 | except (IOError, AttributeError): 126 | frame_data = { 127 | 'dirname': '', 128 | 'filename': '', 129 | 'file_listing': 'No data available', 130 | 'current_line': -1, 131 | 'breakpoints': [], 132 | 'globals': 'No data available', 133 | 'locals': 'No data available' 134 | } 135 | frame_data['console_history'] = self._console_history.contents 136 | self._frame_data.contents = frame_data 137 | WebConsoleSocket.broadcast('ping') # Ping all clients about data update 138 | 139 | write = writeline 140 | 141 | def flush(self): 142 | """ 143 | Wait until history is read but no more than 10 cycles 144 | in case a browser session is closed. 145 | """ 146 | i = 0 147 | while self._frame_data.is_dirty and i < 10: 148 | i += 1 149 | time.sleep(0.1) 150 | 151 | def close(self): 152 | logging.critical('Web-PDB: stopping web-server...') 153 | self._stop_all.set() 154 | self._server_thread.join() 155 | logging.critical('Web-PDB: web-server stopped.') 156 | -------------------------------------------------------------------------------- /web_pdb/wsgi_app.py: -------------------------------------------------------------------------------- 1 | # Author: Roman Miroshnychenko aka Roman V.M. 2 | # E-mail: roman1972@gmail.com 3 | # 4 | # Copyright (c) 2016 Roman Miroshnychenko 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | """ 24 | Web-UI WSGI application 25 | """ 26 | 27 | import gzip 28 | import json 29 | import os 30 | from functools import wraps 31 | from io import BytesIO 32 | 33 | import bottle 34 | 35 | from .buffer import ThreadSafeBuffer 36 | 37 | __all__ = ['app'] 38 | 39 | # bottle.debug(True) 40 | 41 | this_dir = os.path.dirname(os.path.abspath(__file__)) 42 | bottle.TEMPLATE_PATH.append(os.path.join(this_dir, 'templates')) 43 | static_path = os.path.join(this_dir, 'static') 44 | 45 | 46 | def compress(func): 47 | """ 48 | Compress route return data with gzip compression 49 | """ 50 | @wraps(func) 51 | def wrapper(*args, **kwargs): 52 | result = func(*args, **kwargs) 53 | # pylint: disable=no-member 54 | if ('gzip' in bottle.request.headers.get('Accept-Encoding', '') and 55 | isinstance(result, str) and 56 | len(result) > 1024): 57 | if isinstance(result, str): 58 | result = result.encode('utf-8') 59 | tmp_fo = BytesIO() 60 | with gzip.GzipFile(mode='wb', fileobj=tmp_fo) as gzip_fo: 61 | gzip_fo.write(result) 62 | result = tmp_fo.getvalue() 63 | bottle.response.add_header('Content-Encoding', 'gzip') 64 | return result 65 | return wrapper 66 | 67 | 68 | class WebConsoleApp(bottle.Bottle): 69 | def __init__(self): 70 | super().__init__() 71 | self.frame_data = ThreadSafeBuffer() 72 | 73 | 74 | app = WebConsoleApp() 75 | 76 | 77 | @app.route('/') 78 | @compress 79 | def root(): 80 | return bottle.template('index') 81 | 82 | 83 | @app.route('/frame-data') 84 | @compress 85 | def get_frame_data(): 86 | bottle.response.cache_control = 'no-store' 87 | bottle.response.content_type = 'application/json' 88 | return json.dumps(app.frame_data.contents) 89 | 90 | 91 | @app.route('/static/') 92 | def get_static(path): 93 | return bottle.static_file(path, root=static_path) 94 | --------------------------------------------------------------------------------