pax_global_header00006660000000000000000000000064147045123140014513gustar00rootroot0000000000000052 comment=f0b2aa5dbf28cd5eef39b0422dd6e20eef9ce30e pudb-2024.1.3/000077500000000000000000000000001470451231400126765ustar00rootroot00000000000000pudb-2024.1.3/.coveragerc000066400000000000000000000010211470451231400150110ustar00rootroot00000000000000[run] branch = True [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: ignore_errors = True [html] directory = coverage_html_reportpudb-2024.1.3/.editorconfig000066400000000000000000000010021470451231400153440ustar00rootroot00000000000000# https://editorconfig.org/ # https://github.com/editorconfig/editorconfig-vim # https://github.com/editorconfig/editorconfig-emacs root = true [*] indent_style = space end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.py] indent_size = 4 [*.rst] indent_size = 4 [*.cpp] indent_size = 2 [*.hpp] indent_size = 2 # There may be one in doc/ [Makefile] indent_style = tab # https://github.com/microsoft/vscode/issues/1679 [*.md] trim_trailing_whitespace = false pudb-2024.1.3/.github/000077500000000000000000000000001470451231400142365ustar00rootroot00000000000000pudb-2024.1.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001470451231400164215ustar00rootroot00000000000000pudb-2024.1.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011251470451231400211120ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: Bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. **Versions** What version of pudb? What version of Python? pudb-2024.1.3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005531470451231400204140ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: ❓ Question url: https://github.com/inducer/pudb/discussions/categories/q-a about: Ask and answer questions about pudb on Discussions - name: 🔧 Troubleshooting url: https://github.com/inducer/pudb/discussions/categories/troubleshooting about: For troubleshooting help, see the Discussions pudb-2024.1.3/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341470451231400221450ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. pudb-2024.1.3/.github/dependabot.yml000066400000000000000000000002721470451231400170670ustar00rootroot00000000000000version: 2 updates: # Set update schedule for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" # vim: sw=4 pudb-2024.1.3/.github/workflows/000077500000000000000000000000001470451231400162735ustar00rootroot00000000000000pudb-2024.1.3/.github/workflows/autopush.yml000066400000000000000000000010221470451231400206610ustar00rootroot00000000000000name: Gitlab mirror on: push: branches: - main jobs: autopush: name: Automatic push to gitlab.tiker.net if: startsWith(github.repository, 'inducer/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: | curl -L -O https://tiker.net/ci-support-v0 . ./ci-support-v0 mirror_github_to_gitlab env: GITLAB_AUTOPUSH_KEY: ${{ secrets.GITLAB_AUTOPUSH_KEY }} # vim: sw=4 pudb-2024.1.3/.github/workflows/ci.yml000066400000000000000000000055311470451231400174150ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: paths-ignore: - 'doc/*.rst' schedule: - cron: '17 3 * * 0' jobs: ruff: name: Ruff runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: "Main Script" run: | pipx install ruff ruff check typos: name: Typos runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: crate-ci/typos@master pylint: name: Pylint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - name: "Main Script" run: | curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-pylint.sh . ./prepare-and-run-pylint.sh "$(basename $GITHUB_REPOSITORY)" pytest: name: Pytest on Py${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.x", "pypy3.8"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: "Main Script" run: | EXTRA_INSTALL="numpy" REQUIREMENTS_TXT=requirements.dev.txt curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh . ./build-and-test-py-project.sh pytest_coverage: name: Pytest with Coverage Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - name: "Main Script" run: | EXTRA_INSTALL="numpy" REQUIREMENTS_TXT=requirements.dev.txt PYTEST_FLAGS="--cov=pudb" curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh . ./build-and-test-py-project.sh env: COVERAGE: ${{ secrets.COVERAGE }} docs: name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - name: "Main Script" run: | EXTRA_INSTALL="numpy" curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/ci-support.sh . ci-support.sh build_py_project_in_venv build_docs # vim: sw=4 pudb-2024.1.3/.gitignore000066400000000000000000000002761470451231400146730ustar00rootroot00000000000000*.pyc *~ .*.sw[op] *.egg-info dist build distribute-*gz distribute-*egg setuptools-*gz setuptools-*egg traceback*txt .DS_Store .cache/ .coverage coverage.xml tags doc/_build doc/README.rst pudb-2024.1.3/.gitlab-ci.yml000066400000000000000000000015651470451231400153410ustar00rootroot00000000000000Python 3: script: - py_version=3 - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh - export EXTRA_INSTALL="numpy" - export REQUIREMENTS_TXT=requirements.dev.txt - ". ./build-and-test-py-project.sh" tags: - python3 - linux except: - tags artifacts: reports: junit: test/pytest.xml ruff: script: - pipx install ruff - ruff check tags: - docker-runner except: - tags Documentation: script: - EXTRA_INSTALL="numpy mako" - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/main/build-docs.sh - ". ./build-docs.sh" tags: - python3 Pylint: script: | export PY_EXE=python3 curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/prepare-and-run-pylint.sh . ./prepare-and-run-pylint.sh "$CI_PROJECT_NAME" tags: - python3 except: - tags pudb-2024.1.3/.mailmap000066400000000000000000000007521470451231400143230ustar00rootroot00000000000000# Prevent git from showing duplicate names with commands like "git shortlog" # See the manpage of git-shortlog for details. # The syntax is: # Name that should be used Bad name # # You can skip Bad name if it is the same as the one that should be used, and # is unique. # # This file is up-to-date if the command git log --format="%aN <%aE>" | sort # -u # gives no duplicates. Andreas Klöckner Andreas Kloeckner `_ * `PuDB Intro Screencast (2009) `_ Features -------- * Syntax-highlighted source, the stack, breakpoints and variables are all visible at once and continuously updated. This helps you be more aware of what's going on in your program. Variable displays can be expanded, collapsed and have various customization options. * Pre-bundled themes, including dark themes via "Ctrl-P". Could set a custom theme also. * Simple, keyboard-based navigation using single keystrokes makes debugging quick and easy. PuDB understands cursor-keys and Vi shortcuts for navigation. Other keys are inspired by the corresponding pdb commands. * Use search to find relevant source code, or use "m" to invoke the module browser that shows loaded modules, lets you load new ones and reload existing ones. * Breakpoints can be set just by pointing at a source line and hitting "b" and then edited visually in the breakpoints window. Or hit "t" to run to the line under the cursor. * Drop to a Python shell in the current environment by pressing "!". Or open a command prompt alongside the source-code via "Ctrl-X". * PuDB places special emphasis on exception handling. A post-mortem mode makes it easy to retrace a crashing program's last steps. * Ability to control the debugger from a separate terminal. * IPython integration (see `wiki `_) * Should work with Python 3.6 and newer. (Versions 2019.2 and older continue to support Python 2.7.) Links ----- `PuDB documentation `_ PuDB also has a `mailing list `_ that you may use to submit patches and requests for help. You can also send a pull request to the `GitHub repository `_ Development Version ------------------- You may obtain the development version using the `Git `_ version control tool.:: git clone https://github.com/inducer/pudb.git You may also `browse the code `_ online. pudb-2024.1.3/debug_me.py000066400000000000000000000021721470451231400150210ustar00rootroot00000000000000from collections import namedtuple Color = namedtuple("Color", ["red", "green", "blue", "alpha"]) class MyClass(object): def __init__(self, a, b): self.a = a self.b = b self._b = [b] mc = MyClass(15, MyClass(12, None)) from pudb import set_trace set_trace() def simple_func(x): x += 1 s = range(20) z = None # noqa: F841 w = () # noqa: F841 y = {i: i**2 for i in s} # noqa: F841 k = set(range(5, 99)) # noqa: F841 c = Color(137, 214, 56, 88) # noqa: F841 try: x.invalid # noqa: B018 except AttributeError: pass # import sys # sys.exit(1) return 2*x def fermat(n): """Returns triplets of the form x^n + y^n = z^n. Warning! Untested with n > 2. """ # source: "Fermat's last Python script" # https://earthboundkid.jottit.com/fermat.py # :) for x in range(100): for y in range(1, x+1): for z in range(1, x**n+y**n + 1): if x**n + y**n == z**n: yield x, y, z print("SF %s" % simple_func(10)) for i in fermat(2): print(i) print("FINISHED") pudb-2024.1.3/doc/000077500000000000000000000000001470451231400134435ustar00rootroot00000000000000pudb-2024.1.3/doc/Makefile000066400000000000000000000012171470451231400151040ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = pudb SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile sed s,doc/images,images, ../README.rst > README.rst @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pudb-2024.1.3/doc/conf.py000066400000000000000000000014241470451231400147430ustar00rootroot00000000000000import os import sys from urllib.request import urlopen _conf_url = \ "https://raw.githubusercontent.com/inducer/sphinxconfig/main/sphinxconfig.py" with urlopen(_conf_url) as _inf: exec(compile(_inf.read(), _conf_url, "exec"), globals()) sys.path.insert(0, os.path.abspath("..")) copyright = "2017-21, Andreas Kloeckner and contributors" author = "Andreas Kloeckner and contributors" ver_dic = {} with open("../pudb/__init__.py") as ver_file: ver_src = ver_file.read() exec(compile(ver_src, "../pudb/__init__.py", "exec"), ver_dic) version = ver_dic["VERSION"] # The full version, including alpha/beta/rc tags. release = version intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "urwid": ("https://urwid.org/", None), } pudb-2024.1.3/doc/images/000077500000000000000000000000001470451231400147105ustar00rootroot00000000000000pudb-2024.1.3/doc/images/pudb-screenshot-dark.png000066400000000000000000002220771470451231400214540ustar00rootroot00000000000000PNG  IHDR|*J pHYs+ IDATxypg~7q_$A },}>cg&$ݤ6JTI3IfqٞObș%Y$JxEmH@r~OwO?M#Q?4Mt4isҥM c_cXd2{=v`;`;/]xwmnnniiv8j$.+\rmdd,u֭BH~~~2?nlfVww Y6MfV*OY>eիWVk{b umh``_onnnnnz>hY,FXVv#z7NK}0;Rtۗn~‚LsE?h4YYY [:ϒHP4m2M&K/faNܹXYpxhhO>aX;wL?|׮]6>futt)J  ^|篹TYYgt:׌05j̙3?p#6; dʚylg=a \~ąe2S??.]c_߸q#vU"楥%HYPPկ~%l6BHT0>ӧO{<1c «W|>Pmeee1G䣏>AРP(6; dʚyl l^׊vxL&ӽ Nk8xSb_hZWJTǎwk׮5N߳>OǾrBEQ:n#~:BpPХ !N{'YF/|oG;vBN罍NS$JiiiWOap8sجyBH0LTLz}655iM7n^fD"Fs5w>sw{1LrWpz%.b"o!bڮ^K/)ʟ?!N oܸ188p8x<^?zD"I\Jv(S;^>y޽ǏZm'fzȑ#aiii|fY9V׿5illpRMM͉'(" Rnݺr`8z Bկ=P6l5`lZ_{g}Y2 g?;p={ >}&AH$*..?+nt+%%%sss;wpBQQ /rVŜlQ F׮]?~7o lgff!܋ɡC5~=66аzzzD"JFΟ?R[X__#<b~޽{fhh… .'OVWWMfg9}ロfuǭV3{n;DZTi̙3+>O&BDrĉw)xwillܵkz%sHP*Mfŋ$%%%<::OXX洴{0BgTFeo%|+qENG}᭡riZ@?= ql]]]@wuu:s-,,,s/OB+Ο?o2[=EEE^͛LZt׮]+++kjjbG!PU^^ΜJ2#`yyyFz*VWR "L|(S8^;ஶGV_|E+jenߥ&d9k&q2L0w^ټgϞ'N0|+W\.T*mΝ;?x_;*e$]+0###r<-q l NWi4^x*H\t:e2jbⲲk׮˗/l6&g'>V ;>+W2l)g?c>_|1K JJJDL&͛7}fM@ܹsNM7L/׿e9zj27߼Y-9?~ ݻF-jAurrrv`|,RIq8k^(,,$tJ^|YJFNgjZԚ:qk<ׅT,L&h4ˆ[^kMӉ|j߿?68̾Ic+jMRrfovoJC2W-`ŃZA8ò[nMMMzFcmmm2JpeKS8{m6[|3JMML& Nlf+E1RŜuVf%>V\=ޞ>LMj^؂>E"ѳ>KQL&Ni䣏>Jf;l~?sT*]CV*{9Ʉxɧ8UUUwm{."D"7o޼y3~H$ 1+uJM|?IZ^YYYzjAAA}}}:a'㏿ۯV-**dRkVyӏae#&.%-xF"7n\جP({q8ӧO/[%Q(R*ayXPP5::맧}>_\h4_J(2?]eN|Fvc:f4v-?f6f·bK?sŜ_3VK/tosKii3gV|Tj2+wL:2~J bdk r?D{{kמ|ɚ(EV;+V۽MMMvt<oaaڵke MӡPuZ[iiȩSFGGE"ZԚ8Ϩ" 3 B$㻢0w$톾h;uԲp8Xx)R8I Ml6b ~YfG+֬niiQ333===,ri6`ŋϟ?mNԊ$<^Ef23FjZogTMMMI~E^xڵk%%%V foĎ{d2Zo߇ 盝U|>?+ 06vM0R76wfggOOO.-)W;00@lE"hQtcBL_JŶk}'h.ʕ+̐ˬ+B hGЏcf2rMו+W8΋/XUUuڵXPT*X,綶9IRSSSkk+bGѥZԽY|VP._Jlk&q{%޽H$9ѩ"f/eŃA;dfp!JpjZ>-vou}ȜGΝ;n ؈Z-BTt,S`f_SS;UUUfyYfp8ݻwX 3+i2Lbxhh(~"Nݻw_xٳeee6C1rVeZVÇp?vvv_ŲYp߾}Ν{^o{{{aa!d‚n'?=600fcx<( }Ia511188O Ǐ~?+[WW|UPۻ⃀Ip8ׯ_w:=77y,A,b LR:^OVҥK``LX9o~JfiԊ^N-d2IH$:y~{6t`Ge# Qbܽ{3~ݻ|AIIfkkkϒնZɜQڹskט0w5 ΨN"؂쫪N۷>?$U*=\EEŲYl6;;;{݇Z3@ `b`Bi|H$=vX:sss:nzBVsΊWF$mmm.]eeeGa~ntuu12 0S oܸj{mW$tRaa!3 D"9tPSSS]]]ɇG}4]^^~ɔ>tD:رc VךV0$)'@b+F{nWJSiԊӧO_xUVV.TWW \r$~P쨌tp!J|ݽ{Wz\1چx<^UUUlVg2{]ZZ*++;tP2Ql/DQ_1&[apYeo>޽{?+$|>Fcd5Z"H?~\$Bhqxzѣ$VI2]}trRJVG"BHaa!EQcccl6L 'D"!}KK N>s;w{epkkɓ'a4Ϝ9,o=L;w466ڵkrrw]3 .'OVWWmӻw6N8~hh|vz=wQ(Hd׮]$.,, 0t`KQT>YټgϞ'N0_|+W\.T*e?dZvff& 0dbqSSdJScs-,,TSN14Mwwb444\.V+y[Kh4͹Մ& l6񌌌dee JӴdR(<3p(brbr9afXsCWg(..njjr: 2{v`|RIq8*J,L&h4&xYYYYYYR;nfr\.leeeuuj<ptwwBL&S8DL/|:T*U8lf9~H$rƍ>?؝X,^׆0%$D"͛7o޼l:!_}UV[TTTUU`J71Z6//ojjX,B|BfMojjܵkNx ׮]/G\a +++Brák׮=555in`K1(JV;fKBH8&X,c&@"H$h4dӧO3_ӑx_.Ljl6bޕUUUUUUhŋϟ`Iql CIIIl ӹ nX<77|>=l5*jjj~H$ҩ4x9ㄐģ^RP(FGGc~BZ[[,8~>4+grr`0v>n7flll߾}I0??eiiI>ckv%f#G޿b"H;v,cpeeܜNCZ %ɒnv QΞ=!@&Q|8`ڬ@F`;C=6>űq`KAf=C˛.-Cf= lb8 桡p8L9rP(\[G: X,ڧ/ޞ04ZuXz?D"\X믿kW\ TVVVYYfgg;::6Z꙽BZBP۷oB޽fcKT*J 3/SZSqd :M'(@  `WTT899988(J+++k6;.B꙽Z ;h2Kt:۽n@!T᜻!5 <"Lt4ݷoÇ:;;77fxH/..^z gFh4t݄@ D FoW}Dnxo}[Eٟ|-31ѣ_| i=dee yTVr6] Zґ8jJE뛝]6W$Iҡ4A.;02*sلUI//}hαRux6ZY,`_J>O-BH0aZG]YB]j! .\eq'4%Dׯ_onnz}֭vBH__߭[oKpwYW`[FLCp8yyyl6;TgX\㹷nM;wh4---h?Flͭ&,K va~;@ GGw !Erd XeY!J`ogEYYpu)K_g&.--?b!tuu`ȯ~WUQQ_y4r}>EQ</t:]';xFimmXPr\V5|ñethXluTC#.嬽-.Wwbav{-YSSSi=!f緿ب A>OQT:+OJ/_~>lt3Ѩj&'')b8++k`` SȸkYG-S>'ڶRS_Dc^{8:;tL&;fX.)fEjmB\.X,SSSbXղ;wd.l؊\޽{ C(X:gj:ttt4MF\.7 NfQT[nDzfo4F#Bn7!D B!.b\.EQ)-uvv޾}tڵkcxx<999Hdiip8!E;N[ EQ!6%''^$Ŧ(ZN:].ltrRJVG"BԔݵkT*H$2YXP(!\.WRl6!lvvv6޼H )iCVTYfb$ioo߻wocc#M]]]v=H n(((p:4M/`X\a`+K77f977yJ&+JPx$IAAb[lEE^H$ӱNkq\]]]cǎ閖μLD  L&D"zVueFbB***Mfyxx(qȬ3{4 3 yb(ʲX,pf3!D*-dzbxǎ.̞iPRRT* !`03@z@4m4v{~~>gd2YEEZެ yƙTTuuuv[,BT^^^]]Z.--b\YYYsssHdnny MJR (Jz >6X$Q** LAhlV(2ͱYEPUXXzm6!$ -,,Xؐv]$nz Q,tضΞ=ڬ˗/H b4M3366l"d2{=v`;@f lN:zX,Bfyhh(3 ZX, ñdVTTrz͎!zfP(VP(t|>ۄZ2==-Jz=EQ ".w^ 6;N꙽ZLc|4---rPHӹX;V9vAZ}-HT]]<\Rgo4c&BX,6ͱYEd->zMzfx<OkNNN$YZZbXl6; !\.7@ |Ν;ryl ŪˋMQ(:.u\.ә8 I鎍rRiUUZD"4M4b}f3EQin 2(Rbŧrfggg͈!q!*7;;Ki_&`kr:J(T*ez G0d&t:yof/***jzoIPWWg|^w211QSS{E@f3lA4M\.\FmS*@ ;zf?66DjJ &ipp3kff*++CpBr:rrEevH$rH>J%`={vY/_f>0#MMM-2I|NJ4MY2{L&zoCfPp8B @Tp8ӳQ@`;@f ?[ } .uw&HM~CɎߜC}  d"СC/&j4F#bZm&GOV {ͅ9lWku(M3l<|~i5uj3b?rq[U &N !~mx@-wM/ڊ\[oٱQcQ?:2}!H.w^ dV=Si=!D:Nk0,//^7ͭ\-xwLXDr)-sfn|BX|/lPx)[͑za0ArScỴj[D"QuufpI ZVbL/uD%,oB>BQՏ:G-k!pBG7~`EG+rg,;u#+r XQgg|088فoY4uWBξk d9&v|2=[4%*{D[W$}swp>sgάcU#w9Ū[off駟===o_EUEz}~nd'Cw< x<޲KKKgBQXXv'&&Y~J+h4\.h4殸EQ:.+8Vg7a+!zb&6pc;R|^+uZ?C5I#xO^GBHqCcgz{ndRz؜G6//l<89,BhB_>d4IݱgsuPţ'*=5upU4taqw_mE(ާ=3=7GqYHę+W|ooj'?_{aRBȗ2]%J]W5733b}qfwl6;;;f7zfOQNZNs̞ܹS.ONNj5WXg\pg]<+PT_^f.:|9VOjYgua$!$o9ڣD;B¡ 3q8ѿ3_=vǿW0}6ގ"_䲛ǔj=YSՇ4MO{V"WƇJk2ڣ}npbB\Vp߲ 4J{nvv?/b+ ӟ-Y rwA.:rRxSSSbri=֗zf_PP W+JY,V{{[3(wcS]Y92A߄lCY!J`5. !`$ %1:;A Ppqr$. J"1?t!qXf[\vBH4n> BOG̓4(!B>5SᲹ\E|nG <6S{g,+''grr2~("-?=㶞q2PXPP`m/3t:dy͕zf^xl6bU*Bp8X,EBqH46͢N5j(\V$Wb)l}ǿ4Ŏ},yֹNN=Bivl"EQkd '/`$lV(,KՆB^[S=ŒH$ǿȀ?~-bwww4/x3o_Zm4M Oaz3 #O70v\yCO$SyDX*2+Kĝl0tx4ZXXXPPxh4uݱ';hh&\,?|gE#: IS{Ngϕ3_ٜ/OGiBӱ2E֎!dκDQ+susJmh1`dJrnnnZIC M8>¤KKKJ209kh܁BYlJJh{w-Ve|zyfUqE}g gvR]%fsB/T/ˑ#Rоg_b>[\~7TUjE_=Xb)xp\FvWn dj>)H+ڽ{d:kƀ}E?xožgWz}ϟHc&JAoǿv:=IJwM&.2zqjD_?-9?&W>zSAWWwn䋇GGkXVVY\ATysS4 v\_< xbwڕJ@ mTfOh4`pfffhh"5Iܽ9qr:H9Vz0;aujݓ5yRpBAhO?^oxݗpÞ#PpfNQíW|z١޾$\?PՃ. &|鯯[k̢͕`K` 4Fp,"v#(ItcD~hߜHfyCGOV:coٳgWu dV陦iC-Dܧue${U<4{޼MN?>]p_ ú?}:֒ 0zxwlS}̿?d2{=v`;@fy|~DǏ/--o>lѿdx):JQQѢZ_bs^ E8_ 9f7H9B\spK\jJfCه6bSbkOUa$6Z%V.Z2BYY _U)KgMwiz+{v}mYF~F-m:)ںXT,6biϲڧ??78쪫|lC~pd_v\WG]E ,MEuꂚəuyfG0/Do"dVpVx(tf|aBP$*pߟlASs^^Lry&lEJ2ߠ('Dr[9BXQ_Dkk}6Ounv\_@JM]ղX,?qhhej*>%JGtF !t4x&(!D"Vl\[Md0@23hGLr[9JñP8?x`+%!gc>L˶z-ɡO !Pb4BhCBTժ8V/ʡ6~lϖ<ޚ6<[czk@ ]6qC!lieLtDϳs&SӜO]@Y9!ZΪ S=BLi+[?Yih OOwz5 IDAT涷<{L+'W=qj -HbQvyݵ}ĕMmPض̖HS7E{KN!d~k[sfEBdDԪwRgk*'Q "LP/ƽBMKE񛕆Jl_ʑMfzN1?JÑ`ytBH$r{̱Yyy9^CG؋汍QZ61 &~¾ ͍},= }FDF=EQ:j:1 1gXlWH(*[op{.-iL*e9ݦabB<^ G?yRo.}Z]K"osxR>s#wMA'N$MkFb{̇#"$TJr\qɽRۇ)Tٖ9{k 0y6SYz4 ?%D""ICAk w2+Al.A LY2YF-C\_XhbM.,Z0VDXsXMPɛ9!p9|Dl2ֳXlZdL#P,vxv>'.JCjgo"ysy$d->V[lE٠ϱ˯% +KSQzx48pw"m#^{^l6!BdqߖN}#Yy:߉DÄbSXX7~aVQb3<bq!w.lOsdR#hc^bߓKyPef[-H‰YvY,NOd¡L,/=tO%Yj7&oyꞛZ$fk>)K,I,ZF7(B}ٸ$= 6@?t9S,K"B?x[n9̄ T0?w ǧnf~K㓸=ɶح&]_*ǠTh%>I0a,}AUo6I`%E_L3%N62~i !4M{lyʒxU\V[pK;:ͮg!K>\VruQU).|:μ(+Fz): BIyPeJ/meF.W{k&Suٸzp4 w7o,/9 {~lM&_j ED (69gM-o6x?>5>Ց`T9[]v"/31/,kLvG"|q4|~p0ך#مOmxqna0K!פU4)/9$Or{,c%b ȲDlBE$"y~^9.|^* 1u]y/AajU*;yef=f5^T)EܲOB!#K2oV hdva`]E{yHؚ{#T[q-ܲDm*gWSTX'+bŰ%A~CP弬SlekvW&Xs6.GP(j<<0r2g dZu}Ȃydx6h4\_ί ?$aٻ6n;op)DZ,[Mɻ:N4޾is6iNiosz=mϹ]nuNi8iƖ#;dKvJWp_@;0f#AI,@3<Zk[Yw3H{BKWG&*ek7jk-vxQTO}~t~J!l}NF ֝GS7~tOahEZ#1z[6Ϫgl!_<t_5@^}tE'OlC!yγUU$o]]0/vdp7Rc1Ձ WV@DBluń1{=N`'@f  8^* V;0069ysFNrOr]xBHns5UKMj_T*TNlQ*䕚GX,-ÇlUX㯪|r8#@}b Ity H|f>tSwێ6u2gNĭ ?jœv"ٛfaRtwwBSxC5Jfm$߼F rBe4l^Q;Cj|$v_xg>Xۻ nB׳yax|kkp65>6AwDr終6(4onnfYvyyx4Mu\D[."+5,ZgVPm꘽$IyA_:6:m|J.W<_tapѣO{-ѸycS}oW-N7gZvi5pML~ pzRe ɍPw}(Y#$FSoBJQp}bs(eʬR""QLx8 Jjΰ[CC ^7>Y$ZImj^Z" GDf9n9<욙ר ~?N˲{.5ֵYsfVV"_ A=?D,^s"Hei77S6DŽQ_noS/Ⱥab•3D{^.lj(pȇ47526pI.17t@DmCmC3 ΩTzsþ>~q5AUa<[?>ViJdJK괚ə3z]es^yqy3OVG,&?^XG`^m(̞at:^o̾ʕ+gm" GRM -1f "jzw=qZeHkptϕsRޞHĆF$"gm U<D+~"Hdx^ͻ X^cAX!{Q0XUw,LJޖܞFSE*jBn$ʠx"iKYy] $t$I"$"p7ѥfuih3|y㈈X,&+ks9lnߨVp|8A_q,rQ )G\32# "~y=a8bQ_u$3dm" a8a[4Hβ]=y`g:[3hf_5?m\uB.@Dv甩f:rrl:0sl|.|R>3:Mय़8\9;@=˲zN>xWӝ>}gyFTGH4`W%G;ێ67읝p$BvWJ-|wkc_2pQ0~KU͖#C\Xλ.cK^*'+*˛̇Jݞ?q=]O{gv<3{IoM&`  k׮mϴ"QJWpʨFe KW Wޒ>ZD IJm0≘<.X%>7}0"I<"IbI2D1hY0 qgtTQ!0]`YY֤P!W((kq*κZvfS)8@gJ.^urV5J`Q8*}}ݟ$)rT&?\[7JLDv"ڝS Qa.Wc]K)Q7T66,xB;N3I뉨5 .\NCmU]NJ&l/2 m4AMp8b2"*HT9]x$J lPՖðzmY]M$r.^h=ro36ǤAWQQִjM {kN1p=ڿKaDn痮[w=(u=68M͞؁Y.bFS-yq?d0XW R(p'`]]1>뇲nGye4TGԍlM:]NW>f=& &6%W(:nϞ=SI"B0"b'w^=yto"[YOd!m5z{U-U-x4kf?5{WjKS2lpϚjw9awN]wC;?2ANNTkaf>&4t,]>>:|)Qu5u Bx/uӍVj+B(rQ0䡔q~(F)*,.Gg_VFKN,zrZ}Vm?8=p-4٫ɓ3(䰩$I׳Muﹾob"l @.l]wwwʌu wЊp!ߦf/,T[_w &wK%%o_Sgfnupg<{=N`'@f  8^* V;00699XWWԤn(6T\=5ʰV5=x7C!w ,`zY l"ٛfaRٳ',..PqL%夂P< I hٳvQڔJe,*I$俅lB웛YlV`2DV lDI%iBRdPAc555l`0=J$Pn(% ,B766*t(J066Hl$XٹKl!Fo0y$=z4 8q"u{B zWuKϷp…{gA ٳ,u:@IIڝV+˲555 i9Gz488ihhx_"SX<3{IoM&` PThFL-//Oݒ6CO=NlU7Zł1{=N`'@f  vd;2{Ujmmm0L333G[LO}ӕF'/M[Ԗ{4\WW "iW#_1>_fM;D5=$IcvmB_VHL4tUmL]*B,ah-} ]Y9'۪yƪ@hؗY:jW>]ytEVvFMQxfaIR79D</!< ]%I ѭI1K\3\H.BUs}5"*׊P]D! beIw64TJ(ewB3{5(rm#I"I7ML},G:a'FV)# _ܥOM<+}NTMZC׃?䃛avyg3S9q"b8PQW܃z,|ږg|Q+V7W;t_uTZ);v3>3M݌ knj'ϯlgeJjVWJth IDATGh4xf o?<3s-{*ZM_T s/e>Hz.8~ڕ$QzoɞG#A\׺_apr.0B~{M$ߟY6)ddq,8qVa(׵sZ#"BWkݯ4V~)YUԦVc.g& R;r5HR`#cR"^I#ꖨmyu$4;$rZBW)syÑ":;HZo55Y#]%\ D;~gRJfVe#cY9]Z0G-DJc|T gزuo~Pմ{ }{s)Dg Øfzf<;QV.Iҕ+W hbodc;0 aQ1+سޣi /x/ Ye%~]K\p}r {) q6e71 iQDuHp%SD_{ӕ&]KdiOxU6~cCzӔDӹD^8ޕʣ_L^~'e c"XdiptXB_M*J˿~ cђƘ #İeDqCz ;eY%C|Z Fi*{" DW(43,'6A|#Dkg*1{DLIp̲j~ g:en)MFHDTļW#sٛL&Z._bΝE EKKh,˻  (Kk9^W 8p=#6PKf7%1_ rprD#Ţr[ < З%Shdq8LDDH. 8m $^_D <(KD `8'RِW2DXSk:p>w)#~}|estJ m \ʥB9gtrRѷs/pRћ֚ ߔ;yگCܼY_p+e`G?X,`Q+o",z[D$F$C6 D$6GW|E#˫#KcT(OO}p7ʯ.ndI6D9} Wp3gYV@rӧ=O$r$AH(Ep U1 ñJ5Dr4ЗD ١VyU:̞(0~D' }ہmJWKxṫZo; R׵S* w  a06*jҚD!yzqˆ#Qk{# Nye,SʈXV4tIbB#9SpI]4vIڟM6VޛW#C ÇBDR"&r]מzlq<v)xbKKݻAZm~WJ5ͽZ1L%ON%IZJ<*FK+oGrR]kIoCVh1)ŅU7.`ɞGx48N|5eIȪyFy/Q}LS 6Ä.\a+y'V׵Xtyq4sYhYEF5]Q5{jddPHp EC,ӞӥdE7>ˆ ᅵu={CYRSuypV=zW w=K_[Ŷ`{:c;2{=N`'@fi|CE(FqJU`Zv``mrrr8$J>5NW]䫚kc6f>W"K3?&I(L$6TTiN95ry7汯FmD_gW5W2TGӹTKjSGCI$s8Ζ6- ˕=Yi).@V?|"z;#V1{0 X,~xQjKxqtCEP*Ubif*eiM\M~3E}Z\dyuTUmޢB׳yAU뫮\l6umq EUp|y/5B$4z[G__qҾ'Y^KSf8tfy? ٛW#%D4Y}W~%!_CwSմVY;\ 7/oAoiaڦ u?kSV>,/4txF. zYsp-]%{_.x/CI=;[3ּW׶_+;n𜶤ߋ,O1-4u 1:#JYomejbXbXb9Ijkm]rc0y_t_s# SY/>ZeJjVWWngfrr8QTP̵P縉k!4}Qa2>~̽XӕG>Pu úNHJg^#);pdab>;e(jZ=S=P~9[DD Gs3Ơoϗ֮$.VR:X:ܬQu۬-BWkݯ4V~ G ]GBC,5*t^Ip }!7raMf>u-Er]Ր#,Or]~Y?/UTi2cҷԷ,ODǕrV'rǻߗkDTzﯭ3 ׊.O#QɾR;;[vt{J=Y NUu-(g$)0a)_{UuKv]ؗ9Q~]׉({ekKF>4_uabf0lv:^wUf_^^H$ۧEQ\^^vZ,+8VbŃ/ DQǬ`z,7kI*Ub,^!-#ᅑHR"<Ǜ6J=,ym y71ԺZ+AyNSOMH]!F.HYpm<*ZYZ,+J˿X81&>y̴?&B^>W42 >)DpJ/b$`9&_ ]K\p}È=ʸߑ惺qye:Cr[ZK_jJcժcu5x.s/[a|:Xs.+tǃn"*-?:[}^wsɿaIqNWz/%bDY,XCKzQS3]=SS{S"+"W޲N%F>߅_Ҩϰ$&H]zVkL&Z=5Μ]Fq\ pjݻwoAn#s.9jp҂*LI,8爈ӕQ"Prj"ǜ{?RP,y/N m/_K1 +VX^+'C*eD>05bOD써(pɫJb燳rk(擁ElWa8]YpHaȾy01F-Ip |EXhRhBVrpc 8ۄaԥ3óW\$Ţك럲$/Kymѕ))Pte*2%83^Y.&}>QVjp_;"1{ }>XL$=ŋfNV պI W]bbu %y/^ ՏnteZp̄箦NLUĪ4OYSİò÷rr~msor.arE.2\[1 $-⳷zyńr0THpʲJx7̲VQqo(i$ ($ w02^Fh>4_sԦ芕hdyuwhgVʲ^'ƁӧO{^Bt:tm3aD pRr<(T\2tZj-ק ~.ǵqҒDn&+U"㙋RYxF}UeK>QtfsTvE%Q̥H$I^w1DMSwhv(9g?Pd/J$&$QdصEIbrP>[*\uD?Z"YN |ytMn|kApΉh3{Ӆ'K/cGFu-}Zu]{di|G M%Irx+/*k5TsBS*U|y>b4˲{?m \&"eYBWFDIJ }K-Y+Lyᔯ;[Vn3Ox& V$u<~HDq}浝tSĆ`KZ|P9m ˫GVݵ.m#I,u'E)q,hhCԦ]{d(-o೑q]KFvJ?]Eզ]s^˝Hp{3Q["Yva%{^ad;EY/ ՁNpγ`=Ny;2{=N̾8rv2е|7unu p*ªZȑ#Dt yѣG5ͪN>x oX ZϹcђ=Jb"4uqlf& s|[[[[[[+Bmm[(z/*U%{sqc#K= i=ԷfBPUKz;OpcK웛YlvPեKKK6tGcĠ_p[4fq\SSf #B*.c1}A"ҵZ单G>lmõ{ז燍uuu>o[M ϼ&ƢQ}5}_rUԦV^[zh/+r3 c6NMV5m*N̷^!1 #Q,E\ZC<洘UqL&ZrJ}jkkiM)\dq48a)e2foXy^^yaRx:mĐ-vt NnA`!)Lk#&Z) }zk=cAdY}mm$Ipf IDAT*NX48vKms/_`KYAϠMϼeM_ M_\]U\Ddy+Nf'E"#>Zg/6]K{kE `>|'1 /X)ߕDlݛW`Vwͺn'd;2{=N`'@faKV{":qDrdX,ZV666&bmٛfa$IJn)++u8:㸫W̞ԴA8w]P"<γonnfYfnT*h4B!R0Lm@:e555l`0 @D yUPDeJj>;; g4٣GFF i2?gl6;N׻(H Jtᚚ .LM&ZZRaL&b|MmXy8"y^M{nnԩSΝ,ZF^,h`` q``\vM$>661== 5*^z488(O1 pv{GGhDfINgmyyyA%/eh4Z@IϠ]l[[[ooR~lF[@OLL$X,fGFF*S~|||||M۩̞Uf;w.yrr2\qu498;Dz^4h[j'O\ZZ@*IDxUyx-Joi7u^K?FPgۃ:mY0PDDѫ[w՗|>ŵu~[;hYnv{2_ZZbNR#1"Eۯ_;7VEqhhou Ev_g#xuƝqdS?YvIuH-\$ w:JRИ=qMMM6- &7,q\<OnD"DTnqJ0 2J"$)]5wThȼ?$'׺c }UV_p* ꯂٟO TTTgϾp8s[ǽ >Bϝ;o|[8?~رc&?HFU)Zj qfvUxgݰYV٣J DTRk8?āUuV?s<7G^=m1FRiZS7J$IެY~0L!m}/~^s~W/~޿{l\`wgB|4}iyˏvc{|jݡݵ]ewz!"*--g>ogNhdd_|םwwJ__ooxJw_߻w/=쳟^z饓'O7SO=S?s=<򗿼~>8p';-"vg*r"HRjJc)תHOh1NVWW'KSly8{{\ɱ:l'!"!.EtZ~l_(k4//E_KA2544t^xapp_b<5Bu"ݵh6 >3Z6 e_nTD{ߓ B_җ***Ng?>66Q_җ׏>o/nկ~bʳbHhWᕚKf77˱HFJo;Wc&IVOMM-Z\\X,jr׮]D)oDH#w'"u 9#s")$ؽ?JD|dJ7W^)||$Ve٪\\ZZD}rZ`pDTVV-a*++E׮][aY DTSS#9"FCU~|GSss,J~߱\.uC,[,`0x8xb$YVFAD&h!o&r,1~ODxR} r~󟟘կ~Ux1ux~uT*<ѣG++o>69yfݶ ˲psykBq3<3ϤP\lUrl4侶zg;?Fm̱9rl ٳ,h`` q``G^ettTqܝ"Gnɉ?k/W`Cկ>-sss@$FDFR~•: d(J2h`tyf$ &כL@ @DZ6HDQyWQ"[". QR*nNRG74Vyy>;::ۅZTGy^z%m.x\="ߟ:W'բ3DD EWH㢘VBQ,.d(JA*GWri<{I)᰼Eo߾={$w.//'"9$Ȯv6[K/b>u>9Ph0RL+Q$9ݻw'#]7K.C}_|__G˲&x?;t$IrʱcnߵkW2ɓ/={;Y_韞:uh4>ccccccԕUޝ7q ?jmْl˖ 1`jaI!C&;5̽L/o27/LIR526^n˛$K֖?4Yx`_R>uBhxxx۷o///=KKKsΝ;rMGfڳgs=~zs}+2^dK,̬ܿ?; ӧBk׮ݹsOKOM%Փ] gAgPb|!utu]~f&Fq2%VQy'>UJ`KD:\"ܕKկؑ+-M.5<>}Q_XccPߩĉ2bMŢi]vvqTyyy^w` Q"y ;i,}5N7ك ܹslnnNIIYlّ#Gް +4%uÕ]9>}qĭ[nʕQQQv̙31)Kc$+o?S7ӱ0̾HPq\gΜabjjjGp'stQMRLG4`xWN8't,SL!.{_^ NU1)9+9So`xc\P I3O)b_{BBBVV`WWH$t"jR>J2Uӕ/uGJM=F,daھ<=\owJL[}3[0VꞑTf(j6,==],;ɜw 9Tu8&rBLG4ډ'L&S{{L2)1]czPauL$/'Tlà}kZ|90#2-#hE"YB>!;P24.EQFpt SlA{yZ˙su\ 624 )Ap88F.#!+qpb!DӴ{rĠZRʅ7y{l!KOdKO-N̓_SoΝcݻw8p!"\p/,:t裏>byݽn:@pʕ{q$_JO4aׯ_|Bp:UUU}.uŋQQQQ]]/ڵ)+--]bE\\ᨯ裏E|"I-b\qB_t:3v^vO?k׮fT{}?~¸tݼ~^!1bT ^q \K,YreiiiNNaٓ9˃? uJIZO?/ _ݔ4-V?HE<~w(UUU+W$\PPz/rE/[^{gذaH$r\񔔔^zipp>##C  =0۸\;v( \MMM󟙽vؑ,ZҥKoucV /VS2d$ .lݺr~O?eB!B!4MQTy"$Q+EJM=6rIf :LpB+++nÜٷ9B_K0#|Wî5hUcqxBC㞫7 8ϙ3ԩS5x񘘘qw4L!$?2+|Mf|Պd21߳M k֬AaV+++R)ݍRT̡<BL`jFF?v믿} Y۶m۶mB-% yTYY(vF#al@RR\.X,!Zmjjj<[ F!pcfǭ@wcDŽpuZXXX[[!H&?hrotl1r6lXd "1E>y!e 8Ά 6l|vPrp/9ɽb9pmZZZoDc***ЏW ar'cxfR$[lF500p f}UUܹs[PP`glx?ӏ>~k4=ORRҖ-[&y~!x [4!B2Yn%ʨ73wL&S}^FC3Ts#H((L?v~ P49魟d2Sj `=#gΜٻw/2OQT2p8F]!M2IڴiSccczzeʦ|.'Z&h`82,))P4M3.+ւY~S5&F6OZ]@&Ѻ:OqѷaܽhadffM8~_rqq~μ{lN2[uPs޿J;v$믟;wn˖-l?oH8'Oۧ\tl5>₂>TjۙNL"u:lvg7~:nUFd6jjjJJJ^zGIJJR!.\r̞bŊ46ĉ[n]UU<7| /w;wL&[fMSSSSSAeҜH : ?Ԃ甔 0F;̟?|͟gwf7 I2xpTCvoi3J jUP!<\ Mj^|eff*f^ dff) xV9Sg-RI.?Iqi`k?,U_[[<4˗/"˝;w/cL{znJg}^>tPjjb٦ӧOH$;w\ʿ5H&2@=HMgn;yuxg߿|򔔔9ӕ)c]T\rW,IL1NkgΜX_z_WN(NWn[\^k:x=&S,*qd6'rĉ{ `j]4M3Y(??K.t Sf׮]v}U^^wPn^Bd/$ra& yRo\݌n dPaaaccL2e*++wt:SRR-[v.濭vOWfb]_p?4،4\?]g:\/`g\t SرcA\rɒ%vRC[=R»Ϸ+3uj<͠ pCiiLkCJd0<ܔ虎LV)Dg: `")?X;0a +Kc$+o?S7ӱw'gQQQnnnJvJU\\z)O-Nں4eY^3h^uuu3SF_|o999111|>eotky 1+rg:N8a2g:)6/Mw#r ct'|>pVTT={i/^\VVVWWwYFh)4BoMo\?Ge`{555O=!HtI)À(F{ݺuʕ+{$$I~/R4a/_P(NgUU$!pnݺxb ***_~e}X"..pGl">~usTx.o!|A/q:U]駟޵kWKK?O/~ +&!Kݻw۷̚7x>wF(޽0B=\n^?/ѐq?x ^1spH$}}}g&BH$#|>ols̙3g /^pBf jkkv@ HMM]t)Ǒ{"()))E|zyPXVVj7n8oYhR^%K\4''.gܯꔒxٟ~^)iZ~$#O<>l__y7a BT0c߿o>K!y沲fe˖;g}6xMw-,YǒGDUUս cZ?!Z2jhan10,Xl6=zt8.q 6 ƍ7n$sΝ>}:..nժUW^/#GDFFfdd}׷رcVuƍlZjhhx<g0ڤx{%"7tL6zr 6,Y$x<[<3qM$IΝ;wjhxjjQhR%nfz'ٻ\TTTZZy_yGΜ9w^e8~-epi^ !Z}nM6566/[l8&^TWWOe=reƁ&T*RV4t:6mpp0%%.;;wgoq8ǃFÔciӦ+W:u*9990yG&%%% gE`4yo~fh"BwGEE=uuu|80 r/ ø{4=44îdz{{ Y?SvTِcsڨ u~mUhǎ$IΝ۲e [?M<Q\.O₂>TjG͙g7~:nUFd6ϋK.-X]i4ќcJJJR!.\ԴbŊQ;ɉ'n_*!!!;;mo^xwΝdk֬ijjjjjb7l\{6*qEcCNN{q?7~{xH U 555%%%^wf=v'cA5dFO_bP"N__~뇆G={\(2E:@.n7oޑ#GdRQQG͋Doh4wԘfټxb xfoZ/_3u&f~RsɋM__Ι1 ۱cG]v/_^ZZ,ϝ;wܹ.k:2{О={{{?^yq:|L&[dIfffeeYO>ZvΝ;]nWee姟~\rh*1\}Z)g"c^ yfuxxx۷o///adz@q2%VQyǚCZf`p555ׯ_ŅK, `h44|Z/^x//y3+**y<`Xljhh8y^Hn<8"33WAZ`lc5^zW;R6t嶥嵦? k2_b{Cӽk<_Xccm3`*FlŮ]v;8<;00ʋrwݼDf_s&*tMW' ϧ(ҥK3Ȕy$Kqcpdw30kvdܢrΝN9%%eٲeGa~ۚat%k*ʼnC9KaaaccL2e~tޯ2pcǎr%K~w{6Km؏)H >^N\\.M:fLR~~>B*num7_S7‘p2A҃"|u3,5%''0/~JJ |9͙3'**JRޮVy\.ׄ{kH a566|V{ɽ@l}_"B555n[P$9{1ND(e*XC~8eP<(j ޼oeXBY a0<P(D8ON>ORԜ:uCxOׯZJ$n[orʂq76ͻw~ [%%% .O6m;vL/Lnܸ^CnB0//\~t:Nuʕp+y<֭[ٗ?8 òRﯮv\L$O=B諯***R*V-..6Lr_pa…4M?~|ddðܤ$Dz{{n@ ~ƜZ.ܹ!TUUUW7^qUy,cy"2Etd3'ZRaBԈ{П#Qy*m\Mw+- 8̸HF{F^^lziu1#!uå\V&x/u_ olso'"N 8l7-p[8BѐN%_ P [. B$111==]Ӊb<0t.z }F٦ݻw>}pl/^x0pW(ϟd*jdd999sa6hjj:}}TUUeZ|EJKfsfff8_x1999..n܍E"QCC`477744{߆`) EZZZL&M .d:}:ujhhl62-5MZZBjRz###Ңp Ml=EQ4MFsssB!</***11(.rKRv5 x<###QQQ8wwwϙ3G 8r6668t0q@Q=#=,݈%"b1~/ƥS.[C"2(^H,͑pqT4>7<'B}w>ם8H}mC'0l" N!"  O9J휈XDIUp1oj7xL ‥MЉX/"4\u 7:Nd%[BG ꣩|1r53.1<ǎ3g6E"Qjj*Mӷ_ (ʺsT*eҵCjZ~?y$AK,A tNJKK+++p8̾!{ќ,@ (DB=cDyy9i)'!Cg555@ ###cbbq{S{LJJbm6ۺu$9svV&`'OFEEiڢ/6444660S9s&v>?D1b q9qZB裏J҄-m6Ǚ9؞ׯ._d2]zuݺu21jzʕ2,66?LIIYhf;x`? "2r|7^S FST|1|n^|&.RnBa2~/W $'\! 8di[{T'?PG zBs!č`8xLΑF>7Y[pBn1GX'*x4bH}= 07Ժ%6H|G0).'I#"))%CDNNRlhh7!w\bݻ?{)gefo17LB4dժUa? 00 KJJen1CB$;;@gS1F}EQYYYEEER=~[`0:[-0aDuf %B4S\q&x7Ö́qBN^4"93g!=f`L0f.x8\6 }W#0Իgs)8 6:aLLђ.\p8ƻm&GNjBϟi4ur4hA]A!:B[8&fCjn\}QA"*H9haFhx22!B|D*HSӇ TG>'}L&^ZiT*>sZB3v~-3-x<*j|>… %x{)čCp8Gt7=LjGUq8/_n"IիSɑ[=''N,\R"\x1yw–񌌌4V#""7FbR)2{DӤG!tcD/.sq@+SΑrmL76.# cpIB&ݷ4"8Č!L 8)x[" !pZQOD'bd>OVo?44MV+Bip&3ڵkk֬˗/?qիWO]wdee.z.{WgGx<߻:njjjfl^x1L=wߍ*a|gc0ΘtOsWvt[E$\3<<קV׬Yìioo}LJ%R!/DCL6?`GP$P.qy'OD(4g4Y|휀H873P8Bo8CƉ!"2 ?zl6TF<H`G!9ry@P__O 0P$͝#p-SAP#vp-OmZ/_[[[o bR .0W`vrJMM Lb'.>>>77!$Hy䑲p iiiz~L<ɓ+w1}}fffooN{NԦj۝.HHpEQϏtRnn@O7o^||| ꪪ }Lp9qD>DnLt6" \uM) 9pN\6m'btG9~+ 4\uj>H q.~<;3ZY#DPpZGMRyk߭= G Xmcr|]uDN$9`0"斎3ok~`lc@tLV}֭[N 'NtLK\N\$}jmGuOH|=;mf ;;3,Y~ڊxJJL~͡$`)$ Q fv.Q 2{Lsss||<W>ȍM)8q~s c{#\6(PLn%Nd ~@p'~K7‘D9R=̌.*p:{!u/7nx̙VfK.e˖˗/2kRSSL&SyyG&"L^>Iebxɒ%B3N\P(h4aT@Þ]"fl՛K^WKccD=PzynܲثvKBXa¸}]tiyyyDD͛&#z{{{{{ ُ< &v@О={|>rdhhhbZJ.fx<Mqqq4MKRhvl|>Rim6P(TՓ9lx&JV-K8}VVVVVVwwwgggTTT~~>۔Y\\(H233#""5&&F*JRbŊj 麺@ py08Й3gRiaa!{GDGh4^pa֭۷o~ݿћV\v]+/_fW^Z H$*J*:Q6n9t:DGG33$/<BndyOttfcK^obbH$r\^_Tj4J500`6Ciii'N`^EFF2]]]Nb^E) ٌp8Ǐgv,PC+ަyd@$ ,))aq(PBBill&)8G TUU%̙CRBL'`Ru$IVWWi4˭F?+++>>~ҡו6I#p 3$FԶS$a(F.0Y\bFEB0%7ܼ{>%%%|${***B`@8>gΜm۶sαsbif2Zl2L񂛜#Np'|>?b*O[ @F }!aD"1L~I&YVAR`ppf`@٦=׸8pƍ0``ݫ2}i4[&8x F}wF1%%%V3gQ5>{шasAHJJ}nۙ=YYYfyܲ-90bs9kIjO; 9+ďZz/Cbא*,ˁm2f]~j-,,ȐH$SK^|EպgϞ ng٘rpzg!f;5Ms@JRF~yN3~Lu#l18pFm~: B%f!51FB/ ~ppr%&&T*>?*ٳg]]] "Az~````N03Qi:sKeZVRuttLtaF#ddimE}au6pRruGr3_  骪sݻn766N;v숍v'4]OORcY&O9tNOETR$'_?u 1F'nFH)B>?0S+#8xfby=fױ{q |~2!F#8b5r{M[ljjjjjZf\.t(:Rx4MӉ .z{ULy|饗?h?靚 gEQ鄎l^vjTbDGG BRN'T( )**J*r\̔+ DaXTTT*%iDbX r3 E{dXL222~LA* .' ].OKKSLTPP`X\2 @@r8^7<7778x###~GُyRɼeP___WW5k8VIII>~z h4j_vmJìR5SGHDaaa',xAKKKrrr{{1vsɜ%Kbgz+&TmvX;J{g7e*$@ILL|饗x gC ڿdMiz>Hiz+ĔƬ;^HH:.plSJj͠>000%%eϞ=tUUU"(===55l69ǭ>ryXXNA=?C=_gգ2{ ^.Z(44udRBXr%}|FvB 8gījvuuM*J9LMOHH`Y>E"ф㻻FQ*rnwwwWVV:)..N"444hZ_ x^*LLa;0zAzfo._q䚚PQQ ` ?///?{QOOQRRh4 RT—J "`f[JDbeYJEDk׮u]󃃃y}CCR\`AlllfffBBBssnap0Mg:D*fffjںFլ]]]]]],0%V2{ 2{ \*K (xG%hUaʙ6j*yoDVVlw|>I"ԜE"'\yJEjh?cwXhzͱmE *]q~ϫgFnOfU*IIIakSvO+ &s;8xބ&͟{BI y`uݩːqD 4GSI^qАx"rp27nLMMe˖Gy䥗^!Oy.|TWWD}ޜ֭[z}ߨa&"iձQsvceoli/U* rp3 RC˓; ݺQny[֮wt3;vW.MWy5'"x^8KDL#%t IDATX#i2b5 ; 4yvT9b6ttWj"(,8n3 LydN&((/NNN&}e``… D|ޜ͛ظ~Hh}Q IB&S%/t?!fytgՔJ6tD?0'&p?r=M7?8܇.ж4}sFf"xPLۻ*G,C|vLpbqFFos bhh/|S5U]r40'yŸ؂Z oV)!BfOD#Vǩdan Den{7mڤV+++?/l6ہyZ}̙<ϻ:lkkJKKwe0~ma~&"ׂ{L<wf˰F0 DdLrfYY.0LJ⒨9 yah2>i3gL&N)0>ҥKZb.+-{wu:M^k577O?tee+qׯ[.::zhhr!JeYXp8j5qg4mcp]wۿMvG {YfӦMw-2 NˊYV9װ#H"Vt%n_y+|\nۻ+++y&7͛GDzDD?re"H$<_򗴴m۶J_{۷okZ_}Ueg eΣywqܵYȞGc|qq Wjpp *eR\xmNJ%Z. uid_7&.MM\^zY&S%-VG|yoGF8٬hBBBM&SJJJwwt?C=Б#G<r&%%?u8??{j߿'7o^II MuLhƍO>s=j瞸gzF|뭷矟'h͚5;w,,,<|pTTԆ _z%(7wԔHY`|S00p``RXXXTTc?~'ڰaCQQ~7:Lgg>^{M*رf SX,ӪZ쎺,vءjQI%ܹ6%ߡOoRIow~ɫf]|Y5556}555Dj]gggoo~D544,f:xs[Nߞ12|NO.GGzeZ!c߉o)Af˰/-48j3Wk0[V&S 83 Ukkwݨ~/Ν;W,**rs`RRRppPS]]]]]=3r'b}aBڈ(""}f/tn2fLxGDEE;wt̙^$Ҩ+=β%FffC.n/yyy90ܵ===Ɂ}}}n:95@&99Ct꧘6;GDk,3 \(~W=+j9ӟ[hpJ7tǕ\9Jͱ^Te`YdŒ0w^ngowUUUB 7xbX$mٲe˖-c{#JbeY/CCCǿzvGEj E X5_<2o(-Zt-r)K}el#"B"2,%aqmC $',oJkgOJKDF]ia! Iq SVWiD¹6o\RRVGFFΝ;qFGuuu?XxqNN}裏7u=ɜ8qٳ[\GM8w_ I[qM<϶L&S{hlUxl=r]xguZmll𔺣DDaʎ4{ ILщ\ ]02>$(V*U4)Afvn }Mt#+++((Y>M6@$=;w,--v㸡!Ni\pkĹEV8;9 =δ^,dQ&biN$4Mrr20H煬M{V6Mt&= }}}M6?^zmnB{KKZqmHP dsNG6'Z*#5?ڪF<72 +*<>ҥKyiro޼Yxp8JJJD"=ycohK,ٷo_zz15!B{X'?RzpGFCoBx`]L ,Y`.Z-a"T!irbQQQ{oKK˔]',FGG/_n_xq:pm۶m߾}hhhڵGΉB?mweoʝoI Muu0 fFX=/4$AXі)+ACWT747l\,.Yqhh`0uLMn?Z>88xӦM---Bd7?g}?yAAFٴiSmmmmm-]~)L&N>_#>>>;;{ʰO?ݹss=W\\u}~-Un][{땊ĸ2UeE,  v^ӕ$''gffZֺ:!nܫ5MEEş)k׮]~zu^^^^^dff 3 swytsEV?}-ө讖H1I0xTYND]UIYi+5lj1QYQsVSǃ5lj̙3OQ?77Mz97n1kκ%SNu~picJ >~*zҰ勤g7eִjMմɚ;6Yvy^cI] TH_EiIoVfGn];c/M=d)nY9փ䎒Su 2)55LxG/ mTg>:{5 2{- ub2N>=f7HV=>LɫFc]]nwme&111%%acǎ9*Z]]]61 j[[[^঺YkЦit pZH4fK@@@RRR{{;z[ëgJ2??_.JAD111 \zU$qRͫ^*(4+**($$pT*㺺l6bm6˗9#"X\SScrH$S2G1Xg i=$''I" d2&j#hfȈj%"X0X,w6R"Np>---&&V&fypN0p3xk4dg^'"NjYvsVKDCCC^G p@XX7񍺺 _u C\jso|Zi0لs3ki/QȃrREqA"rp3 RC˓; ݺY +jvuMPn611qhhd2M>K,˨\M;vƼy/4M``zs 7W;z"JM\2(*DxkwXҒK DD<3 sb.KáىNsLl4ۈ7?!q(7 9_~ه=ʲ m[Juc]9ev@FIR XGX;[-4bd"t}cU*@RX{l#ϻ;˟{9w=+us믿^^^vڵ`0PT,+Z&"Fк~uEGG UVV޽/~ vgyFV9skXXkV\\_<ӕ q l޼yڵFҥK{ xn~~\ 2qr -ckp}+ˈno^4~ivĺԸ4y+JanGEE y!j< omEҕQ@8uw1Qwalu݄gTWWD}o"zbccoo_~尰I$z-"~hƍO>s=j瞸g,_m۶Ҷ6"y?~ǬVxy敔<۶m;sLUUUhh}_j,>gϞp:lr-96ۈ~ `6TD7lK SDw%(db7WT*ﯩQ(iii<ϏP(4MMʹ˾sN"b.O;O.4 M_ˊlrL"Ρ Z/ Y: QjD4);Q7$ʠ-[( _i^{5Tc&̴cVlذw𶳳gMJJjh}R /VVVfdd {챲Ǐ?Dg;n&iǎ.\F< *X%#>ͅ%-ӆ%WynLSXPնWf|2qD$kjjl,r:8D5DDfbsԸ̇0pݹu%D^{! <̹,ˆ7779Dc2fk>0B #""gF*l_WDND X,&""J2LQppp h bټ̍a)':'lsȥcoƁ:6"($ WweݫR0;ѡDDj8Pgiii&}HF&ѵgՂϊ㞝/-n8nhhȇk\ӻvގ=8YvjcccDDaʎ &&Uw$0MŚ_]LzIh4dg9Gdd$%"QQQ:9$cg}?yAAFٴiSmmChݺuýs%ɓ۶m۾}ڵk]wuNԘ4jbԦv7b]\g4tEVdD<"Hia  3{NWRRiZ\'{_#8]̈V$.||ޫ k׮]~://///d2M{vgN:EDw߷-ӈp߾} 'ӟo{fs 0 syDtsEV?}-$bBN];x-_M>Q?>} Zf!Fv pXݾ:DY皦Zt೛2kU`V߿cǎH|y^{5=|u\ TH_ED+#.Kli-RCfukpdib~z=Uia}=W 5Mc pp`>;l2fD&a C$R43diXKbfCf?D^-&ɔ37sW(wWki7ϾB9|;-R\r ]i43Wt̰h%-$qads7 1;G/#+_>Yxs E)&͇:0n^t#1D)~vtp+:Wj %.gs{^ODb8888777//&ѣ}mKKˁrssyqoVZ|r0DTgv*>+3X=cb7r>?#/;'A趫!2_c< Ki|FRRǨ$|ssssv{MM͑#GX?l5}}}fk}b"c"h!kbJ2yK!OD777$$$""b}b`c'kx䶮&.uχP1YA| +f)5vOTUUv30LbblGp[s<<\DZnQgh%qmtQLȟf#h8ypmTDqJ~AhzRPԱ6- ""y6O*T):w󯡷2">BN ¯7QF^!7wGOYǖneE I^>e u^Ϥ_|_Yz^P8޽[,/_&iܹ֭cJJJJJJ E\\ڵk?`0^Z&9f{w,˓O>)'&&oܸ100PY8RLJJ:裏}/^vZgZ֤ 6( +>}D,yٳՃR4))i*mYYn˿;LU2Ys*3':es]&檁+kf:~ %ZnN 8ĜrDz+2 ڍѦ+:RDŽ 9 K%փEќh& Ck%(KR-7Z1hnUGDIѫH񫎃+p>31F\a|y G_l6ƒW溶 ._~….]jhh ˗/>|888xٵAAS7l0+W|OzaNuV|tzƾ͛7oذ RT֭3gNiiح[nݺU,`͛7_zD$Hnݺe" 6,Xa߾}w߾*\SSc6=4SzY3hс\\e*>VQO=4d@MD2k%,w5I)&"3,#98ʑ: a\e-^]¨dbUFtf&=sx"7q)(A5pO>3%ѡPzFxTٸ1b)K̵?NV0IP;|WQQQQQ!NII37Ngg3<fZ855_Opp~ۻpu M2/0 K\r=DEE97JM6Q||<:t;""866v۶mnJjZH<8R+th4ϝ;gXd2Yzz9szAuuuzzw-+))1jZؒk0fe<ЍJz`ee<1T fkdMbOvb";D?P<mDۙd0"|&,!,מ alW1 / 1twOöOIE/_^JJʒ%KGwws>#ᑶ 88XD<_(vr>gѠUޔ*Oz+_gzuL(@_D"ersMVTT̝;H~cra|ғ'O8gP/HD"H$r}Bl]Dz,0Ρz755U,>C%%%2D ea71Y G Zw4u"x0aT@ :|vyyVL0Q{͇⤔*uC>4la&3gP&w:0!!!/555/R[[9ѥ---* aJH$LVL(00afnBӔESh4}}}/ ?~… Fe93LΣjkkz#GxyvDLDndlT=(y(=ogTTH(&k)˨$;L먴@o4A+i"73„<XP\\߿j*L!1(wEǴ~ijLa/ S5|weK"+f(I':yo00#LʟQuz6#fd,ŧ:7ቸJssnX*QHn>W OLŶ&%83뗓ne[ g;QsssssH$ Z`ʕ+==}DrJHTQQQVVP(׬Y: 33#11i=ܞ>1|u ZfH.Zp80 nwkX?cpppVb-ñk.BOTUUvD2;uVJIIٵk?L;vv,Q||>~O:22"}r"ialVV3<3gΜZR&$`MMMzz5k?ѥK \|/ -Z~[__?'OD3")))ٽ{wuu6Ϛݭ,X0H}ǎ6??|G}е~͚5?+W^yid2}_6uܹO>lYWW'Ա ߿ڵ+U;vLo:t .]VVhN"p˙v}MkvvO?]\\|ԩɓ'yV[,)ٽ{N4|&\ֳ'aÆ;vԬeDYf >cz~,\fxxWNH*~gfY1=N]ط~;,,l6O={lu6-ې0hmIC5Xgx^bbsJg]E,6+ek\1D XݲM>~hERȇV$5 NX^g?7*2Xnq<\-D̸,z;c[чvLMÉ =(4>>\ED}:" 3{[}ZFD=zstQZ6ہ,Lш>,#V;$fODK%"@nT`QR:,uL~8@0:7NDDѡJ;f0eDt :\=l>.t#> 64j%"f^b(sd6)BhNC"IDYDT6nCцL`n>Lߺh]R1;sMXf^2ov--a*@7ٮvķyI!\&b3W:gi. *??7rEVĔ3DL8sJ`쩻eðh/}Ǽ?Zo{~٪)2q}a:;O; [Nwmϋ8y=9Fxse"gCOtر[ (yᵷ:8޷Y;*62{ 2{ 2{ 2{ 2{ 2{ 2{ 2{ 2{ 2{ 2{ 2{ [y{GHf;ۊ}[f4I&@5?@f[]gp;[<%$D)뺇u [Dͥq>Cf\5aLFmn͍$"&X!Y̪$!"xnCIEp":Zs'M__K HӪjK[h_KH&= bWfGE<%ω [ą)ozt<'D%Y":^kwUCvŤiU&F!ِ+1rc]1;!xev숆×۪奄楄V4_nk _"bI*f^llO!xrjs ad)e{CӲ.%($#6GM'eCf;Ŀx ”-=Zg*+Z"5dEjHcHewMi*񬤲UK•YA2"x~ HD".\hNxf|d՛hqD*U3#>YoNyo@G(L%}நo, [ھ#Wm~!<ёʞ W⃬vdk^L:@,bTШsp{F(e"edb6:(`nL๫W"[ahW0 ٟCDcLT2[fQs9}udfVHVRa. dnݡYd+u%Ա["̟Gl[T)2h,}۠dڇ3f%F>oCy UřTş\,!"ups(=ך59WQ?Xf-_Q?+Jr' Ŷ2zb Gp{dc.FDs"+3Duf>)'M 9m=IƆ ;_秤ڧ=˪IY; BE<+H rE-6/ѕQ?qx MJ =IDAT<<=XdzlI(("Džan57Ŕ4Yk+2%nq -$v3) m $)%ꞱjhIC6k5$՗W( ҕ.W=g[ɡBhuq_(4١ߝobkwӸ@(ƾ4I{\ +4Jc"uWNQ)N{:O? 59D[5톝*d3k&>z$ŖNgtUB bsjoX]Ot/5K@H^6?M=ΐczvQs>G|0׫-GČ(4V%7kުNV mc3{AmLtTkls 8Ethnw$3nו.#ڰbq6K;XdtGX\bhn7TI}J0]NQVPġb%۔^|JCsn`%Y7-Zf5[1=;kXzh8MXt,/~zMBKS?+vߑRˑKF|,Taݛk m}x,/~-Z|LGK,GK,Yc|K3S)? 1ry"6=їj´!ˣdM=k F NI"U4I(~wNwK3 O/WZ$XBpIL^)@F? B!e\]Xճ&ϵM.:D[s_;a.~^/B#B!B3{B!6B! ֻξ~B!V :YKi9IENDB`pudb-2024.1.3/doc/images/pudb-screenshot-light.png000066400000000000000000002205541470451231400216400ustar00rootroot00000000000000PNG  IHDRO pHYs+ IDATxyXw$@d (* /׫]l*նjoSvz*jkJ +kXB:cc >}9g337sN,B!Bt!B!D!B>}B! LB!Iiiibb5kv޽gX\YYd2^x-[v}᜜+++77V8pi$;Љ'&%%8q200`P?iӦ}2ܹs}3tf{lܸq۶mǏ/++gXƋ,^Ν;]QVVҥKw޽{P3~lhNt,m{{r<$$EY]ئ2?e%'E&K֝6Y{?S{뭷lgggd?MB/t:ݭ[oݺrJ 0aBssS[6l0U**;;Ǐ7^۹s~Ǩ'VTT9rwX8tWTTTUU}&2BPlٲ%--vt͕u#FL:?H$FhA-]֭[½{r͛7{:d)~ņAb}Ĉ7n|rRqqq~͛UGaw^??e˖QovhccsQ+VPlllmV^^bZ)#ayyyuuu6?;Pomd2@ [A?+GNjDt <)R:Nj 'J}Ν; ɓ'׷y׷J{$YVVFdEEOrK1gΜjĉl%K[NNNvL4-$$;I"IA ^l=Qm.zT*XxqppEUyyyVZjoRPSSjD"00÷r FѨj6_uuhnn6j) 0|rtB{innd>36mJ111ͣ./ILKKxsy]Jr_-Tw`&-YߎT(ϟ9s)S: 'ǰO#JII2d$ŋz7.11100pٲehF(#k۷k4p3O ҿ)^;W\[FJzhrycc/\0a„&Z}mj[nݸqeK.\8y?YYYr<;;wgy&&&ٳ6m=z411W^5jTvvUHp{__vw]dٳ/\x+^ Ƃ ^@}}]Ǝb.]Ju9rҥK.]Cb={ڵ+ >>~ʔ)yyy Zx+e~'MTXXت]+V<8S9rŋ`ݻ_?~|ffڵkuӦM˖-m9P?)ݤ#f⒓,//OфwO.66mJKK۸q#u >ɓ~mgE 322|㫌_Qdsr8]vuLaf 'IR&tw&&& `jgdddddP 2iҤ.ճ{EtNG-J|˗LrK\ߣNLqD~zooV&LPSS ] cǎ577[YY=z4,,L_$mذ͛ ˥~flܸۭ~p6q߾}T^)# >|### ٳNennnɄ `ꭿX,nhhσf2Si2y#ocа]]6K]vm/_niiSL7oj֭9rԩS5#'wkrddc4 5Znn;'M:|r Ȱ;v;_~~nsՐ!C_Qq0nڥ3b3Po ѯtwvv~ ;`С?j[7PStyÆ 3f~͛7{=ZG999JM+eA˗/H$ǩMvRTn/QQQQQQo?h AAAGuuGeeeddL&VQTVVvlTUU{yyV骫׮6Kuخy_~eII믿}OBaiii}}=]p6a$iɏ|SO=_B)/+7'u}j1^J$988㏡ëRTJ_~P__OKJJƌ3n80<)kځI>t .ufGF>ٌCފ#F8::ZYY4/4 e{_Q}I$QFW'''wƴ^P/ K,hN}A[[@oܸanݺ+W>|3&<c&A$Hnݺ삂ÇرcѢE111Q |ڻ*;IIIG8qbHH)--ݱc~BJeZZZW{$r<oHUADDD.X ''zРAqdW۳m ֈk{{WQ.ŋ/^`V:`2 .U= `0h4@ 0;Y},vٳgРA%%%iiicǎկhAVWW߽{LM]d2ZmbbƍRFJСC7Q"TO,vu+*###&&&>>z0A˗/?~; k08PMc:HMMy nu5xC0 ^&]zuРA*˞/W saڿ)ޠYw 999?܌$?Ʀpdee@U?w@ xZA8;;g6-βڲe c}wR=-vRRҡCt:}Ȑ!Zmx6#ɏ|KKʕ+WM*4lW{RmKZ6rC:444>>dݥ͓Mʲ6ń"#appՁ***Zj?P\\_rԩ5kr+ ø|iÜSBAwcbbQFݾ}'TuuuVVB*..~wmUjڵk'N έ[Bٳg R4iO?z򔔔*e7%55l֬YfxL+E9r3 >u5FH޼yUddٳ# XLmv[YY?|||#J_}nnvi4חC(>sԌ(>>>T٧zJ.}B! LB!0G!B'ߩK$OOOjZ}=[[[svB!ӔY+TUUB뭷iQQ{'CBB:ŋܹ𪬬K޽{---ГuR%;w̔d/~Nwԩ4N@#BȒ<== ifݓ7o44hҥK`ݺu׆ГìDܹs|>4 LX,V(\.Z/_Y>B! yzz&''k47Rggg+++3?~H$8p?`~m=9JƌCe JSSSgΜOFD!z-///Fs.7̯FuB;99|2Ţs766:u*,, |B7ROOϦ&D2ydjILKKxs̡nG}d2yM6IҘywQL6mܹ}?w^TꫯvfX,>~xYYu@@ܹs}ݻ̙3UUU'44^ iiiIIIAAA˖-3 "+Y`BQVVv'X,8V3fZ!P/`mmMuӧOw_7>>~񙙙k׮5,V<8S9rŋWK2V2^իW333~I&~$IѣίʨQWZEڳgϮ]Ljeryvv\.7!ԛuG|0s TUUd2On!B~<.5W߱SL7oj֭ԯpڵ;88^|v7^J㏩.Bp֭o0`ȏ=;PoE"ц n޼陕5lذ PbqCC> &x>>uuu)))nnnݥ͚5 'A}Oe2999</((hԩx+<oƍP!0ǎaXpM:pF9k֬êN8!ϟ?gϞ=R4((W_066>yf++ٳgS^x&y?&$$dΜ9 Fyu^aFOB!2}zXrB=^fΜުݻw?HBwB>#d2}B!P>B!B}G!\J!]>B!B}&!BA#B!aB!Pd`ܼtDNf`͚56~7c#I񶃟 փa$%%mڴq„ \.Wロ7o^ňB u {*Y==k1z☕藖ܹ;**>33S&SOT*.]xƋ 99_״ioܸi&DeTo#BƵc[3xS.,=BOsh4`2bXPp\???-O:% eڵQQQ۶m 5kH$>?cƌiӦ=4ND"hhhhYCCÝ;wBBBف% IDAT2WF"Iڵk2l7o t/6-z0ZBOO/;nC$o`H=̺d,$IPql5AcچV9_, ]=)V|0y:G(*ې!o?;۷#  RՉoE?`{UeX|(.A"_s:[ݞ\X,GG//׿p9*WT#GMMM%6lpWBZ>K_4kt_f<x\fgEL^CFmdY6lP,0B(++;x`NNĉYyyyvH~IK+/ʉ$_,;N|@LыF/ހ(ыF/w6Kns_RQj,wp3e[ڊ$;;;Wm۶%&&;ܿbꫜ۷@eeիΝY>B&En̯ khQkɊFZKP\_ ,sg̙3kkkʦNj =ɿN+iVTBLk?joKM@!'1h`7Jj\%tZ;ЎPi,;o Y_]A[ @ubcc8@DXX| Qw`|{ HR#rU6Q0m]@$a mP- uЭ RE=_KBkkd7770a1|p}իWϚ5ٳ֭ׯ9-B!7rIUZl"vWT67iՍcŊ4 $......aaa޻w~mAAPKӹ֭: 64mVc}G{p:H- Ϙ1ƍv;v@k,D"ѿ?8<<|Μ9] !PE#ܪБki^327>I,;Zlllfffqq#%bڴ|TG&mnڗi.\xUXԥ$IAtWBR6~VW~{ܽ{:\:ٳ%j~ oX1|(?:TAANG44$Nj:+o2X,?쳮f_}հarrr#Bt3g-,ezORSSKJJJY$??tYi|/|WV_o٩˩7˩ ;(j}1͟?WXp~g}O?B%%e7k3135*P}1:ؘ!|||3f<~6kN\)^k5^Ne;xܖ{E7d\Ece29\X%7p",((pttF cu9Ҙ\P!πAv:UsӍ,$ygo0FZV& Q"Jׯ__`BXl$WT˗/qvv>`Μ98>B!#۲Nlno38i"})Bt !FbUJG BQ<0!B^}B! LB!0G!BD!B>}B! LB!0G!BD!B>aftDNfU/^LOO'Of0B! gUK;XzByܹ;**>33S&p֭wذa555OVO?F!P/jisғY\^OǂͬDܹs|>4 LX,V(\.777j޼y*RYPP>B!ʛg[T_sמ'Y~\\ܘ1cTD"444pfkkk*PҢjt#B7xssܝӱ D3k0˗Y,xxxTVVVTTV-((pss,!Ut$y6Z~ If~ɝ#s;Yk? : ̺P(<3qDÇߺukYY^UU5eB! DHA}vZԺJWhosE%+j-s"߿???fΜN-dXjbbuh4ڜ9shB!dY-B.a7"ohq*Ť=$BHccc8@DXXʕ+UUU/^5B^(EpZz"ȆS!Lf;...aaas̉ڻwoCC\vm߾}K,7o^QQQJJB!q4?'^\-bzt8!˰qcccZmqq1M> AM8̙3B!,{#tgRP8j$+++}}} ~g>$ !Bע]1rB}>FKMMMKK/)))ggg x<^UU~}*B!ťrFD qUڊF剒ڂ2YO2Yc;F===322D" >\,lllD8.B!ԻHd`7N tWW ܬU4)MJ3'A1bqLfNNNaa! :u*4ܱcR \~B!z Icp9L g^ayG?~*f=D1ChB!cK,*[(#A=Jx!B>}B! LB!0G!BD!B>}B! LB!0G!BD!B>aftDNfUOVL8N0B!gUK;XzB藖ܹ;**>33S&pݻwFEEeddhiӦY(lBR?})=t,=Jϝ;ϟOdbBrO:%ϟOq̙qq\B^W,8;nҦ#t8=pB*H H,BCCZݻw͋!B]̬sW2x=BO:;99|2Ųh4*潍 477;BYVR{6Uȡht5~NVN;ܩk),Yl:Skw sBə6m"INj6ZH4w!>g+J}_-UHن^CZKV4*ZI'z(RPY ߿~~>̜93<< eeemܹcB!dq"Z$iR%E<q*Y>B $~~~ ",, r񌌌G#BYȆ}I(`$TIU86G=.,軸>|x޽^^^qqqGIII!bȐ!.\0_!,äUJV\1GO@Y\\sɓ'٩T*LBٚ])UJ͠1{:">N;{lEE~ j nmmmSSwwwxL@!z [6*4R5 {#7hԴ4pvv[gdܼy!B=ŤתJ+'Jj d=Bdfu݉>vNlllD>>>0jԨ={lٲ% !##Ba#Bbh8 Z4jfImR*8B8&SXX񂂂NJ=(7""$CqI&Y(fBY ^CEN۲] !dBt !FbUJEBsjPwk7!B#BϨ) [A=0G!T6Y1YtL ;!z)5f5>' >ƒB!͠Mu(BwB!0G!BD!B>džT*ݻwo֨ZdOǂ2~zzN>o߾A 6L"dee-X"kɦ뙶!4}lOs@.ܮ˻S?APR_h5О}A@ӽk\.wNNNnd2׮]+J#F= B!=UK۾RzX ojj5; %%E"88t-"m i[dږPJk"n$JMRMlΝ.\عsgg| A?={tkx!P+fq6Ae>YYYZ6((H$ɚ___H$=:U39YՙR-j4x!#MEhI&u믿VVVM!Bmx_{AIg}Ju̙ {{{BVhX,~ݻw@T$uvod:{`õ'eJ:q=O@$I괠jM@υ-jfhptj6Xt;޸'+cMqH)(oVi]m'87*?]v+l8C܅G *vصkxRSS[߾}|0zm۶?D!˵ԿFVLr=9Sl\-"B$& "66 t:ݑ#GN:ՙڤGǸC܆6xʉs;x8FZ"V5L(OT#6_8Xc|_?ehTߤЊȹUw":̣_C:Vm%~ԃyF QY"jϞ {ۧ]bЈ?Ԣ#UIWVRlimCzvN<F ]lQ`r~~>DDD< ?sqqhonZܻwoccÇlmm%B}^@#oʛT;_pE%+j-ɤ=#Bf%Z633յD_.o߾֭[љV(vvz[SpJkE[SUYv:vȳ@cڬЌ!ZT;:'Cݨv|֡FȆK-)W,bݭ4:jIvӁ?]ZTY/We-&:StqqT2E:_d*H_F)HHRkpם߻w/&&|B$xfiYna>;ЎPi,z544Ij|'_dVnC9uue7 ,+u<ŭptf8IB\,ig@\OYVLӈ f5Auѿ<v;FSkIMJ5i>^yyy ,ptt ES2.AcA#*R6\Eg;3dt 2|rVWIA"F3\K#_wUIPPP7oΞ=Bu[_?B0}} | Bj !ڵk^ <- <;EE~AKU*s< w6_Y}jD`g_e%ҕEwMSӱ /U+o )ݟ{4v:;;9rĄ=zTӍ1ÇGFF.[̄:B= .́vjsF剒ڂN=!{X_M+#Fؿ/(H|||:|,7ž? QחT>Hm'לR70J"kvb;xwG=m?{;\ɾY*roq4.N} '.WqYwL /lܸ޽{]0`aZ-߼yիW^;!JUZ5EЁb1 YhRj g>Nc}*<{C|~xx)S:,e0I"jb66tFujk$HJcrynCm<f ٥ +L gAjJ(R,-&iw FطhwH D^͛7/[~#:įz7>\駟>B}ߖЬ=fVQ/cǎ7x7XjUg?uԳ>k׮ !P'[ty %G_裏6l/67w0Tয়~zg e˖GB!0<,]tZy[ZZ^ZY>!B=B!aLxG!B>}B! LB!0G!BD!B>}B! $uuu+VXbEEEE |Eba$Yݸb(3*['z 7 7 wiJթH4 7u,>K25Bՙ3UA5k:С]GiHq{U mZSeډz-k+k&M&m!Bǝeto+={$%%iZhJ4Y#?h?qGҒKsN*U_*? `5035oVsd_Z;ִ~`rmYYw|1FB`_ESSSnn. vrʌ3RiFF{t?UtW7)6p~A}˓,_ t>Ug2S IDAT2FtD5׮7,y3u _o1| -\q=",pG?++K.2eo=|pFL&d4wI@׻-5֯_Oaad&0nN֛#~)/3yլYjz:0BB}J:sLPPr;;;3+nTΤ0s*g˩wLR7zwRk++z_m q꽭 ?_[Hz5.~2#nLfV/}լY!C6b߶wZۋՒʝ1~Oj5glj"> h<A ([u #BB}YVruu}} W*t:ŋk̀hޚ7VR1z6̙:4`R;Ou1TՕ5b9]_.5lVRLFC9S&[@mռ}V"DN060۞l~MESo14[Xa7Lwr&|vmn="3>\:>dsݯ; yRu4ތDh]Ԓ(ĉqdWGO57oأFq1Uz g$i)x(h5RJ:ҢJi4gy6R&LhmB߾`jW)/h~-u"J}YKESۧطz &&t[8&U _޺mה`x"f- xQT9/ {9hiMn7BN=~FiguSZ$6~UNW_3Б^#E h/NќIQu {hjii6JeS_,[w tS!$tGGG\V@.sV3<9qqW_:?n>R M?l0ܘDdQniիf͖Dbf ƙ>3wMi! '7dj_Թ zb˦32TAwv,y[ !V/R]FPiN&_ϛ g$D<Ԓ*f54Pv#Bã υdKKoҜlVљM!Pdzh !!A0!!7pww@h!{T4M(2y6ޫWY4ܘ9}CЌ~c@#GT2?IWN#`&3vSl$FsOA8ɾ=aqXyKfg  NuEg7#!PezOoϟ?`GGGKcr&Nl^zr &tPnO:h4Rs]͕qc 4g_,w 4ٱ1ϘMojjz| M&L`0\v)v0.vO[Y:VD*'yNYiG&LؼmHy8g8趟"vNѣwV`=!L BZ}wtug-b#G*ܩiy⢹vz'twwUNwMU{5tYZֲ"|0#2#322uaPEq B)ZBNtKDž&My|j瞜<9\ ^m1jzM _Zd!B&=9eFFj9BDΝd+O,oS+Wq]qw^F2~Hƌa<''oj5a[o>>-_}r=D$n/=t}a_VZ*2PUܴIԯw|+DDzt#^?˗6 r}}h)>Xӫɟ|ŗ[6mmcnOtyKs4CޞrmBJwYmX:wn ]&NT}-#:'j|i)(8Iwid8ok忛//^Xps!*KL[g?'??nz5e4-5's\SjN83<_{sJZ7\׿ѡ {+#Z}; ;cr+-_}ղy Ϛ?3}naD+֭$cx|m(оSv{IF hٲ~~|-d]gS.::]+u}"b=,iz 9\NU}do.dvDeֽڼ<رz=Foh.umhq:KWPP;?nÁ,ekd2u0Hs0 Yc!;z~aww|HNv{Jw%p3.jYY|* !腐BHz!$s}\lٲe˖o<~5kVX;ٳG9-P}2,PQavt]ڬͫV9BN. W~_=񥥦_~ ם?ņ Jnb:֨(3uݩS|(?A2i"i%"5v~H<|φ D?Q+?X2iwΝo~.v`\\|6l _X:kD$LHQ~y+; O?1b5k\ UO? v7Dd0j5)d7zD՗?H7keg^VYsψD6Ԋj6WvPo0222bbbBCC$<yz}"d\NF ;mˉa}mSzx4**L3K{8ע3RwHP}{G w544ZAV?YH[N'ɸ:J4 ѨVKKY>ٰj5W_gܵkcQQP"bb߯{/]4pi$27Ү8Cyx8SxHOٞ08+}9OGD+%DΜxukVoR]H$PCEeADFaLlZ!>ܔ7лlad="7._?sƖ мvg<`!8t/===00P&)J~|R)JxZZZ>'|2>>9: D;{ʂsMZU;wo`~(n3VwIGu:2EI#Ba|d8MJ tGȭ?\|e,- D$ 8޷O{4C㦕Wλ #5\.65[u d2 o1ѣ5kǏGݺE2n\5'K~{@e/.\hz`@"******={vH,3TUU*6(P=vL:_Բkt^o]_hW'bD"̛v< FjhnVRc˓ bYRR}o$>>qcoN,Ι#3eox%Iʄ뫹wB7qLQ#%#<ɦcP=./O0qf|||-...DTPP6j(dYQ` ⚚LkkVzzfeuC|,+ fv17_lR=q´V&W 0L7O~XV>pֻz߾ʰp"jzM 7JRSHjkІhc%%*AD02R߉k)BTݻvl='"O?n>jqIMm\a@Ukq{xxqqq=fք (F[A "ˈFڱ_66W|,jX򗺇Ι)?D,NJDDܴR!*M~fjt{]¸XB! ebATNߔv#_|юtg ؀վ}%ƙhfK}\.yv˔*Κ HS-_}ղ2?kxx(qt:_keL%Ee1oq60$'z[ܸ4'=-X㚚T;wǹ̜J$??nz5e4-%%"HZv~ھ b9_d_FX//_ED>u7{>cs7ǔ̺W';Y>@ \{AC3<`&8pRQFv+#[ɡuK=`ŦtF*ucHp2{.S^}K<4Y{d˶mnXx- ~䓤*Yc'K;@a> sz!$}^>@/DrN/˗-[l2yyyk׮]bʕ+wޭhҖU1˖&#*&V^z $e:m6sn۶)m9 Q%up ENN|O=]$8p@RIR[tF H+ppLh155)&Ǜs& -jͷ<X,s9bBt"+**%%RJU^^ߞ8qbԩbL$b7y"!###&&&44|ӧhÆ swCԯ_pUNjzyY!%%%!!ܹs{a&99ّF%遁2LTt:"R*RtL&d]bbb5Xe}^_]]}ڵGrVVVv甔p9B[E_ ,\2777//o?~<""_U\\\]Da8b-yGDԧOB[ucǎ駟Atttcc#GͿ@D?55U$8qٳnnn3f0ݤ ]Ё,ekd2pB !腐BHz!$}^>@/D:\zj"ZrEEsE?==@qBP(rrrX@qB i0;U]D_fee%&&^]D?;;[RtC`Ȉ C~^^^CCCjjj9觧d2RHTiZ) v&z~m?2RQxx}}8`([#do4/^hz`0Dbѐ3ܙv0L\\eqq+EZ^>@/d};m IDAT-`> Wz!$}^>@/DB !腐BHz!x-r<--z1~A[3| n[ZZW`C]{pqLUZèh_+o8+TEyzpXsKmZuF9>ߞrKtB1Kae>Ŗ 2N%==8] 0uTKX씶+0Dzxo!|'y+@0ZtczHD~wS*= F2U)fU)6=1)x\lV 1psBP(rrrZeD斔xpkXF2MFF식m@O$"zn$BvR"/0WSWlM;T+MrHcv9!11 YFȲDt_mYn+M@D);2(_oWSj<u^jYYYOt"s_ى9v)7.|*Cf<A*}.5:l'+9UqU&$B">kDϊ}ݲ._v'BCjTo.-Wx]57He 8o n*V/[-KZi˖m_HD~~EDKu:7!޿ö˚UOTܙS猫%ҍ.'7tM%"(ke l^7N+rպC3Ft<ΡD`0dddĄMJeppkkk8yiΉbbP~%""' xijrHu m^>p=_w9xrmKkT4^<*%9@D]zXѠy|[>.o|Qߟ/lwCeUM>6fDűq'*×'M\42ϕ: " 4*w\*WCk[2ZwÃ4AI i뭸Imö~-XJ[ohlhqFj3)_8%#ՆH/QRi 03\ѻKGTG +fGYJoXv'\Wf^B">zoX;KT*}ꩧtÇO>յ㬷'wV54kIC¼yg4UO 49:w QIM~k ,3KTެpzR]"ۚ`JD *S7 o'-Wu=)l=U٨&" , KDsjOk~JNoj)fH?."$|u5aT/g4f^ǖ y[~Af}s󗮾X٠ rҐOmvtoq¼^o5B^ϡD?===00P&)JNGDJR*,;|gDԷoC;!임[ױQC#5Υbt[sk=Io0rFޮ"9=gג-1?Dg0{ZK lh{^i g%{OWU5M9Ne7+:-"v5gR[j= 3CrBh߼&n|MKD7GD( "pC史ސOmaCD"{30B^D_WWWQZZicZZڳ>knnnDVg9#"جַh DԢ5(5z7|:l!/pe%®"qG[:V*r1aHoqO… M/sss,XHDKNNKH*)OY/;{NU9Uo\#UwH,` 6 HWXѾޮ"!h."_"FwHme~aFkVaJ_4(]"`K*]йa;S5#ߦ]G]G,28?u~v-Zi˾୘dyQȅqqYvٶg,^YXضOҖK1ˍ g&ΌŅ>q̙3 ,,9Q; t$oxto)ܸԒ/Uo!gre0_=ˉ(ϕB|\JMFՆ#d&ٞ?^#_EG/r*eߐ~3dKb$8l/wyJ.׶//}yYuZz"Bmu#}%/JJVS_:ۖk+KuB|_Vpz.dvnذ>8p`UUUffq=A4G*J;AI +d5͚GCCaɎWZ(Ke=oɛt2z+GGp7cKZfW&y|tߟ_%oڀ ynkviR*?oɊ+u-WZIvBu) шHݧ*<\2oi~Yc^i oj g 6iER`']_q>D%%ũ17,hՉ5a= o+|T*?pqJK_11JHeŞ'O:z:wDa[ C) ?T5־^X + +o   ܔqE7<{c<m53M]9_ҩ% 69~V<ɉ෵VO8`([#ԅ{{3 G-b0bq_)NM+o:R Aa^RQw=@7+*,spg}^>@/DB !?]9W-* 0DD_./[lٲet4\SdoD[0ù% 2k}֛?Z*5poޔqSE]pQM[k)[Cۥ.ɇuLyoSdZ9aG?:\lc&]qkZvj ܛ?N0cO3Uz`G9~zz:ǵL'qo'|b4خEkȾ\?o;R\lV XkJF_TpbBCtɊ*%_O\z>'<WPl aϟ9rdjj*0yNRAJ* 32׹uh'ٮ+;Dzo,恁D_fee%&&ooll49U)E;Ԭ7L^pŌnY/l;I!|Qۖ2pÙ*7`|Zgxf~ޮ-/ztd؟&ǚ¶v䔭ܡe$zSZ݃R֏ۼʯի|w=?146 Q=k}֘X3M AsV+J~w^"k_!ڤsQEGD,x, &0,y;J] o?m@齘AqOs4VT)))sss ĉu:@ h;{־كdŵʍG.q|T35իyF3LWKt&ɸlw' $@7"R;V,79Ku.eg?`6e|rʼGGy\kl;^z? ךÅ5Mj)1.oڝӶ5' ?9AD٥ 1D^x>CjTo.-WxX2Kwǟhxh߻b|r}h^䘈ze#wPY\DYI]˿]\ODorݡK}E|[Kd5\n]Sm||zHD~w՞6V]1~J})=,ӕoPjgNYez2.?pX?Sm裟lTO0B},EVG/ p1owGykkzQ)RM["%!###&&&44U%X_VWWDٳgKRǢu@@w-G:_w{'GxwxJkF03\ѻK8#EMw3AaRU,|ef_>"z Պ ' xijېp'+@*jRi_7v|-:|\ʹᾮ_Ń#–L~]"&MЍ0 *v/}jJk-!i=2.^s"_ l rmKI,h\4o뛓~xec(OO WID9vHC}DįebldC¼WK_? lH[=0/5aޜX٠v~f?# "zK k5|ѐ07f7eD7E#6-mxBmJ?EX*W7]AED&F5t| 0_`/" =2xJOJ 26:ڗ >Q>WXf FyCZ;]ָYkoS+T)%ua7OszR% ., μ涣wP׈(w[*3"g`u4zy0:v1OIPjھјyI$]OOO dJRRJ,*Gy\\\[[[j*H"H"ZҢ nf3!kp= c#|FxOh>AߺWZti[7_K>w F"2Kj}3[+MlcT&VT|mJg-Qbk3BH3D$X:9[?ev$V9nVF/C1oK!#jGE66tnD_WWWQZZicZZڳ>hbcoާOD555=$9#"جַh DԢ5(5zӭ.WkC5/o.]+3Hc˱ k=\"}l/=8A[k^ڑS6/9ddXpճz庩-ZH*ỼB;;kp;C4>ouKґj}\EB-؟  ^-X 00eYX_9/;{NU9Uo\#Uw,aYeÍK K5qq~֋4?(5"rq)T,` 6 HWXѾ]4RT36OȶNp- a"&THc zE)[1y"dF2m"yml̟y΋ew%ɫX8?~#vS&{=?eY*]~YӠ0/wzVXV°4zc}9-'$:Y^T8rv\\;ca[\\\(**@;;wj-prȆGxS7<:dJ;7C)\{}"/:aQ^i)i8V\GD!>R"Z{] Y?g+o~}:\X3C;DTPtYSo-b&Z7C5:|H5pf7 +Ma+Qe0_7EQy^8Ba\+Y%rU t4'nذ?2dHMMͯ:h _ߞ~ VډHrGY7g/WўSf"]%oZbUj@ w" +d5͚GLvRr噔hw~)q }G[ f&x%x2D#"}vpʼeymWz}n^(3CCaɎWZ(Kܙ=>xMSRQ#DD ?tRvuW=7j>Dt^EDX"oeBK%VNf%+l7GGp7cKZf{`)xXX`Ζ1َ"s"̘X ]DóvU~ddO>{?~]VN#c IDATĘf~o~%gM+Y)5@)J}nx$/%?W}W&I1?OXP4<򚇋jGE7MRnɾzw05q^'}EJł.N~3%|o|qE7, S*9 /8m@PezgnSZjGG5({T4&x$^ݢYqaקpLz}2%{Fuv1Z''1wtց,ek:^AnSwգ3,#Us{CjYfll;Z)nDch U6PPnq̋u,ÌNTOYJ[qu!=ᢚ>k=Vlw89e~?ncho¼<z;P{- Qݢo#H/߽OM!*WjU40;X:厞p'};=GB !腐;*EwG@D߉=YΎIkիWʕ+-Vo۳>¹O}~.YvP,b9~zz:q,{O՚^9snnѳYfxZ C½];"s9!W(999Y>%$$yyy!!!=jΌ!^o+L =HV\xGךÅ5MjK> zW5wA6q芾`Ȉ O]]]yyib&  P~CýhhX?T:/9m V71@g8tE?//a޼yV9}40III4Ӥ H`e7P6ܲxZq~zzz``L&S*:J%q燇8F\Dxtj"JKK3mLKK3*VMMMee`Ns=GD:YoEkPjn',9Z$4+؟  ^-X 00д/śKhϩ=*KvxHph {;%O3,..nN>i]䐱qK>34aU7p)2B=\X_EmZ|Ҍu88Qت WUU]vG-o#M Irw|y$ڔ:Yq&m׹!aNf]>}e^oJ@w0o@5Wڰp\$-2:uԩSo5f=)9%+9ϭ ihŌA"p[571:͑vxʹ D\&,[h.J >t;:wGGww3@/DB !腐BHz!,)W^MD+W4m}[ s333 CbbƖOOOSV @MZmVVVbbJ"2 aaa&**%%ѣG۷o>}mJ CFFFLLLhhh"X?A [̙3=XTT#mJRSS--**ӧhfHxba(==|饗:5[ J/.\hz`@XUU5i$>'~۷>-`0L\\eqq77jS)Φdv=۷ojll \W%&MD'NϗJ1cAХn ΄K}^>@/DB !腐BH555ڵ*z2$НΜ93jԨ]Qƍ'MqƮ'sN/˗-[l2999֭{WVZo>hf}Vҝn>ȑ#sJ[| ҐsQkO@㸧~Z*~7FBs=#Z}ٟ砠~{̙_~?aH$zϟ/ݲvBǫP(999, G ~ꩧ!"a&O,JoщԕAsFΠn6d#|ɘvw Nm۶'On۶E<{UUU4ƍÆ ۱c200W^pB\\""bOOO_|_}9~ff`HLL4XSSgD4h PZZxsi[X;1 4n#;Ajj_wd_~Mf1--ȑ# .lh,..8qiˤIŋVL[<==-ZtС8D_fee%&&6zVk~ˋZZZlΉ8ҠQ934 Fi~)r]9TTʪ恵 ,yd4ZiH>|ʔ)}G>>>999#ܵk +V0/JJJz?裾}=ifϞ/sF5k^|EݱV˗/ }g{oo'N}ل d2YRRҢE#lhh8v*ak4Fc>NS^2kt?xXS[p[,QbqjX,~w͛7?cUUU+W|LJ f=ѣGDO-=|0mڴܹs^߼y-'h֮];Z*55uԩUx_|lW\o~>hԩӧO?{omjeƍK.,XPRRaÆ/<-Ç%VzkGu(22///]v566~ v39 y04W:PWGΠ#"V(拚Ib&)zs+E^mmqBw~ Nm9s9s+bka2lD3ϴ-ussg3ojjqeee)J":wܢEV\<$$J[ȑ#oߛ6m6migyӃ&"~lDEEum+OWr<88q\iiiJJh̘1߿"|2?Ɩ%'LPXXgϞŋXdj>{'Nx: @@LT*:JR*,Z__o߾~a!C3{>gâewY ƊAFћ`5 ׆ %*р(( bATma~L2n('93ޝ9猀' $ ^B"\6^B}Z^~}hhmP(?~{SNp|,^ER rE{jooČxѣS*\{{h[};жmm۶-]TOOD"|KI%ӝ'!|%a!T͛###-ZNEW@@@Æ 6բs4eʔX㄄UꁷJ)t?'H ޻w .@QTyyyyyyz;d2Y{(C /l!"Y\٥]v9::otL={bu1!DPH$6V":GԈ#BÇT*JSyݷbD[`2^;c eee{i`lp諑>_F{W*!ȊҴqFYY3g̞=;,,^8{P(p8Qo޼YD"QWW733/u^ejjJLLd0999خ/K&xիWB&&&bD[HMMNNNZNJJ_f|)z͸rqqp‰'xƍaÆu::3`^{~h^sU+;ݐyҥ*++O[nݿ_|%%%  eԩc' ݻw\燅zzz>~xՊ+,Y2m4Çۿ{ȑCJ׿իWϙ3Guss}+ wqGGFGG;Hz+ѷ YYY4޾;P?g_ĖsOd卝mt5vZ;;!iӦKh̙݃50x|AK.a+n;uҥK.]?^T7oޜ={sz[/#%7|`|ի4M___7oMqQQQf=z'z}Vo  }nnn\\ܒ%K$oa= ڴi7r芊 55snذAF>=08A p08} H `D "  ; | Eaa5kZZZ;DFFN8122IJJJDKTUU׭[m߹sΝ;555 |-s!Ww74_)YywDeeenٲcaϟ?>}fZZ^Xݻw^aDDD((([L& @.yR(lΝ;cMMMvvv}}… <$a=ǬkxUHVkxO>mjjza?H.^nذ!>>^4{b {innްayD/>N֭[zzzmH?F]]]\">ctPgY [[[w =ٳ?{,#\gϞWl {~M.\/ +cBamm)^bffN*"V ;H$jNÇ JKKBl6G8L>U( |>npxmKqf[ߜY\viCݝ8sGUֻ6uy}i$騸`?/immسg Z|yDDkkk%)SL6-++`Æ !$ gccinnj*|vx<ަMx⤤$ĸXYYFؘ3k֬a?{ñ4ioPDDr^^~L]]0V0fΜ):`vNMMΝ;TE{@ HNNy}*.uv!w"]{6Z̍ns֤B(Eޏ]4Kڐ133355MNNJ\pG8vXЏ?akBǏqhh(Njl嗼;w{xxڵkΝ~U:/СC=nݺ3f,\͛7G}Iff&@@aS$ (ʮ]f̘PUUcǎyJ߸:'$$455%%%5662 ;@"md2===kkkӉDٳEk_~=nܸ,)rA6!ȌѳBD><ˠhNXhJ~6UN!vȫjc{.uq}JII1446ltttMZxt:>\SSwfEE./޽{sqq}6D=~8((hǎ1C ׉'~W'O`Ǐ2eJLL kbbx5 !x1442Z)O>=,,;$$BjjjB˓H$ 722pN```YY+dCwh4Z`` ڰ{nѢEƍ6/UÔ=@#Riié&!$ZiޡX"͕xB@њn`//oocOKKv~o߾Dccc===JgΜŲ|U__ӗ@ (--ussë @Ç [իW!I>Hoǎ,kܹ.\ SQQʝK ׿-+((`"jԩ]$}Pߕ(++322B :4 `444@P N*BH}?D FKK+%%e„ 999 'ñ^,SNq+ IDATp|,^pp^8N%Yׯ :fB.z]*0`KXƖ 6モvtРiO]gJJݻmy"7oh"www:uV); 06z6f!񀫺z-{ ^%UjccbCi4D|lAf2"I%!D$K򊎎.((HMM2eJv?:{/>d(B!HGXQ#F@ 'ܵkcNNN\\ܼyzY&+??i T)`yykA$IWW@ >?}d ǠF (hW> XBdEmiZ?~}^x|B#,HDnff&^r-455c% #''544JNNPxƍeeeϜ93{찰zZyycǎM0S#IMMNNNʓ׬Y `0ꎾѣGcbb]\\NNN/^1bDMMMvv)$R0s=]}m?Mw4YM |ɝ!ɓ'OpF}_|%%%  eԩ҄)z w]<#oaaa?~:^bŊ%KL6ǧg-9rlСzlj .̼t'[[ۭ[߿?J~;~c␐XxHuGϏd&&&M<믿ƪg̘ԔXXXhkkwDWo䍝5ܗK,6wĉ}֭[ׯ0!4sLEEũSBx|Az^aaѣG}}};^qҥ߾}W`N:tҥKϒ2k֬:!C}͛gvuu=w\?Vuӗ5M4߁ofիWi4o޼9""Ճx%%%sN=!**j֬YG>qD/}90%'';88(++w =k >777..nɒ%rrrzjssx 3 YZ,+((hӦM\ @۷o_lYғ<]QQ6w 6[KKK? _>0ACwˁZ!z L`D}zEsssBBBG#//>=q͚5--- d'N@HGEE]vPTTm۶M6I$dB>vLW6^p:u=| ͛ xall14551Nߴ%j!Fߔy[.&&ݺukƍAAAG} GǏ_hQ4蒴~B`l6;)))77W—<$a=ǬkxUHVkxO>mjjza?H.^6l{F+y@ Sb)d2yϞ=6lprr2773---wޥֆw\iI;tN[";wӧOwrr:HXETU}cІXY \x1**agϞ={Do]paϞ=?cqСz"H"~w__T1U5 S޽{$ ӵBBB+__߇666w\i}OOcǎxN*"V ;H$jNg 8L8⏭o,.NێWU*}K>4 eee@nn;<Z|yDDkkk%)SL6-++`Æ !$ gccinnj*|vx<ަMx⤤$늉quuѱ 1''g֬Y322233B$p&M$/ܿ!ѣXggg1U:!:?`?/immسgO!Ƈ'7e̙ --Cy||~@@}SUU}%+EQQQ^^b"Ξ=#:X_(^tiŊ~~~׿"##MXz%KDpl6{AAAddd̟?Ϟ=x &555;w =w鑰@U\+=]U܊s+*cUϮ9I)uǴܬ<$zPm*JQSU+ Kuuuş5vXNo\vӧǏ ?W\)ᅳ/L2}vںu޽{,XpyuסC"""lmmgyy?)V222ڹs9sf͚uĦIsP(]v#v1o<[[[˗/a)UDS_y333SSdʕ+˫/U|8j(6аh>}0oo ey~~~sa0s̙1c|hii6Ԅ'H^^^Ǐp8,CCaÆFbb"KmddН2WWW!};++ |X9Httt!]]]<Ǽysخ\lll]]jbba֮]U)++VWWkhh'''߼y411mvɵVVVt:׮BDFbP\k<==z"6T SP#E(Ci#Z,%+}/)#"d&Ӣ-[Oxe6e'!Tb,M[[!TUU%5|orvv>|0BH |˫fz k!$رvܹ999PQQ/_=WUFLWɩ+ Ey{{۷/++k„ |>?-- aRPPPN:uT_>|8''TSV^mp86jhhPQQ]UCCC47xɓ''LW_uh᳛WJDG(,,,222zp82)BO(H CE lDNb.\.} v#;?6~ԩ28Vp8\.7<<ÕFCCCmmm; 1baB}壣lvss_PP7B!dcc2a„|!kAh˖-هrLѹ4 ]NP.\W%U5ܰX]ظq/RSS~QDb'o޼922rѢEt:=??_Wttn VA!ކ  v_{add4n8@ xzz޽;99YMM_ÉPz= 9gg~Wɓ'OzxxX[[ʾ|ԩSt.lvFFF5=KD?--MUUp HO!"]|9&&&,, 1={&ɧP B"Dķ*77NnT 'ܵkcNNN\\$]PP:eʔN?n}LW;z׃E&&&BKD"@F#KKKmllnܸꊯQ?۔!z[ZZ-,,aڵW\9uꔍ @ryyk$EW@ d4+G!vX|+z ž& 9s7ovyHKnݺW FNNkhh%\ƍeeeϜ93{찰O;vl„ Ǐo߾/^`cf"Fjj@ IIIk|<>gϟ?z("@  /_w^PrE)<~͛7w܉8rEcǎuΝ;+WW `$}wwGX[[WUUeggxϖ{}kiyUOjZ3qa卍/^ 񉡝%B痔$$$P(ޘH \]]O8`0޽g͟??,,ׯ_ǫVXdɒiӦ>|^t쩯#Gʆ*y.\k*dff^t?֭[/yJNPƑɓ'_pF}AGX8~AFB!!!ݻwȑ)))nnnq'Lpܹ8W:88++t:ٳOO:ȑ#3fLEE˗Gᗛ<)#--|ܹR*700LLL'O߿@n޼9{lWWsw,FuxUUUիWi4WUUm߾QQQV$33sH$DEEmڴi̘1'NXu>>>:|_FqБ }nnn\\ܒ%K>5pW677p4KXXݐSrr}`XAAA6mA 0:q芊 55snذAF?~8W]]mnn}e˖w,; N$ NН.wahw:}O X00A@("z^ptH[t䨨s]޽{/_ͭ566&HbZQ\H@?*}r|NSҶCMgԎ|w()b[.P43E/@.2-*+RZc L4L4lU1ڹnjek}5i諫B`=JHH;vlmmmvv6;+nk}هU3.^c-uGꘌ'PB2NB%3E/@.ƛ#^ԴReCMyA0Ht++322]*zʕVyyy){+kj}XQE>{]p9;VٹsL DbkEY)n,m>Ve~866vҥzzz;^yz刿xzu[W-B=kTB"Q.n$(Ǎ8JWr Ejǎ; 3w袧wZrü3VW)2$Şѓfuyp}śu~^K spXmAcg,p}߃"WjPm2*~9A6޼2jǪf$"Ż6M=t?{Bk*%KLZɴɏy!Y())L6񴺅ESeVN2GEx0M]-<,_2\ Q@(<󺱍K q&C(l.EM'l;qɻ^!DWKJ4P2i!WO555;w =ww!CX30?}'2CM#{VxŞ=Co$CU^!lpm*?ϻ^x-ݢ犩zS[WkI˜_J%EwNEG_E[M>kbsڙ*1&-X}jZdYf]Wms>)lމWZdelfqM5a$iP(D|8mBl d!Gʘdu<\KU*C$]ԱB5yjBL=U8!\L]5T2D*c=!@"0|d{c}V2j i=p@~ܹBL&S[[ӳ6==H$Ξ=1W\yٳů8 * c?uZa_? C|"K0#Sa_ڎv Ib.Ü'S Yc8f*JX!;lt[SCNb,*'n) 9Lf"(rN؎p"%ɐB/g14 5Olb4h8߾vye޻QgԽa+T6}(bn8iZ6Uv2Q3ѐ\ IDAT7ѐQz~Y;/56#1@tw !mB7Kjhr3M&:&;>H]PLVu-3e]8#:{>:@>^_N1Tiݑx.c!u#%Q:2J{ȯZٸ|yӦ%yh@,r׮]k$.:wܓ'O;·qi5B*'O}lܿ[q|܋]_cVN6BhX͇ !ih.-kL _?qL !6u_$^(|xrW?u65!$QƙaNGWU¨=]3B^vzJ4BQy_ >m8Sf2}1D!]R^2M5i ;!"}*kh/kZk[ T%Z)$:8ɻG!v5G(-,,222Bmmmjkk,X%yeuDhrMpEFU$R6Z/wS[W$ABm4L^Y,+gY ^Y[82YU]qؗ#Е&-X#a={]F !q&jXaJ4(]e/xE=wZauBjk))  H'7[ܽKW9}"4 yt:!bٳK.ј'߯i}{KBHYKWwhN#nj{q?k~"cY'+l|.lnm@p6US ַ,~UT8Y2'.EmEuXާy 5FLKo[{M0 usPƎU,I“TUUmllݖFC={ٳgӦM,|ֆۗbY^l6#6^TE~q1ѡ{!Jyf\b|b4`,gSǼY\󺎉U-q7!l um˺vS|.{W~ŐU,EMp !D$Xj# ? B //;]1t3ї=H$" I$.BN;::vߝ\ qx|~y.Vo UNR7Oak'E\y jkl397ַVyw^ֱ;(6/!Ln*~J4EUy 1?Mm!1ܿR%USɌWF䯡;VudwwGX[[WUUegg" E755ƪ2@o| Yio>hk|ozt=' Ý=>"IpاY}K^ ,f(g9x]}{pb}ś7|~wgu6~ߕN˻}Udjj6sE$ LNs;WFdl@~:*}!VYK"黱:e``痙H'OUXӧO/YSx-a%lE~l|}>{56\dBPuV"KLm?n` Cv}  : ZI3D񫪗OF@@X6߳Y>&;&7FO!GaqjןUw{P_ pkKmuE٦6εg&j4?~-#uy@Xw=q|qqPM נ~'͸w:} /dAWoDQ;HI%n1W@0A@ik|d̸@ɸQrJ*k; >ɸ ɸ `0t} H `j՝UUua<~zMM N777RtwO3))-wjnö{|#+ ]כj=+)*Iص?,RdÞ޾r9b m9oYDgD5=<<] m|ٳ&&& YYY---1 SFf jz3.c7+~B%߱?MtՇwޕ $!2NѤN)(jRT%<^FNYF!T{xo(h(k R߱?zk?e!mmmPccc/u"i#HT@Go _{{jޖ_Ftgތrdh'O(jtnSey&q? Uzn%+o[3[_6W6ߒdw' g xrz6rCGu!qvvf0iii!nkkۍK...JZ[[֭[f͚n4*--Pngg׽K%E.FzͿ40Z5h؊" =Skocv=! 2ҕT B#|R\ȕ-m=mv%74hWer %r o5D/ }ADfΝ T*r0}P{{{}}}^^^nnԩSEYIV'B;mjSt};sw^KmtNcW(B15?Nm~'mhNCiK]FHPv5cƌ;v̛7{Y>B499O\p B(<< v Fyq1 jO_dKSmB@ { [\hhxN7~=b`eo65#nCUHZ\;PHx۱]mc?{]ih8if`' VnS!_"ih@kk &2tss3gKݻ'e_`0#ඳ!PV^ ;sB^K51n:!nkMg1hYM3 ܱcŚ;w TTT$&|QEEE[l@TU\kT,!D  DB@$a|W(4+ӷ|O!PV(4VVZt=R /5]&Pc{: ͡S~^W+'V.GTw}||B!leeeff%F::::::666III CڐDU3$me46D_( G4=|P@ fF IzG[[{1666ZZZ)))&LihhDb-\PKK+<<\C WN~gM"w[ <퐩! *YX_w$+[$?؊: /T~N#X,ևGeee=~ɩ'{$C^V$ n*i&.tlO/ !l>gJJݻ>+7n|EjjB{ܿ[a3eΝĸks~?ao>M4SXy[8~%Zkmy{bLٳCGlاo +/T~ZZ ҂hݻت!YYYXStINwLKq&m{Qd@A"*/:KYB>/$ܵkcNNN\\ܼy$S^^^SL׫˗/Ą@oPTӚqw⮟=tǎ'I2</BYIF@$Vnd<<*1}D҇_R ׮]laa!D%iiiO>E? YaD2(!Vxs{ş%CWm|%qFYY3g̞=;,,^6>>^__?^QQq߾}/^P VTT,_uŊݦ 0C]X̑QYsGyx»!em=%ZU!R졀vK2*~6^~qnˋ (r#|M Twݏ=cmm]UUݼ7nܟI"nܸmj*X :E hCd(7uy-BH5W1_!elBJ#u*}AjizFU7?K̵ki 嬪ǝQffKjaf{&;1rzq8k}w!--M6 *))I~^^^.\8q"Zԧ6X\\, E !|LM$fqr񞮾K*[Ϲm-E7Kں:Z~ s\[pA3[7Y« %=E(/_gSDr'B(/lRht.sR@C=[G"y/rVY233%?ѣ.ٍPzzǪu2@_K^QD!#;())̜3g'F"<<VښY>}wSl>oii٫:D~Xpn߾mii#$$Н;w644PTKKӧܹVTT$uO }&斟,ccCJ$H{GFH$˽v횧܃gϞA>Tw}||cY>BB(!_d2L&S H1KRg0t:!bx<޻wB۶mkm۶tR===izHBD?--MUUmiiAh4h"{=x`…tT~yyyAAѣWbH$]]]`jj%^%}wwژܤl'''YYٞ =R%~~~L&311h_uOE6+YzzǪu2@_Y$ @0A>$ @0Aߵ愄 D|jHPXXf͚ =9qO"UveU/ߺux<{ӧ37o*(( PXelҴq#B]elJJ\=bXCӺ¿39֟Vuǽ_Ll:_*SIBǏ+u֬FةoFKK #lٲ.J7)[HH blˍS~L'?Q!cccMMǟ8quȐ!G^zukk+V1o޼ut8EL)WWWw)$UӧMMM>?H.^>#<_DAĿ>,]kX줸y3-3\T<ޠbh(IIFFbzg?PK롈]TNrB^]?&***//۷o?|ff&Bh]Օ;666!?J.&&&>>~ݺu?k/ӟ 6NjxlVۻ3dɾj%ACPҠZ*U)J+^BJBU/UMTRbJDB}Y8i$D||;s>ysdO,L8Yf5]Ν;k׮JY>""|n{yct4Td sv:_ :bss7P8tTy=j[]5jTbb(!!CѴi̟ !!Cjyyy8y%UQ^reVV֚5kGEEui[>֭[-Z "aVZ.X`Ϟ=M4rj|~^km۶0rPDrii Daoد_OBahԧO3oJ5֒ZNJJ8pKwEEED$v=bĈg*Ϩ%K;vlʔ):޽{Db"333+ 7/I5M0cƌk׶nmĈw50`СǏ ;w.˲dz022k„ yyyDP(윜hҥXbSN > ԙ˃3oebY-Q2h ήoY}EPòe|ۧoVV;}.;k鯿M˲˗/֭{-ó꾔t;;QF{3fNN6o}˦kZF_TT5qaz>|̙3Tپ}{nn[մiӖ-[>JRTR԰ܹsDTRRbmذa,>9&x[lٲBXܮ]0C-&zƍ.\[o%%%1 õTovgϞ]lY^ozѣDiӦk׮-\PFGGK$[,;qÇDԺuk(sU(2\1 w0F1c?-__F"yr[Rpy7UQȬ|݃{E($k-o/A5.;j"u^^rsh IDATzizXbŊSL1cׯ_…Ǐ _ /^,Hϟ?w}RM'@fϞoL<ݻׯ~zBBQ!!!^7k֌%Ӫ<(::zƌ OIIٰaý{~n3&ZvѢEO6:::^pᩚE`nP( &LYv-uޝf̘qCrrrrrwHHɓ' T-ɸݻZD.:T?7W䄮֭{tdǭgjԨJ=|y3?܅ mF:dlCNŁ$_{+ڥmڴ/vĨ 6(yI$Es;֬Yӭ[;vpϟ]2yF7a„'O_M6>DXODѣ2dqr֥K"s!k֬7&B ;t۵kgzW !k ĉNKYC׭crG^k~jxHЪS۷X\0f*n qWy.Wt|WW"*Tc[zjjjD" ;v옳~[- F\]]/^FDݺu֭[mx<2*2}??zذaݺuqf͚%''Yqڵڵ۵k9aٸq"}5`OOO}rrr[DXqݻwcbbm۶%"1OaIJ벫ObssETʈDҰ0U+]N1OZ,5SlkI,9222o|P_G=D"wppOO>ebq υR [ bbb +<%bq``ܧQGPܾ}R4M"0+ǧU͛waY_~rM"0f^z_~́YYYIII!!!\M&d2СCG~6mxIiiiDRRR+ RC;ve.\uVQG^n]FF  ͘Y.~mJb#jS_5Nyav<|ND1bQɲ{uYYM[ Zv8p}NNN[lIHHpqq1=P,wԩ׭[׶m[2K01̙3}nTnnLDEEEz7}]nk׮;vư>&Oܯ_޽{{3wߝ5k֨Q|֭ٓ{,M|}}+1/}__1c$$$8p@&pIDiAAA_}U~~+zjӫѶm K+Z&wɲlXXؗ_~ih #""~c "iX[XX=Zu$,4fGUGuoh"{C_x1v&"SB`X VٰaCJJGGٳgWh߿cξ]^ٹ"ȸexe,Y007їdU6tׯwҥW^3|`VllkF٩SgxR܌h*m)..NLL B̽lٲBXܮ]0L?99Y[|4s}B>p87b"JMMD۶m ڵ:tT*&RtҤI|>4ѣGheeUXXV]]]훕uѲ)SGP#Gz.'"-[;$򜜜!j0ɬy;;;{{{[LFDJeYJռysCS˖-(77ל@-&''ޖT*x"x)eHכ;%̣GrI<]|{yyիWZ-t5"rss3/ZE<رc:ܹsIII=zhӦ הΟ?_P+iii5el2hHVǚ3ҥK 2s={4ܛfggd:G(Gk:hH!s> @H,} D!@H,} DNII޽{kvK.5@<P%$._ܵk׈R=^}ը *3xÆ )))[gϞMD˖-+,,5Ni^q<1<ۀiǎ;iҤE=Q&TC{/""^}*Zi|Inݺkccmܶm?ү_%KHR"={P(8qb>9;;V$q/x Zm~/dYz+,]"¯b6fY7n_xFyٳg߻woΝCCCkzԩSR}\]]9s!CL?nٲBʕ+KJJΝ/jժ1xd'WJk3U]0)KuD1qf͚6(:t0o-f<51 ϵG2..6>߫WZ1s EEED$v=bĈl۶-00pΝb ې^-\p@}%DKz}kU+t2=ӳ:L*c Ck̈́~i77vVSahz/V*od߼gIz}ݻO?j0cƌH// &8aQQQ~~Vh4EEE͛7zxxO8`C?~<((wܹ,˦ٍ5_߻w1ct:o&((յUV1wOBsrr"KrGW_yԩÇW ~_+|3l0eccc+7n\ԁ;}b]vaaa<"pB&MjsZE>D$q [FD%)kIM˞ڒ5%Yn}6g5WpK+_NuuV":ujGѦM]pBVmz{q5kFD~~~ϟ'ٳg'O{_0 _R}˗/?{ezպuk"x~ŋ%Ν߷o+V,[7322֮]{Ν}I$[,;qÇDMFܹcٶmc_t.\y޽ſ[QQi+ wwxJe' l=cu³?1|]Dmڴ/vЁےrI_? Կŋ{sMMM۷kÿK\.?~fff&Mjߟ;@c\፟YlYLLڵkBaM>Y3iҤPdςT*4i'"FsсZYY:\xa*o}إxh^[cG;JYS8ظ 6DdH:.==]v&tڕlmme2YϞ= YMKK4hP~?qp7߾}۰f͚q˕Qzz:|x<Vq "RY^^NDYYYM41}\%%%DT/iӦ,_~ 7!C 2~JG빜322Zlis%ooo{{{svgzV11a _jy0.[^WTTTZhV^zJ mjOR^zǎ,8P"QEEœ ޽W_rxEE74JE\T>}AC]7-ƥϢLҫW/Lv?6tsrr{{7ořENNN=.E aVĽp1ӣ<'`x|lV""Pl-Hy̴k׮ѣG\{k|>!w^~[Om۶Dtҥ6m䲸+V ۷njS_GaỤGj?._󽼼 .^knZaKŃk WH(w7wkѢELL!>pݩS6^WILL407;ýsss;tP9wGGI&oLL ˲/r[pppDDM`̺߫Wo޼9000+++)))$$PVqM^gvVQ[Urp'n?>--t:]JJ޽{E"QUFuU73g|:r>88888n1 e;;;oo cǎ]l… nj|\G^n]FFaK\\\ z=i&___cc?͛7o޼駟bzgV;f̘d~י\xr0 ֟gv%rfF3m +WSeڵs̱>;vq?;wdY6,,̰'?a!ɓ'OgHwyĉu 6,99k<#VW+DcǎӧO>}ҥ韘8bĈݻw?N<2hHR <#cǎ"##ǎ[^^n ޱc-[4HtH)̚5+::Zո.R ߿t¡tń+X?W,} Dd\ g6ʻJR(TE+mLug܋d -e,ni&>߷o)ddd(۷o7v 1zX-Rgk "Ǝ,eLQxe&*}\7Y|)5~U*K9婅6iDT*uC]v%W^yeΜ9Do߾~~~E@DB]O!BY=W=l: X%;gJE/E!mRꁼ"Z5p `ZlQ<QI2{uMeW.%^g^]OY^s/L\O[+NZtJ+7ȹu}@a4l]MFh l*=3~kTrN-uRuֿnJMYF$UQ ,VU(Ň>w\~[5'M4hРC?bbb IDAT&NrKz"ڻwΝ;[l9eʔ}?~|ʕO~xADD ŋ~)SRRR/_MwҥK||+!f͚5kPX_/]t޽>h̘1ΝH$͚5?&ݻsù%"ڳgݻ۴i3eʔ^paɒ%:0gRRB8}B0sxjX^lSax;&IQL:JBGrk+?V-U 5qWĉ(:кTӤ0A+ \ ۊEy1B]DT]SUWƂ'P O#:PF-,>R@t_7MY;<ӡ"M^q[Ϙu1+nh qsбcǎ{aǎ+s֭5k8;;mJbbbt###o߾ݴiӻw8p„ \֭[d}СĩS6oxǞ CsGW(kgxJ}2]͗Jix5*Qi1z-Q]VS!tW\w*9/ҕr+y/} *g ѧAͳ6<*!rkYVO0Nk&6mø@ߥk8U=ë:|=FymD2A^k|@WaW7} ֹsg"JMM5$L5{޽ooo"ڶm[LLL%IZZڎ; }***XUT\uwߝ>}ҥKێ;Mqԅبg:m-,{5\v댆|xǎAAA\QГ=+**H, >R*5q1 c6''޽{AAAD$ {ѣGNnݺ:Do#" ߚ[^q!T\rΡ OzFsYD븿G/ZLOTSR{-ߊ{ᠸue I÷rn{5Fϊe[~ki[\lL7s_})\r&O̰-OnwLL÷Vݹrr<bq#$a<<<Ξ=k=pԩ#G޸qCk4U^jb:^X2WNZg;>i!@藔ݻgϞ 9ѿ|r׮]#""JKK7EEEQQQ@.y֭0WWXF^DFF̞=[(N8Zòl>}rrr9(1;^{8pرcZjpJwvuܹ+WZLOD ìZv &W^tU[Sm8` nƎ^\<"*++0`@NZqsݚjTTTtԩÇoT՝;w۷^DFF۟={.GFtM;;_~8f+WmB" {™3g 6e:L<">rHpp~K/mٲEVsukŋsE"ъ+Μ9MDYYY_~;uTckٲe-:draZ=hРMHDWjK&yyyuttpB`;-[Μ9ѱcǍ7r{ݚLZ*mիWXXŋ ?SHh"soРA\rϽ?Zn] ;{ZͭRMt?nmڴidds<==#""4UrM_~R|뭷ٳxbKKK?ND:.66֜Ʈ^hѢ#G=VEUwfΜyJ)ukD"!'̙sԩN:7蟂":uTaaa$JrnnnWCEEwq^{mРA*j߾}5DD999Ur72 cq 8K:ԥK7o^jjM{WNN;o^zqukQ۶mҥKO6=x`Ŋ]v=s?Aedd\t)&&f<8x͛,XTeBqvڙ#:{N߾}cbb<b\c0o>>OU+o&55uZvϘ1#44t̙aY_}Fŋ׭[iؘf͚qeeeui~CXXXBBݼyť2eʞ={R۷+?Ç_t}{e|߿z>0S\\\uMU  4$sX,<<\,GDDV?Ν;D6m۶?0kҤI ~_ 7>7oϟl$55aΝ'N3g|hDvEv1}ӧ/]]˗/_ʥ00auG{ngbj+D4v>,22rر0jԨQll,߿}MX 6 >cǎ[l16s4fODf͊t\u~SСC]t T*oeeelhҝ:̼w^׮]k?$''UV_||P/t/Ki!x15r< H,} Tu_^^}YyfcGЪHoܸ/Ϙ1 P=R(~ݷmر4ʉ͛7_{;wrn#˲K.ۿ{m jK/hfΜqFoD_V7uDTPP0rȯJ.lB{{;wN:̙sz~TTmee?;88pǎ{ȑ>}8;;.xe˖C͙3^v{̙3 ۗ/_>o޼ݻwK$zeO0ʕ+诿JNN=<WW\`f=q ?ΝϜ9Tx>=ϕyxXӰwN=NӉE3ԲeKztq_VVFD3-ZZZ؁4ljH$" K,7v qJDo`Уcx}"zJz)͛פI~KL>!q kr`'/D"Q*{gee[NP_w6?zbccW^2!sq/J FD+Wh4okk+J|>}B"*** P/DrڵErT*ݳgm}Ǐ̟hu͛7Df{uիr 'B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B`X $>B` #GV/4d$`:/=!ur4˜-;7vQvvUjU> &H$9&&fƍ7nڵٳ{mo߾} 0`/RP;ė}wo׫) j Ìݯ\s>hѴR4πv2dZwGR j"r|؝l83fl/Y:7ipdDPJ+$`IxDkeggϝ;}DԷoߨVZ:ujذa}Ǎ\|GLLL޽Gu<'xKuZװ*ǤNy;կ^M[XK|w[ ;J|"gjPsmR*6u x|; )n畷]Vz4wc;:t駟 `={QF|xaaa x篷3ː .!kOKH+ްQԱݗ9i9FN‘0y~˄v^dz/ig/\ze\׼_.kD'wjjm1?nm׮ۏ?啛yfDrſD"ѤIΝ;7w\":rHVVV@%R`aĆ;J3ڗTcD[Okt3w I0;/8}OO.WNDnr ]".>?n 3vy^UiY7&L^X^S/Vw7o޼fR0޽{|Mtt4YYYd jZTvx[N(xFeo:V!^ie/\GNA ^xXWhȦFWZum"֭ۜ9szIDQQQ}~~>6lٳo߾z]vi4L6mڴ3f8:_Fړ;{9n9wAQQuձD mDDz"X%X0S1^vVBV\EzeӧO*~=̙5'&&|>7ߜ5kV˖-N:t:3gN>pyPn<~g_Fuvۺl;y4!b;<JF5q̻UZ(%"=Qϯ5._tzl-ܹ3)cbD;;wx ǧiӦs֭qsb1c"""6mjdmm=uήACrnˉ?^W2KLt,RuV"]6nVs&2*TZV,n5ow[w>|*؃~<~3S;{1oL._tʦϛ ng3TZSk(5;5W|JŅ{DIa=2Q27IjOݛW cf(唫u%*W8j|G"ұ2V$x&/Si__{ ֘H٫kB.I]dž-IDAT^{v^9:ZΌ.p5Oyi_N_ֱ u̕i+4;gJZn~}W)hu ? #G_~iHu^zCxj=hH!=@B`X $sIENDB`pudb-2024.1.3/doc/index.rst000066400000000000000000000006541470451231400153110ustar00rootroot00000000000000Welcome to pudb's documentation! ================================ .. include:: README.rst Table of Contents ----------------- .. toctree:: :maxdepth: 2 :caption: Contents: starting usage shells misc 🚀 Github 💾 Download Releases Indices and Tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` pudb-2024.1.3/doc/misc.rst000066400000000000000000000026031470451231400151310ustar00rootroot00000000000000Installing ---------- Install PuDB using the command:: pip install pudb If you are using Python 2.5, PuDB version 2013.5.1 is the last version to support that version of Python. urwid 1.1.1 works with Python 2.5, newer versions do not. .. _faq: FAQ --- **Q: I navigated to the Variables/Stack/Breakpoints view. How do I get back to the source view?** A: Press your left arrow key. **Q: Where are breakpoints, PuDB settings file or shell history stored?** A: All PuDB information is stored in a location specified by the `XDG Base Directory Specification `_. Usually, it is ``~/.config/pudb``. Breakpoints are stored in a file called ``saved-breakpoints``. Also in this location are the shell history from the ``!`` shell (``shell-history``) and the PuDB settings (``pudb.cfg``). **Q: I killed PuDB and now my terminal is broken. How do I fix it?** A: Type the ``reset`` command (even if you cannot see what you are typing, it should work). If this happens on a regular basis, please report it as a bug. License and Dependencies ------------------------ PuDB is distributed under the MIT license. It relies on the following excellent pieces of software: * Ian Ward's `urwid `_ console UI library * Georg Brandl's `pygments `_ syntax highlighter .. include:: ../LICENSE pudb-2024.1.3/doc/shells.rst000066400000000000000000000062461470451231400154770ustar00rootroot00000000000000Shells ====== Internal shell -------------- At any point while debugging, press ``Ctrl-x`` to switch to the built in interactive shell. From here, you can execute Python commands at the current point of the debugger. Press ``Ctrl-x`` again to move back to the debugger. Keyboard shortcuts defined in the internal shell: +--------------------+--------------------+ |Enter |Execute the current | | |command | +--------------------+--------------------+ |Ctrl-v |Insert a newline | | |(for multiline | | |commands) | +--------------------+--------------------+ |Ctrl-n/p |Browse command | | |history | +--------------------+--------------------+ |Up/down arrow |Select history | +--------------------+--------------------+ |TAB |Tab completion | +--------------------+--------------------+ |+/- |grow/shrink the | | |shell (when a | | |history item is | | |selected) | +--------------------+--------------------+ |_/= |minimize/maximize | | |the shell (when a | | |history item is | | |selected) | +--------------------+--------------------+ External shells --------------- To open the external shell, press the ``!`` key while debugging. Unlike the internal shell, external shells close the debugger UI while the shell is active. Press ``Ctrl-d`` at any time to exit the shell and return to the debugger. To configure the shell used by PuDB, open the settings (``Ctrl-p``) and select the shell. PuDB supports the following external shells. - Internal (same as pressing ``Ctrl-x``). This is the default. - Classic (similar to the default ``python`` interactive shell) - `IPython `_ The `IPython` shell can also be used in a server-client fashion, which is enabled by selecting the shell `ipython_kernel` in the settings. When set, the ``!`` key will start an `IPython` kernel and wait for connection from, e.g., `qtconsole`. Like other shells, `ipython_kernel` blocks the debugger UI while it is active. Type `quit` or `exit` from a client to exit the kernel and return to the debugger. - `bpython `_ - `ptpython `_ Custom shells ------------- To define a custom external shell, create a file with a function ``pudb_shell(_globals, _locals)`` at the module level. Then, in the settings (``Ctrl-p``), select "Custom" under the shell settings, and add the path to the file. Here is an example custom shell file: .. literalinclude:: ../examples/shell.py :language: python Note, many shells do not allow passing in globals and locals dictionaries separately. In this case, you can merge the two with .. code-block:: python from pudb.shell import SetPropagatingDict ns = SetPropagatingDict([_locals, _globals], _locals) Here is more information on ``SetPropagatingDict``: .. autoclass:: pudb.shell.SetPropagatingDict pudb-2024.1.3/doc/starting.rst000066400000000000000000000134131470451231400160320ustar00rootroot00000000000000Starting the debugger --------------------- To start debugging, simply insert:: from pudb import set_trace; set_trace() A shorter alternative to this is:: import pudb; pu.db Or, if pudb is already imported, just this will suffice:: pu.db If you are using Python 3.7 or newer, you can add:: # Set breakpoint() in Python to call pudb export PYTHONBREAKPOINT="pudb.set_trace" in your ``~/.bashrc``. Then use:: breakpoint() to start pudb. Insert one of these snippets into the piece of code you want to debug, or run the entire script with:: python -m pudb my-script.py which is useful if you want to run PuDB in a version of Python other than the one you most recently installed PuDB with. Debugging from a separate terminal ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It's possible to control the debugger from a separate terminal. This is useful if there are several threads running that are printing to stdout while you're debugging and messing up the terminal, or if you want to keep the original terminal available for any other reason. Open a new terminal. First, you need to get the path of the tty of the terminal you want to debug from. To do that, use the standard unix command ``tty``. It will print something like ``/dev/pts/3``. Then you need to make sure that your terminal doesn't have a shell actively reading and possibly capturing some of the input that should go to pudb. To do that run a placeholder command that does nothing, such as ``perl -MPOSIX -e pause``. Then set the PUDB_TTY environment variable to the path tty gave you, for example:: PUDB_TTY=/dev/pts/3 pudb my-script.py Now instead of using the current terminal, pudb will use this tty for its UI. You may want to use the internal shell in pudb, as others will still use the original terminal. Logging Internal Errors ^^^^^^^^^^^^^^^^^^^^^^^ Some kinds of internal exceptions encountered by pudb will be logged to the terminal window when the debugger is active. To send these messages to a file instead, use the ``--log-errors`` flag:: python -m pudb --log-errors pudberrors.log Remote debugging ^^^^^^^^^^^^^^^^ Rudimentary remote debugging is also supported. To break into the debugger, enabling you to connect via ``telnet``, use the following code:: from pudb.remote import set_trace set_trace(term_size=(80, 24)) The terminal size can be defined via the environment variable as well:: export PUDB_TERM_SIZE=80x24 The following precedence (from highest to lowest) is used to determine the terminal size: #. ``term_size`` keyword argument #. ``PUDB_TERM_SIZE`` environment variable #. Size of the terminal in which the debugged program is running (as returned by ``os.get_terminal_size()``) #. Default fallback value of ``(80, 20)`` At this point, the debugger will look for a free port and wait for a telnet connection:: pudb:6899: Please start a telnet session using a command like: telnet 127.0.0.1 6899 pudb:6899: Waiting for client... The host and port can be specified as keyword arguments to ``set_trace()``, or via the ``PUDB_RDB_HOST`` and ``PUDB_RDB_PORT`` env vars. To debug a function in a remote debugger (and examine any exceptions that may occur), use code like the following: .. literalinclude:: ../examples/remote-debug.py Upon running this, again, the debugger will wait for a telnet connection. The following programming interface is available for the remote debugger: .. automodule:: pudb.remote "Reverse" remote debugging ^^^^^^^^^^^^^^^^^^^^^^^^^^ In "reverse" remote debugging, pudb connects to a socket, rather than listening to one. First open the socket and listen using the netcat(``nc``), as below. Netcat of course is not a telnet client, so it can behave differently than a telnet client. By using the ```stty``` with "no echo: and "no buffering" input options, we can make a socket that nonetheless behave simillarly:: stty -echo -icanon && nc -l -p 6899 When using the BSD version netcat that ships with MacOS, a server can be started like this:: stty -echo -icanon && nc -l 6899 Specify host and port in set_trace and set the *reverse* parameter to *True*:: from pudb.remote import set_trace set_trace(reverse=True) The "reverse" mode can also be enabled by setting the environment variable to a non-empty value (the keyword argument has priority over the env var):: export PUDB_RDB_REVERSE=1 Then watch the debugger connect to netcat:: pudb:9999: Now in session with 127.0.0.1:6899. Using the debugger after forking ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In a forked process, no TTY is usually attached to stdin/stdout, which leads to errors when debugging with standard pudb. E.g. consider this ``script.py``:: from multiprocessing import Process def f(name): # breakpoint was introduced in Python 3.7 breakpoint() print('hello', name) p = Process(target=f, args=('bob',)) p.start() p.join() Running it with standard pudb breaks:: PYTHONBREAKPOINT=pudb.set_trace python script.py However, on Unix systems, e.g. Linux & MacOS, debugging a forked process is supported using ``pudb.forked.set_trace``:: PYTHONBREAKPOINT=pudb.forked.set_trace python script.py Usage with pytest ^^^^^^^^^^^^^^^^^ To use PuDB with `pytest `_, consider using the `pytest-pudb `_ plugin, which provides a ``--pudb`` option that simplifies the procedure below. Alternatively, as of version 2017.1.2, pudb can be used to debug test failures in `pytest `_, by running the test runner like so:: $ pytest --pdbcls pudb.debugger:Debugger --pdb --capture=no Note the need to pass --capture=no (or its synonym -s) as otherwise pytest tries to manage the standard streams itself. (contributed by Antony Lee) pudb-2024.1.3/doc/upload-docs.sh000077500000000000000000000001201470451231400162050ustar00rootroot00000000000000#! /bin/sh rsync --verbose --archive --delete _build/html/ doc-upload:doc/pudb pudb-2024.1.3/doc/usage.rst000066400000000000000000000040671470451231400153100ustar00rootroot00000000000000Starting the debugger without breaking -------------------------------------- To start the debugger without actually pausing use:: from pudb import set_trace; set_trace(paused=False) at the top of your code. This will start the debugger without breaking, and run it until a predefined breakpoint is hit. You can also press ``b`` on a ``set_trace`` call inside the debugger, and it will prevent it from stopping there. Interrupt Handlers ------------------ ``set_trace`` sets ``SIGINT`` (i.e., ``Ctrl-c``) to run ``set_trace``, so that typing ``Ctrl-c`` while your code is running will break the code and start debugging. See the docstring of ``set_interrupt_handler`` for more information. Note that this only works in the main thread. Programming PuDB ---------------- At the programming language level, PuDB displays the same interface as Python's built-in `pdb module `_. Just replace ``pdb`` with ``pudb``. (One exception: ``run`` is called ``runstatement``.) Controlling How Values Get Shown -------------------------------- * Set a custom stringifer in the preferences. An example file might look like this:: def pudb_stringifier(obj): return "HI" * Add a method ``safely_stringify_for_pudb`` to the type. A stringifier is expected to *never* raise an exception. If an exception is raised, pudb will silently fall back to its built-in stringification behavior. A stringifier that takes a long time will further stall the debugger UI while it runs. Configuring PuDB ---------------- Overriding default key bindings ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Configure in the settings file (see :ref:`faq`). - Add the bindings under mentioned section in the config file (see :ref:`urwid:keyboard-input`). - Only few actions are supported currently, coverage will increase with time. (Contributions welcome!) .. code-block:: ini [pudb] # window chooser bindings hotkeys_breakpoints = B hotkeys_code = C hotkeys_stack = S hotkeys_variables = V hotkeys_toggle_cmdline_focus = ctrl x pudb-2024.1.3/examples/000077500000000000000000000000001470451231400145145ustar00rootroot00000000000000pudb-2024.1.3/examples/mpi4py-debug.py000066400000000000000000000010341470451231400173720ustar00rootroot00000000000000#!/usr/bin/env python # This example demonstrates how to debug an mpi4py application. # Run this with 'mpirun -n 2 python mpi4py-debug.py'. # You can then attach to the debugger by running 'telnet 127.0.0.1 6899' # (when using the default pudb configuration) in another terminal. from mpi4py import MPI from pudb.remote import debug_remote_on_single_rank def debugged_function(x): y = x + fail # noqa: F821 return y # debug 'debugged_function' on rank 0 debug_remote_on_single_rank(MPI.COMM_WORLD, 0, debugged_function, 42) pudb-2024.1.3/examples/remote-debug.py000066400000000000000000000002361470451231400174460ustar00rootroot00000000000000def debugged_function(x): y = x + fail # noqa: F821 return y from pudb.remote import debugger dbg = debugger() dbg.runcall(debugged_function, 5) pudb-2024.1.3/examples/shell.py000066400000000000000000000034671470451231400162070ustar00rootroot00000000000000""" This file shows how you can define a custom shell for PuDB. This is the shell used when pressing the ! key in the debugger (it does not affect the Ctrl-x shell that is built into PuDB). To create a custom shell, create a file like this one with a function called pudb_shell(_globals, _locals) defined at the module level. Note that the file will be execfile'd. Then, go to the PuDB preferences window (type Ctrl-p inside of PuDB) and add the path to the file in the "Custom" field under the "Shell" heading. The example in this file """ # Define this a function with this name and signature at the module level. def pudb_shell(_globals, _locals): """ This example shell runs a classic Python shell. It is based on run_classic_shell in pudb.shell. """ # Many shells only let you pass in a single locals dictionary, rather than # separate globals and locals dictionaries. In this case, you can use # pudb.shell.SetPropagatingDict to automatically merge the two into a # single dictionary. It does this in such a way that assignments propagate # to _locals, so that when the debugger is at the module level, variables # can be reassigned in the shell. from pudb.shell import SetPropagatingDict ns = SetPropagatingDict([_locals, _globals], _locals) try: import readline import rlcompleter have_readline = True except ImportError: have_readline = False if have_readline: readline.set_completer( rlcompleter.Completer(ns).complete) readline.parse_and_bind("tab: complete") readline.clear_history() from code import InteractiveConsole cons = InteractiveConsole(ns) cons.interact("Press Ctrl-D to return to the debugger") # When the function returns, control will be returned to the debugger. pudb-2024.1.3/examples/stringifier.py000066400000000000000000000064551470451231400174250ustar00rootroot00000000000000#!/usr/bin/env python """ This file shows how you can define a custom stringifier for PuDB. A stringifier is a function that is called on the variables in the namespace for display in the variables list. The default is type()*, as this is fast and cannot fail. PuDB also includes built-in options for using str() and repr(). Note that str() and repr() will be slower than type(), which is especially noticeable when you have many variables, or some of your variables have very large string/repr representations. Also note that if you just want to change the type for one or two variables, you can do that by selecting the variable in the variables list and pressing Enter, or by pressing t, s, or r. To define a custom stringifier, create a file like this one with a function called pudb_stringifier() at the module level. pudb_stringifier(obj) should return a string value for an object (note that str() will always be called on the result). Note that the file will be execfile'd. Then, go to the PuDB preferences window (type Ctrl-p inside of PuDB), and add the path to the file in the "Custom" field under the "Variable Stringifier" heading. The example in this file returns the string value, unless it take more than 500 ms (1 second in Python 2.5-) to compute, in which case it falls back to the type. TIP: Run "python -m pudb.run example-stringifier.py and set this file to be your stringifier in the settings to see how it works. You can use custom stringifiers to do all sorts of things: callbacks, custom views on variables of interest without having to use a watch variable or the expanded view, etc. * - Actually, the default is a mix between type() and str(). str() is used for a handful of "safe" types for which it is guaranteed to be fast and not to fail. """ import signal import time class TimeOutError(Exception): pass def timeout(signum, frame, time): raise TimeOutError("Timed out after %d seconds" % time) def run_with_timeout(code, time, globals=None): """ Evaluate ``code``, timing out after ``time`` seconds. In Python 2.5 and lower, ``time`` is rounded up to the nearest integer. The return value is whatever ``code`` returns. """ # Set the signal handler and a ``time``-second alarm signal.signal(signal.SIGALRM, lambda s, f: timeout(s, f, time)) signal.setitimer(signal.ITIMER_REAL, time) r = eval(code, globals) signal.alarm(0) # Disable the alarm return r def pudb_stringifier(obj): """ This is the custom stringifier. It returns str(obj), unless it take more than a second to compute, in which case it falls back to type(obj). """ try: return run_with_timeout("str(obj)", 0.5, {"obj": obj}) except TimeOutError: return (type(obj), "(str too slow to compute)") # Example usage class FastString(object): def __str__(self): return "This was fast to compute." class SlowString(object): def __str__(self): time.sleep(10) # Return the string value after ten seconds return "This was slow to compute." fast = FastString() slow = SlowString() # If you are running this in PuDB, set this file as your custom stringifier in # the prefs (Ctrl-p) and run to here. Notice how fast shows the string value, # but slow shows the type, as the string value takes too long to compute. pudb-2024.1.3/examples/theme.py000066400000000000000000000017711470451231400161760ustar00rootroot00000000000000# Supported 16 color values: # 'h0' (color number 0) through 'h15' (color number 15) # or # 'default' (use the terminal's default foreground), # 'black', 'dark red', 'dark green', 'brown', 'dark blue', # 'dark magenta', 'dark cyan', 'light gray', 'dark gray', # 'light red', 'light green', 'yellow', 'light blue', # 'light magenta', 'light cyan', 'white' # # Supported 256 color values: # 'h0' (color number 0) through 'h255' (color number 255) # # 256 color chart: http://en.wikipedia.org/wiki/File:Xterm_color_chart.png # # "setting_name": (foreground_color, background_color), # See pudb/theme.py # (https://github.com/inducer/pudb/blob/main/pudb/theme.py) to see what keys # there are. # Note, be sure to test your theme in both curses and raw mode (see the bottom # of the preferences window). Curses mode will be used with screen or tmux. palette.update({ # noqa: F821 "source": (add_setting("black", "underline"), "dark green"), # noqa: F821 "comment": ("h250", "default") }) pudb-2024.1.3/manual-tests/000077500000000000000000000000001470451231400153135ustar00rootroot00000000000000pudb-2024.1.3/manual-tests/.not-actually-ci-tests000066400000000000000000000000001470451231400214470ustar00rootroot00000000000000pudb-2024.1.3/manual-tests/test-api.py000066400000000000000000000001161470451231400174110ustar00rootroot00000000000000def f(): fail # noqa: B018, F821 from pudb import runcall runcall(f) pudb-2024.1.3/manual-tests/test-postmortem.py000066400000000000000000000001721470451231400210530ustar00rootroot00000000000000def f(): fail # noqa: B018, F821 try: f() except Exception: from pudb import post_mortem post_mortem() pudb-2024.1.3/pudb/000077500000000000000000000000001470451231400136305ustar00rootroot00000000000000pudb-2024.1.3/pudb/__init__.py000066400000000000000000000254621470451231400157520ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import re import sys from importlib import metadata from pudb.settings import load_config VERSION = metadata.version("pudb") _ver_match = re.match("^([0-9.]+)([a-z0-9]*?)$", VERSION) assert _ver_match NUM_VERSION = tuple(int(nr) for nr in _ver_match.group(1).split(".")) __version__ = VERSION class PudbShortcuts: @property def db(self): dbg = _get_debugger() import threading if isinstance(threading.current_thread(), threading._MainThread): set_interrupt_handler() dbg.set_trace(sys._getframe().f_back) @property def go(self): dbg = _get_debugger() import threading if isinstance(threading.current_thread(), threading._MainThread): set_interrupt_handler() dbg.set_trace(sys._getframe().f_back, paused=False) import builtins builtins.__dict__["pu"] = PudbShortcuts() def _tty_override(): import os return os.environ.get("PUDB_TTY") def _open_tty(tty_path): import io import os tty_file = io.TextIOWrapper(open(tty_path, "r+b", buffering=0)) term_size = os.get_terminal_size(tty_file.fileno()) return tty_file, term_size def _get_debugger(**kwargs): from pudb.debugger import Debugger if not Debugger._current_debugger: tty_path = _tty_override() if tty_path and ("stdin" not in kwargs or "stdout" not in kwargs): tty_file, term_size = _open_tty(tty_path) kwargs.setdefault("stdin", tty_file) kwargs.setdefault("stdout", tty_file) kwargs.setdefault("term_size", term_size) kwargs.setdefault("tty_file", tty_file) from pudb.debugger import Debugger dbg = Debugger(**kwargs) return dbg else: return Debugger._current_debugger[0] def _have_debugger(): try: from pudb.debugger import Debugger return bool(Debugger._current_debugger) except ImportError: # Import cycles may happen if function is called during early startup return False import signal # noqa DEFAULT_SIGNAL = signal.SIGINT del signal def runmodule(*args, **kwargs): kwargs["run_as_module"] = True runscript(*args, **kwargs) def runscript(mainpyfile, steal_output=False, _continue_at_start=False, **kwargs): try: dbg = _get_debugger( steal_output=steal_output, _continue_at_start=_continue_at_start, ) _runscript(mainpyfile, dbg, **kwargs) finally: dbg.__del__() def _runscript(mainpyfile, dbg, args=None, pre_run="", run_as_module=False): # Note on saving/restoring sys.argv: it's a good idea when sys.argv was # modified by the script being debugged. It's a bad idea when it was # changed by the user from the command line. The best approach would be to # have a "restart" command which would allow explicit specification of # command line arguments. if args is not None: prev_sys_argv = sys.argv[:] if run_as_module: sys.argv = args else: sys.argv = [mainpyfile] + args # replace pudb's dir with script's dir in front of module search path. from pathlib import Path prev_sys_path = sys.path[:] sys.path[0] = str(Path(mainpyfile).resolve().parent) import os cwd = os.getcwd() while True: # Script may have changed directory. Restore cwd before restart. os.chdir(cwd) if pre_run: from subprocess import call retcode = call(pre_run, close_fds=True, shell=True) if retcode: print("*** WARNING: pre-run process exited with code %d." % retcode) input("[Hit Enter]") status_msg = "" try: if run_as_module: try: dbg._runmodule(mainpyfile) except ImportError as e: print(e, file=sys.stderr) sys.exit(1) else: try: dbg._runscript(mainpyfile) except SystemExit: se = sys.exc_info()[1] status_msg = "The debuggee exited normally with " \ "status code %s.\n\n" % se.code except Exception: dbg.post_mortem = True dbg.interaction(None, sys.exc_info()) while True: import urwid pre_run_edit = urwid.Edit("", pre_run) if not load_config()["prompt_on_quit"]: return result = dbg.ui.call_with_ui(dbg.ui.dialog, urwid.ListBox(urwid.SimpleListWalker([urwid.Text( "Your PuDB session has ended.\n\n%s" "Would you like to quit PuDB or restart your program?\n" "You may hit 'q' to quit." % status_msg), urwid.Text("\n\nIf you decide to restart, this command " "will be run prior to actually restarting:"), urwid.AttrMap(pre_run_edit, "input", "focused input") ])), [ ("Restart", "restart"), ("Examine", "examine"), ("Quit", "quit"), ], focus_buttons=True, bind_enter_esc=False, title="Finished", extra_bindings=[ ("q", "quit"), ("esc", "examine"), ]) if result == "quit": return if result == "examine": dbg.post_mortem = True dbg.interaction(None, sys.exc_info(), show_exc_dialog=False) if result == "restart": break pre_run = pre_run_edit.get_edit_text() dbg.restart() if args is not None: sys.argv = prev_sys_argv sys.path = prev_sys_path def runstatement(statement, globals=None, locals=None): return _get_debugger().run(statement, globals, locals) def runeval(expression, globals=None, locals=None): return _get_debugger().runeval(expression, globals, locals) def runcall(*args, **kwargs): return _get_debugger().runcall(*args, **kwargs) def set_trace(paused=True): """ Start the debugger If paused=False (the default is True), the debugger will not stop here (same as immediately pressing 'c' to continue). """ import sys dbg = _get_debugger() import threading if isinstance(threading.current_thread(), threading._MainThread): set_interrupt_handler() dbg.set_trace(sys._getframe().f_back, paused=paused) start = set_trace def _interrupt_handler(signum, frame): from pudb import _get_debugger _get_debugger().set_trace(frame, as_breakpoint=False) def set_interrupt_handler(interrupt_signal=None): """ Set up an interrupt handler, to activate PuDB when Python receives the signal `interrupt_signal`. By default it is SIGINT (i.e., Ctrl-c). To use a different signal, pass it as the argument to this function, like `set_interrupt_handler(signal.SIGALRM)`. You can then break your code with `kill -ALRM pid`, where `pid` is the process ID of the Python process. Note that PuDB will still use SIGINT once it is running to allow breaking running code. If that is an issue, you can change the default signal by hooking `pudb.DEFAULT_SIGNAL`, like >>> import pudb >>> import signal >>> pudb.DEFAULT_SIGNAL = signal.SIGALRM Note, this may not work if you use threads or subprocesses. Note, this only works when called from the main thread. """ if interrupt_signal is None: interrupt_signal = DEFAULT_SIGNAL import signal old_handler = signal.getsignal(interrupt_signal) if old_handler is not signal.default_int_handler \ and old_handler != signal.SIG_DFL and old_handler != _interrupt_handler: # Since we don't currently have support for a non-default signal handlers, # let's avoid undefined-behavior territory and just show a warning. from warnings import warn if old_handler is None: # This is the documented meaning of getsignal()->None. old_handler = "not installed from python" return warn("A non-default handler for signal %d is already installed (%s). " "Skipping pudb interrupt support." % (interrupt_signal, old_handler), stacklevel=2) import threading if not isinstance(threading.current_thread(), threading._MainThread): from warnings import warn # Setting signals from a non-main thread will not work return warn("Setting the interrupt handler can only be done on the main " "thread. The interrupt handler was NOT installed.", stacklevel=2) try: signal.signal(interrupt_signal, _interrupt_handler) except ValueError: import sys from traceback import format_exception from warnings import warn warn("setting interrupt handler on signal %d failed: %s" % (interrupt_signal, "".join(format_exception(*sys.exc_info()))), stacklevel=2) def post_mortem(tb=None, e_type=None, e_value=None): if tb is None: import sys exc_info = sys.exc_info() else: exc_info = (e_type, e_value, tb) dbg = _get_debugger() dbg.reset() dbg.interaction(None, exc_info) def pm(): import sys exc_type, _exc_val, _tb = sys.exc_info() if exc_type is None: # No exception on record. Do nothing. return post_mortem() if __name__ == "__main__": print("You now need to type 'python -m pudb.run'. Sorry.") # vim: foldmethod=marker:expandtab:softtabstop=4 pudb-2024.1.3/pudb/__main__.py000066400000000000000000000023031470451231400157200ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ if __name__ == "__main__": from pudb.run import main main() pudb-2024.1.3/pudb/_shtab.py000066400000000000000000000002401470451231400154360ustar00rootroot00000000000000FILE = None DIRECTORY = DIR = None def add_argument_to(parser, *args, **kwargs): from argparse import Action Action.complete = None return parser pudb-2024.1.3/pudb/b.py000066400000000000000000000010251470451231400144210ustar00rootroot00000000000000import sys from pudb import _get_debugger, set_interrupt_handler def __myimport__(name, *args, **kwargs): # noqa: N807 if name == "pudb.b": set_trace() return __origimport__(name, *args, **kwargs) # noqa: F821, E501 # pylint: disable=undefined-variable # Will only be run on first import __builtins__["__origimport__"] = __import__ __builtins__["__import__"] = __myimport__ def set_trace(): dbg = _get_debugger() set_interrupt_handler() dbg.set_trace(sys._getframe().f_back.f_back) set_trace() pudb-2024.1.3/pudb/debugger.py000066400000000000000000003353261470451231400160020ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import bdb import gc import os import sys from collections import deque from functools import partial from itertools import count from types import TracebackType import urwid from pudb.lowlevel import decode_lines, ui_log from pudb.settings import get_save_config_path, load_config, save_config CONFIG = load_config() save_config(CONFIG) HELP_HEADER = r""" Key Assignments: Use Arrow Down/Up or Page Down/Up to scroll. """ HELP_MAIN = rf""" Keys: Ctrl-p - edit preferences n - step over ("next") s - step into c - continue r/f - finish current function t - run to cursor J - jump to line e - show traceback [post-mortem or in exception state] b - set/clear breakpoint Ctrl-e - open file at current line to edit with $EDITOR H - move to current line (bottom of stack) u - move up one stack frame d - move down one stack frame o - show console/output screen m - open module j/k - down/up l/h - right/left Ctrl-f/b - page down/up Ctrl-d/u - page down/up G/g - end/home L - show (file/line) location / go to line / - search ,/. - search next/previous {CONFIG["hotkeys_variables"]} - focus variables {CONFIG["hotkeys_stack"]} - focus stack {CONFIG["hotkeys_breakpoints"]} - focus breakpoint list {CONFIG["hotkeys_code"]} - focus code F1/? - show this help screen q - quit Ctrl-r - reload breakpoints from saved-breakpoints file Ctrl-c - when in continue mode, break back to PuDB Ctrl-l - redraw screen Shell-related: ! - open the external shell (configured in the settings) {CONFIG["hotkeys_toggle_cmdline_focus"]} - toggle the internal shell focus +/- - grow/shrink inline shell (active in results scrollback) _/= - minimize/maximize inline shell (active in results scrollback) Ctrl-v - insert newline Ctrl-n/p, Arrow down/up - browse command history or clear/recall prompt Shift-Page down/up - browse in the results scrollback Tab - yes, there is (simple) tab completion """ HELP_SIDE = rf""" Sidebar-related (active in sidebar): +/- - grow/shrink sidebar width _/= - minimize/maximize sidebar width [/] - grow/shrink relative height of active sidebar box Keys in variables list: \/enter/space - expand/collapse h - collapse l - expand d/t/r/s/i/c - show default/type/repr/str/id/custom for this variable H - toggle highlighting @ - toggle repetition at top * - cycle attribute visibility: public/_private/__dunder__ m - toggle method visibility w - toggle line wrapping n/insert - add new watch expression delete - remove watch expression e - edit options Keys in stack list: enter - jump to frame Ctrl-e - open file at line to edit with $EDITOR Keys in breakpoints list: enter - jump to breakpoint b - toggle breakpoint d - delete breakpoint e - edit breakpoint Other keys: j/k - down/up l/h - right/left Ctrl-f/b - page down/up Ctrl-d/u - page down/up G/g - end/home {CONFIG["hotkeys_variables"]} - focus variables {CONFIG["hotkeys_stack"]} - focus stack {CONFIG["hotkeys_breakpoints"]} - focus breakpoint list {CONFIG["hotkeys_code"]} - focus code F1/? - show this help screen q - quit Ctrl-l - redraw screen """ HELP_LICENSE = r""" License: -------- PuDB is licensed to you under the MIT/X Consortium license: Copyright (c) 2009-16 Andreas Kloeckner and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # {{{ debugger interface class Debugger(bdb.Bdb): _current_debugger = [] def __init__(self, stdin=None, stdout=None, term_size=None, steal_output=False, _continue_at_start=False, tty_file=None, **kwargs): if Debugger._current_debugger: raise ValueError("a Debugger instance already exists") # Pass remaining kwargs to python debugger framework bdb.Bdb.__init__(self, **kwargs) self.ui = DebuggerUI(self, stdin=stdin, stdout=stdout, term_size=term_size) self.steal_output = steal_output self._continue_at_start__setting = _continue_at_start self._tty_file = tty_file self.setup_state() if steal_output: raise NotImplementedError("output stealing") from io import StringIO self.stolen_output = sys.stderr = sys.stdout = StringIO() sys.stdin = StringIO("") # avoid spurious hangs from pudb.settings import load_breakpoints for bpoint_descr in load_breakpoints(): self.set_break(*bpoint_descr) # Okay, now we have a debugger self._current_debugger.append(self) def __del__(self): # according to https://stackoverflow.com/a/1481512/1054322, the garbage # collector cannot be relied on to call this, so we call it explicitly # in a finally (see __init__.py:runscript). But then, the garbage # collector *might* call it, so it should tolerate being called twice. if self._current_debugger: assert self._current_debugger == [self] self._current_debugger.pop() if self._tty_file: self._tty_file.close() self._tty_file = None def set_jump(self, frame, line): frame.f_lineno = line def set_trace(self, frame=None, as_breakpoint=None, paused=True): """Start debugging from `frame`. If frame is not specified, debugging starts from caller's frame. Unlike Bdb.set_trace(), this does not call self.reset(), which causes the debugger to enter bdb source code. This also implements treating set_trace() calls as breakpoints in the PuDB UI. If as_breakpoint=True (the default), this call will be treated like a breakpoint in the UI (you can press 'b' on it to disable breaking here). If paused=False, the debugger will not break here. """ if as_breakpoint is None: if not paused: as_breakpoint = False else: as_breakpoint = True if frame is None: frame = sys._getframe().f_back assert frame is not None # See pudb issue #52. If this works well enough we should upstream to # stdlib bdb.py. # self.reset() if paused: self.enterframe = frame thisframe = frame while thisframe: thisframe.f_trace = self.trace_dispatch self.botframe = thisframe if sys.version_info >= (3, 13): # save trace flags, to be restored by set_continue self.frame_trace_lines_opcodes[thisframe] = ( # pylint: disable=no-member thisframe.f_trace_lines, thisframe.f_trace_opcodes) # We need f_trace_lines == True for the debugger to work thisframe.f_trace_lines = True thisframe = thisframe.f_back frame_info = (self.canonic(frame.f_code.co_filename), frame.f_lineno) if frame_info not in self.set_traces or self.set_traces[frame_info]: if as_breakpoint: self.set_traces[frame_info] = True if self.ui.source_code_provider is not None: self.ui.set_source_code_provider( self.ui.source_code_provider, force_update=True) if paused: self._set_stopinfo(frame, None) else: self.set_continue() sys.settrace(self.trace_dispatch) else: return def save_breakpoints(self): from pudb.settings import save_breakpoints save_breakpoints([ bp for fn, bp_lst in self.get_all_breaks().items() for lineno in bp_lst for bp in self.get_breaks(fn, lineno) if not bp.temporary]) def enter_post_mortem(self, exc_tuple): self.post_mortem = True def setup_state(self): self.bottom_frame = None self.mainpyfile = "" self._wait_for_mainpyfile = False self._continue_at_start = self._continue_at_start__setting self.current_bp = None self.post_mortem = False # Mapping of (filename, lineno) to bool. If True, will stop on the # set_trace() call at that location. self.set_traces = {} def restart(self): from linecache import checkcache checkcache() self.ui.set_source_code_provider(NullSourceCodeProvider()) self.setup_state() def do_clear(self, arg): self.clear_bpbynumber(int(arg)) def set_frame_index(self, index): self.curindex = index if index < 0 or index >= len(self.stack): return self.curframe, lineno = self.stack[index] filename = self.curframe.f_code.co_filename import linecache if not linecache.getlines(filename): code = self.curframe.f_globals.get("_MODULE_SOURCE_CODE") if code is not None: self.ui.set_current_line(lineno, DirectSourceCodeProvider( self.curframe.f_code.co_name, code)) else: self.ui.set_current_line(lineno, NullSourceCodeProvider()) else: self.ui.set_current_line(lineno, FileSourceCodeProvider(self, filename)) self.ui.update_var_view() self.ui.update_stack() self.ui.stack_list._w.set_focus(self.ui.translate_ui_stack_index(index)) @staticmethod def open_file_to_edit(filename, line_number): if not os.path.isfile(filename): raise FileNotFoundError(f"'{filename}' not found or is not a file.") if not line_number: line_number = 1 editor = os.environ.get("EDITOR", "nano") import subprocess subprocess.call([editor, f"+{line_number}", filename], shell=False) return filename def move_up_frame(self): if self.curindex > 0: self.set_frame_index(self.curindex-1) def move_down_frame(self): if self.curindex < len(self.stack)-1: self.set_frame_index(self.curindex+1) def get_shortened_stack(self, frame, tb): if tb is not None: frame = None stack, index = self.get_stack(frame, tb) for i, (s_frame, _lineno) in enumerate(stack): if s_frame is self.bottom_frame and index >= i: stack = stack[i:] index -= i return stack, index def interaction(self, frame, exc_tuple=None, show_exc_dialog=True): if exc_tuple is None: tb = None elif isinstance(exc_tuple, TracebackType): # For API compatibility with other debuggers, the second variable # can be a traceback object. In that case, we need to retrieve the # corresponding exception tuple. tb = exc_tuple exc, = (exc for exc in gc.get_referrers(tb) if getattr(exc, "__traceback__", None) is tb) exc_tuple = type(exc), exc, tb else: tb = exc_tuple[2] if frame is None: assert tb is not None walk_frame = tb.tb_frame else: walk_frame = frame found_bottom_frame = False while True: if walk_frame is self.bottom_frame: found_bottom_frame = True break if walk_frame is None: break walk_frame = walk_frame.f_back if not found_bottom_frame and not self.post_mortem: # We aren't supposed to be debugging this. return self.stack, index = self.get_shortened_stack(frame, tb) if self.post_mortem: index = len(self.stack)-1 self.set_frame_index(index) self.ui.call_with_ui(self.ui.interaction, exc_tuple, show_exc_dialog=show_exc_dialog) def get_stack_situation_id(self): return str(id(self.stack[self.curindex][0].f_code)) def user_call(self, frame, argument_list): """This method is called when there is the remote possibility that we ever need to stop in this function.""" if self._wait_for_mainpyfile: return if self.stop_here(frame): self.interaction(frame) def user_line(self, frame): """This function is called when we stop or break at this line.""" if self._waiting_for_mainpyfile(frame): return if self.get_break(self.canonic(frame.f_code.co_filename), frame.f_lineno): self.current_bp = ( self.canonic(frame.f_code.co_filename), frame.f_lineno) else: self.current_bp = None try: self.ui.update_breakpoints() self.interaction(frame) except Exception: self.ui.show_internal_exc_dlg(sys.exc_info()) def user_return(self, frame, return_value): """This function is called when a return trap is set here.""" if frame.f_code.co_name != "": frame.f_locals["__return__"] = return_value if self._waiting_for_mainpyfile(frame): return if "__exception__" not in frame.f_locals: self.interaction(frame) def _waiting_for_mainpyfile(self, frame): if self._wait_for_mainpyfile: if (self.mainpyfile != self.canonic(frame.f_code.co_filename) or frame.f_lineno <= 0): return True self._wait_for_mainpyfile = False self.bottom_frame = frame if self._continue_at_start: self._continue_at_start = False self.set_continue() return True return False def user_exception(self, frame, exc_info): """This function is called if an exception occurs, but only if we are to stop at or just below this level.""" exc_type, exc_value, _exc_traceback = exc_info frame.f_locals["__exception__"] = exc_type, exc_value if not self._wait_for_mainpyfile: self.interaction(frame, exc_info) # {{{ entrypoints def _runscript(self, filename): # Provide separation from current __main__, which is likely # pudb.__main__ run. Preserving its namespace is not important, and # having the script share it ensures that, e.g., pickle can find # types defined there: # https://github.com/inducer/pudb/issues/331 import __main__ __main__.__dict__.clear() __main__.__dict__.update({ "__name__": "__main__", "__file__": filename, "__builtins__": __builtins__, }) # When bdb sets tracing, a number of call and line events happens # BEFORE debugger even reaches user's code (and the exact sequence of # events depends on python version). So we take special measures to # avoid stopping before we reach the main script (see user_line and # user_call for details). self._wait_for_mainpyfile = True self.mainpyfile = self.canonic(filename) statement = 'exec(compile(open("{}").read(), "{}", "exec"))'.format( filename, filename) # Set up an interrupt handler from pudb import set_interrupt_handler set_interrupt_handler() # Implicitly runs in the namespace of __main__. self.run(statement) def _runmodule(self, module_name): # This is basically stolen from the pdb._runmodule from CPython 3.8 # https://github.com/python/cpython/blob/a1d3be4623c8ec7069bd34ccdce336be9cdeb644/Lib/pdb.py#L1530 import runpy _mod_name, mod_spec, code = runpy._get_module_details(module_name) self.mainpyfile = self.canonic(code.co_filename) import __main__ __main__.__dict__.clear() __main__.__dict__.update({ "__name__": "__main__", "__file__": self.mainpyfile, "__spec__": mod_spec, "__builtins__": __builtins__, "__package__": mod_spec.parent, "__loader__": mod_spec.loader, }) self._wait_for_mainpyfile = True self.run(code) def runstatement(self, statement, globals=None, locals=None): try: return self.run(statement, globals, locals) except Exception: self.post_mortem = True self.interaction(None, sys.exc_info()) raise def runeval(self, expression, globals=None, locals=None): try: return super().runeval(expression, globals, locals) except Exception: self.post_mortem = True self.interaction(None, sys.exc_info()) raise def runcall(self, *args, **kwargs): try: return super().runcall(*args, **kwargs) except Exception: self.post_mortem = True self.interaction(None, sys.exc_info()) raise # }}} # }}} # UI stuff -------------------------------------------------------------------- from pudb.ui_tools import ( BreakpointFrame, SelectableText, SignalWrap, StackFrame, focus_widget_in_container, labelled_value, make_hotkey_markup, ) from pudb.var_view import FrameVarInfoKeeper # {{{ display setup try: import curses except ImportError: curses = None from urwid.display.raw import Screen as RawScreen try: from urwid.display.curses import Screen as CursesScreen except ImportError: CursesScreen = None class ThreadsafeScreenMixin: """A Screen subclass that doesn't crash when running from a non-main thread.""" def signal_init(self): """Initialize signal handler, ignoring errors silently.""" try: super().signal_init() except ValueError: pass def signal_restore(self): """Restore default signal handler, ignoring errors silently.""" try: super().signal_restore() except ValueError: pass class ThreadsafeRawScreen(ThreadsafeScreenMixin, RawScreen): pass class ThreadsafeFixedSizeRawScreen(ThreadsafeScreenMixin, RawScreen): def __init__(self, **kwargs): self._term_size = kwargs.pop("term_size", None) super().__init__(**kwargs) def get_cols_rows(self): if self._term_size is not None: return self._term_size else: return 80, 24 if curses is not None: class ThreadsafeCursesScreen(ThreadsafeScreenMixin, RawScreen): pass # }}} # {{{ source code providers class SourceCodeProvider: def __ne__(self, other): return not (self == other) class NullSourceCodeProvider(SourceCodeProvider): def __eq__(self, other): return type(self) is type(other) def identifier(self): return "" def get_source_identifier(self): return None def clear_cache(self): pass def get_lines(self, debugger_ui): from pudb.source_view import SourceLine return [ SourceLine(debugger_ui, ""), SourceLine(debugger_ui, ""), SourceLine(debugger_ui, "If this is generated code and you would " "like the source code to show up here,"), SourceLine(debugger_ui, "add it to linecache.cache, like"), SourceLine(debugger_ui, ""), SourceLine(debugger_ui, " import linecache"), SourceLine(debugger_ui, " linecache.cache[filename] = " "(size, mtime, lines, fullname)"), SourceLine(debugger_ui, ""), SourceLine(debugger_ui, "You can also set the attribute " "_MODULE_SOURCE_CODE in the module in which this function"), SourceLine(debugger_ui, "was compiled to a string containing " "the code."), ] class FileSourceCodeProvider(SourceCodeProvider): def __init__(self, debugger, file_name): self.file_name = debugger.canonic(file_name) def __eq__(self, other): return type(self) is type(other) and self.file_name == other.file_name def identifier(self): return self.file_name def get_source_identifier(self): return self.file_name def clear_cache(self): from linecache import clearcache clearcache() def get_lines(self, debugger_ui): from pudb.source_view import SourceLine, format_source if self.file_name == "": return [SourceLine(debugger_ui, self.file_name)] breakpoints = debugger_ui.debugger.get_file_breaks(self.file_name)[:] breakpoints = [lineno for lineno in breakpoints if any(bp.enabled for bp in debugger_ui.debugger.get_breaks(self.file_name, lineno))] breakpoints += [i for f, i in debugger_ui.debugger.set_traces if f == self.file_name and debugger_ui.debugger.set_traces[f, i]] try: from linecache import getlines lines = getlines(self.file_name) return format_source( debugger_ui, list(decode_lines(lines)), set(breakpoints)) except Exception: from pudb.lowlevel import format_exception debugger_ui.message("Could not load source file '{}':\n\n{}".format( self.file_name, "".join(format_exception(sys.exc_info()))), title="Source Code Load Error") return [SourceLine(debugger_ui, "Error while loading '%s'." % self.file_name)] class DirectSourceCodeProvider(SourceCodeProvider): def __init__(self, func_name, code): self.function_name = func_name self.code = code def __eq__(self, other): return ( type(self) is type(other) and self.function_name == other.function_name and self.code is other.code) def identifier(self): return "" % self.function_name def get_source_identifier(self): return None def clear_cache(self): pass def get_lines(self, debugger_ui): from pudb.source_view import format_source lines = self.code.splitlines(True) return format_source(debugger_ui, list(decode_lines(lines)), set()) # }}} class StoppedScreen: def __init__(self, screen): self.screen = screen def __enter__(self): self.screen.stop() def __exit__(self, exc_type, exc_value, exc_traceback): self.screen.start() class DebuggerUI(FrameVarInfoKeeper): # {{{ constructor def __init__(self, dbg, stdin, stdout, term_size): FrameVarInfoKeeper.__init__(self) self.debugger = dbg from urwid import AttrMap from pudb.ui_tools import SearchController self.search_controller = SearchController(self) self.last_module_filter = "" # {{{ build ui # {{{ key bindings def move_up(w, size, key): w.keypress(size, "up") def move_down(w, size, key): w.keypress(size, "down") def move_left(w, size, key): w.keypress(size, "left") def move_right(w, size, key): w.keypress(size, "right") def page_up(w, size, key): w.keypress(size, "page up") def page_down(w, size, key): w.keypress(size, "page down") def move_home(w, size, key): w.keypress(size, "home") def move_end(w, size, key): w.keypress(size, "end") def add_vi_nav_keys(widget): widget.listen("k", move_up) widget.listen("j", move_down) widget.listen("h", move_left) widget.listen("l", move_right) widget.listen("ctrl b", page_up) widget.listen("ctrl f", page_down) widget.listen("ctrl u", page_up) widget.listen("ctrl d", page_down) widget.listen("g", move_home) widget.listen("G", move_end) def add_help_keys(widget, helpfunc): widget.listen("f1", helpfunc) widget.listen("?", helpfunc) # }}} # {{{ left/source column self.source = urwid.SimpleListWalker([]) self.source_list = urwid.ListBox(self.source) self.source_sigwrap = SignalWrap(self.source_list) self.source_attr = urwid.AttrMap(self.source_sigwrap, "source") self.source_hscroll_start = 0 self.cmdline_contents = urwid.SimpleFocusListWalker([]) self.cmdline_list = urwid.ListBox(self.cmdline_contents) import urwid_readline self.cmdline_edit = urwid_readline.ReadlineEdit([ ("command line prompt", ">>> ") ]) cmdline_edit_attr = urwid.AttrMap(self.cmdline_edit, "command line edit") self.cmdline_edit_sigwrap = SignalWrap( cmdline_edit_attr, is_preemptive=True) def clear_cmdline_history(btn): del self.cmdline_contents[:] # clear the command input text too, # but save it to be retrieved on cmdline_history_prev() self.cmdline_history_position = -1 cmdline_history_browse(1) def initialize_cmdline_history(path): dq = partial(deque, maxlen=5000) try: # Load global history if present with open(path, "r") as histfile: return dq(histfile.read().splitlines()) except FileNotFoundError: return dq() self.cmdline_history_path = os.path.join(get_save_config_path(), "internal-cmdline-history.txt") self.cmdline_history = initialize_cmdline_history(self.cmdline_history_path) self.cmdline_saved_edit_text = "" self.cmdline_history_position = -1 self.cmdline_edit_bar = urwid.Columns([ self.cmdline_edit_sigwrap, (urwid.FIXED, 10, AttrMap( urwid.Button("Clear", clear_cmdline_history), "command line clear button", "command line focused button")) ]) self.cmdline_pile = urwid.Pile([ (urwid.FLOW, urwid.Text( f"Command line: [{CONFIG['hotkeys_toggle_cmdline_focus']}]")), (urwid.WEIGHT, 1, urwid.AttrMap( self.cmdline_list, "command line output")), (urwid.FLOW, self.cmdline_edit_bar), ]) self.cmdline_sigwrap = SignalWrap( urwid.AttrMap(self.cmdline_pile, None, "focused sidebar") ) self.cmdline_on = not CONFIG["hide_cmdline_win"] self.cmdline_weight = float(CONFIG.get("cmdline_height", 1)) self.lhs_col = urwid.Pile([ (urwid.WEIGHT, 5, self.source_attr), (urwid.WEIGHT, self.cmdline_weight if self.cmdline_on else 0, self.cmdline_sigwrap), ]) # }}} # {{{ right column self.locals = urwid.SimpleListWalker([]) self.var_list = SignalWrap( urwid.ListBox(self.locals)) self.stack_walker = urwid.SimpleListWalker([]) self.stack_list = SignalWrap( urwid.ListBox(self.stack_walker)) self.bp_walker = urwid.SimpleListWalker([]) self.bp_list = SignalWrap( urwid.ListBox(self.bp_walker)) self.rhs_col = urwid.Pile([ (urwid.WEIGHT, float(CONFIG["variables_weight"]), AttrMap(urwid.Pile([ (urwid.FLOW, urwid.Text(make_hotkey_markup("_Variables:"))), AttrMap(self.var_list, "variables"), ]), None, "focused sidebar"),), (urwid.WEIGHT, float(CONFIG["stack_weight"]), AttrMap(urwid.Pile([ (urwid.FLOW, urwid.Text(make_hotkey_markup("_Stack:"))), AttrMap(self.stack_list, "stack"), ]), None, "focused sidebar"),), (urwid.WEIGHT, float(CONFIG["breakpoints_weight"]), AttrMap(urwid.Pile([ (urwid.FLOW, urwid.Text(make_hotkey_markup("_Breakpoints:"))), AttrMap(self.bp_list, "breakpoint"), ]), None, "focused sidebar"),), ]) self.rhs_col_sigwrap = SignalWrap(self.rhs_col) def helpside(w, size, key): help(HELP_HEADER + HELP_SIDE + HELP_MAIN + HELP_LICENSE) add_vi_nav_keys(self.rhs_col_sigwrap) add_help_keys(self.rhs_col_sigwrap, helpside) # }}} self.columns = urwid.Columns( [ (urwid.WEIGHT, 1, self.lhs_col), (urwid.WEIGHT, float(CONFIG["sidebar_width"]), self.rhs_col_sigwrap), ], dividechars=1) self.caption = urwid.Text("") header = urwid.AttrMap(self.caption, "header") self.top = SignalWrap(urwid.Frame( urwid.AttrMap(self.columns, "background"), header)) # }}} def change_rhs_box(name, index, direction, w, size, key): from pudb.settings import save_config weight = self.rhs_col.contents[index][1][1] if direction < 0: if weight > 1/5: weight /= 1.25 else: if weight < 5: weight *= 1.25 CONFIG[name+"_weight"] = weight save_config(CONFIG) self.rhs_col.contents[index] = ( self.rhs_col.contents[index][0], (urwid.WEIGHT, weight)) self.rhs_col._invalidate() # {{{ variables listeners def get_inspect_info(id_path, read_only=False): return (self.get_frame_var_info(read_only) .get_inspect_info(id_path, read_only)) def collapse_current(var, pos, iinfo): if iinfo.show_detail: # collapse current variable iinfo.show_detail = False else: # collapse parent/container variable if var.parent is not None: p_iinfo = get_inspect_info(var.parent.id_path) p_iinfo.show_detail = False return self.locals.index(var.parent) return None def change_var_state(w, size, key): try: pos = self.var_list._w.focus_position except IndexError: return var = self.var_list._w.focus if var is None: return iinfo = get_inspect_info(var.id_path) focus_index = None if key == "enter" or key == "\\" or key == " ": iinfo.show_detail = not iinfo.show_detail elif key == "h": focus_index = collapse_current(var, pos, iinfo) elif key == "l": iinfo.show_detail = True elif key == "d": iinfo.display_type = "default" elif key == "t": iinfo.display_type = "type" elif key == "r": iinfo.display_type = "repr" elif key == "s": iinfo.display_type = "str" elif key == "i": iinfo.display_type = "id" elif key == "c": iinfo.display_type = CONFIG["custom_stringifier"] elif key == "H": iinfo.highlighted = not iinfo.highlighted elif key == "@": iinfo.repeated_at_top = not iinfo.repeated_at_top elif key == "*": levels = ["public", "private", "all", "public"] iinfo.access_level = levels[levels.index(iinfo.access_level)+1] elif key == "w": iinfo.wrap = not iinfo.wrap elif key == "m": iinfo.show_methods = not iinfo.show_methods elif key == "delete": fvi = self.get_frame_var_info(read_only=False) for i, watch_expr in enumerate(fvi.watches): if watch_expr is var.watch_expr: del fvi.watches[i] break self.update_var_view(focus_index=focus_index) def edit_inspector_detail(w, size, key): var = self.var_list._w.focus if var is None: return fvi = self.get_frame_var_info(read_only=False) iinfo = fvi.get_inspect_info(var.id_path, read_only=False) buttons = [ ("OK", True), ("Cancel", False), ] if var.watch_expr is not None: watch_edit = urwid.Edit([ ("label", "Watch expression: ") ], var.watch_expr.expression) id_segment = [ urwid.AttrMap(watch_edit, "input", "focused input"), urwid.Text(""), ] buttons.extend([None, ("Delete", "del")]) title = "Watch Expression Options" else: id_segment = [ labelled_value("Identifier Path: ", var.id_path), urwid.Text(""), ] title = "Variable Inspection Options" rb_grp_show = [] rb_show_default = urwid.RadioButton(rb_grp_show, "Default", iinfo.display_type == "default") rb_show_type = urwid.RadioButton(rb_grp_show, "Show type()", iinfo.display_type == "type") rb_show_repr = urwid.RadioButton(rb_grp_show, "Show repr()", iinfo.display_type == "repr") rb_show_str = urwid.RadioButton(rb_grp_show, "Show str()", iinfo.display_type == "str") rb_show_id = urwid.RadioButton(rb_grp_show, "Show id()", iinfo.display_type == "id") rb_show_custom = urwid.RadioButton( rb_grp_show, "Show custom (set in prefs)", iinfo.display_type == CONFIG["custom_stringifier"]) rb_grp_access = [] rb_access_public = urwid.RadioButton(rb_grp_access, "Public members", iinfo.access_level == "public") rb_access_private = urwid.RadioButton( rb_grp_access, "Public and private members", iinfo.access_level == "private") rb_access_all = urwid.RadioButton( rb_grp_access, "All members (including __dunder__)", iinfo.access_level == "all") wrap_checkbox = urwid.CheckBox("Line Wrap", iinfo.wrap) expanded_checkbox = urwid.CheckBox("Expanded", iinfo.show_detail) highlighted_checkbox = urwid.CheckBox("Highlighted", iinfo.highlighted) repeated_at_top_checkbox = urwid.CheckBox( "Repeated at top", iinfo.repeated_at_top) show_methods_checkbox = urwid.CheckBox( "Show methods", iinfo.show_methods) lb = urwid.ListBox(urwid.SimpleListWalker( id_segment + rb_grp_show + [urwid.Text("")] + rb_grp_access + [urwid.Text("")] + [ wrap_checkbox, expanded_checkbox, highlighted_checkbox, repeated_at_top_checkbox, show_methods_checkbox, ])) result = self.dialog(lb, buttons, title=title) if result is True: iinfo.show_detail = expanded_checkbox.get_state() iinfo.wrap = wrap_checkbox.get_state() iinfo.highlighted = highlighted_checkbox.get_state() iinfo.repeated_at_top = repeated_at_top_checkbox.get_state() iinfo.show_methods = show_methods_checkbox.get_state() if rb_show_default.get_state(): iinfo.display_type = "default" elif rb_show_type.get_state(): iinfo.display_type = "type" elif rb_show_repr.get_state(): iinfo.display_type = "repr" elif rb_show_str.get_state(): iinfo.display_type = "str" elif rb_show_id.get_state(): iinfo.display_type = "id" elif rb_show_custom.get_state(): iinfo.display_type = CONFIG["custom_stringifier"] if rb_access_public.get_state(): iinfo.access_level = "public" elif rb_access_private.get_state(): iinfo.access_level = "private" elif rb_access_all.get_state(): iinfo.access_level = "all" if var.watch_expr is not None: var.watch_expr.expression = watch_edit.get_edit_text() elif result == "del": for i, watch_expr in enumerate(fvi.watches): if watch_expr is var.watch_expr: del fvi.watches[i] break self.update_var_view() def insert_watch(w, size, key): watch_edit = urwid.Edit([ ("label", "Watch expression: ") ]) if self.dialog( urwid.ListBox(urwid.SimpleListWalker([ urwid.AttrMap(watch_edit, "input", "focused input") ])), [ ("OK", True), ("Cancel", False), ], title="Add Watch Expression"): from pudb.var_view import WatchExpression we = WatchExpression(watch_edit.get_edit_text()) fvi = self.get_frame_var_info(read_only=False) fvi.watches.append(we) self.update_var_view() self.var_list.listen("\\", change_var_state) self.var_list.listen(" ", change_var_state) self.var_list.listen("h", change_var_state) self.var_list.listen("l", change_var_state) self.var_list.listen("d", change_var_state) self.var_list.listen("t", change_var_state) self.var_list.listen("r", change_var_state) self.var_list.listen("s", change_var_state) self.var_list.listen("i", change_var_state) self.var_list.listen("c", change_var_state) self.var_list.listen("H", change_var_state) self.var_list.listen("@", change_var_state) self.var_list.listen("*", change_var_state) self.var_list.listen("w", change_var_state) self.var_list.listen("m", change_var_state) self.var_list.listen("enter", change_var_state) self.var_list.listen("e", edit_inspector_detail) self.var_list.listen("n", insert_watch) self.var_list.listen("insert", insert_watch) self.var_list.listen("delete", change_var_state) self.var_list.listen("[", partial(change_rhs_box, "variables", 0, -1)) self.var_list.listen("]", partial(change_rhs_box, "variables", 0, 1)) # }}} # {{{ stack listeners def examine_frame(w, size, key): pos = self.stack_list._w.focus_position self.debugger.set_frame_index(self.translate_ui_stack_index(pos)) self.stack_list.listen("enter", examine_frame) def open_file_editor(file_name, line_number): file_changed = False try: original_modification_time = os.path.getmtime(file_name) with StoppedScreen(self.screen): filename_edited = self.debugger.open_file_to_edit(file_name, line_number) new_modification_time = os.path.getmtime(file_name) file_changed = new_modification_time - original_modification_time > 0 except Exception: from traceback import format_exception self.message("Exception happened when trying to edit the file:" "\n\n%s" % ("".join(format_exception(*sys.exc_info()))), title="File Edit Error") return if file_changed: self.message("File is changed, but the execution is continued with" " the 'old' codebase.\n" f"Changed file: {filename_edited}\n\n" "Please quit and restart to see changes", title="File is changed") def open_editor_on_stack_frame(w, size, key): pos = self.stack_list._w.focus_position index = self.translate_ui_stack_index(pos) curframe, line_number = self.debugger.stack[index] file_name = curframe.f_code.co_filename open_file_editor(file_name, line_number) self.stack_list.listen("ctrl e", open_editor_on_stack_frame) def move_stack_top(w, size, key): self.debugger.set_frame_index(len(self.debugger.stack)-1) def move_stack_up(w, size, key): self.debugger.move_up_frame() def move_stack_down(w, size, key): self.debugger.move_down_frame() self.stack_list.listen("H", move_stack_top) self.stack_list.listen("u", move_stack_up) self.stack_list.listen("d", move_stack_down) self.stack_list.listen("[", partial(change_rhs_box, "stack", 1, -1)) self.stack_list.listen("]", partial(change_rhs_box, "stack", 1, 1)) # }}} # {{{ breakpoint listeners def set_breakpoint_source(bp): bp_source_identifier = \ self.source_code_provider.get_source_identifier() if (bp.file and bp_source_identifier == bp.file and bp.line-1 < len(self.source)): self.source[bp.line-1].set_breakpoint(bp.enabled) def save_breakpoints(w, size, key): self.debugger.save_breakpoints() def handle_delete_breakpoint(w, size, key): bp_list = self._get_bp_list() if bp_list: pos = self.bp_list._w.focus_position bp = bp_list[pos] delete_breakpoint(bp) def delete_breakpoint(bp): err = self.debugger.clear_break(bp.file, bp.line) if err: self.message("Error clearing breakpoint:\n" + err) else: bp.enabled = False self.update_breakpoints() set_breakpoint_source(bp) def enable_disable_breakpoint(w, size, key): pos = self.bp_list._w.focus_position bp_entry = self.bp_list._w.focus if bp_entry is None: return bp = self._get_bp_list()[pos] bp.enabled = not bp.enabled self.update_breakpoints() set_breakpoint_source(bp) def examine_breakpoint(w, size, key): pos = self.bp_list._w.focus_position bp_entry = self.bp_list._w.focus if bp_entry is None: return bp = self._get_bp_list()[pos] if bp.cond is None: cond = "" else: cond = str(bp.cond) enabled_checkbox = urwid.CheckBox( "Enabled", bp.enabled) cond_edit = urwid.Edit([ ("label", "Condition: ") ], cond) ign_count_edit = urwid.IntEdit([ ("label", "Ignore the next N times: ") ], bp.ignore) lb = urwid.ListBox(urwid.SimpleListWalker([ labelled_value("File: ", bp.file), labelled_value("Line: ", bp.line), labelled_value("Hits: ", bp.hits), urwid.Text(""), enabled_checkbox, urwid.AttrMap(cond_edit, "input", "focused input"), urwid.AttrMap(ign_count_edit, "input", "focused input"), ])) result = self.dialog(lb, [ ("OK", True), ("Cancel", False), None, ("Delete", "del"), ("Location", "loc"), ], title="Edit Breakpoint") if result is True: bp.enabled = enabled_checkbox.get_state() bp.ignore = int(ign_count_edit.value()) cond = cond_edit.get_edit_text() if cond: bp.cond = cond else: bp.cond = None elif result == "loc": self.show_line(bp.line, FileSourceCodeProvider(self.debugger, bp.file)) self.columns.focus_position = 0 elif result == "del": delete_breakpoint(bp) self.update_breakpoints() set_breakpoint_source(bp) def show_breakpoint(w, size, key): pos = self.bp_list._w.focus_position bp_entry = self.bp_list._w.focus if bp_entry is not None: bp = self._get_bp_list()[pos] self.show_line(bp.line, FileSourceCodeProvider(self.debugger, bp.file)) self.columns.focus_position = 0 self.bp_list.listen("enter", show_breakpoint) self.bp_list.listen("d", handle_delete_breakpoint) self.bp_list.listen("s", save_breakpoints) self.bp_list.listen("e", examine_breakpoint) self.bp_list.listen("b", enable_disable_breakpoint) self.bp_list.listen("H", move_stack_top) self.bp_list.listen("[", partial(change_rhs_box, "breakpoints", 2, -1)) self.bp_list.listen("]", partial(change_rhs_box, "breakpoints", 2, 1)) # }}} # {{{ source listeners def end(): self.debugger.save_breakpoints() self.quit_event_loop = True def next_line(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: self.debugger.set_next(self.debugger.curframe) end() def step(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: self.debugger.set_step() end() def finish(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: self.debugger.set_return(self.debugger.curframe) end() def cont(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: self.debugger.set_continue() end() def run_to_cursor(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: pos = self.source.focus lineno = pos+1 bp_source_identifier = \ self.source_code_provider.get_source_identifier() if bp_source_identifier is None: self.message( "Cannot currently set a breakpoint here--" "source code does not correspond to a file location. " "(perhaps this is generated code)") from pudb.lowlevel import get_breakpoint_invalid_reason invalid_reason = get_breakpoint_invalid_reason( bp_source_identifier, lineno) if invalid_reason is not None: self.message( "Cannot run to the line you indicated, " "for the following reason:\n\n" + invalid_reason) else: err = self.debugger.set_break( bp_source_identifier, pos+1, temporary=True) if err: self.message("Error dealing with breakpoint:\n" + err) self.debugger.set_continue() end() def jump_to_cursor(w, size, key): if self.debugger.post_mortem: self.message("Post-mortem mode: Can't modify state.") else: pos = self.source.focus lineno = pos+1 bp_source_identifier = \ self.source_code_provider.get_source_identifier() if bp_source_identifier is None: self.message( "Cannot jump here--" "source code does not correspond to a file location. " "(perhaps this is generated code)") from pudb.lowlevel import get_breakpoint_invalid_reason invalid_reason = get_breakpoint_invalid_reason( bp_source_identifier, lineno) if invalid_reason is not None: self.message( "Cannot jump to the line you indicated, " "for the following reason:\n\n" + invalid_reason) else: try: self.debugger.set_jump( self.debugger.curframe, lineno) self.debugger.stack[self.debugger.curindex] = \ self.debugger.stack[self.debugger.curindex][0], lineno self.debugger.set_step() except ValueError as e: self.message("""\ Error with jump. Note that jumping only works on the topmost stack frame. (The error was: %s)""" % (e.args[0],)) # Update UI. end() will run past the line self.set_current_line(lineno, self.source_code_provider) self.update_stack() def go_to_line(w, size, key): line = self.source.focus lineno_edit = urwid.IntEdit([ ("label", "Go to Line :") ], None) if self.dialog( urwid.ListBox(urwid.SimpleListWalker([ labelled_value("File :", self.source_code_provider.identifier()), labelled_value("Current Line :", line+1), urwid.AttrMap(lineno_edit, "input", "focused input") ])), [ ("OK", True), ("Cancel", False), ], title="Go to Line Number"): value = lineno_edit.value() if value: lineno = min(max(0, int(value)-1), len(self.source)-1) self.source_list.focus_position = lineno def scroll_left(w, size, key): self.source_hscroll_start = max( 0, self.source_hscroll_start - 4) for sl in self.source: sl._invalidate() def scroll_right(w, size, key): self.source_hscroll_start += 4 for sl in self.source: sl._invalidate() def search(w, size, key): self.search_controller.open_search_ui() def search_next(w, size, key): self.search_controller.perform_search(dir=1, update_search_start=True) def search_previous(w, size, key): self.search_controller.perform_search(dir=-1, update_search_start=True) def toggle_breakpoint(w, size, key): bp_source_identifier = \ self.source_code_provider.get_source_identifier() if bp_source_identifier: pos = self.source_list.focus_position sline = self.source[pos] lineno = pos+1 existing_breaks = self.debugger.get_breaks( bp_source_identifier, lineno) if existing_breaks: err = None for bp in existing_breaks: if not bp.enabled: bp.enable() sline.set_breakpoint(True) # Unsure about this. Are multiple breakpoints even # possible? break else: err = self.debugger.clear_break(bp_source_identifier, lineno) sline.set_breakpoint(False) else: file_lineno = (bp_source_identifier, lineno) if file_lineno in self.debugger.set_traces: self.debugger.set_traces[file_lineno] = \ not self.debugger.set_traces[file_lineno] sline.set_breakpoint(self.debugger.set_traces[file_lineno]) return from pudb.lowlevel import get_breakpoint_invalid_reason invalid_reason = get_breakpoint_invalid_reason( bp_source_identifier, pos+1) if invalid_reason is not None: do_set = not self.dialog( urwid.ListBox( urwid.SimpleListWalker([ urwid.Text( "The breakpoint you just set may be " "invalid, for the following reason:\n\n" + invalid_reason), ])), [ ("Cancel", True), ("Set Anyway", False), ], title="Possibly Invalid Breakpoint", focus_buttons=True) else: do_set = True if do_set: err = self.debugger.set_break(bp_source_identifier, pos+1) sline.set_breakpoint(True) else: err = None if err: self.message("Error dealing with breakpoint:\n" + err) self.update_breakpoints() else: self.message( "Cannot currently set a breakpoint here--" "source code does not correspond to a file location. " "(perhaps this is generated code)") def pick_module(w, size, key): import sys from os.path import splitext def mod_exists(mod): if not hasattr(mod, "__file__"): return False if mod.__file__ is None: return False filename = mod.__file__ base, ext = splitext(filename) ext = ext.lower() from os.path import exists if ext == ".pyc": return exists(base+".py") else: return ext == ".py" new_mod_text = SelectableText("-- update me --") new_mod_entry = urwid.AttrMap(new_mod_text, None, "focused selectable") def build_filtered_mod_list(filt_string=""): modules = sorted(name # mod_exists may change the size of sys.modules, # causing this to crash. Copy to a list. for name, mod in list(sys.modules.items()) if mod_exists(mod)) result = [urwid.AttrMap(SelectableText(mod), None, "focused selectable") for mod in modules if filt_string in mod] new_mod_text.set_text("<<< IMPORT MODULE '%s' >>>" % filt_string) result.append(new_mod_entry) return result def show_mod(mod): filename = self.debugger.canonic(mod.__file__) base, ext = splitext(filename) if ext == ".pyc": ext = ".py" filename = base+".py" self.set_source_code_provider( FileSourceCodeProvider(self.debugger, filename)) self.source_list.set_focus(0) class FilterEdit(urwid.Edit): def keypress(self, size, key): result = urwid.Edit.keypress(self, size, key) if result is None: mod_list[:] = build_filtered_mod_list( self.get_edit_text()) return result filt_edit = FilterEdit([("label", "Filter: ")], self.last_module_filter) mod_list = urwid.SimpleListWalker( build_filtered_mod_list(filt_edit.get_edit_text())) lb = urwid.ListBox(mod_list) w = urwid.Pile([ (urwid.FLOW, urwid.AttrMap(filt_edit, "input", "focused input")), (urwid.FIXED, 1, urwid.SolidFill()), urwid.AttrMap(lb, "selectable")]) while True: result = self.dialog(w, [ ("OK", True), ("Cancel", False), ("Reload", "reload"), ], title="Pick Module") self.last_module_filter = filt_edit.get_edit_text() if result is True: pos = lb.focus_position widget = lb.focus if widget is new_mod_entry: new_mod_name = filt_edit.get_edit_text() try: __import__(str(new_mod_name)) except Exception: from traceback import format_exception self.message( "Could not import module '{}':\n\n{}".format( new_mod_name, "".join( format_exception(*sys.exc_info()))), title="Import Error") else: show_mod(__import__(str(new_mod_name))) break else: show_mod(sys.modules[widget.base_widget.get_text()[0]]) break elif result is False: break elif result == "reload": pos = lb.focus_position widget = lb.focus if widget is not new_mod_entry: mod_name = widget.base_widget.get_text()[0] mod = sys.modules[mod_name] import importlib importlib.reload(mod) self.message("'%s' was successfully reloaded." % mod_name) if self.source_code_provider is not None: self.source_code_provider.clear_cache() self.set_source_code_provider(self.source_code_provider, force_update=True) pos = self.stack_list._w.focus_position self.debugger.set_frame_index( self.translate_ui_stack_index(pos)) def helpmain(w, size, key): help(HELP_HEADER + HELP_MAIN + HELP_SIDE + HELP_LICENSE) self.source_sigwrap.listen("n", next_line) self.source_sigwrap.listen("s", step) self.source_sigwrap.listen("f", finish) self.source_sigwrap.listen("r", finish) self.source_sigwrap.listen("c", cont) self.source_sigwrap.listen("t", run_to_cursor) self.source_sigwrap.listen("J", jump_to_cursor) self.source_sigwrap.listen("L", go_to_line) self.source_sigwrap.listen("/", search) self.source_sigwrap.listen(",", search_previous) self.source_sigwrap.listen(".", search_next) self.source_sigwrap.listen("b", toggle_breakpoint) self.source_sigwrap.listen("m", pick_module) self.source_sigwrap.listen("H", move_stack_top) self.source_sigwrap.listen("u", move_stack_up) self.source_sigwrap.listen("d", move_stack_down) # left/right scrolling have to be handled specially, normal vi keys # don't cut it self.source_sigwrap.listen("h", scroll_left) self.source_sigwrap.listen("l", scroll_right) add_vi_nav_keys(self.source_sigwrap) add_help_keys(self.source_sigwrap, helpmain) # }}} # {{{ command line listeners def cmdline_get_namespace(): curframe = self.debugger.curframe from pudb.shell import SetPropagatingDict return SetPropagatingDict( [curframe.f_locals, curframe.f_globals], curframe.f_locals) def cmdline_tab_complete(w, size, key): try: from jedi import Interpreter except ImportError: self.add_cmdline_content( "Tab completion requires jedi to be installed. ", "command line error") return try: from packaging.version import parse as LooseVersion # noqa: N812 except ImportError: from distutils.version import LooseVersion import jedi if LooseVersion(jedi.__version__) < LooseVersion("0.16.0"): self.add_cmdline_content( "jedi 0.16.0 is required for Tab completion", "command line error") text = self.cmdline_edit.edit_text pos = self.cmdline_edit.edit_pos chopped_text = text[:pos] suffix = text[pos:] try: completions = Interpreter( chopped_text, [cmdline_get_namespace()]).complete() except Exception as e: # Jedi sometimes produces errors. Ignore them. self.add_cmdline_content( "Could not tab complete (Jedi error: '%s')" % e, "command line error") return full_completions = [i.name_with_symbols for i in completions] chopped_completions = [i.complete for i in completions] def common_prefix(a, b): for i, (a_i, b_i) in enumerate(zip(a, b)): if a_i != b_i: return a[:i] return a[:max(len(a), len(b))] common_compl_prefix = None for completion in chopped_completions: if common_compl_prefix is None: common_compl_prefix = completion else: common_compl_prefix = common_prefix( common_compl_prefix, completion) completed_chopped_text = common_compl_prefix if completed_chopped_text is None: return if ( len(completed_chopped_text) == 0 and len(completions) > 1): self.add_cmdline_content( " ".join(full_completions), "command line output") return self.cmdline_edit.edit_text = \ chopped_text+completed_chopped_text+suffix self.cmdline_edit.edit_pos = ( len(chopped_text) + len(completed_chopped_text)) def cmdline_append_newline(w, size, key): self.cmdline_edit.insert_text("\n") def cmdline_exec(w, size, key): cmd = self.cmdline_edit.get_edit_text() if not cmd: # blank command -> refuse service return self.add_cmdline_content(">>> " + cmd, "command line input") if not self.cmdline_history or cmd != self.cmdline_history[-1]: self.cmdline_history.append(cmd) self.cmdline_history_position = -1 self.cmdline_saved_edit_text = "" prev_sys_stdin = sys.stdin prev_sys_stdout = sys.stdout prev_sys_stderr = sys.stderr from io import StringIO sys.stdin = None sys.stderr = sys.stdout = StringIO() try: eval(compile(cmd, "", "single"), cmdline_get_namespace()) except Exception: tp, val, tb = sys.exc_info() import traceback tblist = traceback.extract_tb(tb) del tblist[:1] tb_lines = traceback.format_list(tblist) if tb_lines: tb_lines.insert(0, "Traceback (most recent call last):\n") tb_lines[len(tb_lines):] = traceback.format_exception_only(tp, val) self.add_cmdline_content("".join(tb_lines), "command line error") else: self.cmdline_edit.set_edit_text("") finally: if sys.stdout.getvalue(): self.add_cmdline_content(sys.stdout.getvalue(), "command line output") sys.stdin = prev_sys_stdin sys.stdout = prev_sys_stdout sys.stderr = prev_sys_stderr def cmdline_history_browse(direction): # Browsing the command line history can be illustrated by moving up/down # in the following table (no wrap-around). # The first column shows what is written in the command input text field, # the second one the corresponding value of self.cmdline_history_position # The actual index into the history list is given by the last column. # | history | history # command line text | position | list index # ----------------------------|----------|----------- # oldest_command | 2 | 0 # medium_command | 1 | 1 # recent_command | 0 | 2 # | -1 | # | -1 | def pos_text(pos, text): if pos == -1: if text: # currently editing a command, save it to return to it later self.cmdline_saved_edit_text = text if direction > 0: # clear command to be able to write a different one return -1, "" if direction < 0 and not text and self.cmdline_saved_edit_text: # return to last saved command return -1, self.cmdline_saved_edit_text max_hist_index = len(self.cmdline_history) - 1 pos = max(-1, min(pos - direction, max_hist_index)) if pos == -1: return -1, self.cmdline_saved_edit_text return pos, self.cmdline_history[max_hist_index - pos] self.cmdline_history_position, self.cmdline_edit.edit_text = pos_text( self.cmdline_history_position, self.cmdline_edit.edit_text) self.cmdline_edit.edit_pos = len(self.cmdline_edit.edit_text) def cmdline_history_prev(w, size, key): cmdline_history_browse(-1) def cmdline_history_next(w, size, key): cmdline_history_browse(1) def toggle_cmdline_focus(w, size, key): focus_widget_in_container(self.columns, self.lhs_col) if self.lhs_col.focus is self.cmdline_sigwrap: if CONFIG["hide_cmdline_win"]: self.set_cmdline_state(False) focus_widget_in_container( self.lhs_col, self.search_controller.search_AttrMap if self.search_controller.search_box else self.source_attr) else: if CONFIG["hide_cmdline_win"]: self.set_cmdline_state(True) focus_widget_in_container(self.cmdline_pile, self.cmdline_edit_bar) focus_widget_in_container(self.lhs_col, self.cmdline_sigwrap) self.cmdline_edit_sigwrap.listen("tab", cmdline_tab_complete) self.cmdline_edit_sigwrap.listen("ctrl v", cmdline_append_newline) self.cmdline_edit_sigwrap.listen("enter", cmdline_exec) self.cmdline_edit_sigwrap.listen("down", cmdline_history_next) self.cmdline_edit_sigwrap.listen("up", cmdline_history_prev) self.cmdline_edit_sigwrap.listen("ctrl n", cmdline_history_next) self.cmdline_edit_sigwrap.listen("ctrl p", cmdline_history_prev) self.cmdline_edit_sigwrap.listen("esc", toggle_cmdline_focus) self.top.listen(CONFIG["hotkeys_toggle_cmdline_focus"], toggle_cmdline_focus) # {{{ command line sizing def set_cmdline_default_size(weight): from pudb.settings import save_config self.cmdline_weight = weight CONFIG["cmdline_height"] = weight save_config(CONFIG) self.set_cmdline_size() def max_cmdline(w, size, key): set_cmdline_default_size(5) def min_cmdline(w, size, key): set_cmdline_default_size(1/2) def grow_cmdline(w, size, key): weight = self.cmdline_weight if weight < 5: weight *= 1.25 set_cmdline_default_size(weight) def shrink_cmdline(w, size, key): weight = self.cmdline_weight if weight > 1/2: weight /= 1.25 set_cmdline_default_size(weight) def cmdline_results_scroll(w, size, key): size = self.cmdline_pile.get_item_size(size, 1, True) self.cmdline_list.keypress(size, key.lstrip("shift ")) self.cmdline_sigwrap.listen("=", max_cmdline) self.cmdline_sigwrap.listen("+", grow_cmdline) self.cmdline_sigwrap.listen("_", min_cmdline) self.cmdline_sigwrap.listen("-", shrink_cmdline) for key in ("page up", "page down"): self.cmdline_sigwrap.listen("shift " + key, cmdline_results_scroll) # }}} # }}} # {{{ sidebar sizing def max_sidebar(w, size, key): from pudb.settings import save_config weight = 5 CONFIG["sidebar_width"] = weight save_config(CONFIG) self.columns.contents[1] = ( self.columns.contents[1][0], (urwid.WEIGHT, weight)) self.columns._invalidate() def min_sidebar(w, size, key): from pudb.settings import save_config weight = 1/5 CONFIG["sidebar_width"] = weight save_config(CONFIG) self.columns.contents[1] = ( self.columns.contents[1][0], (urwid.WEIGHT, weight)) self.columns._invalidate() def grow_sidebar(w, size, key): from pudb.settings import save_config weight = self.columns.column_types[1][1] if weight < 5: weight *= 1.25 CONFIG["sidebar_width"] = weight save_config(CONFIG) self.columns.column_types[1] = urwid.WEIGHT, weight self.columns._invalidate() def shrink_sidebar(w, size, key): from pudb.settings import save_config weight = self.columns.column_types[1][1] if weight > 1/5: weight /= 1.25 CONFIG["sidebar_width"] = weight save_config(CONFIG) self.columns.column_types[1] = urwid.WEIGHT, weight self.columns._invalidate() self.rhs_col_sigwrap.listen("=", max_sidebar) self.rhs_col_sigwrap.listen("+", grow_sidebar) self.rhs_col_sigwrap.listen("_", min_sidebar) self.rhs_col_sigwrap.listen("-", shrink_sidebar) # }}} # {{{ top-level listeners def show_output(w, size, key): with StoppedScreen(self.screen): input("Hit Enter to return:") def reload_breakpoints_and_redisplay(): reload_breakpoints() curr_line = self.current_line self.set_source_code_provider(self.source_code_provider, force_update=True) if curr_line is not None: self.current_line = self.source[int(curr_line.line_nr)-1] self.current_line.set_current(True) def reload_breakpoints(): self.debugger.clear_all_breaks() from pudb.settings import load_breakpoints for bpoint_descr in load_breakpoints(): dbg.set_break(*bpoint_descr) self.update_breakpoints() def show_traceback(w, size, key): if self.current_exc_tuple is not None: from traceback import format_exception result = self.dialog( urwid.ListBox(urwid.SimpleListWalker([urwid.Text( "".join(format_exception(*self.current_exc_tuple)))])), [ ("Close", "close"), ("Location", "location") ], title="Exception Viewer", focus_buttons=True, bind_enter_esc=False) if result == "location": self.debugger.set_frame_index(len(self.debugger.stack)-1) else: self.message("No exception available.") def run_external_cmdline(w, size, key): with StoppedScreen(self.screen): curframe = self.debugger.curframe import pudb.shell as shell if CONFIG["shell"] == "ipython" and shell.have_ipython(): runner = shell.run_ipython_shell elif CONFIG["shell"] == "ipython_kernel" and shell.have_ipython(): runner = shell.run_ipython_kernel elif CONFIG["shell"] == "bpython" and shell.HAVE_BPYTHON: runner = shell.run_bpython_shell elif CONFIG["shell"] == "ptpython" and shell.HAVE_PTPYTHON: runner = shell.run_ptpython_shell elif CONFIG["shell"] == "ptipython" and shell.HAVE_PTIPYTHON: runner = shell.run_ptipython_shell elif CONFIG["shell"] == "classic": runner = shell.run_classic_shell else: def fallback(error_message): fallback_message = "Falling back to classic shell." message = f"{error_message} {fallback_message}" ui_log.error(message) return partial(shell.run_classic_shell, message=message) try: if not shell.custom_shell_dict: # Only execfile once from os.path import expanduser, expandvars cshell_fname = expanduser(expandvars(CONFIG["shell"])) with open(cshell_fname) as inf: exec(compile(inf.read(), cshell_fname, "exec"), shell.custom_shell_dict, shell.custom_shell_dict) except FileNotFoundError: runner = fallback( "Unable to locate custom shell file {!r}." .format(CONFIG["shell"]) ) except Exception: runner = fallback("Error when importing custom shell.") else: if "pudb_shell" not in shell.custom_shell_dict: runner = fallback( "%s does not contain a function named pudb_shell at " "the module level." % CONFIG["shell"] ) else: runner = shell.custom_shell_dict["pudb_shell"] runner(curframe.f_globals, curframe.f_locals) self.update_var_view() def run_cmdline(w, size, key): if CONFIG["shell"] == "internal": return toggle_cmdline_focus(w, size, key) else: return run_external_cmdline(w, size, key) def focus_code(w, size, key): focus_widget_in_container(self.columns, self.lhs_col) focus_widget_in_container(self.lhs_col, self.source_attr) class RHColumnFocuser: def __init__(self, idx): self.idx = idx def __call__(subself, w, size, key): # noqa # pylint: disable=no-self-argument focus_widget_in_container(self.columns, self.rhs_col_sigwrap) self.rhs_col.focus_position = subself.idx def quit(w, size, key): with open(self.cmdline_history_path, "w") as history: history.write("\n".join((self.cmdline_history))) self.debugger.set_quit() end() def do_edit_config(w, size, key): self.run_edit_config() def redraw_screen(w, size, key): self.screen.clear() def help(pages): self.message(pages, title="PuDB - The Python Urwid Debugger") def edit_current_frame(w, size, key): pos = self.source.focus source_identifier = \ self.source_code_provider.get_source_identifier() if source_identifier is None: self.message( "Cannot edit the current file--" "source code does not correspond to a file location. " "(perhaps this is generated code)") open_file_editor(source_identifier, pos+1) self.top.listen("o", show_output) self.top.listen("ctrl r", lambda w, size, key: reload_breakpoints_and_redisplay()) self.top.listen("!", run_cmdline) self.top.listen("e", show_traceback) self.top.listen(CONFIG["hotkeys_code"], focus_code) self.top.listen(CONFIG["hotkeys_variables"], RHColumnFocuser(0)) self.top.listen(CONFIG["hotkeys_stack"], RHColumnFocuser(1)) self.top.listen(CONFIG["hotkeys_breakpoints"], RHColumnFocuser(2)) self.top.listen("q", quit) self.top.listen("ctrl p", do_edit_config) self.top.listen("ctrl l", redraw_screen) self.top.listen("ctrl e", edit_current_frame) # }}} # {{{ setup want_curses_display = ( CONFIG["display"] == "curses" or ( CONFIG["display"] == "auto" and not ( os.environ.get("TERM", "").startswith("xterm") or os.environ.get("TERM", "").startswith("rxvt") ))) if (want_curses_display and not (stdin is not None or stdout is not None) and CursesScreen is not None): self.screen = ThreadsafeCursesScreen() else: screen_kwargs = {} if stdin is not None: screen_kwargs["input"] = stdin if stdout is not None: screen_kwargs["output"] = stdout if term_size is not None: screen_kwargs["term_size"] = term_size if screen_kwargs: self.screen = ThreadsafeFixedSizeRawScreen(**screen_kwargs) else: self.screen = ThreadsafeRawScreen() del want_curses_display if curses: try: curses.setupterm() except Exception: # Something went wrong--oh well. Nobody will die if their # 256 color support breaks. Just carry on without it. # https://github.com/inducer/pudb/issues/78 pass else: color_support = curses.tigetnum("colors") if color_support == 256 and isinstance(self.screen, RawScreen): self.screen.set_terminal_properties(256) self.setup_palette(self.screen) self.show_count = 0 self.source_code_provider = None self.current_line = None self.quit_event_loop = False # }}} # }}} # {{{ UI helpers def add_cmdline_content(self, s, attr): s = s.rstrip("\n") from pudb.ui_tools import SelectableText self.cmdline_contents.append( urwid.AttrMap(SelectableText(s), attr, "focused "+attr)) # scroll to end of last entry self.cmdline_list.set_focus_valign("bottom") self.cmdline_list.set_focus(len(self.cmdline_contents) - 1, coming_from="above") # Force the commandline to be visible self.set_cmdline_state(True) def reset_cmdline_size(self): self.lhs_col.contents[-1] = ( self.lhs_col.contents[-1][0], (urwid.WEIGHT, self.cmdline_weight if self.cmdline_on else 0)) def set_cmdline_size(self, weight=None): if weight is None: weight = self.cmdline_weight self.lhs_col.contents[-1] = ( self.lhs_col.contents[-1][0], (urwid.WEIGHT, weight)) self.lhs_col._invalidate() def set_cmdline_state(self, state_on): if state_on != self.cmdline_on: self.cmdline_on = state_on self.set_cmdline_size(None if state_on else 0) def translate_ui_stack_index(self, index): # note: self-inverse if CONFIG["current_stack_frame"] == "top": return len(self.debugger.stack)-1-index elif CONFIG["current_stack_frame"] == "bottom": return index else: raise ValueError("invalid value for 'current_stack_frame' pref") def message(self, msg, title="Message", **kwargs): self.call_with_ui(self.dialog, urwid.ListBox(urwid.SimpleListWalker([urwid.Text(msg)])), [("OK", True)], title=title, **kwargs) def run_edit_config(self): from pudb.settings import edit_config, save_config edit_config(self, CONFIG) save_config(CONFIG) def dialog(self, content, buttons_and_results, title=None, bind_enter_esc=True, focus_buttons=False, extra_bindings=None): if extra_bindings is None: extra_bindings = [] class ResultSetter: def __init__(subself, res): # noqa: N805, E501 # pylint: disable=no-self-argument subself.res = res def __call__(subself, btn): # noqa: N805, E501 # pylint: disable=no-self-argument self.quit_event_loop = [subself.res] Attr = urwid.AttrMap # noqa if bind_enter_esc: content = SignalWrap(content) def enter(w, size, key): self.quit_event_loop = [True] def esc(w, size, key): self.quit_event_loop = [False] content.listen("enter", enter) content.listen("esc", esc) button_widgets = [] for btn_descr in buttons_and_results: if btn_descr is None: button_widgets.append(urwid.Text("")) else: btn_text, btn_result = btn_descr button_widgets.append( Attr(urwid.Button(btn_text, ResultSetter(btn_result)), "button", "focused button")) w = urwid.Columns([ content, (urwid.FIXED, 15, urwid.ListBox(urwid.SimpleListWalker(button_widgets))), ], dividechars=1) if focus_buttons: w.focus_position = 1 if title is not None: w = urwid.Pile([ (urwid.FLOW, urwid.AttrMap( urwid.Text(title, align="center"), "dialog title")), (urwid.FIXED, 1, urwid.SolidFill()), w]) class ResultSettingEventHandler: def __init__(subself, res): # noqa: N805, E501 # pylint: disable=no-self-argument subself.res = res def __call__(subself, w, size, key): # noqa: N805, E501 # pylint: disable=no-self-argument self.quit_event_loop = [subself.res] w = SignalWrap(w) for key, binding in extra_bindings: if isinstance(binding, str): w.listen(key, ResultSettingEventHandler(binding)) else: w.listen(key, binding) w = urwid.LineBox(w) w = urwid.Overlay(w, self.top, align="center", valign="middle", width=("relative", 75), height=("relative", 75), ) w = Attr(w, "background") return self.event_loop(w)[0] @staticmethod def setup_palette(screen): may_use_fancy_formats = not hasattr(urwid.escape, "_fg_attr_xterm") from pudb.theme import get_palette palette = get_palette(may_use_fancy_formats, CONFIG["theme"]) if palette: screen.register_palette(palette) def show_exception_dialog(self, exc_tuple): from traceback import format_exception desc = ( "The program has terminated abnormally because of an exception.\n\n" "A full traceback is below. You may recall this traceback at any " "time using the 'e' key. The debugger has entered post-mortem mode " "and will prevent further state changes." ) tb_txt = "".join(format_exception(*exc_tuple)) self._show_exception_dialog( description=desc, error_info=tb_txt, title="Program Terminated for Uncaught Exception", exit_loop_on_ok=True, ) def show_internal_exc_dlg(self, exc_tuple): try: self._show_internal_exc_dlg(exc_tuple) except Exception: ui_log.exception("Error while showing error dialog") def _show_internal_exc_dlg(self, exc_tuple): from traceback import format_exception from pudb import VERSION desc = ( "Pudb has encountered and safely caught an internal exception.\n\n" "The full traceback and some other information can be found " "below. Please report this information, along with details on " "what you were doing at the time the exception occurred, at: " "https://github.com/inducer/pudb/issues" ) error_info = ( "python version: {python}\n" "pudb version: {pudb}\n" "urwid version: {urwid}\n" "{tb}\n" ).format( python=sys.version.replace("\n", " "), pudb=VERSION, urwid=".".join(map(str, urwid.version.version)), tb="".join(format_exception(*exc_tuple)) ) self._show_exception_dialog( description=desc, error_info=error_info, title="Pudb Internal Exception Encountered", ) def _show_exception_dialog(self, description, error_info, title, exit_loop_on_ok=False): res = self.dialog( urwid.ListBox(urwid.SimpleListWalker([urwid.Text( "\n\n".join([description, error_info]) )])), title=title, buttons_and_results=[ ("OK", exit_loop_on_ok), ("Save traceback", "save"), ], ) if res == "save": self._save_traceback(error_info) def _save_traceback(self, error_info): try: from os.path import exists filename = next( fname for n in count() for fname in ["traceback-%d.txt" % n if n else "traceback.txt"] if not exists(fname) ) with open(filename, "w") as outf: outf.write(error_info) self.message("Traceback saved as %s." % filename, title="Success") except Exception: from traceback import format_exception io_tb_txt = "".join(format_exception(*sys.exc_info())) self.message( "An error occurred while trying to write " "the traceback:\n\n" + io_tb_txt, title="I/O error") # }}} # {{{ UI enter/exit def _show(self): if self.show_count == 0: self.screen.start() self.show_count += 1 def _hide(self): self.show_count -= 1 if self.show_count == 0: self.screen.stop() def call_with_ui(self, f, *args, **kwargs): import warnings def myshowwarning( message, category, filename, lineno, file=None, line=None ) -> None: msg = warnings.formatwarning( message=message, category=category, filename=filename, lineno=lineno, line=line) self.add_cmdline_content(msg, "command line error") with warnings.catch_warnings(): warnings.resetwarnings() warnings.showwarning = myshowwarning self._show() try: return f(*args, **kwargs) finally: self._hide() # }}} # {{{ event loop def event_loop(self, toplevel=None): prev_quit_loop = self.quit_event_loop try: import pygments # noqa except ImportError: if not hasattr(self, "pygments_message_shown"): self.pygments_message_shown = True self.message("Package 'pygments' not found. " "Syntax highlighting disabled.") WELCOME_LEVEL = "e049" # noqa if CONFIG["seen_welcome"] < WELCOME_LEVEL: CONFIG["seen_welcome"] = WELCOME_LEVEL from pudb import VERSION self.message("Welcome to PudB %s!\n\n" "PuDB is a full-screen, console-based visual debugger for " "Python. Its goal is to provide all the niceties of modern " "GUI-based debuggers in a more lightweight and " "keyboard-friendly package. " "PuDB allows you to debug code right where you write and test " "it--in a terminal. If you've worked with the excellent " "(but nowadays ancient) DOS-based Turbo Pascal or C tools, " "PuDB's UI might look familiar.\n\n" "If you're new here, welcome! The help screen " "(invoked by hitting '?' after this message) should get you " "on your way.\n" "\nChanges in version 2024.1.3:\n\n" "- Switch to hatchling build system\n" "- Fix compatibility with Python 3.13\n" "- Fix startup without write permissions (Fergal Armstrong)\n" "\nChanges in version 2024.1.2:\n\n" "- Fix separate-terminal debugging (Matt Rixman)\n" "\nChanges in version 2024.1.1:\n\n" "- Fix some urwid.util deprecation warnings\n" "- Redirect pudb warnings to console\n" "- Catch IndexError on empty Variables state " "(Michael van der Kamp)\n" "\nChanges in version 2024.1:\n\n" "- Control remote debugging via env vars (Max Arnold)\n" "- Adapt to, depend on urwid 2.4\n" "- Make compatible with Python 3.13 (Will Shanks)\n" "- Use co_lines mechanism for line finding executable lines" "when available\n" "\nChanges in version 2023.1:\n\n" "- Add nord-256 theme (Jorge Gomez, Michael van der Kamp)\n" "- Reorganize themes, add light gray theme " "(Michael van der Kamp)\n" "- Improve command line history handling (raphTec)\n" "- Implement jump command (Aaron Meurer)\n" "- Drop support for Python 3.6, 3.7\n" "- Improve sidebar help (kwmiebach)\n" "- Bug fixes\n" "\nChanges in version 2022.1.3:\n\n" "- Fix finding executable lines for Python 3.11 (Lumir Balhar)\n" "- Fix the midnight theme (Aaron Meurer)\n" "- Add a --continue flag (Michael van der Kamp)\n" "- Various fixes\n" "\nChanges in version 2022.1.2:\n\n" "- Various fixes\n" "\nChanges in version 2022.1.1:\n\n" "- Fix ptpython shell invocation with nonempty argv (gh-510)\n" "- Make some key bindings configurable (Cibin Mathew)\n" "- Various cleanups (Michael van der Kamp)\n" "\nChanges in version 2022.1:\n\n" "- Add debug_remote_on_single_rank " "(PR #498 by Matthias Diener)\n" "- Improve remote debugging usability\n" "- Bug fixes\n" "\nChanges in version 2021.2:\n\n" "- Remaster themes (Michael van der Kamp)\n" "- Add more internal shell shortcuts (Huy Nguyen Quang)\n" "- Save internal shell history between sessions " "(Diego Velazquez)\n" "- Various bug fixes\n" "\nChanges in version 2021.1:\n\n" "- Add shortcut to edit files in source and stack view " "(Gábor Vecsei)\n" "- Major improvements to the variable view " "(Michael van der Kamp)\n" "- Better internal error reporting (Michael van der Kamp)\n" "\nChanges in version 2020.1:\n\n" "- Add vi keys for the sidebar (Asbjørn Apeland)\n" "- Add -m command line switch (Elias Dorneles)\n" "- Debug forked processes (Jonathan Striebel)\n" "- Robustness and logging for internal errors " "(Michael Vanderkamp)\n" "- 'Reverse' remote debugging (jen6)\n" "\nChanges in version 2019.2:\n\n" "- Auto-hide the command line (Mark Blakeney)\n" "- Improve help and add jump to breakpoint (Mark Blakeney)\n" "- Drop Py2.6 support\n" "- Show callable attributes in var view\n" "- Allow scrolling sidebar with j/k\n" "- Fix setting breakpoints in Py3.8 (Aaron Meurer)\n" "\nChanges in version 2019.1:\n\n" "- Allow 'space' as a key to expand variables (Enrico Troeger)\n" "- Have a persistent setting on variable visibility \n" " (Enrico Troeger)\n" "- Enable/partially automate opening the debugger in another \n" " terminal (Anton Barkovsky)\n" "- Make sidebar scrollable with j/k (Clayton Craft)\n" "- Bug fixes.\n" "\nChanges in version 2018.1:\n\n" "- Bug fixes.\n" "\nChanges in version 2017.1.4:\n\n" "- Bug fixes.\n" "\nChanges in version 2017.1.3:\n\n" "- Add handling of safely_stringify_for_pudb to allow custom \n" " per-type stringification.\n" "- Add support for custom shells.\n" "- Better support for 2-wide characters in the var view.\n" "- Bug fixes.\n" "\nChanges in version 2017.1.2:\n\n" "- Bug fixes.\n" "\nChanges in version 2017.1.1:\n\n" "- IMPORTANT: 2017.1 and possibly earlier versions had a \n" " bug with exponential growth of shell history for the \n" " 'classic' shell, which (among other problems) could lead\n" " to slow startup of the classic shell. Check the file\n\n" " ~/.config/pudb/shell-history\n\n" " for size (and useful content) and delete/trim as needed.\n" "\nChanges in version 2017.1:\n\n" "- Many, many bug fixes (thank you to all who contributed!)\n" "\nChanges in version 2016.2:\n\n" "- UI improvements for disabled breakpoints.\n" "- Bug fixes.\n" "\nChanges in version 2016.1:\n\n" "- Fix module browser on Py3.\n" "\nChanges in version 2015.4:\n\n" "- Support for (somewhat rudimentary) remote debugging\n" " through a telnet connection.\n" "- Fix debugging of generated code in Python 3.\n" "\nChanges in version 2015.3:\n\n" "- Disable set_trace lines from the UI (Aaron Meurer)\n" "- Better control over attribute visibility (Ned Batchelder)\n" "\nChanges in version 2015.2:\n\n" "- ptpython support (P. Varet)\n" "- Improved rxvt support (Louper Rouch)\n" "- More keyboard shortcuts in the command line" "(Alex Sheluchin)\n" "\nChanges in version 2015.1:\n\n" "- Add solarized theme (Rinat Shigapov)\n" "- More keyboard shortcuts in the command line" "(Alexander Corwin)\n" "\nChanges in version 2014.1:\n\n" "- Make prompt-on-quit optional (Mike Burr)\n" "- Make tab completion in the built-in shell saner\n" "- Fix handling of unicode source\n" " (reported by Morten Nielsen and Buck Golemon)\n" "\nChanges in version 2013.5.1:\n\n" "- Fix loading of saved breakpoint conditions " "(Antoine Dechaume)\n" "- Fixes for built-in command line\n" "- Theme updates\n" "\nChanges in version 2013.5:\n\n" "- Add command line window\n" "- Uses curses display driver when appropriate\n" "\nChanges in version 2013.4:\n\n" "- Support for debugging generated code\n" "\nChanges in version 2013.3.5:\n\n" "- IPython fixes (Aaron Meurer)\n" "- Py2/3 configuration fixes (Somchai Smythe)\n" "- PyPy fixes (Julian Berman)\n" "\nChanges in version 2013.3.4:\n\n" "- Don't die if curses doesn't like what stdin/out are\n" " connected to.\n" "\nChanges in version 2013.3.3:\n\n" "- As soon as pudb is loaded, you can break to the debugger by\n" " evaluating the expression 'pu.db', where 'pu' is a new \n" " 'builtin' that pudb has rudely shoved into the interpreter.\n" "\nChanges in version 2013.3.2:\n\n" "- Don't attempt to do signal handling if a signal handler\n" " is already set (Fix by Buck Golemon).\n" "\nChanges in version 2013.3.1:\n\n" "- Don't ship {ez,distribute}_setup at all.\n" " It breaks more than it helps.\n" "\nChanges in version 2013.3:\n\n" "- Switch to setuptools as a setup helper.\n" "\nChanges in version 2013.2:\n\n" "- Even more bug fixes.\n" "\nChanges in version 2013.1:\n\n" "- Ctrl-C will now break to the debugger in a way that does\n" " not terminate the program\n" "- Lots of bugs fixed\n" "\nChanges in version 2012.3:\n\n" "- Python 3 support (contributed by Brad Froehle)\n" "- Better search box behavior (suggested by Ram Rachum)\n" "- Made it possible to go back and examine state from " "'finished' window. (suggested by Aaron Meurer)\n" "\nChanges in version 2012.2.1:\n\n" "- Don't touch config files during install.\n" "\nChanges in version 2012.2:\n\n" "- Add support for BPython as a shell.\n" "- You can now run 'python -m pudb script.py' on Py 2.6+.\n" " '-m pudb.run' still works--but it's four " "keystrokes longer! :)\n" "\nChanges in version 2012.1:\n\n" "- Work around an API change in IPython 0.12.\n" "\nChanges in version 2011.3.1:\n\n" "- Work-around for bug in urwid >= 1.0.\n" "\nChanges in version 2011.3:\n\n" "- Finer-grained string highlighting " "(contributed by Aaron Meurer)\n" "- Prefs tweaks, instant-apply, top-down stack " "(contributed by Aaron Meurer)\n" "- Size changes in sidebar boxes (contributed by Aaron Meurer)\n" "- New theme 'midnight' (contributed by Aaron Meurer)\n" "- Support for IPython 0.11 (contributed by Chris Farrow)\n" "- Support for custom stringifiers " "(contributed by Aaron Meurer)\n" "- Line wrapping in variables view " "(contributed by Aaron Meurer)\n" "\nChanges in version 2011.2:\n\n" "- Fix for post-mortem debugging (contributed by 'Sundance')\n" "\nChanges in version 2011.1:\n\n" "- Breakpoints saved between sessions\n" "- A new 'dark vim' theme\n" "(both contributed by Naveen Michaud-Agrawal)\n" "\nChanges in version 0.93:\n\n" "- Stored preferences (no more pesky IPython prompt!)\n" "- Themes\n" "- Line numbers (optional)\n" % VERSION) from pudb.settings import save_config save_config(CONFIG) self.run_edit_config() try: if toplevel is None: toplevel = self.top self.size = self.screen.get_cols_rows() self.quit_event_loop = False while not self.quit_event_loop: canvas = toplevel.render(self.size, focus=True) self.screen.draw_screen(self.size, canvas) keys = self.screen.get_input() for k in keys: # pylint: disable=not-an-iterable if k == "window resize": self.size = self.screen.get_cols_rows() else: try: toplevel.keypress(self.size, k) except Exception: self.show_internal_exc_dlg(sys.exc_info()) return self.quit_event_loop finally: self.quit_event_loop = prev_quit_loop # }}} # {{{ debugger-facing interface def interaction(self, exc_tuple, show_exc_dialog=True): self.current_exc_tuple = exc_tuple from pudb import VERSION caption = [(None, "PuDB %s - ?:help n:next s:step into b:breakpoint " "!:python command line" % VERSION)] if self.debugger.post_mortem: if show_exc_dialog and exc_tuple is not None: self.show_exception_dialog(exc_tuple) caption.extend([ (None, " "), ("header warning", "[POST-MORTEM MODE]") ]) elif exc_tuple is not None: caption.extend([ (None, " "), ("header warning", "[PROCESSING EXCEPTION - hit 'e' to examine]") ]) self.caption.set_text(caption) self.event_loop() def set_source_code_provider(self, source_code_provider, force_update=False): if self.source_code_provider != source_code_provider or force_update: self.source[:] = source_code_provider.get_lines(self) self.source_code_provider = source_code_provider self.current_line = None def show_line(self, line, source_code_provider=None): """Updates the UI so that a certain line is currently in view.""" changed_file = False if source_code_provider is not None: changed_file = self.source_code_provider != source_code_provider self.set_source_code_provider(source_code_provider) line -= 1 if line >= 0 and line < len(self.source): self.source_list.focus_position = line if changed_file: self.source_list.set_focus_valign("middle") def set_current_line(self, line, source_code_provider): """Updates the UI to show the line currently being executed.""" if self.current_line is not None: self.current_line.set_current(False) self.show_line(line, source_code_provider) line -= 1 if line >= 0 and line < len(self.source): self.current_line = self.source[line] self.current_line.set_current(True) def update_var_view(self, locals=None, globals=None, focus_index=None): if locals is None: locals = self.debugger.curframe.f_locals if globals is None: globals = self.debugger.curframe.f_globals from pudb.var_view import make_var_view self.locals[:] = make_var_view( self.get_frame_var_info(read_only=True), locals, globals) if focus_index is not None: # Have to set the focus _after_ updating the locals list, as there # appears to be a brief moment while resetting the list when the # list is empty but urwid will attempt to set the focus anyway, # which causes problems. try: self.var_list._w.focus_position = focus_index except IndexError: # sigh oh well we tried pass def _get_bp_list(self): return [bp for fn, bp_lst in self.debugger.get_all_breaks().items() for lineno in bp_lst for bp in self.debugger.get_breaks(fn, lineno) if not bp.temporary] def _format_fname(self, fname): from os.path import basename, dirname name = basename(fname) if name == "__init__.py": name = "..."+dirname(fname)[-10:]+"/"+name return name def update_breakpoints(self): self.bp_walker[:] = [ BreakpointFrame(self.debugger.current_bp == (bp.file, bp.line), self._format_fname(bp.file), bp) for bp in self._get_bp_list()] def update_stack(self): def make_frame_ui(i, frame_lineno): frame, lineno = frame_lineno code = frame.f_code class_name = None if code.co_argcount and code.co_varnames[0] == "self": try: class_name = frame.f_locals["self"].__class__.__name__ except Exception: from pudb.lowlevel import ui_log message = "Failed to determine class name" ui_log.exception(message) class_name = "!! %s !!" % message return StackFrame(i == self.debugger.curindex, code.co_name, class_name, self._format_fname(code.co_filename), lineno) frame_uis = [make_frame_ui(i, fl) for i, fl in enumerate(self.debugger.stack)] if CONFIG["current_stack_frame"] == "top": frame_uis = frame_uis[::-1] elif CONFIG["current_stack_frame"] == "bottom": pass else: raise ValueError("invalid value for 'current_stack_frame' pref") self.stack_walker[:] = frame_uis def update_cmdline_win(self): self.set_cmdline_state(not CONFIG["hide_cmdline_win"]) # }}} # vim: foldmethod=marker:expandtab:softtabstop=4 pudb-2024.1.3/pudb/forked.py000066400000000000000000000022121470451231400154510ustar00rootroot00000000000000import os import sys from pudb.debugger import Debugger def set_trace(paused=True, frame=None, term_size=None): """Set a breakpoint in a forked process on Unix system, e.g. Linux & MacOS. In- and output will be redirected to /dev/stdin & /dev/stdout. You can call pudb.forked.set_trace() directly or use it with python's built-in breakpoint(): PYTHONBREAKPOINT=pudb.forked.set_trace python … """ if frame is None: frame = sys._getframe().f_back if term_size is None: term_size = os.environ.get("PUDB_TERM_SIZE") if term_size is not None: term_size = tuple(map(int, term_size.split("x"))) if len(term_size) != 2: raise ValueError("PUDB_TERM_SIZE should have two dimensions") else: try: # Getting terminal size s = os.get_terminal_size() term_size = (s.columns, s.lines) except Exception: term_size = (80, 24) Debugger( stdin=open("/dev/stdin"), stdout=open("/dev/stdout", "w"), term_size=term_size, ).set_trace(frame, paused=paused) pudb-2024.1.3/pudb/ipython.py000066400000000000000000000022071470451231400156750ustar00rootroot00000000000000import os import sys from IPython import get_ipython from IPython.core.magic import register_line_magic def pudb(line): """ Debug a script (like %run -d) in the IPython process, using PuDB. Usage: %pudb test.py [args] Run script test.py under PuDB. """ # Get the running instance if not line.strip(): print(pudb.__doc__) return from IPython.utils.process import arg_split args = arg_split(line) path = os.path.abspath(args[0]) args = args[1:] if not os.path.isfile(path): from IPython.core.error import UsageError raise UsageError("%%pudb: file %s does not exist" % path) from pudb import runscript runscript(path, args) register_line_magic(pudb) def debugger(self, force=False): """Call the PuDB debugger.""" from logging import error if not (force or self.call_pdb): return if not hasattr(sys, "last_traceback"): error("No traceback has been produced, nothing to debug.") return from pudb import pm with self.readline_no_record: pm() ip = get_ipython() ip.__class__.debugger = debugger pudb-2024.1.3/pudb/lowlevel.py000066400000000000000000000203411470451231400160330ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import logging import sys from datetime import datetime logfile = [None] def getlogfile(): return logfile[0] def setlogfile(destfile): logfile[0] = destfile with open(destfile, "a") as openfile: openfile.write( "\n*** Pudb session error log started at {date} ***\n".format( date=datetime.now() )) class TerminalOrStreamHandler(logging.StreamHandler): """ Logging handler that sends errors either to the terminal window or to stderr, depending on whether the debugger is active. """ def emit(self, record): from pudb import _get_debugger, _have_debugger logfile = getlogfile() self.acquire() try: if logfile is not None: message = self.format(record) with open(logfile, "a") as openfile: openfile.write("\n%s\n" % message) elif _have_debugger(): dbg = _get_debugger() message = self.format(record) dbg.ui.add_cmdline_content(message, "command line error") else: super().emit(record) finally: self.release() def _init_loggers(): ui_handler = TerminalOrStreamHandler() ui_formatter = logging.Formatter( fmt="*** Pudb UI Exception Encountered: %(message)s ***\n" ) ui_handler.setFormatter(ui_formatter) ui_log = logging.getLogger("ui") ui_log.addHandler(ui_handler) settings_handler = TerminalOrStreamHandler() settings_formatter = logging.Formatter( fmt="*** Pudb Settings Exception Encountered: %(message)s ***\n" ) settings_handler.setFormatter(settings_formatter) settings_log = logging.getLogger("settings") settings_log.addHandler(settings_handler) return ui_log, settings_log ui_log, settings_log = _init_loggers() # {{{ breakpoint validity def generate_executable_lines_for_code(code): if sys.version_info >= (3, 10): for _start, _end, lineno in code.co_lines(): if lineno is not None: yield lineno else: lineno = code.co_firstlineno yield lineno # See https://github.com/python/cpython/blob/master/Objects/lnotab_notes.txt for line_incr in code.co_lnotab[1::2]: # NB: This code is specific to Python 3.6 and higher # https://github.com/python/cpython/blob/v3.6.0/Objects/lnotab_notes.txt if line_incr >= 0x80: line_incr -= 0x100 lineno += line_incr yield lineno def get_executable_lines_for_codes_recursive(codes): codes = codes[:] from types import CodeType execable_lines = set() while codes: code = codes.pop() execable_lines |= set(generate_executable_lines_for_code(code)) codes.extend(const for const in code.co_consts if isinstance(const, CodeType)) return execable_lines def get_executable_lines_for_file(filename): # inspired by rpdb2 from linecache import getlines codes = [compile("".join(getlines(filename)), filename, "exec", dont_inherit=1)] return get_executable_lines_for_codes_recursive(codes) def get_breakpoint_invalid_reason(filename, lineno): # simple logic stolen from pdb import linecache line = linecache.getline(filename, lineno) if not line: return "Line is beyond end of file." try: executable_lines = get_executable_lines_for_file(filename) except SyntaxError: return "File failed to compile." if lineno not in executable_lines: return "No executable statement found in line." def lookup_module(filename): """Helper function for break/clear parsing -- may be overridden. lookupmodule() translates (possibly incomplete) file or module name into an absolute file name. """ # stolen from pdb import os import sys if os.path.isabs(filename) and os.path.exists(filename): return filename f = os.path.join(sys.path[0], filename) if os.path.exists(f): # and self.canonic(f) == self.mainpyfile: return f _root, ext = os.path.splitext(filename) if ext == "": filename = filename + ".py" if os.path.isabs(filename): return filename for dirname in sys.path: while os.path.islink(dirname): dirname = os.readlink(dirname) fullname = os.path.join(dirname, filename) if os.path.exists(fullname): return fullname return None # }}} # {{{ file encoding detection # the main idea stolen from Python 3.1's tokenize.py, by Ka-Ping Yee import re cookie_re = re.compile(br"^\s*#.*coding[:=]\s*([-\w.]+)") from codecs import BOM_UTF8, lookup def detect_encoding(line_iter): """ The detect_encoding() function is used to detect the encoding that should be used to decode a Python source file. It requires one argument, line_iter, an iterator on the lines to be read. It will read a maximum of two lines, and return the encoding used (as a string) and a list of any lines (left as bytes) it has read in. It detects the encoding from the presence of a utf-8 bom or an encoding cookie as specified in pep-0263. If both a bom and a cookie are present, but disagree, a SyntaxError will be raised. If the encoding cookie is an invalid charset, raise a SyntaxError. If no encoding is specified, then the default of 'utf-8' will be returned. """ bom_found = False def read_or_stop(): try: return next(line_iter) except StopIteration: return "" def find_cookie(line): try: line_string = line except UnicodeDecodeError: return None matches = cookie_re.findall(line_string) if not matches: return None encoding = matches[0].decode() try: codec = lookup(encoding) except LookupError as err: # This behaviour mimics the Python interpreter raise SyntaxError("unknown encoding: " + encoding) from err if bom_found and codec.name != "utf-8": # This behaviour mimics the Python interpreter raise SyntaxError("encoding problem: utf-8") return encoding first = read_or_stop() if isinstance(first, str): return None, [first] if first.startswith(BOM_UTF8): bom_found = True first = first[3:] if not first: return "utf-8", [] encoding = find_cookie(first) if encoding: return encoding, [first] second = read_or_stop() if not second: return "utf-8", [first] encoding = find_cookie(second) if encoding: return encoding, [first, second] return "utf-8", [first, second] def decode_lines(lines): line_iter = iter(lines) source_enc, detection_read_lines = detect_encoding(line_iter) from itertools import chain for line in chain(detection_read_lines, line_iter): if hasattr(line, "decode") and source_enc is not None: yield line.decode(source_enc) else: yield line # }}} # vim: foldmethod=marker pudb-2024.1.3/pudb/remote.py000066400000000000000000000232741470451231400155050ustar00rootroot00000000000000""" .. autoclass:: RemoteDebugger .. autofunction:: set_trace .. autofunction:: debugger .. autofunction:: debug_remote_on_single_rank """ __copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer Copyright (C) 2020-2020 Son Geon """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # mostly stolen from celery.contrib.rdb import atexit import errno import os import socket import sys from typing import Any, Callable from pudb.debugger import Debugger __all__ = ["PUDB_RDB_HOST", "PUDB_RDB_PORT", "PUDB_RDB_REVERSE", "default_port", "debugger", "set_trace", "debug_remote_on_single_rank"] default_port = 6899 PUDB_RDB_HOST = os.environ.get("PUDB_RDB_HOST") or "127.0.0.1" PUDB_RDB_PORT = int(os.environ.get("PUDB_RDB_PORT") or default_port) PUDB_RDB_REVERSE = bool(os.environ.get("PUDB_RDB_REVERSE")) #: Holds the currently active debugger. _current = [None] _frame = sys._getframe NO_AVAILABLE_PORT = """\ {self.ident}: Couldn't find an available port. Please specify one using the PUDB_RDB_PORT environment variable. """ BANNER = """\ {self.ident}: Please start a telnet session using a command like: telnet {self.host} {self.port} {self.ident}: Waiting for client... """ SESSION_STARTED = "{self.ident}: Now in session with {self.remote_addr}." SESSION_ENDED = "{self.ident}: Session with {self.remote_addr} ended." CONN_REFUSED = """\ Cannot connect to the reverse telnet client {self.host} {self.port}. Try to open reverse client by running stty -echo -icanon && nc -l -p 6899 # Linux stty -echo -icanon && nc -l 6899 # BSD/MacOS Please specify one using the PUDB_RDB_PORT environment variable. """ class TelnetCharacters: """Collection of characters from the telnet protocol RFC 854 This format for the telnet characters was adapted from the telnetlib module which was removed from the C Python standard library in version 3.13. Only the characters needed by pudb have been copied here. Additional characters can be found by looking in the telnetlib code in Python 3.12 or in the telnet RFC. .. note:: This class is not intended to be instantiated. """ # Telnet protocol characters IAC = b"\xff" # "Interpret As Command" DO = b"\xfd" WILL = b"\xfb" # Telnet protocol options codes # These ones all come from arpa/telnet.h ECHO = b"\x01" # echo SGA = b"\x03" # suppress go ahead class RemoteDebugger(Debugger): """ .. automethod:: __init__ """ me = "pudb" _prev_outs = None _sock = None def __init__( self, host=PUDB_RDB_HOST, port=PUDB_RDB_PORT, port_search_limit=100, out=sys.stdout, term_size=None, reverse=PUDB_RDB_REVERSE, ): """ :arg term_size: A two-tuple ``(columns, rows)``, or *None*. If *None*, try to determine the terminal size automatically. Currently, this uses a heuristic: It uses the terminal size of the debuggee as that for the debugger. The idea is that you might be running both in two tabs of the same terminal window, hence using terminals of the same size. """ self.out = out if term_size is None: term_size = os.environ.get("PUDB_TERM_SIZE") if term_size is not None: term_size = tuple(map(int, term_size.split("x"))) if len(term_size) != 2: raise ValueError("PUDB_TERM_SIZE should have two dimensions") else: try: s = os.get_terminal_size() term_size = (s.columns, s.lines) except Exception: term_size = (80, 24) self._prev_handles = sys.stdin, sys.stdout self._client, (address, port) = self.get_client( host=host, port=port, search_limit=port_search_limit, reverse=reverse ) self.remote_addr = ":".join(str(v) for v in address) self.say(SESSION_STARTED.format(self=self)) # makefile ignores encoding if there's no buffering. raw_sock_file = self._client.makefile("rwb", 0) import codecs sock_file = codecs.StreamReaderWriter( raw_sock_file, codecs.getreader("utf-8"), codecs.getwriter("utf-8")) self._handle = sys.stdin = sys.stdout = sock_file # nc negotiation doesn't support telnet options if not reverse: tn = TelnetCharacters raw_sock_file.write(tn.IAC + tn.WILL + tn.SGA) resp = raw_sock_file.read(3) assert resp == tn.IAC + tn.DO + tn.SGA raw_sock_file.write(tn.IAC + tn.WILL + tn.ECHO) resp = raw_sock_file.read(3) assert resp == tn.IAC + tn.DO + tn.ECHO Debugger.__init__( self, stdin=self._handle, stdout=self._handle, term_size=term_size ) def get_client(self, host, port, search_limit=100, reverse=False): if reverse: self.host, self.port = host, port client, address = self.get_reverse_socket_client(host, port) self.ident = f"{self.me}:{self.port}" else: self._sock, conn_info = self.get_socket_client( host, port, search_limit=search_limit, ) self.host, self.port = conn_info self.ident = f"{self.me}:{self.port}" self.say(BANNER.format(self=self)) client, address = self._sock.accept() client.setblocking(1) return client, (address, self.port) def get_reverse_socket_client(self, host, port): _sock = socket.socket() try: _sock.connect((host, port)) _sock.setblocking(1) except OSError as exc: if exc.errno == errno.ECONNREFUSED: raise ValueError(CONN_REFUSED.format(self=self)) from exc raise exc return _sock, _sock.getpeername() def get_socket_client(self, host, port, search_limit): _sock, this_port = self.get_avail_port(host, port, search_limit) _sock.setblocking(1) _sock.listen(1) return _sock, (host, this_port) def get_avail_port(self, host, port, search_limit=100, skew=+0): this_port = None for i in range(search_limit): _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) this_port = port + i try: _sock.bind((host, this_port)) except OSError as exc: if exc.errno in [errno.EADDRINUSE, errno.EINVAL]: continue raise else: return _sock, this_port else: raise Exception(NO_AVAILABLE_PORT.format(self=self)) def say(self, m): print(m, file=self.out) def close_remote_session(self): self.stdin, self.stdout = sys.stdin, sys.stdout = self._prev_handles self._handle.close() self._client.close() if self._sock: self._sock.close() self.say(SESSION_ENDED.format(self=self)) def debugger( term_size=None, host=PUDB_RDB_HOST, port=PUDB_RDB_PORT, reverse=PUDB_RDB_REVERSE ): """Return the current debugger instance (if any), or creates a new one.""" rdb = _current[0] if rdb is None: rdb = _current[0] = RemoteDebugger( host=host, port=port, term_size=term_size, reverse=reverse ) atexit.register(lambda e: e.close_remote_session(), rdb) return rdb def set_trace( frame=None, term_size=None, host=PUDB_RDB_HOST, port=PUDB_RDB_PORT, reverse=PUDB_RDB_REVERSE ): """Set breakpoint at current location, or a specified frame""" if frame is None: frame = _frame().f_back return debugger( term_size=term_size, host=host, port=port, reverse=reverse ).set_trace(frame) def debug_remote_on_single_rank(comm: Any, rank: int, func: Callable, *args: Any, **kwargs: Any) -> None: """Run a remote debugger on a single rank of an ``mpi4py`` application. *func* will be called on rank *rank* running in a :class:`RemoteDebugger`, and will be called normally on all other ranks. :param comm: an ``mpi4py`` ``Comm`` object. :param rank: the rank to debug. All other ranks will spin until this rank exits. :param func: the callable to debug. :param args: the arguments passed to ``func``. :param kwargs: the kwargs passed to ``func``. """ if comm.rank == rank: debugger().runcall(func, *args, **kwargs) else: try: func(*args, **kwargs) finally: from time import sleep while True: sleep(1) pudb-2024.1.3/pudb/run.py000066400000000000000000000063331470451231400150130ustar00rootroot00000000000000COMMAND = {"zsh": "{_command_names -e}"} PREAMBLE = { "zsh": """\ _script_args() { # pudb -m if (($words[(I)-m] == $#words - 1)); then _python_modules # pudb -m XXX elif (($words[(I)-m])); then _files # pudb else _arguments -S -s '(-)1:script_args:_files -g "*.py"' '*: :_files' fi } """, } SCRIPT_ARGS = {"zsh": "_script_args"} def get_argparse_parser(): import argparse import os import sys try: import shtab except ImportError: from . import _shtab as shtab from pudb import VERSION version_info = "%(prog)s v" + VERSION if sys.argv[1:] == ["-v"]: print(version_info % {"prog": "pudb"}) sys.exit(os.EX_OK) parser = argparse.ArgumentParser( "pudb", usage="%(prog)s [options] [-m] SCRIPT-OR-MODULE-TO-RUN [SCRIPT_ARGS]", epilog=version_info ) shtab.add_argument_to(parser, preamble=PREAMBLE) # dest="_continue_at_start" needed as "continue" is a python keyword parser.add_argument( "-c", "--continue", action="store_true", dest="_continue_at_start", help="Let the script run until an exception occurs or a breakpoint is hit", ) parser.add_argument("-s", "--steal-output", action="store_true") # note: we're implementing -m as a boolean flag, mimicking pdb's behavior, # and makes it possible without much fuss to support cases like: # python -m pudb -m http.server -h # where the -h will be passed to the http.server module parser.add_argument("-m", "--module", action="store_true", help="Debug as module or package instead of as a script") parser.add_argument("-le", "--log-errors", nargs=1, metavar="FILE", help="Log internal errors to the given file" ).complete = shtab.FILE parser.add_argument("--pre-run", metavar="COMMAND", help="Run command before each program run", default="").complete = COMMAND parser.add_argument("--version", action="version", version=version_info) parser.add_argument("script_args", nargs=argparse.REMAINDER, help="Arguments to pass to script or module" ).complete = SCRIPT_ARGS return parser def main(**kwargs): import sys parser = get_argparse_parser() options = parser.parse_args() args = options.script_args if options.log_errors: from pudb.lowlevel import setlogfile setlogfile(options.log_errors[0]) options_kwargs = { "pre_run": options.pre_run, "steal_output": options.steal_output, "_continue_at_start": options._continue_at_start, } if len(args) < 1: parser.print_help() sys.exit(2) mainpyfile = args[0] sys.argv = args if options.module: from pudb import runmodule runmodule(mainpyfile, **options_kwargs) else: from os.path import exists if not exists(mainpyfile): print("Error: %s does not exist" % mainpyfile, file=sys.stderr) sys.exit(1) from pudb import runscript runscript(mainpyfile, **options_kwargs) if __name__ == "__main__": main() pudb-2024.1.3/pudb/settings.py000066400000000000000000000517161470451231400160540ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import os import sys from configparser import ConfigParser from pudb.lowlevel import get_breakpoint_invalid_reason, lookup_module, settings_log # see https://github.com/inducer/pudb/pull/453 for context _home = os.environ.get("HOME", os.path.expanduser("~")) XDG_CONF_RESOURCE = "pudb" XDG_CONFIG_HOME = os.environ.get( "XDG_CONFIG_HOME", os.path.join(_home, ".config") if _home else None) if XDG_CONFIG_HOME: XDG_CONFIG_DIRS = [XDG_CONFIG_HOME] else: XDG_CONFIG_DIRS = os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":") def get_save_config_path(): # This may not raise, as it is called during import. if not XDG_CONFIG_HOME: return None path = os.path.join(XDG_CONFIG_HOME, XDG_CONF_RESOURCE) try: os.makedirs(path, mode=0o700, exist_ok=True) except Exception: settings_log.exception("Failed to make config dir") return path CONF_SECTION = "pudb" CONF_FILE_NAME = "pudb.cfg" SAVED_BREAKPOINTS_FILE_NAME = "saved-breakpoints-%d.%d" % sys.version_info[:2] BREAKPOINTS_FILE_NAME = "breakpoints-%d.%d" % sys.version_info[:2] _config_ = [None] def load_config(): # This may not raise, as it is called during import. # Only ever do this once if _config_[0] is not None: return _config_[0] from os.path import isdir, join cparser = ConfigParser() conf_dict = {} try: cparser.read([ join(cdir, XDG_CONF_RESOURCE, CONF_FILE_NAME) for cdir in XDG_CONFIG_DIRS if isdir(cdir)]) if cparser.has_section(CONF_SECTION): conf_dict.update(dict(cparser.items(CONF_SECTION))) except Exception: settings_log.exception("Failed to load config") conf_dict.setdefault("shell", "internal") conf_dict.setdefault("theme", "classic") conf_dict.setdefault("line_numbers", "False") conf_dict.setdefault("seen_welcome", "a") conf_dict.setdefault("sidebar_width", 0.5) conf_dict.setdefault("variables_weight", 1) conf_dict.setdefault("stack_weight", 1) conf_dict.setdefault("breakpoints_weight", 1) conf_dict.setdefault("current_stack_frame", "top") conf_dict.setdefault("stringifier", "default") conf_dict.setdefault("custom_theme", "") conf_dict.setdefault("custom_stringifier", "") conf_dict.setdefault("custom_shell", "") conf_dict.setdefault("wrap_variables", "True") conf_dict.setdefault("default_variables_access_level", "public") conf_dict.setdefault("display", "auto") conf_dict.setdefault("prompt_on_quit", "True") conf_dict.setdefault("hide_cmdline_win", "False") # hotkeys conf_dict.setdefault("hotkeys_code", "C") conf_dict.setdefault("hotkeys_variables", "V") conf_dict.setdefault("hotkeys_stack", "S") conf_dict.setdefault("hotkeys_breakpoints", "B") conf_dict.setdefault("hotkeys_toggle_cmdline_focus", "ctrl x") def normalize_bool_inplace(name): try: if conf_dict[name].lower() in ["0", "false", "off"]: conf_dict[name] = False else: conf_dict[name] = True except Exception: settings_log.exception("Failed to process config") normalize_bool_inplace("line_numbers") normalize_bool_inplace("wrap_variables") normalize_bool_inplace("prompt_on_quit") normalize_bool_inplace("hide_cmdline_win") _config_[0] = conf_dict return conf_dict def save_config(conf_dict): # This may not raise, as it is called during import. from os.path import join cparser = ConfigParser() cparser.add_section(CONF_SECTION) for key in sorted(conf_dict): cparser.set(CONF_SECTION, key, str(conf_dict[key])) try: save_path = get_save_config_path() if not save_path: return outf = open(join(save_path, CONF_FILE_NAME), "w") cparser.write(outf) outf.close() except Exception: settings_log.exception("Failed to save config") def edit_config(ui, conf_dict): import urwid old_conf_dict = conf_dict.copy() def _update_theme(): ui.setup_palette(ui.screen) ui.screen.clear() def _update_line_numbers(): for sl in ui.source: sl._invalidate() def _update_prompt_on_quit(): pass def _update_hide_cmdline_win(): ui.update_cmdline_win() def _update_current_stack_frame(): ui.update_stack() def _update_stringifier(): import pudb.var_view pudb.var_view.custom_stringifier_dict = {} ui.update_var_view() def _update_default_variables_access_level(): ui.update_var_view() def _update_wrap_variables(): ui.update_var_view() def _update_config(check_box, new_state, option_newvalue): option, newvalue = option_newvalue new_conf_dict = {option: newvalue} if option == "theme": # only activate if the new state of the radio button is 'on' if new_state: if newvalue is None: # Select the custom theme entry dialog lb.set_focus(lb_contents.index(theme_edit_list_item)) return conf_dict.update(theme=newvalue) _update_theme() elif option == "line_numbers": new_conf_dict["line_numbers"] = not check_box.get_state() conf_dict.update(new_conf_dict) _update_line_numbers() elif option == "prompt_on_quit": new_conf_dict["prompt_on_quit"] = not check_box.get_state() conf_dict.update(new_conf_dict) _update_prompt_on_quit() elif option == "hide_cmdline_win": new_conf_dict["hide_cmdline_win"] = not check_box.get_state() conf_dict.update(new_conf_dict) _update_hide_cmdline_win() elif option == "current_stack_frame": # only activate if the new state of the radio button is 'on' if new_state: conf_dict.update(new_conf_dict) _update_current_stack_frame() elif option == "stringifier": # only activate if the new state of the radio button is 'on' if new_state: if newvalue is None: lb.set_focus(lb_contents.index(stringifier_edit_list_item)) return conf_dict.update(stringifier=newvalue) _update_stringifier() elif option == "default_variables_access_level": # only activate if the new state of the radio button is 'on' if new_state: conf_dict.update(default_variables_access_level=newvalue) _update_default_variables_access_level() elif option == "wrap_variables": new_conf_dict["wrap_variables"] = not check_box.get_state() conf_dict.update(new_conf_dict) _update_wrap_variables() heading = urwid.Text("This is the preferences screen for PuDB. " "Hit Ctrl-P at any time to get back to it.\n\n" "Configuration settings are saved in " "$HOME/.config/pudb or $XDG_CONFIG_HOME/pudb " "environment variable. If both variables are not set " "configurations settings will not be saved.\n") cb_line_numbers = urwid.CheckBox("Show Line Numbers", bool(conf_dict["line_numbers"]), on_state_change=_update_config, user_data=("line_numbers", None)) cb_prompt_on_quit = urwid.CheckBox("Prompt before quitting", bool(conf_dict["prompt_on_quit"]), on_state_change=_update_config, user_data=("prompt_on_quit", None)) hide_cmdline_win = urwid.CheckBox("Hide command line" f"({conf_dict['hotkeys_toggle_cmdline_focus']}) window " "when not in use", bool(conf_dict["hide_cmdline_win"]), on_state_change=_update_config, user_data=("hide_cmdline_win", None)) # {{{ shells shell_info = urwid.Text("This is the shell that will be " "used when you hit '!'.\n") shells = ["internal", "classic", "ipython", "ipython_kernel", "bpython", "ptpython", "ptipython"] known_shell = conf_dict["shell"] in shells shell_edit = urwid.Edit(edit_text=conf_dict["custom_shell"]) shell_edit_list_item = urwid.AttrMap(shell_edit, "input", "focused input") shell_rb_group = [] shell_rbs = [ urwid.RadioButton(shell_rb_group, name, conf_dict["shell"] == name) for name in shells]+[ urwid.RadioButton(shell_rb_group, "Custom:", not known_shell, on_state_change=_update_config, user_data=("shell", None)), shell_edit_list_item, urwid.Text("\nTo use a custom shell, see examples/shell.py " "in the pudb distribution. Enter the full path to a " "file like it in the box above. '~' will be expanded " "to your home directory. The file should contain a " "function called pudb_shell(_globals, _locals) " "at the module level. See the PuDB documentation for " "more information."), ] # }}} # {{{ themes from pudb.themes import THEMES known_theme = conf_dict["theme"] in THEMES theme_rb_group = [] theme_edit = urwid.Edit(edit_text=conf_dict["custom_theme"]) theme_edit_list_item = urwid.AttrMap(theme_edit, "input", "focused input") theme_rbs = [ urwid.RadioButton(theme_rb_group, name, conf_dict["theme"] == name, on_state_change=_update_config, user_data=("theme", name)) for name in THEMES]+[ urwid.RadioButton(theme_rb_group, "Custom:", not known_theme, on_state_change=_update_config, user_data=("theme", None)), theme_edit_list_item, urwid.Text("\nTo use a custom theme, see examples/theme.py in the " "pudb distribution. Enter the full path to a file like it in " "the box above. '~' will be expanded to your home directory. " "Note that a custom theme will not be applied until you close " "this dialog."), ] # }}} # {{{ stack stack_rb_group = [] stack_opts = ["top", "bottom"] stack_info = urwid.Text("Show the current stack frame at the\n") stack_rbs = [ urwid.RadioButton(stack_rb_group, name, conf_dict["current_stack_frame"] == name, on_state_change=_update_config, user_data=("current_stack_frame", name)) for name in stack_opts ] # }}} # {{{ stringifier from pudb.var_view import STRINGIFIERS stringifier_opts = list(STRINGIFIERS.keys()) known_stringifier = conf_dict["stringifier"] in stringifier_opts stringifier_rb_group = [] stringifier_edit = urwid.Edit(edit_text=conf_dict["custom_stringifier"]) stringifier_info = urwid.Text( "This is the default function that will be called on variables in the " "variables list. You can also change this on a per-variable basis by " "selecting a variable and typing 'e' to edit the variable's display " "settings, or by typing one of d/t/r/s/i/c. Note that str and repr will " "be slower than the default, type, or id stringifiers.\n") stringifier_edit_list_item = urwid.AttrMap(stringifier_edit, "input", "focused input") stringifier_rbs = [ urwid.RadioButton(stringifier_rb_group, name, conf_dict["stringifier"] == name, on_state_change=_update_config, user_data=("stringifier", name)) for name in stringifier_opts ]+[ urwid.RadioButton(stringifier_rb_group, "Custom:", not known_stringifier, on_state_change=_update_config, user_data=("stringifier", None)), stringifier_edit_list_item, urwid.Text("\nTo use a custom stringifier, see " "examples/stringifier.py in the pudb distribution. Enter the " "full path to a file like it in the box above. " "'~' will be expanded to your home directory. " "The file should contain a function called pudb_stringifier() " "at the module level, which should take a single argument and " "return the desired string form of the object passed to it. " "Note that if you choose a custom stringifier, the variables " "view will not be updated until you close this dialog."), ] # }}} # {{{ variables access level default_variables_access_level_opts = ["public", "private", "all"] default_variables_access_level_rb_group = [] default_variables_access_level_info = urwid.Text( "Set the default attribute visibility " "of variables in the variables list.\n" "\nNote that you can change this option on " "a per-variable basis by selecting the " "variable and pressing '*'.") default_variables_access_level_rbs = [ urwid.RadioButton(default_variables_access_level_rb_group, name, conf_dict["default_variables_access_level"] == name, on_state_change=_update_config, user_data=("default_variables_access_level", name)) for name in default_variables_access_level_opts ] # }}} # {{{ wrap variables cb_wrap_variables = urwid.CheckBox("Wrap variables", bool(conf_dict["wrap_variables"]), on_state_change=_update_config, user_data=("wrap_variables", None)) wrap_variables_info = urwid.Text("\nNote that you can change this option on " "a per-variable basis by selecting the " "variable and pressing 'w'.") # }}} # {{{ display display_info = urwid.Text("What driver is used to talk to your terminal. " "'raw' has the most features (colors and highlighting), " "but is only correct for " "XTerm and terminals like it. 'curses' " "has fewer " "features, but it will work with just about any terminal. 'auto' " "will attempt to pick between the two based on availability and " "the $TERM environment variable.\n\n" "Changing this setting requires a restart of PuDB.") displays = ["auto", "raw", "curses"] display_rb_group = [] display_rbs = [ urwid.RadioButton(display_rb_group, name, conf_dict["display"] == name) for name in displays] # }}} lb_contents = ( [heading] + [urwid.AttrMap(urwid.Text("General:\n"), "group head")] + [cb_line_numbers] + [cb_prompt_on_quit] + [hide_cmdline_win] + [urwid.AttrMap(urwid.Text("\nShell:\n"), "group head")] + [shell_info] + shell_rbs + [urwid.AttrMap(urwid.Text("\nTheme:\n"), "group head")] + theme_rbs + [urwid.AttrMap(urwid.Text("\nStack Order:\n"), "group head")] + [stack_info] + stack_rbs + [urwid.AttrMap(urwid.Text("\nVariable Stringifier:\n"), "group head")] + [stringifier_info] + stringifier_rbs + [urwid.AttrMap(urwid.Text("\nVariables Attribute Visibility:\n"), "group head")] + [default_variables_access_level_info] + default_variables_access_level_rbs + [urwid.AttrMap(urwid.Text("\nWrap Variables:\n"), "group head")] + [cb_wrap_variables] + [wrap_variables_info] + [urwid.AttrMap(urwid.Text("\nDisplay driver:\n"), "group head")] + [display_info] + display_rbs ) lb = urwid.ListBox(urwid.SimpleListWalker(lb_contents)) if ui.dialog(lb, [ ("OK", True), ("Cancel", False), ], title="Edit Preferences"): # Only update the settings here that instant-apply (above) doesn't take # care of. # if we had a custom theme, it wasn't updated live if theme_rb_group[-1].state: newvalue = theme_edit.get_edit_text() conf_dict.update(theme=newvalue, custom_theme=newvalue) _update_theme() # Ditto for custom stringifiers if stringifier_rb_group[-1].state: newvalue = stringifier_edit.get_edit_text() conf_dict.update(stringifier=newvalue, custom_stringifier=newvalue) _update_stringifier() if shell_rb_group[-1].state: newvalue = shell_edit.get_edit_text() conf_dict.update(shell=newvalue, custom_shell=newvalue) else: for shell, shell_rb in zip(shells, shell_rbs): if shell_rb.get_state(): conf_dict["shell"] = shell for display, display_rb in zip(displays, display_rbs): if display_rb.get_state(): conf_dict["display"] = display else: # The user chose cancel, revert changes conf_dict.update(old_conf_dict) _update_theme() # _update_line_numbers() is equivalent to _update_theme() _update_current_stack_frame() _update_stringifier() # {{{ breakpoint saving def parse_breakpoints(lines): # b [ (filename:lineno | function) [, "condition"] ] breakpoints = [] for arg in lines: if not arg: continue arg = arg[1:] filename = None lineno = None cond = None comma = arg.find(",") if comma > 0: # parse stuff after comma: "condition" cond = arg[comma+1:].lstrip() arg = arg[:comma].rstrip() colon = arg.rfind(":") funcname = None if colon > 0: filename = arg[:colon].strip() f = lookup_module(filename) if not f: continue else: filename = f arg = arg[colon+1:].lstrip() try: lineno = int(arg) except ValueError: continue else: continue if get_breakpoint_invalid_reason(filename, lineno) is None: breakpoints.append((filename, lineno, False, cond, funcname)) return breakpoints def get_breakpoints_file_name(): from os.path import join save_path = get_save_config_path() if not save_path: return None else: return join(save_path, SAVED_BREAKPOINTS_FILE_NAME) def load_breakpoints(): """ Loads and check saved breakpoints out from files Returns: list of tuples """ from os.path import isdir, join file_names = [] for cdir in XDG_CONFIG_DIRS: if isdir(cdir): for name in [SAVED_BREAKPOINTS_FILE_NAME, BREAKPOINTS_FILE_NAME]: file_names.append(join(cdir, XDG_CONF_RESOURCE, name)) lines = [] for fname in file_names: try: rc_file = open(fname) except OSError: pass else: lines.extend([line.strip() for line in rc_file.readlines()]) rc_file.close() return parse_breakpoints(lines) def save_breakpoints(bp_list): """ :arg bp_list: a list of `bdb.Breakpoint` objects """ save_path = get_breakpoints_file_name() if not save_path: return histfile = open(get_breakpoints_file_name(), "w") bp_list = {(bp.file, bp.line, bp.cond) for bp in bp_list} for bp in bp_list: line = "b %s:%d" % (bp[0], bp[1]) if bp[2]: line += ", %s" % bp[2] line += "\n" histfile.write(line) histfile.close() # }}} # vim:foldmethod=marker pudb-2024.1.3/pudb/shell.py000066400000000000000000000203571470451231400153200ustar00rootroot00000000000000try: import bpython # noqa # Access a property to verify module exists in case # there's a demand loader wrapping module imports # See https://github.com/inducer/pudb/issues/177 bpython.__version__ # noqa: B018 except ImportError: HAVE_BPYTHON = False else: HAVE_BPYTHON = True try: from ptpython.ipython import embed as ptipython_embed except ImportError: HAVE_PTIPYTHON = False else: HAVE_PTIPYTHON = True try: from ptpython.repl import embed as ptpython_embed, run_config except ImportError: HAVE_PTPYTHON = False else: HAVE_PTPYTHON = True # {{{ combined locals/globals dict class SetPropagatingDict(dict): """ Combine dict into one, with assignments affecting a target dict The source dicts are combined so that early dicts in the list have higher precedence. Typical usage is ``SetPropagatingDict([locals, globals], locals)``. This is used for functions like ``rlcompleter.Completer`` and ``code.InteractiveConsole``, which only take a single dictionary. This way, changes made to it are propagated to locals. Note that assigning to locals only actually works at the module level, when ``locals()`` is the same as ``globals()``, so propagation doesn't happen at all if the debugger is inside a function frame. """ def __init__(self, source_dicts, target_dict): dict.__init__(self) for s in source_dicts[::-1]: self.update(s) self.target_dict = target_dict def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.target_dict[key] = value def __delitem__(self, key): dict.__delitem__(self, key) del self.target_dict[key] # }}} custom_shell_dict = {} SHELL_FIRST_TIME = [True] def run_classic_shell(globals, locals, message=""): if SHELL_FIRST_TIME: banner = "Hit Ctrl-D to return to PuDB." SHELL_FIRST_TIME.pop() else: banner = "" if message: banner = f"{message}\n{banner}" ns = SetPropagatingDict([locals, globals], locals) from os.path import join from pudb.settings import get_save_config_path hist_file = join( get_save_config_path(), "shell-history") try: import readline import rlcompleter have_readline = True except ImportError: have_readline = False if have_readline: readline.set_completer( rlcompleter.Completer(ns).complete) readline.parse_and_bind("tab: complete") readline.clear_history() try: readline.read_history_file(hist_file) except OSError: pass from code import InteractiveConsole cons = InteractiveConsole(ns) cons.interact(banner) if have_readline: readline.write_history_file(hist_file) def run_bpython_shell(globals, locals): ns = SetPropagatingDict([locals, globals], locals) import bpython bpython.embed(args=[], locals_=ns) # {{{ ipython def have_ipython(): # IPython has started being obnoxious on import, only import # if absolutely needed. # https://github.com/ipython/ipython/issues/9435 try: import IPython # Access a property to verify module exists in case # there's a demand loader wrapping module imports # See https://github.com/inducer/pudb/issues/177 IPython.core # noqa: B018 except (ImportError, ValueError): # Old IPythons versions (0.12?) may fail to import with # ValueError: fallback required, but not specified # https://github.com/inducer/pudb/pull/135 return False else: return True def ipython_version(): if have_ipython(): from IPython import version_info return version_info else: return None def run_ipython_shell_v10(globals, locals): """IPython shell from IPython version 0.10""" if SHELL_FIRST_TIME: banner = "Hit Ctrl-D to return to PuDB." SHELL_FIRST_TIME.pop() else: banner = "" # avoid IPython's namespace litter ns = locals.copy() from IPython.Shell import IPShell IPShell(argv=[], user_ns=ns, user_global_ns=globals) \ .mainloop(banner=banner) def _update_ipython_ns(shell, globals, locals): """Update the IPython 0.11 namespace at every visit""" shell.user_ns = locals.copy() try: shell.user_global_ns = globals except AttributeError: class DummyMod: """A dummy module used for IPython's interactive namespace.""" pass user_module = DummyMod() user_module.__dict__ = globals shell.user_module = user_module shell.init_history() shell.init_user_ns() shell.init_completer() def run_ipython_shell_v11(globals, locals): """IPython shell from IPython version 0.11""" if SHELL_FIRST_TIME: banner = "Hit Ctrl-D to return to PuDB." SHELL_FIRST_TIME.pop() else: banner = "" try: # IPython 1.0 got rid of the frontend intermediary, and complains with # a deprecated warning when you use it. from IPython.terminal.interactiveshell import TerminalInteractiveShell from IPython.terminal.ipapp import load_default_config except ImportError: from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell from IPython.frontend.terminal.ipapp import load_default_config # XXX: in the future it could be useful to load a 'pudb' config for the # user (if it exists) that could contain the user's macros and other # niceities. config = load_default_config() shell = TerminalInteractiveShell.instance(config=config, banner2=banner) # XXX This avoids a warning about not having unique session/line numbers. # See the HistoryManager.writeout_cache method in IPython.core.history. shell.history_manager.new_session() # Save the originating namespace old_locals = shell.user_ns old_globals = shell.user_global_ns # Update shell with current namespace _update_ipython_ns(shell, globals, locals) args = [] if ipython_version() < (5, 0, 0): args.append(banner) else: print(banner) # XXX Quick and dirty way to fix issues with IPython 8.0.0+, introduced # by commit 08d54c0e367b535fd88aca5273fd09e5e70d08f8. # Setting _atexit_once_called = True will prevent call to # IPython.core.interactiveshell.InteractiveShell._atexit_once() from inside # IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop() # This allows us to repeatedly re-call mainloop() and the whole # run_ipython_shell_v11() function shell._atexit_once_called = True shell.mainloop(*args) del shell._atexit_once_called # Restore originating namespace _update_ipython_ns(shell, old_globals, old_locals) def run_ipython_shell(globals, locals): import IPython if have_ipython() and hasattr(IPython, "Shell"): return run_ipython_shell_v10(globals, locals) else: return run_ipython_shell_v11(globals, locals) def run_ipython_kernel(globals, locals): from IPython import embed_kernel class DummyMod: pass user_module = DummyMod() user_module.__dict__ = globals embed_kernel(module=user_module, local_ns=locals) # }}} def get_ptpython_history_file(): from argparse import ArgumentParser from ptpython.entry_points.run_ptpython import ( # pylint: disable=import-error get_config_and_history_file, ) parser = ArgumentParser() parser.add_argument("--history_file") parser.add_argument("--config_file") return get_config_and_history_file(parser.parse_args([]))[1] def run_ptpython_shell(globals, locals): # Use the default ptpython history history_filename = get_ptpython_history_file() ptpython_embed(globals=globals.copy(), locals=locals.copy(), history_filename=history_filename, configure=run_config) def run_ptipython_shell(globals, locals): # Use the default ptpython history history_filename = get_ptpython_history_file() ptipython_embed(globals=globals.copy(), locals=locals.copy(), history_filename=history_filename, configure=run_config) # vim: foldmethod=marker pudb-2024.1.3/pudb/source_view.py000066400000000000000000000302131470451231400165330ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import urwid TABSTOP = 8 class SourceLine(urwid.FlowWidget): def __init__(self, dbg_ui, text, line_nr="", attr=None, has_breakpoint=False): self.dbg_ui = dbg_ui self.text = text self.attr = attr self.line_nr = line_nr self.has_breakpoint = has_breakpoint self.is_current = False self.highlight = False def selectable(self): return True def set_current(self, is_current): self.is_current = is_current self._invalidate() def set_highlight(self, highlight): self.highlight = highlight self._invalidate() def set_breakpoint(self, has_breakpoint): self.has_breakpoint = has_breakpoint self._invalidate() def rows(self, size, focus=False): return 1 def render(self, size, focus=False): from pudb.debugger import CONFIG render_line_nr = CONFIG["line_numbers"] maxcol = size[0] hscroll = self.dbg_ui.source_hscroll_start # attrs is a list of words like "focused" and "breakpoint" attrs = [] if self.is_current: crnt = ">" attrs.append("current") else: crnt = " " if self.has_breakpoint: bp = "*" attrs.append("breakpoint") else: bp = " " if focus: attrs.append("focused") elif self.highlight: if not self.has_breakpoint: attrs.append("highlighted") text = self.text if not attrs and self.attr is not None: attr = self.attr + [("source", None)] else: attr = [(" ".join(attrs+["source"]), None)] from urwid.util import apply_target_encoding, trim_text_attr_cs # build line prefix --------------------------------------------------- line_prefix = "" line_prefix_attr = [] if render_line_nr and self.line_nr: line_prefix_attr = [("line number", len(self.line_nr))] line_prefix = self.line_nr line_prefix = crnt+bp+line_prefix line_prefix_attr = [("current line marker", 1), ("breakpoint marker", 1)] \ + line_prefix_attr # assume rendered width is same as len line_prefix_len = len(line_prefix) encoded_line_prefix, line_prefix_cs = apply_target_encoding(line_prefix) assert len(encoded_line_prefix) == len(line_prefix) # otherwise we'd have to adjust line_prefix_attr... :/ # shipout, encoding --------------------------------------------------- cs = [] encoded_text_segs = [] encoded_attr = [] i = 0 for seg_attr, seg_len in attr: if seg_len is None: # means: gobble up remainder of text and rest of line # and fill with attribute rowlen = hscroll+maxcol remaining_text = text[i:] encoded_seg_text, seg_cs = apply_target_encoding( remaining_text + rowlen*" ") encoded_attr.append((seg_attr, len(remaining_text)+rowlen)) else: unencoded_seg_text = text[i:i+seg_len] encoded_seg_text, seg_cs = apply_target_encoding(unencoded_seg_text) adjustment = len(encoded_seg_text) - len(unencoded_seg_text) encoded_attr.append((seg_attr, seg_len + adjustment)) i += seg_len encoded_text_segs.append(encoded_seg_text) cs.extend(seg_cs) encoded_text = b"".join(encoded_text_segs) encoded_text, encoded_attr, cs = trim_text_attr_cs( encoded_text, encoded_attr, cs, hscroll, hscroll+maxcol-line_prefix_len) encoded_text = encoded_line_prefix + encoded_text encoded_attr = line_prefix_attr + encoded_attr cs = line_prefix_cs + cs return urwid.TextCanvas([encoded_text], [encoded_attr], [cs], maxcol=maxcol) def keypress(self, size, key): return key class ParseState: """States for the ArgumentParser class""" idle = 1 found_function = 2 found_open_paren = 3 class ArgumentParser: """Parse source code tokens and identify function arguments. This parser implements a state machine which accepts Pygments tokens, delivered sequentially from the beginning of a source file to its end. parse_token() processes each token (and its associated string) and returns None if that token does not require modification. When it finds a token which represents a function argument, it returns the correct token type for that item (the caller should then replace the associated item's token type with the returned type) """ def __init__(self, pygments_token): self.t = pygments_token self.state = ParseState.idle self.paren_level = 0 def parse_token(self, token, s): """Parse token. Return None or replacement token type""" if self.state == ParseState.idle: if token in (self.t.Name.Function, self.t.Name.Function.Magic): self.state = ParseState.found_function self.paren_level = 0 elif self.state == ParseState.found_function: if token is self.t.Punctuation and s == "(": self.state = ParseState.found_open_paren self.paren_level = 1 else: if (token is self.t.Name): return self.t.Token.Argument elif token is self.t.Punctuation and s == ")": self.paren_level -= 1 elif token is self.t.Punctuation and s == "(": self.paren_level += 1 if self.paren_level == 0: self.state = ParseState.idle return None try: import pygments # noqa except ImportError: def format_source(debugger_ui, lines, breakpoints): lineno_format = "%%%dd " % (len(str(len(lines)))) return [ SourceLine( debugger_ui, line.rstrip("\n\r").expandtabs(TABSTOP), lineno_format % (i+1), None, has_breakpoint=i+1 in breakpoints, ) for i, line in enumerate(lines) ] else: import pygments.token as t from pygments import highlight from pygments.formatter import Formatter from pygments.lexers import PythonLexer argument_parser = ArgumentParser(t) # NOTE: Tokens of the form t.Token. are not native # Pygments token types; they are user defined token # types. # # t.Token is a Pygments token creator object # (see http://pygments.org/docs/tokens/) # # The user defined token types get assigned by # one of several translation operations at the # beginning of add_snippet(). # ATTR_MAP = { # noqa: N806 t.Token: "source", t.Keyword.Namespace: "namespace", t.Token.Argument: "argument", t.Token.Dunder: "dunder", t.Token.Keyword2: "keyword2", t.Keyword: "keyword", t.Literal: "literal", t.Name: "name", t.Name.Exception: "exception", t.Name.Function: "function", t.Name.Function.Magic: "magic", t.Name.Class: "class", t.Name.Builtin: "builtin", t.Name.Builtin.Pseudo: "pseudo", t.Name.Variable.Magic: "magic", t.Punctuation: "punctuation", t.Operator: "operator", t.String: "string", t.String.Double: "doublestring", t.String.Single: "singlestring", t.String.Backtick: "backtick", t.String.Doc: "docstring", t.Comment: "comment", } # Token translation table. Maps token types and their # associated strings to new token types. ATTR_TRANSLATE = { # noqa: N806 t.Keyword: { "class": t.Token.Keyword2, "def": t.Token.Keyword2, "exec": t.Token.Keyword2, "lambda": t.Token.Keyword2, "print": t.Token.Keyword2, }, t.Operator: { ".": t.Token, }, } class UrwidFormatter(Formatter): def __init__(self, debugger_ui, lineno_format, breakpoints, **options): Formatter.__init__(self, **options) self.current_line = "" self.current_attr = [] self.lineno = 1 self.result = [] self.debugger_ui = debugger_ui self.lineno_format = lineno_format self.breakpoints = breakpoints def add_snippet(self, ttype, s): if not s: return # Find function arguments. When found, change their # ttype to t.Token.Argument new_ttype = argument_parser.parse_token(ttype, s) if new_ttype: ttype = new_ttype # Translate tokens if ttype in ATTR_TRANSLATE: if s in ATTR_TRANSLATE[ttype]: ttype = ATTR_TRANSLATE[ttype][s] # Translate dunder method tokens # NOTE: leaves "Magic" name tokens alone if (ttype == t.Name.Function and s.startswith("__") and s.endswith("__")): ttype = t.Token.Dunder while ttype not in ATTR_MAP: if ttype.parent is not None: ttype = ttype.parent else: raise RuntimeError( "untreated token type: %s" % str(ttype)) attr = ATTR_MAP[ttype] self.current_line += s self.current_attr.append((attr, len(s))) def shipout_line(self): self.result.append( SourceLine( self.debugger_ui, self.current_line, self.lineno_format % self.lineno, self.current_attr, has_breakpoint=self.lineno in self.breakpoints, )) self.current_line = "" self.current_attr = [] self.lineno += 1 def format(self, tokensource, outfile): for ttype, value in tokensource: while True: newline_pos = value.find("\n") if newline_pos == -1: self.add_snippet(ttype, value) break else: self.add_snippet(ttype, value[:newline_pos]) self.shipout_line() value = value[newline_pos+1:] if self.current_line: self.shipout_line() def format_source(debugger_ui, lines, breakpoints): lineno_format = "%%%dd " % (len(str(len(lines)))) formatter = UrwidFormatter(debugger_ui, lineno_format, breakpoints) highlight( "".join(line.expandtabs(TABSTOP) for line in lines), PythonLexer(stripnl=False), formatter, ) return formatter.result pudb-2024.1.3/pudb/test/000077500000000000000000000000001470451231400146075ustar00rootroot00000000000000pudb-2024.1.3/pudb/test/__init__.py000066400000000000000000000000001470451231400167060ustar00rootroot00000000000000pudb-2024.1.3/pudb/test/test_lowlevel.py000066400000000000000000000055441470451231400200610ustar00rootroot00000000000000import sys from pudb.lowlevel import decode_lines, detect_encoding def test_detect_encoding_nocookie(): lines = ["Test Проверка"] lines = [line.encode("utf-8") for line in lines] encoding, _ = detect_encoding(iter(lines)) assert encoding == "utf-8" def test_detect_encoding_cookie(): lines = [ "# coding=utf-8", "Test", "Проверка" ] lines = [line.encode("utf-8") for line in lines] encoding, _ = detect_encoding(iter(lines)) assert encoding == "utf-8" def test_decode_lines(): unicode_lines = [ "# coding=utf-8", "Test", "Проверка", ] lines = [line.encode("utf-8") for line in unicode_lines] assert unicode_lines == list(decode_lines(iter(lines))) # {{{ remove common indentation def _remove_common_indentation(code, require_leading_newline=True): if "\n" not in code: return code if require_leading_newline and not code.startswith("\n"): return code lines = code.split("\n") while lines[0].strip() == "": lines.pop(0) while lines[-1].strip() == "": lines.pop(-1) if lines: base_indent = 0 while lines[0][base_indent] in " \t": base_indent += 1 for line in lines[1:]: if line[:base_indent].strip(): raise ValueError("inconsistent indentation") return "\n".join(line[base_indent:] for line in lines) # }}} def test_executable_lines(): def get_exec_lines(src): code = compile( _remove_common_indentation(test_code), "", "exec") from pudb.lowlevel import get_executable_lines_for_codes_recursive return get_executable_lines_for_codes_recursive([code]) test_code = """ def main(): import pudb; pu.db conf = ''. \\ replace('', '') conf_tpl = '' # <-- impossible to set breakpoint here main() """ expected = {1, 2, 3, 4, 6, 8} if sys.version_info >= (3, 11): # See https://github.com/python/cpython/pull/94562 and # https://peps.python.org/pep-0626/ expected |= {0} assert get_exec_lines(test_code) == expected test_code = "a = 3*5\n" + 333 * "\n" + "b = 15" expected = { 1, 335 } if sys.version_info < (3, 10): # if co_lines is unavailable, we appear to see these expected.update([ 128, # bogus, 255, # bogus, ]) if sys.version_info >= (3, 11): # See https://github.com/python/cpython/pull/94562 and # https://peps.python.org/pep-0626/ expected |= {0} assert get_exec_lines(test_code) == expected if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1]) else: from pytest import main main([__file__]) pudb-2024.1.3/pudb/test/test_make_canvas.py000066400000000000000000000033041470451231400204700ustar00rootroot00000000000000from pudb.ui_tools import make_canvas def test_simple(): text = "aaaaaa" canvas = make_canvas( txt=[text], attr=[[("var value", len(text))]], maxcol=len(text) + 5 ) content = list(canvas.content()) assert content == [ [("var value", None, b"aaaaaa"), (None, None, b" " * 5)] ] def test_multiple(): canvas = make_canvas( txt=["Return: None"], attr=[[("return label", 8), ("return value", 4)]], maxcol=100 ) content = list(canvas.content()) assert content == [ [("return label", None, b"Return: "), ("return value", None, b"None"), (None, None, b" " * 88)] ] def test_boundary(): text = "aaaaaa" canvas = make_canvas( txt=[text], attr=[[("var value", len(text))]], maxcol=len(text) ) assert list(canvas.content()) == [[("var value", None, b"aaaaaa")]] def test_byte_boundary(): text = "aaaaaaé" canvas = make_canvas( txt=[text], attr=[[("var value", len(text))]], maxcol=len(text) ) assert list(canvas.content()) == [[("var value", None, b"aaaaaa\xc3\xa9")]] def test_wide_chars(): text = "data: '中文'" canvas = make_canvas( txt=[text], attr=[[("var label", 6), ("var value", 4)]], maxcol=47, ) assert list(canvas.content()) == [[ ("var label", None, b"data: "), ("var value", None, "'中文'".encode()), (None, None, b" "*(47 - 12)), # 10 chars, 2 of which are double width ]] if __name__ == "__main__": import sys if len(sys.argv) > 1: exec(sys.argv[1]) else: from pytest import main main([__file__]) pudb-2024.1.3/pudb/test/test_run.py000066400000000000000000000021621470451231400170250ustar00rootroot00000000000000#!/usr/bin/env python3 import os import pytest from pudb.run import main def csv(x): return "('" + "', '".join(x) + "',)" main_version_scenarios = [ ("-v",), ("--version",), ("--version", "dont_look_at_me.py"), ] @pytest.mark.parametrize( "argv", [pytest.param(s, id=csv(s)) for s in main_version_scenarios], ) def test_main_version(capsys, mocker, argv): mocker.patch("sys.argv", [os.path.basename(main.__code__.co_filename), *argv]) with pytest.raises(SystemExit) as ex: main() assert ex.value == 0 captured = capsys.readouterr() assert "pudb v" in captured.out def test_main_v_with_args(capsys, mocker): """ This will fail, because args is not only ``-v``, and that's reserved for future use ... """ mocker.patch("sys.argv", [ os.path.basename(main.__code__.co_filename), "-v", "dont_look_at_me.py" ]) with pytest.raises(SystemExit) as ex: main() assert ex.value == 2 captured = capsys.readouterr() assert "error: unrecognized arguments: -v" in captured.err assert not captured.out pudb-2024.1.3/pudb/test/test_settings.py000066400000000000000000000024711470451231400200640ustar00rootroot00000000000000import builtins import collections import pytest # noqa: F401 from pudb.settings import load_breakpoints, save_breakpoints def test_load_breakpoints(mocker): fake_data = ["b /home/user/test.py:41"], ["b /home/user/test.py:50"] mock_open = mocker.mock_open() mock_open.return_value.readlines.side_effect = fake_data mocker.patch.object(builtins, "open", mock_open) mocker.patch("pudb.settings.lookup_module", mocker.Mock(return_value="/home/user/test.py")) mocker.patch("pudb.settings.get_breakpoint_invalid_reason", mocker.Mock(return_value=None)) result = load_breakpoints() expected = [("/home/user/test.py", 41, False, None, None), ("/home/user/test.py", 50, False, None, None)] assert result == expected def test_save_breakpoints(mocker): MockBP = collections.namedtuple("MockBreakpoint", "file line cond") mock_breakpoints = [MockBP("/home/user/test.py", 41, None), MockBP("/home/user/test.py", 50, None)] mocker.patch("pudb.settings.get_breakpoints_file_name", mocker.Mock(return_value="saved-breakpoints")) mock_open = mocker.mock_open() mocker.patch.object(builtins, "open", mock_open) save_breakpoints(mock_breakpoints) mock_open.assert_called_with("saved-breakpoints", "w") pudb-2024.1.3/pudb/test/test_source_code_providers.py000066400000000000000000000025271470451231400226150ustar00rootroot00000000000000import pytest # noqa: F401 from pudb.debugger import ( DirectSourceCodeProvider, FileSourceCodeProvider, NullSourceCodeProvider, ) from pudb.source_view import SourceLine class TestNullSourceCodeProvider: def test_get_lines(self, mocker): provider = NullSourceCodeProvider() result = provider.get_lines(mocker.Mock()) assert len(result) == 10 assert isinstance(result[0], SourceLine) class TestFileSourceCodeProvider: def test_string_file_name(self, mocker): mock_debugger = mocker.Mock() mock_debugger.canonic = mocker.Mock(return_value="") provider = FileSourceCodeProvider(mock_debugger, "test file name") result = provider.get_lines(mocker.MagicMock()) assert len(result) == 1 assert isinstance(result[0], SourceLine) def test_get_lines(self, mocker): provider = FileSourceCodeProvider(mocker.Mock(), "test file name") result = provider.get_lines(mocker.MagicMock()) assert len(result) == 1 assert isinstance(result[0], SourceLine) class TestDirectSourceCodeProvider: def test_get_lines(self, mocker): provider = DirectSourceCodeProvider(mocker.Mock(), "test code") result = provider.get_lines(mocker.Mock()) assert len(result) == 1 assert isinstance(result[0], SourceLine) pudb-2024.1.3/pudb/test/test_var_view.py000066400000000000000000000273151470451231400200520ustar00rootroot00000000000000import contextlib import itertools import string import unittest from pudb.var_view import ( STRINGIFIERS, BasicValueWalker, FrameVarInfo, InspectInfo, PudbCollection, PudbMapping, PudbSequence, ValueWalker, get_stringifier, ui_log, ) class A: pass class A2: pass def test_get_stringifier(): try: import numpy as np except ImportError: numpy_values = [] else: numpy_values = [np.float32(5), np.zeros(5)] for value in [ A, A2, A(), A2(), "lól".encode(), "lól", 1233123, ["lól".encode(), "lól"], ] + numpy_values: for display_type in STRINGIFIERS: iinfo = InspectInfo() iinfo.display_type = display_type strifier = get_stringifier(iinfo) s = strifier(value) assert isinstance(s, str) class FrameVarInfoForTesting(FrameVarInfo): def __init__(self, paths_to_expand=None): super().__init__() if paths_to_expand is None: paths_to_expand = set() self.paths_to_expand = paths_to_expand def get_inspect_info(self, id_path, read_only): iinfo = super().get_inspect_info( id_path, read_only) iinfo.access_level = "all" iinfo.display_type = "repr" iinfo.show_methods = True if id_path in self.paths_to_expand: iinfo.show_detail = True return iinfo class Reasonable: def __init__(self): self.x = 42 def bar(self): return True @property def red(self): return "red" @classmethod def blue(cls): return "blue" @staticmethod def green(): return "green" def _private(self): return "shh" def __magicsomething__(self): return "amazing" def method_factory(method_name): def method(self, *args, **kwargs): func = getattr(self.__internal_dict__, method_name) try: return func(*args, **kwargs) except KeyError: # Classes without __iter__ are expected to raise IndexError in this # sort of case. Frustrating, I know. if (method_name == "__getitem__" and args and isinstance(args[0], int)): raise IndexError from None raise return method def generate_containerlike_class(): methods = { "__contains__", "__getitem__", "__iter__", "__len__", "__reversed__", "count", "get", "index", "items", "keys", "values", } # Deliberately starting from 0 for r in range(0, len(methods) + 1): for selected_methods in sorted( map(sorted, itertools.combinations(methods, r))): class ContainerlikeClass: def __init__(self, iterable): self.__internal_dict__ = dict(iterable) @classmethod def name(cls): return "ContainerlikeClass:{}".format( ":".join(selected_methods)) # noqa: B023 for method in selected_methods: func = method_factory(method) setattr(ContainerlikeClass, method, func) yield ContainerlikeClass class BaseValueWalkerTestCase(unittest.TestCase): """ There are no actual tests defined in this class, it provides utilities useful for testing the variable view in various ways. """ EMPTY_ITEM = (ValueWalker.EMPTY_LABEL, None) MOD_STR = " [all+()]" def setUp(self): self.values_to_expand = [] self.class_counts = { "mappings": 0, "sequences": 0, "collections": 0, "other": 0, } def value_string(self, obj, expand=True): if expand and obj in self.values_to_expand: return repr(obj) + self.MOD_STR return repr(obj) def walked_values(self): return [(w.var_label, w.value_str) for w in self.walker.widget_list] def expected_attrs(self, obj): """ `dir()` the object and return (label, value string) pairs for each attribute. Should match the order that these attributes would appear in the var_view. """ return [("." + str(label), self.value_string(getattr(obj, label), expand=False)) for label in sorted(dir(obj))] @contextlib.contextmanager def patched_logging(self): """ Context manager that patches ui_log.exception such that the test will fail if it is called. """ def fake_exception_log(*args, **kwargs): self.fail("ui_log.exception was unexpectedly called") old_logger = ui_log.exception ui_log.exception = fake_exception_log try: yield finally: ui_log.exception = old_logger def assert_walks_contents(self, container, label="xs"): expand_paths = {label} self.values_to_expand = [container] self.walker = BasicValueWalker(FrameVarInfoForTesting(expand_paths)) # Build out list of expected view contents according to container type. expected = [(label, self.value_string(container))] if isinstance(container, PudbMapping): expected.extend([(f"[{repr(key)}]", repr(container[key])) for key in container.keys()] or [self.EMPTY_ITEM]) self.class_counts["mappings"] += 1 elif isinstance(container, PudbSequence): expected.extend([(f"[{repr(index)}]", repr(entry)) for index, entry in enumerate(container)] or [self.EMPTY_ITEM]) self.class_counts["sequences"] += 1 elif isinstance(container, PudbCollection): expected.extend([("[]", repr(entry)) for entry in container] or [self.EMPTY_ITEM]) self.class_counts["collections"] += 1 else: self.class_counts["other"] += 1 expected.extend(self.expected_attrs(container)) with self.patched_logging(): self.walker.walk_value(parent=None, label=label, value=container) received = self.walked_values() self.assertListEqual(expected, received) def assert_class_counts_equal(self, seen=None): """ This is kinda weird since at first it looks like its testing the test code, but really it's testing the `isinstance` checks. But it is also true that it tests the test code, kind of like a sanity check. """ expected = { "mappings": 0, "sequences": 0, "collections": 0, "other": 0, } if seen is not None: expected.update(seen) self.assertDictEqual(expected, self.class_counts) class ValueWalkerTest(BaseValueWalkerTestCase): def test_simple_values(self): self.walker = BasicValueWalker(FrameVarInfoForTesting()) values = [ # numbers 0, 1, -1234567890412345243, float(4.2), float("inf"), complex(1.3, -1), # strings "", "a", "foo bar", " lots\tof\nspaces\r ", "♫", # other False, True, None, ] for label, value in enumerate(values): with self.patched_logging(): self.walker.walk_value(parent=None, label=str(label), value=value) expected = [(str(_label), repr(x)) for _label, x in enumerate(values)] received = self.walked_values() self.assertListEqual(expected, received) def test_simple_values_expandable(self): """ Simple values like numbers and strings are now expandable so we can peak under the hood and take a look at their attributes. Make sure that's working properly. """ values = [ # numbers 0, 1, -1234567890412345243, float(4.2), float("inf"), complex(1.3, -1), # strings "", "a", "foo bar", # " lots\tof\nspaces\r ", # long, hits continuation item "♫", # other False, True, None, ] for value in values: self.assert_walks_contents(value) def test_set(self): self.assert_walks_contents({ 42, "foo", None, False, (), ("a", "tuple") }) self.assert_class_counts_equal({"collections": 1}) def test_frozenset(self): self.assert_walks_contents(frozenset([ 42, "foo", None, False, (), ("a", "tuple") ])) self.assert_class_counts_equal({"collections": 1}) def test_dict(self): self.assert_walks_contents({ 0: 42, "a": "foo", "": None, True: False, frozenset(range(3)): "abc", (): "empty tuple", (1, 2, "c", ()): "tuple", }) self.assert_class_counts_equal({"mappings": 1}) def test_list(self): self.assert_walks_contents([ 42, "foo", None, False, (), ("a", "tuple") ]) self.assert_class_counts_equal({"sequences": 1}) def test_tuple(self): self.assert_walks_contents(( 42, "foo", None, False, (), ("a", "tuple") )) self.assert_class_counts_equal({"sequences": 1}) def test_containerlike_classes(self): class_count = 0 for cls_idx, containerlike_class in enumerate( generate_containerlike_class()): label = containerlike_class.name() value = containerlike_class(zip(string.ascii_lowercase, range(3, 10))) self.assert_walks_contents(container=value, label=label) class_count = cls_idx + 1 self.assert_class_counts_equal({ "mappings": 256, "sequences": 256, "collections": 256, "other": 1280, }) walked_total = (self.class_counts["mappings"] + self.class_counts["sequences"] + self.class_counts["collections"] + self.class_counts["other"]) # +1 here because enumerate starts from 0, not 1 self.assertEqual(class_count, walked_total) def test_empty_frozenset(self): self.assert_walks_contents(frozenset()) def test_empty_set(self): self.assert_walks_contents(set()) def test_empty_dict(self): self.assert_walks_contents({}) def test_empty_list(self): self.assert_walks_contents([]) def test_reasonable_class(self): """ Are the class objects themselves expandable? """ self.assert_walks_contents(Reasonable, label="Reasonable") self.assert_class_counts_equal({"other": 1}) def test_maybe_unreasonable_classes(self): """ Are class objects, that might look like containers if we're not careful, reasonably expandable? """ for containerlike_class in generate_containerlike_class(): self.assert_walks_contents( container=containerlike_class, label=containerlike_class.name() ) # This effectively makes sure that class definitions aren't considered # containers. self.assert_class_counts_equal({"other": 2048}) pudb-2024.1.3/pudb/theme.py000066400000000000000000000242761470451231400153170ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from dataclasses import astuple, dataclass, replace from typing import Optional from pudb.lowlevel import ui_log from pudb.themes import THEMES from pudb.themes.utils import ( add_setting, inheritance_overrides, link, reset_inheritance_overrides, ) @dataclass class PaletteEntry: name: str foreground: str = "default" background: str = "default" mono: Optional[str] = None foreground_high: Optional[str] = None background_high: Optional[str] = None def handle_256_colors(self): if self.foreground.lower().strip().startswith("h"): self.foreground_high = self.foreground self.foreground = "default" if self.background.lower().strip().startswith("h"): self.background_high = self.background self.background = "default" # ------------------------------------------------------------------------------ # Reference for some palette items: # # "namespace" : "import", "from", "using" # "operator" : "+", "-", "=" etc. # NOTE: Does not include ".", which is assigned the type "source" # "argument" : Function arguments # "builtin" : "range", "dict", "set", "list", etc. # "pseudo" : "self", "cls" # "dunder" : Class method names of the form ____ within # a class definition # "magic" : Subset of "dunder", methods that the python language assigns # special meaning to. ("__str__", "__init__", etc.) # "exception" : Exception names # "keyword" : All keywords except those specifically assigned to "keyword2" # ("from", "and", "break", "is", "try", "True", "None", etc.) # "keyword2" : "class", "def", "exec", "lambda", "print" # ------------------------------------------------------------------------------ # {{{ style inheritance BASE_STYLES = { "background": None, "selectable": None, "focused selectable": None, "highlighted": None, "hotkey": None, } # Map styles to their parent. If a style is not defined, use the parent style # recursively. # focused > highlighted > current > breakpoint line/disabled breakpoint INHERITANCE_MAP = { # {{{ general ui "label": "background", "header": "background", "dialog title": "header", "group head": "header", "focused sidebar": "header", "input": "selectable", "focused input": "focused selectable", "button": "input", "focused button": "focused input", "value": "input", "fixed value": "label", "warning": "highlighted", "header warning": "warning", "search box": "focused input", "search not found": "warning", # }}} # {{{ source view "source": "selectable", "focused source": "focused selectable", "highlighted source": "highlighted", "current source": "source", "current focused source": "focused source", "current highlighted source": "current source", "breakpoint source": "source", "breakpoint focused source": "focused source", "current breakpoint source": "current source", "current breakpoint focused source": "current focused source", "line number": "source", "breakpoint marker": "line number", "current line marker": "breakpoint marker", # }}} # {{{ sidebar "sidebar one": "selectable", "sidebar two": "selectable", "sidebar three": "selectable", "focused sidebar one": "focused selectable", "focused sidebar two": "focused selectable", "focused sidebar three": "focused selectable", # }}} # {{{ variables view "variables": "selectable", "variable separator": "background", "var value": "sidebar one", "var label": "sidebar two", "focused var value": "focused sidebar one", "focused var label": "focused sidebar two", "highlighted var label": "highlighted", "highlighted var value": "highlighted", "focused highlighted var label": "focused var label", "focused highlighted var value": "focused var value", "return label": "var label", "return value": "var value", "focused return label": "focused var label", "focused return value": "focused var value", # }}} # {{{ stack "stack": "selectable", "frame name": "sidebar one", "frame class": "sidebar two", "frame location": "sidebar three", "focused frame name": "focused sidebar one", "focused frame class": "focused sidebar two", "focused frame location": "focused sidebar three", "current frame name": "frame name", "current frame class": "frame class", "current frame location": "frame location", "focused current frame name": "focused frame name", "focused current frame class": "focused frame class", "focused current frame location": "focused frame location", # }}} # {{{ breakpoints view "breakpoint": "sidebar two", "disabled breakpoint": "sidebar three", "current breakpoint": "breakpoint", "disabled current breakpoint": "disabled breakpoint", "focused breakpoint": "focused sidebar two", "focused current breakpoint": "focused breakpoint", "focused disabled breakpoint": "focused sidebar three", "focused disabled current breakpoint": "focused disabled breakpoint", # }}} # {{{ shell "command line edit": "source", "command line output": "source", "command line prompt": "source", "command line input": "source", "command line error": "warning", "focused command line output": "focused source", "focused command line input": "focused source", "focused command line error": "focused source", "command line clear button": "button", "command line focused button": "focused button", # }}} # {{{ Code syntax "comment": "source", "keyword": "source", "literal": "source", "name": "source", "operator": "source", "punctuation": "source", "argument": "name", "builtin": "name", "exception": "name", "function": "name", "pseudo": "builtin", "class": "function", "dunder": "function", "magic": "dunder", "namespace": "keyword", "keyword2": "keyword", "string": "literal", "doublestring": "string", "singlestring": "string", "docstring": "string", "backtick": "string", # }}} } def set_style(palette_dict: dict, style_name: str, inheritance_overrides: dict) -> PaletteEntry: """ Recursively search up the style hierarchy for the first style which has been defined, and add it to the palette_dict under the given style_name. """ try: style = palette_dict[style_name] if not isinstance(style, PaletteEntry): style = PaletteEntry(style_name, *style) style.handle_256_colors() palette_dict[style_name] = style return style except KeyError: parent_name = inheritance_overrides.get( style_name, INHERITANCE_MAP[style_name], ) style = replace( set_style(palette_dict, parent_name, inheritance_overrides), name=style_name ) palette_dict[style_name] = style return style # }}} # {{{ get palette def get_palette(may_use_fancy_formats: bool, theme: str = "classic") -> list: """ Load the requested theme and return a list containing all palette entries needed to highlight the debugger UI, including syntax highlighting. """ reset_inheritance_overrides() try: palette_dict = THEMES[theme] except KeyError: # {{{ custom try: # {{{ base styles palette_dict = { "background": ("black", "light gray"), "hotkey": (add_setting("black", "underline"), "light gray"), "selectable": ("black", "dark cyan"), "focused selectable": ("black", "dark green"), "input": (add_setting("yellow", "bold"), "dark blue"), "warning": (add_setting("white", "bold"), "dark red"), "highlighted": ("white", "dark cyan"), "source": ("white", "dark blue"), } # }}} symbols = { "palette": palette_dict, "add_setting": add_setting, "link": link, } from os.path import expanduser, expandvars fname = expanduser(expandvars(theme)) with open(fname) as inf: exec(compile(inf.read(), fname, "exec"), symbols) except FileNotFoundError: ui_log.error("Unable to locate custom theme file {!r}" .format(theme)) return None except Exception: ui_log.exception("Error when importing theme:") return None # }}} # Apply style inheritance for style_name in set(INHERITANCE_MAP.keys()).union(BASE_STYLES.keys()): set_style(palette_dict, style_name, inheritance_overrides) palette_list = [ astuple(entry) for entry in palette_dict.values() if isinstance(entry, PaletteEntry) ] return palette_list # }}} # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/000077500000000000000000000000001470451231400151155ustar00rootroot00000000000000pudb-2024.1.3/pudb/themes/__init__.py000066400000000000000000000014601470451231400172270ustar00rootroot00000000000000from .agr_256 import palette_dict as agr256 from .classic import palette_dict as classic from .dark_vim import palette_dict as darkvim from .gray_light_256 import palette_dict as graylight256 from .midnight import palette_dict as midnight from .mono import palette_dict as mono from .monokai import palette_dict as monokai from .monokai_256 import palette_dict as monokai256 from .nord_dark_256 import palette_dict as norddark256 from .solarized import palette_dict as solarized from .vim import palette_dict as vim THEMES = { "classic": classic, "vim": vim, "dark vim": darkvim, "midnight": midnight, "monokai": monokai, "solarized": solarized, "mono": mono, "agr-256": agr256, "gray-light-256": graylight256, "monokai-256": monokai256, "nord-dark-256": norddark256, } pudb-2024.1.3/pudb/themes/agr_256.py000066400000000000000000000053171470451231400166420ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link # Give the colors some comprehensible names black = "h235" blacker = "h233" dark_cyan = "h24" dark_gray = "h241" dark_green = "h22" dark_red = "h88" dark_teal = "h23" light_blue = "h111" light_cyan = "h80" light_gray = "h252" light_green = "h113" light_red = "h160" medium_gray = "h246" salmon = "h223" orange = "h173" white = "h255" yellow = "h192" link("focused breakpoint", "focused selectable") link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": (black, light_gray), "selectable": (white, blacker), "focused selectable": (yellow, dark_cyan), "hotkey": (add_setting(black, "underline"), light_gray), "highlighted": (white, dark_green), # }}} # {{{ general ui "focused sidebar": (dark_cyan, light_gray), "group head": (add_setting(dark_cyan, "bold"), light_gray), "dialog title": (add_setting(light_gray, "bold"), black), "warning": (add_setting(white, "bold"), dark_red), "fixed value": (add_setting(white, "bold"), dark_gray), "button": (add_setting(white, "bold"), black), "focused button": (add_setting(yellow, "bold"), dark_cyan), # }}} # {{{ source view "line number": (dark_gray, black), "current line marker": (add_setting(yellow, "bold"), black), "breakpoint marker": (add_setting(light_red, "bold"), black), "source": (white, black), "breakpoint source": (add_setting(white, "bold"), dark_red), "current source": (add_setting(light_gray, "bold"), dark_teal), # }}} # {{{ sidebar "sidebar two": (light_blue, blacker), "focused sidebar two": (light_gray, dark_cyan), "sidebar three": (medium_gray, blacker), "focused sidebar three": (salmon, dark_cyan), # }}} # {{{ variables view "highlighted var label": (light_gray, dark_green), "return label": (light_green, blacker), "focused return label": (add_setting(light_gray, "bold"), dark_cyan), # }}} # {{{ stack "current frame name": (yellow, blacker), "focused current frame name": (add_setting(yellow, "bold"), dark_cyan), # }}} # {{{ shell "command line prompt": (add_setting(yellow, "bold"), black), "command line output": (light_cyan, black), "command line error": (light_red, black), # }}} # {{{ Code syntax "comment": (medium_gray, black), "exception": (orange, black), "function": (yellow, black), "keyword": (light_blue, black), "literal": (orange, black), "operator": (yellow, black), "pseudo": (medium_gray, black), "punctuation": (salmon, black), "string": (light_green, black), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/classic.py000066400000000000000000000042471470451231400171170ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": ("black", "light gray"), "selectable": ("black", "dark cyan"), "focused selectable": ("black", "light cyan"), "highlighted": ("dark blue", "yellow"), "hotkey": (add_setting("black", "underline"), "light gray"), # }}} # {{{ general ui "header": ("dark blue", "light gray"), "dialog title": (add_setting("white", "bold"), "dark blue"), "warning": (add_setting("white", "bold"), "dark red"), # }}} # {{{ source view "source": ("yellow", "dark blue"), "current source": ("dark blue", "dark green"), "breakpoint source": (add_setting("yellow", "bold"), "dark red"), "line number": ("light gray", "dark blue"), "breakpoint marker": (add_setting("dark red", "bold"), "dark blue"), # }}} # {{{ sidebar "sidebar two": ("dark blue", "dark cyan"), "sidebar three": ("dark gray", "dark cyan"), "focused sidebar two": ("dark blue", "light cyan"), "focused sidebar three": ("dark gray", "light cyan"), # }}} # {{{ variables view "return label": ("white", "dark blue"), "focused return label": ("light gray", "dark blue"), # }}} # {{{ stack "current frame name": (add_setting("white", "bold"), "dark cyan"), "focused current frame name": (add_setting("black", "bold"), "light cyan"), # }}} # {{{ shell "command line output": ("light cyan", "dark blue"), "command line prompt": (add_setting("white", "bold"), "dark blue"), "command line error": (add_setting("light green", "bold"), "dark blue"), "command line clear button": (add_setting("white", "bold"), "dark blue"), "command line focused button": ("dark blue", "dark cyan"), # }}} # {{{ Code syntax "keyword": (add_setting("white", "bold"), "dark blue"), "function": ("light cyan", "dark blue"), "literal": (add_setting("light green", "bold"), "dark blue"), "punctuation": ("light gray", "dark blue"), "comment": ("dark cyan", "dark blue"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/dark_vim.py000066400000000000000000000036031470451231400172650ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": ("black", "light gray"), "selectable": ("white", "dark gray"), "focused selectable": (add_setting("white", "bold"), "light blue"), "highlighted": ("black", "dark green"), "hotkey": (add_setting("dark blue", "underline"), "light gray"), # }}} # {{{ general ui "header": ("dark blue", "light gray"), "dialog title": (add_setting("white", "bold"), "black"), "warning": (add_setting("light red", "bold"), "black"), "header warning": (add_setting("light red", "bold"), "light gray"), # }}} # {{{ source view "source": ("white", "black"), "current source": (add_setting("white", "bold"), "dark gray"), "line number": (add_setting("dark gray", "bold"), "black"), "breakpoint marker": (add_setting("light red", "bold"), "black"), "breakpoint source": (add_setting("white", "bold"), "dark red"), # }}} # {{{ sidebar "sidebar two": ("yellow", "dark gray"), "focused sidebar two": ("light cyan", "light blue"), "sidebar three": ("light gray", "dark gray"), "focused sidebar three": ("yellow", "light blue"), # }}} # {{{ stack "current frame name": ( add_setting("white", "bold"), "dark gray"), # }}} # {{{ shell "command line output": (add_setting("yellow", "bold"), "black"), # }}} # {{{ Code syntax "keyword": ("yellow", "black"), "literal": ("light magenta", "black"), "function": (add_setting("light cyan", "bold"), "black"), "punctuation": ("yellow", "black"), "comment": ("dark cyan", "black"), "exception": ("light red", "black"), "builtin": ("light green", "black"), "pseudo": ("dark green", "black"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/gray_light_256.py000066400000000000000000000023741470451231400202220ustar00rootroot00000000000000from pudb.themes.utils import add_setting palette_dict = { # {{{ base styles "background": ("h232", "h248"), "selectable": ("h232", "h252"), "focused selectable": ("h232", "h251"), "highlighted": (add_setting("h234", "bold, underline"), "h252"), "hotkey": (add_setting("h232", "underline"), "h248"), # }}} # {{{ general ui "focused sidebar": (add_setting("h232", "bold"), "h248"), "warning": (add_setting("h232", "bold"), "h253"), "group head": (add_setting("h232", "bold"), "h248"), "dialog title": (add_setting("h232", "underline, bold"), "h248"), # }}} # {{{ source view "source": ("h235", "h253"), "current source": (add_setting("h232", "underline"), "h253"), "line number": ("h244", "h253"), # }}} # {{{ sidebar "sidebar two": (add_setting("h234", "bold"), "h252"), "focused sidebar two": (add_setting("h234", "bold"), "h251"), "sidebar three": ("h239", "h252"), "focused sidebar three": ("h239", "h251"), # }}} # {{{ Code syntax "exception": (add_setting("h236", "underline"), "h253"), "class": (add_setting("h234", "bold, underline"), "h253"), "keyword": (add_setting("h234", "bold"), "h253"), "comment": ("h244", "h253"), # }}} } pudb-2024.1.3/pudb/themes/midnight.py000066400000000000000000000073731470451231400173040ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link # Based on XCode's midnight theme # Looks best in a console with green text against black background link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": ("black", "light gray"), "selectable": ("white", "black"), "focused selectable": ("white", "dark blue"), "hotkey": (add_setting("black", "underline, italics"), "light gray"), "highlighted": ("black", "dark green"), # }}} # {{{ general ui "input": ("light green", "black"), "focused input": ("light green", "black"), "warning": (add_setting("white", "bold"), "dark red"), "dialog title": (add_setting("white", "bold"), "dark blue"), "group head": (add_setting("dark blue", "bold"), "light gray"), "button": (add_setting("white", "bold"), "dark blue"), "focused button": ("white", "black"), "focused sidebar": ("black", "white"), "value": (add_setting("yellow", "bold"), "dark blue"), # }}} # {{{ source view "source": ("light green", "black"), "highlighted source": ("black", "dark green"), "current source": ("black", "brown"), "current focused source": (add_setting("yellow", "bold"), "dark blue"), "breakpoint source": (add_setting("yellow", "bold"), "dark red"), "current breakpoint source": ("black", "dark red"), "line number": ("light gray", "black"), "current line marker": ("dark red", "black"), "breakpoint marker": ("dark red", "black"), # }}} # {{{ sidebar "sidebar two": ("light blue", "black"), "sidebar three": ("light cyan", "black"), # }}} # {{{ variables view "return label": ("white", "dark blue"), "return value": ("black", "dark cyan"), "focused return label": ("light gray", "dark blue"), # }}} # {{{ stack "current frame name": (add_setting("white", "bold"), "black"), "current frame class": (add_setting("light blue", "bold"), "black"), "current frame location": (add_setting("light cyan", "bold"), "black"), "focused current frame name": (add_setting("white", "bold"), "dark blue"), "focused current frame class": (add_setting("white", "bold"), "dark blue"), "focused current frame location": (add_setting("white", "bold"), "dark blue"), # }}} # {{{ breakpoints view "breakpoint": ("white", "black"), "disabled breakpoint": ("dark gray", "black"), "focused disabled breakpoint": ("light gray", "dark blue"), "current breakpoint": (add_setting("white", "bold"), "black"), "disabled current breakpoint": (add_setting("dark gray", "bold"), "black"), "focused current breakpoint": (add_setting("white", "bold"), "dark blue"), "focused disabled current breakpoint": ( add_setting("light gray", "bold"), "dark blue"), # }}} # {{{ shell "command line edit": ("white", "black"), "command line prompt": (add_setting("white", "bold"), "black"), "command line input": ("white", "black"), "command line error": (add_setting("light red", "bold"), "black"), "command line clear button": (add_setting("white", "bold"), "black"), "command line focused button": ("white", "dark blue"), # }}} # {{{ Code syntax "keyword": ("dark magenta", "black"), "operator": ("dark green", "black"), "pseudo": ("light magenta", "black"), "function": (add_setting("light blue", "bold"), "black"), "builtin": ("dark gray", "black"), "literal": ("dark cyan", "black"), "string": ("dark red", "black"), "docstring": ("yellow", "black"), "backtick": ("dark green", "black"), "punctuation": ("white", "black"), "comment": ("white", "black"), "exception": ("dark green", "black"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/mono.py000066400000000000000000000002721470451231400164400ustar00rootroot00000000000000palette_dict = { "background": ("standout",), "selectable": (), "focused selectable": ("underline",), "highlighted": ("bold",), "hotkey": ("underline, standout",), } pudb-2024.1.3/pudb/themes/monokai.py000066400000000000000000000051731470451231400171320ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": ("black", "light gray"), "selectable": ("white", "black"), "focused selectable": ("white", "dark gray"), "highlighted": ("black", "dark green"), "hotkey": (add_setting("black", "underline"), "light gray"), # }}} # {{{ general ui "input": ("white", "black"), "button": (add_setting("white", "bold"), "black"), "focused button": (add_setting("white", "bold"), "dark gray"), "focused sidebar": ("dark blue", "light gray"), "warning": (add_setting("white", "bold"), "dark red"), "group head": (add_setting("black", "bold"), "light gray"), "dialog title": (add_setting("white", "bold"), "black"), # }}} # {{{ source view "current source": ("black", "dark cyan"), "breakpoint source": (add_setting("white", "bold"), "dark red"), "line number": ("dark gray", "black"), "current line marker": (add_setting("dark cyan", "bold"), "black"), "breakpoint marker": (add_setting("dark red", "bold"), "black"), # }}} # {{{ sidebar "sidebar two": ("light cyan", "black"), "focused sidebar two": ("light cyan", "dark gray"), "sidebar three": ("light magenta", "black"), "focused sidebar three": ("light magenta", "dark gray"), # }}} # {{{ variables view "return label": ("light green", "black"), "focused return label": ("light green", "dark gray"), # }}} # {{{ stack "current frame name": ("light green", "black"), "focused current frame name": ("light green", "dark gray"), # }}} # {{{ shell "command line prompt": (add_setting("yellow", "bold"), "black"), "command line output": ("light cyan", "black"), "command line error": ("yellow", "black"), "focused command line output": ("light cyan", "dark gray"), "focused command line error": (add_setting("yellow", "bold"), "dark gray"), # }}} # {{{ Code syntax "literal": ("light magenta", "black"), "builtin": ("light cyan", "black"), "exception": ("light cyan", "black"), "keyword2": ("light cyan", "black"), "function": ("light green", "black"), "class": (add_setting("light green", "underline"), "black"), "keyword": ("light red", "black"), "operator": ("light red", "black"), "comment": ("dark gray", "black"), "docstring": ("dark gray", "black"), "argument": ("brown", "black"), "pseudo": ("brown", "black"), "string": ("yellow", "black"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/monokai_256.py000066400000000000000000000057341470451231400175310ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link # Give the colors some comprehensible names black = "h236" blacker = "h234" dark_gray = "h240" dark_green = "h28" dark_red = "h124" dark_teal = "h30" dark_magenta = "h141" light_blue = "h111" light_cyan = "h51" light_gray = "h252" light_green = "h155" light_red = "h160" light_magenta = "h198" medium_gray = "h243" orange = "h208" white = "h255" yellow = "h228" link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": (black, light_gray), "selectable": (white, blacker), "focused selectable": (white, dark_gray), "highlighted": (white, dark_green), "hotkey": (add_setting(black, "underline"), light_gray), # }}} # {{{ general ui "input": (white, black), "button": (add_setting(white, "bold"), black), "focused button": (add_setting(white, "bold"), dark_gray), "focused sidebar": (dark_teal, light_gray), "warning": (add_setting(white, "bold"), dark_red), "group head": (add_setting(black, "bold"), light_gray), "dialog title": (add_setting(white, "bold"), blacker), # }}} # {{{ source view "source": (white, black), "current source": (add_setting(light_gray, "bold"), dark_teal), "breakpoint source": (add_setting(white, "bold"), dark_red), "line number": (dark_gray, black), "current line marker": (add_setting(light_cyan, "bold"), black), "breakpoint marker": (add_setting(light_red, "bold"), black), # }}} # {{{ sidebar "sidebar two": (light_cyan, blacker), "focused sidebar two": (light_cyan, dark_gray), "sidebar three": (dark_magenta, blacker), "focused sidebar three": (dark_magenta, dark_gray), # }}} # {{{ variables view "highlighted var label": (light_gray, dark_green), "return label": (light_green, blacker), "focused return label": (light_green, dark_gray), # }}} # {{{ stack "current frame name": (light_green, blacker), "focused current frame name": (light_green, dark_gray), # }}} # {{{ shell "command line prompt": (add_setting(yellow, "bold"), black), "command line output": (light_cyan, black), "command line error": (orange, black), "focused command line output": (light_cyan, dark_gray), "focused command line error": (add_setting(orange, "bold"), dark_gray), # }}} # {{{ Code syntax "literal": (dark_magenta, black), "builtin": (light_cyan, black), "exception": (light_cyan, black), "keyword2": (light_cyan, black), "function": (light_green, black), "class": (add_setting(light_green, "underline"), black), "keyword": (light_magenta, black), "operator": (light_magenta, black), "comment": (medium_gray, black), "docstring": (medium_gray, black), "argument": (orange, black), "pseudo": (orange, black), "string": (yellow, black), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/nord_dark_256.py000066400000000000000000000120221470451231400200230ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link # ------------------------------------------------------------------------------ # Colors are approximations of https://www.nordtheme.com/docs/colors-and-palettes # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Polar Night is made up of four darker colors (nord 0, nord1, nord2, and nord3) # that are commonly used for base elements like backgrounds or text color in # bright ambiance designs. # ------------------------------------------------------------------------------ # nord0 is the origin color or the Polar Night palette. nord0 = "h236" # nord1 is a brighter shade color based on nord0. nord1 = "h238" # nord2 is an even more brighter shade color of nord0. nord2 = "h239" # nord3 is the brightest shade color based on nord0. nord3 = "h243" # ------------------------------------------------------------------------------ # Snow Storm is made up of three bright colors that are commonly used for text # colors or base UI elements in bright ambiance designs. # ------------------------------------------------------------------------------ # nord4 is the origin color or the Snow Storm palette. nord4 = "h253" # nord5 is a brighter shade color of nord4. nord5 = "h254" # nord6 is the brightest shade color based on nord4. nord6 = "h255" # ------------------------------------------------------------------------------ # Frost can be described as the heart palette of Nord, a group of four bluish # colors that are commonly used for primary UI component and text highlighting # and essential code syntax elements. # ------------------------------------------------------------------------------ # nord7 is a calm and highly contrasted color reminiscent of # frozen polar water. nord7 = "h109" # nord8 is a bright and shiny primary accent color reminiscent of pure # and clear ice. nord8 = "h110" # nord9 is a more darkened and less saturated color reminiscent of # arctic waters. nord9 = "h111" # nord10 is a dark and intensive color reminiscent of the deep arctic ocean. nord10 = "h67" # ------------------------------------------------------------------------------ # Aurora consists of five colorful components reminiscent of the "Aurora # borealis", sometimes referred to as polar lights or northern lights. # ------------------------------------------------------------------------------ # nord11 is a reddish color. nord11 = "h138" # nord12 is an orangey color. nord12 = "h174" # nord13 is a yellowy color. nord13 = "h216" # nord14 is a greenish color. nord14 = "h150" # nord15 is a purplish color. nord15 = "h139" link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": (nord0, nord7), "selectable": (nord4, nord1), "focused selectable": (nord4, nord2), "highlighted": (nord14, nord1), "hotkey": (add_setting(nord0, "underline"), nord7), # }}} # {{{ general ui "input": (nord4, nord0), "button": (add_setting(nord4, "bold"), nord0), "focused button": (add_setting(nord4, "bold"), nord2), "focused sidebar": (nord0, nord7), "warning": (nord12, nord2), "group head": (add_setting(nord0, "bold"), nord7), "dialog title": (add_setting(nord4, "bold"), nord1), # }}} # {{{ source view "source": (nord4, nord0), "current source": (nord13, nord1), "current focused source": (nord13, nord2), "breakpoint source": (nord12, nord0), "line number": (nord2, nord0), "current line marker": (add_setting(nord13, "bold"), nord0), "breakpoint marker": (add_setting(nord13, "bold"), nord0), # }}} # {{{ sidebar "sidebar two": (nord7, nord1), "focused sidebar two": (nord7, nord2), "sidebar three": (nord10, nord1), "focused sidebar three": (nord10, nord2), # }}} # {{{ variables view "highlighted var label": (nord14, nord1), "focused highlighted var label": (nord14, nord2), "focused highlighted var value": (nord14, nord2), "return label": (nord0, nord8), "focused return label": (nord0, nord8), # }}} # {{{ stack "current frame name": (nord14, nord1), "focused current frame name": (nord14, nord2), # }}} # {{{ shell "command line prompt": (add_setting(nord5, "bold"), nord0), "command line output": (nord7, nord0), "command line error": (nord12, nord0), "focused command line output": (nord7, nord2), "focused command line error": (nord12, nord2), # }}} # {{{ Code syntax "literal": (nord15, nord0), "builtin": (add_setting(nord7, "bold"), nord0), "pseudo": (nord7, nord0), "exception": (nord11, nord0), "function": (nord7, nord0), "class": (add_setting(nord9, "underline"), nord0), "keyword": (nord8, nord0), "keyword2": (nord9, nord0), "operator": (nord8, nord0), "comment": (nord3, nord0), "string": (nord14, nord0), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/solarized.py000066400000000000000000000037461470451231400174750ustar00rootroot00000000000000from pudb.themes.utils import add_setting palette_dict = { # {{{ base styles "background": ("light green", "light gray"), "selectable": ("light green", "white"), "focused selectable": ("white", "dark blue"), "highlighted": ("white", "dark cyan"), "hotkey": (add_setting("black", "underline"), "light gray"), # }}} # {{{ general ui "dialog title": (add_setting("white", "bold"), "dark cyan"), "warning": (add_setting("light red", "bold"), "white"), "header warning": (add_setting("light red", "bold"), "light gray"), "focused sidebar": ("dark red", "light gray"), "group head": (add_setting("yellow", "bold"), "light gray"), # }}} # {{{ source view "source": ("yellow", "white"), "breakpoint source": ("light red", "light gray"), "current source": ("light gray", "light blue"), "line number": ("light blue", "white"), "current line marker": (add_setting("light blue", "bold"), "white"), "breakpoint marker": (add_setting("light red", "bold"), "white"), # }}} # {{{ sidebar "sidebar two": ("dark blue", "white"), "sidebar three": ("light cyan", "white"), "focused sidebar three": ("light gray", "dark blue"), # }}} # {{{ variables view "return label": ("white", "yellow"), "focused return label": ("white", "yellow"), # }}} # {{{ stack "current frame name": (add_setting("light green", "bold"), "white"), "focused current frame name": (add_setting("white", "bold"), "dark blue"), # }}} # {{{ shell "command line output": ("light green", "white"), # }}} # {{{ Code syntax "namespace": ("dark red", "white"), "exception": ("light red", "white"), "keyword": ("brown", "white"), "keyword2": ("dark magenta", "white"), "function": ("dark green", "white"), "literal": ("dark cyan", "white"), "builtin": ("dark blue", "white"), "comment": ("light cyan", "white"), "pseudo": ("light cyan", "white"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/themes/utils.py000066400000000000000000000005751470451231400166360ustar00rootroot00000000000000import urwid may_use_fancy_formats = not hasattr(urwid.escape, "_fg_attr_xterm") def add_setting(color, setting): if may_use_fancy_formats: return f"{color}, {setting}" return color inheritance_overrides = {} def link(child: str, parent: str): inheritance_overrides[child] = parent def reset_inheritance_overrides(): inheritance_overrides.clear() pudb-2024.1.3/pudb/themes/vim.py000066400000000000000000000045041470451231400162650ustar00rootroot00000000000000from pudb.themes.utils import add_setting, link link("current breakpoint", "current frame name") link("focused current breakpoint", "focused current frame name") palette_dict = { # {{{ base styles "background": ("black", "light gray"), "selectable": ("black", "dark cyan"), "focused selectable": ("black", "light cyan"), "hotkey": (add_setting("black", "bold, underline"), "light gray"), "highlighted": ("black", "yellow"), # }}} # {{{ general ui "header": (add_setting("black", "bold"), "light gray"), "group head": ("dark blue", "light gray"), "dialog title": (add_setting("white", "bold"), "dark blue"), "input": ("black", "dark cyan"), "focused input": ("black", "light cyan"), "warning": (add_setting("dark red", "bold"), "white"), "header warning": (add_setting("dark red", "bold"), "light gray"), # }}} # {{{ source view "source": ("black", "white"), "current source": ("black", "dark cyan"), "breakpoint source": ("dark red", "light gray"), "line number": ("dark gray", "white"), "current line marker": ("dark red", "white"), "breakpoint marker": ("dark red", "white"), # }}} # {{{ sidebar "sidebar one": ("black", "dark cyan"), "sidebar two": ("dark blue", "dark cyan"), "sidebar three": ("dark gray", "dark cyan"), "focused sidebar one": ("black", "light cyan"), "focused sidebar two": ("dark blue", "light cyan"), "focused sidebar three": ("dark gray", "light cyan"), # }}} # {{{ variables view "highlighted var label": ("dark blue", "yellow"), "return label": ("white", "dark blue"), "focused return label": ("light gray", "dark blue"), # }}} # {{{ stack "current frame name": (add_setting("white", "bold"), "dark cyan"), "focused current frame name": (add_setting("black", "bold"), "light cyan"), # }}} # {{{ shell "command line output": ( add_setting("dark gray", "bold"), "white"), # }}} # {{{ Code syntax "keyword2": ("dark magenta", "white"), "namespace": ("dark magenta", "white"), "literal": ("dark red", "white"), "exception": ("dark red", "white"), "comment": ("dark gray", "white"), "function": ("dark blue", "white"), "pseudo": ("dark gray", "white"), "builtin": ("light blue", "white"), # }}} } # vim: foldmethod=marker pudb-2024.1.3/pudb/ui_tools.py000066400000000000000000000232051470451231400160410ustar00rootroot00000000000000import urwid from urwid import calc_text_pos, calc_width # generic urwid helpers ------------------------------------------------------- def text_width(txt): """Return the width of the text in the terminal. :arg txt: A Unicode text object. Use this instead of len() whenever txt could contain double- or zero-width Unicode characters. """ return calc_width(txt, 0, len(txt)) def encode_like_urwid(s): from urwid.display import escape from urwid.util import _target_encoding # Consistent with # https://github.com/urwid/urwid/blob/2cc54891965283faf9113da72202f5d405f90fa3/urwid/util.py#L126-L128 s = s.replace(escape.SI+escape.SO, "") # remove redundant shifts s = s.encode(_target_encoding, "replace") return s def make_canvas(txt, attr, maxcol, fill_attr=None): processed_txt = [] processed_attr = [] processed_cs = [] for line, line_attr in zip(txt, attr): # filter out zero-length attrs line_attr = [(aname, la) for aname, la in line_attr if la > 0] diff = maxcol - text_width(line) if diff > 0: line += " "*diff line_attr.append((fill_attr, diff)) else: from urwid.util import rle_subseg line = line[:calc_text_pos(line, 0, len(line), maxcol)[0]] line_attr = rle_subseg(line_attr, 0, maxcol) from urwid.util import apply_target_encoding encoded_line, line_cs = apply_target_encoding(line) # line_cs contains byte counts as requested by TextCanvas, but # line_attr still contains column counts at this point: let's fix this. def get_byte_line_attr(line, line_attr): i = 0 for label, column_count in line_attr: byte_count = len(encode_like_urwid(line[i:i+column_count])) i += column_count yield label, byte_count line_attr = list(get_byte_line_attr(line, line_attr)) processed_txt.append(encoded_line) processed_attr.append(line_attr) processed_cs.append(line_cs) return urwid.TextCanvas( processed_txt, processed_attr, processed_cs, maxcol=maxcol) def make_hotkey_markup(s): import re match = re.match(r"^([^_]*)_(.)(.*)$", s) assert match is not None return [ (None, match.group(1)), ("hotkey", match.group(2)), (None, match.group(3)), ] def labelled_value(label, value): return urwid.AttrMap(urwid.Text([ ("label", label), str(value)]), "fixed value", "fixed value") def find_widget_in_container(container, widget) -> int: for i, (w, _) in enumerate(container.contents): if w == widget: return i raise ValueError(f"Widget not found in '{type(container).__name__}': {widget!r}") def focus_widget_in_container(container, widget) -> None: container.focus_position = find_widget_in_container(container, widget) class SelectableText(urwid.Text): def selectable(self): return True def keypress(self, size, key): return key class SignalWrap(urwid.WidgetWrap): def __init__(self, w, is_preemptive=False): urwid.WidgetWrap.__init__(self, w) self.event_listeners = [] self.is_preemptive = is_preemptive def listen(self, mask, handler): self.event_listeners.append((mask, handler)) def keypress(self, size, key): result = key if self.is_preemptive: for mask, handler in self.event_listeners: if mask is None or mask == key: result = handler(self, size, key) break if result is not None: result = self._w.keypress(size, key) if result is not None and not self.is_preemptive: for mask, handler in self.event_listeners: if mask is None or mask == key: return handler(self, size, key) return result # {{{ debugger-specific stuff class StackFrame(urwid.FlowWidget): def __init__(self, is_current, name, class_name, filename, line): self.is_current = is_current self.name = name self.class_name = class_name self.filename = filename self.line = line def selectable(self): return True def rows(self, size, focus=False): return 1 def render(self, size, focus=False): maxcol = size[0] if focus: apfx = "focused " else: apfx = "" if self.is_current: apfx += "current " crnt_pfx = ">> " else: crnt_pfx = " " text = crnt_pfx+self.name attr = [(apfx+"frame name", 4+len(self.name))] if self.class_name is not None: text += " [%s]" % self.class_name attr.append((apfx+"frame class", len(self.class_name)+2)) loc = " %s:%d" % (self.filename, self.line) text += loc attr.append((apfx+"frame location", len(loc))) return make_canvas([text], [attr], maxcol, apfx+"frame location") def keypress(self, size, key): return key class BreakpointFrame(urwid.FlowWidget): def __init__(self, is_current, filename, breakpoint): self.is_current = is_current self.filename = filename self.breakpoint = breakpoint self.line = breakpoint.line # Starts at 1 self.enabled = breakpoint.enabled self.hits = breakpoint.hits def selectable(self): return True def rows(self, size, focus=False): return 1 def render(self, size, focus=False): maxcol = size[0] if focus: apfx = "focused " else: apfx = "" bp_pfx = "" if not self.enabled: apfx += "disabled " bp_pfx += "X" if self.is_current: apfx += "current " bp_pfx += ">>" bp_pfx = bp_pfx.ljust(3) hits_label = "hits" if self.hits != 1 else "hit" loc = f" {self.filename}:{self.line} ({self.hits} {hits_label})" text = bp_pfx+loc attr = [(apfx+"breakpoint", len(text))] return make_canvas([text], [attr], maxcol, apfx+"breakpoint") def keypress(self, size, key): return key class SearchController: def __init__(self, ui): self.ui = ui self.highlight_line = None self.search_box = None self.last_search_string = None def cancel_highlight(self): if self.highlight_line is not None: self.highlight_line.set_highlight(False) self.highlight_line = None def cancel_search(self): self.cancel_highlight() self.hide_search_ui() def hide_search_ui(self): self.search_box = None del self.ui.lhs_col.contents[0] self.ui.lhs_col.set_focus(self.ui.lhs_col.widget_list[0]) def open_search_ui(self): lhs_col = self.ui.lhs_col if self.search_box is None: _, self.search_start = self.ui.source.get_focus() self.search_box = SearchBox(self) self.search_AttrMap = urwid.AttrMap( self.search_box, "search box") lhs_col.item_types.insert( 0, ("flow", None)) lhs_col.widget_list.insert(0, self.search_AttrMap) self.ui.reset_cmdline_size() self.ui.columns.set_focus(lhs_col) lhs_col.set_focus(self.search_AttrMap) def perform_search(self, dir, s=None, start=None, update_search_start=False): self.cancel_highlight() # self.ui.lhs_col.set_focus(self.ui.lhs_col.widget_list[1]) if s is None: s = self.last_search_string if s is None: self.ui.message("No previous search term.") return False else: self.last_search_string = s if start is None: start = self.search_start case_insensitive = s.lower() == s if start > len(self.ui.source): start = 0 i = (start+dir) % len(self.ui.source) if i >= len(self.ui.source): i = 0 while i != start: sline = self.ui.source[i].text if case_insensitive: sline = sline.lower() if s in sline: sl = self.ui.source[i] sl.set_highlight(True) self.highlight_line = sl self.ui.source.set_focus(i) if update_search_start: self.search_start = i return True i = (i+dir) % len(self.ui.source) return False class SearchBox(urwid.Edit): def __init__(self, controller): urwid.Edit.__init__(self, [("label", "Search: ")], "") self.controller = controller def keypress(self, size, key): result = urwid.Edit.keypress(self, size, key) txt = self.get_edit_text() if result is not None: if key == "esc": self.controller.cancel_search() return None elif key == "enter": if txt: self.controller.hide_search_ui() self.controller.perform_search(dir=1, s=txt, update_search_start=True) else: self.controller.cancel_search() return None else: if self.controller.perform_search(dir=1, s=txt): self.controller.search_AttrMap.set_attr_map({None: "search box"}) else: self.controller.search_AttrMap.set_attr_map( {None: "search not found"}) return result # }}} pudb-2024.1.3/pudb/var_view.py000066400000000000000000000614741470451231400160400ustar00rootroot00000000000000__copyright__ = """ Copyright (C) 2009-2017 Andreas Kloeckner Copyright (C) 2014-2017 Aaron Meurer """ __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # {{{ constants and imports import inspect import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Sized from typing import List, Tuple import urwid from pudb.lowlevel import ui_log from pudb.ui_tools import text_width try: import numpy HAVE_NUMPY = 1 except ImportError: HAVE_NUMPY = 0 # }}} # {{{ abstract base classes for containers class PudbCollection(ABC): # noqa: B024 @classmethod def __subclasshook__(cls, c): if cls is PudbCollection: try: return all([ any("__contains__" in b.__dict__ for b in c.__mro__), any("__iter__" in b.__dict__ for b in c.__mro__), ]) except Exception: pass return NotImplemented @classmethod def entries(cls, collection, label: str): """ :yield: ``(label, entry, id_path_ext)`` tuples for each entry in the collection. """ assert isinstance(collection, cls) try: for count, entry in enumerate(collection): yield None, entry, f"[{count:d}]" except Exception as error: ui_log.error("Object {l!r} appears to be a collection, but does " "not behave like one: {m}".format( l=label, m=error)) @classmethod def length(cls, collection): return len(collection) class PudbSequence(ABC): # noqa: B024 @classmethod def __subclasshook__(cls, c): if cls is PudbSequence: try: return all([ any("__getitem__" in b.__dict__ for b in c.__mro__), any("__iter__" in b.__dict__ for b in c.__mro__), ]) except Exception: pass return NotImplemented @classmethod def entries(cls, sequence, label: str): """ :yield: ``(label, entry, id_path_ext)`` tuples for each entry in the sequence. """ assert isinstance(sequence, cls) try: for count, entry in enumerate(sequence): yield str(count), entry, f"[{count:d}]" except Exception as error: ui_log.error("Object {l!r} appears to be a sequence, but does " "not behave like one: {m}".format( l=label, m=error)) @classmethod def length(cls, sequence): return len(sequence) class PudbMapping(ABC): # noqa: B024 @classmethod def __subclasshook__(cls, c): if cls is PudbMapping: try: return all([ any("__getitem__" in b.__dict__ for b in c.__mro__), any("__iter__" in b.__dict__ for b in c.__mro__), any("keys" in b.__dict__ for b in c.__mro__), ]) except Exception: pass return NotImplemented @classmethod def _safe_key_repr(cls, key): try: return repr(key) except Exception: return f"!! repr error on key with id: {id(key):#x} !!" @classmethod def entries(cls, mapping, label: str): """ :yield: ``(label, entry, id_path_ext)`` tuples for each entry in the mapping. """ assert isinstance(mapping, cls) try: for key in mapping.keys(): key_repr = cls._safe_key_repr(key) yield (key_repr, mapping[key], f"[{key_repr}]") except Exception as error: ui_log.error("Object {l!r} appears to be a mapping, but does " "not behave like one: {m}".format( l=label, m=error)) @classmethod def length(cls, mapping): return len(mapping.keys()) # Order is important here- A mapping without keys could be viewed as a # sequence, and they're both collections. CONTAINER_CLASSES = ( PudbMapping, PudbSequence, PudbCollection, ) # }}} # {{{ data class FrameVarInfo: def __init__(self): self.id_path_to_iinfo = {} self.watches = [] def get_inspect_info(self, id_path, read_only): if read_only: return self.id_path_to_iinfo.get( id_path, InspectInfo()) else: return self.id_path_to_iinfo.setdefault( id_path, InspectInfo()) class InspectInfo: def __init__(self): # Do not globalize: cyclic import from pudb.debugger import CONFIG self.show_detail = False self.display_type = CONFIG["stringifier"] self.highlighted = False self.repeated_at_top = False self.access_level = CONFIG["default_variables_access_level"] self.show_methods = False self.wrap = CONFIG["wrap_variables"] class WatchExpression: def __init__(self, expression): self.expression = expression class WatchEvalError: def __str__(self): return "" # }}} # {{{ widget class VariableWidget(urwid.FlowWidget): PREFIX = "| " def __init__(self, parent, var_label, value_str, id_path, attr_prefix=None, watch_expr=None, iinfo=None): assert isinstance(id_path, str) self.parent = parent self.nesting_level = 0 if parent is None else parent.nesting_level + 1 self.prefix = self.PREFIX * self.nesting_level self.var_label = var_label self.value_str = value_str self.id_path = id_path self.attr_prefix = attr_prefix or "var" self.watch_expr = watch_expr if iinfo is None: # Do not globalize: cyclic import from pudb.debugger import CONFIG self.wrap = CONFIG["wrap_variables"] else: self.wrap = iinfo.wrap def __str__(self): return ("VariableWidget: {value_str}, level {nesting_level}, at {id_path}" .format( value_str=self.value_str, nesting_level=self.nesting_level, id_path=self.id_path, )) def selectable(self): return True def _get_wrapped_lines(self, maxcol: int) -> List[str]: """ :param maxcol: the number of columns available to this widget :return: list of string lines, including prefixes, wrapped to fit in the available space """ maxcol -= len(self.prefix) # self.prefix is padding var_label = self.var_label or "" value_str = self.value_str or "" alltext = var_label + ": " + value_str # The first line is not indented firstline = self.prefix + alltext[:maxcol] if not alltext[maxcol:]: return [firstline] fulllines, rest = divmod(text_width(alltext) - maxcol, maxcol - 2) restlines = [alltext[(maxcol - 2)*i + maxcol:(maxcol - 2)*i + 2*maxcol - 2] for i in range(fulllines + bool(rest))] return [firstline] + [self.prefix + " " + i for i in restlines] def rows(self, size: Tuple[int], focus: bool = False) -> int: """ :param size: (maxcol,) the number of columns available to this widget :param focus: True if this widget or one of its children is in focus :return: The number of rows required for this widget """ if self.wrap: return len(self._get_wrapped_lines(size[0])) if len(self._get_wrapped_lines(size[0])) > 1: return 2 else: return 1 def render(self, size: Tuple[int], focus: bool = False) -> urwid.Canvas: """ :param size: (maxcol,) the number of columns available to this widget :param focus: True if this widget or one of its children is in focus :return: A Canvas subclass instance containing the rendered content of this widget """ from pudb.ui_tools import make_canvas maxcol = size[0] if focus: apfx = "focused "+self.attr_prefix+" " else: apfx = self.attr_prefix+" " var_label = self.var_label or "" if self.wrap: text = self._get_wrapped_lines(maxcol) extralabel_full, extralabel_rem = divmod( text_width(var_label[maxcol:]), maxcol) totallen = sum(text_width(i) for i in text) labellen = ( len(self.prefix) # Padding of first line + (len(self.prefix) + 2) # Padding of subsequent lines * (extralabel_full + bool(extralabel_rem)) + text_width(var_label) + 1 # for ":" ) _attr = [(apfx+"label", labellen), (apfx+"value", totallen - labellen)] from urwid.util import rle_subseg fullcols, rem = divmod(totallen, maxcol) attr = [rle_subseg(_attr, i*maxcol, (i + 1)*maxcol) for i in range(fullcols + bool(rem))] return make_canvas(text, attr, maxcol, apfx+"value") lprefix = len(self.prefix) if self.value_str is not None: if self.var_label is not None: if len(self._get_wrapped_lines(maxcol)) > 1: # label too long? generate separate value line text = [self.prefix + self.var_label + ":", self.prefix+" " + self.value_str] attr = [ [(apfx+"label", lprefix+text_width(self.var_label))], [(apfx+"value", lprefix+3+text_width(self.value_str))] ] else: text = [self.prefix + self.var_label + ": " + self.value_str] attr = [[ (apfx+"label", lprefix+text_width(self.var_label)+1), (apfx+"value", text_width(self.value_str)+1), ]] else: text = [self.prefix + self.value_str] attr = [[ (apfx+"label", len(self.prefix)), (apfx+"value", text_width(self.value_str)), ]] else: text = [self.prefix + self.var_label] attr = [[(apfx+"label", lprefix + text_width(self.var_label)), ]] # Ellipses to show text was cut off # encoding = urwid.util.detected_encoding for i in range(len(text)): if text_width(text[i]) > maxcol: text[i] = text[i][:maxcol-3] + "..." return make_canvas(text, attr, maxcol, apfx+"value") def keypress(self, size, key): return key # }}} # {{{ stringifiers BASIC_TYPES = ( type(None), int, str, float, complex, ) # {{{ safe types def get_str_safe_types(): import types return tuple(getattr(types, s) for s in "BuiltinFunctionType BuiltinMethodType ClassType " "CodeType FileType FrameType FunctionType GetSetDescriptorType " "LambdaType MemberDescriptorType MethodType ModuleType " "SliceType TypeType TracebackType UnboundMethodType XRangeType".split() if hasattr(types, s)) + (WatchEvalError,) STR_SAFE_TYPES = get_str_safe_types() # }}} def default_stringifier(value): if isinstance(value, BASIC_TYPES): return repr(value) if HAVE_NUMPY and isinstance(value, numpy.ndarray): return "%s(%s) %s" % ( type(value).__name__, value.dtype, value.shape) elif HAVE_NUMPY and isinstance(value, numpy.number): return str(f"{value} ({value.dtype})") elif isinstance(value, STR_SAFE_TYPES): try: return str(value) except Exception: message = "string safe type stringifier failed" ui_log.exception(message) return "!! %s !!" % message elif hasattr(type(value), "safely_stringify_for_pudb"): try: # (E.g.) Mock objects will pretend to have this # and return nonsense. result = value.safely_stringify_for_pudb() except Exception: message = "safely_stringify_for_pudb call failed" ui_log.exception(message) result = "!! %s !!" % message if isinstance(result, str): return str(result) elif isinstance(value, Sized): try: # Example: numpy arrays with shape == () raise on len() obj_len = len(value) except TypeError: pass else: return f"{type(value).__name__} ({obj_len})" return str(type(value).__name__) def type_stringifier(value): return str(type(value).__name__) def id_stringifier(obj): return "{id:#x}".format(id=id(obj)) def error_stringifier(_): return "ERROR: Invalid custom stringifier file." custom_stringifier_dict = {} STRINGIFIERS = { "default": default_stringifier, "type": type_stringifier, "repr": repr, "str": str, "id": id_stringifier, } def get_stringifier(iinfo: InspectInfo) -> Callable: """ :return: a function that turns an object into a Unicode text object. """ try: return STRINGIFIERS[iinfo.display_type] except KeyError: if "" == iinfo.display_type.strip(): return lambda _: "ERROR: custom stringifier is not set" try: if not custom_stringifier_dict: # Only execfile once from os.path import expanduser, expandvars custom_stringifier_fname = expanduser(expandvars(iinfo.display_type)) with open(custom_stringifier_fname) as inf: exec(compile(inf.read(), custom_stringifier_fname, "exec"), custom_stringifier_dict, custom_stringifier_dict) except FileNotFoundError: ui_log.error("Unable to locate custom stringifier file {!r}" .format(iinfo.display_type)) return error_stringifier except Exception: ui_log.exception("Error when importing custom stringifier") return error_stringifier else: if "pudb_stringifier" not in custom_stringifier_dict: ui_log.error(f"{iinfo.display_type} does not contain a function " "named pudb_stringifier at the module level.") return lambda value: str( "ERROR: Invalid custom stringifier file: " "pudb_stringifier not defined.") else: return (lambda value: str(custom_stringifier_dict["pudb_stringifier"](value))) # }}} # {{{ tree walking class ValueWalker(ABC): EMPTY_LABEL = "" CONTINUATION_LABEL = "[...]" def __init__(self, frame_var_info): self.frame_var_info = frame_var_info @abstractmethod def add_item(self, parent, var_label, value_str, id_path, attr_prefix=None): pass def add_continuation_item(self, parent: VariableWidget, id_path: str, count: int, length: int) -> bool: """ :arg length: the total length of the container. Negative if not known. :returns: True if a continuation item ("[...]") was added, else False. If a continuation item was added, no further entries in the container should be added. If no continuation item was added, continue adding entries from the container. """ cont_id_path = "%s.cont-%d" % (id_path, count) if not self.frame_var_info.get_inspect_info( cont_id_path, read_only=True).show_detail: if length > 0: omitted = f"{length - count}" else: omitted = "some" self.add_item(parent, self.CONTINUATION_LABEL, f"<{omitted} items omitted, expand to see more>", id_path=cont_id_path) return True return False def walk_container(self, parent: VariableWidget, label: str, value, id_path: str = None): try: container_cls = next(cls for cls in CONTAINER_CLASSES if isinstance(value, cls)) except StopIteration: # Not recognized as a container return False is_empty = True for count, (entry_label, entry, id_path_ext) in enumerate( container_cls.entries(value, label)): is_empty = False if count > 0 and count % 10 == 0: try: length = container_cls.length(value) except Exception: length = -1 if self.add_continuation_item(parent, id_path, count, length): return True entry_id_path = f"{id_path}{id_path_ext}" self.walk_value(parent, "[{}]".format(entry_label if entry_label else ""), entry, entry_id_path) if is_empty: self.add_item(parent, self.EMPTY_LABEL, None, id_path=f"{id_path}{self.EMPTY_LABEL}") return True def walk_attributes(self, parent, label, value, id_path, iinfo): try: keys = dir(value) except Exception: ui_log.exception(f"Failed to look up attributes on {label}") return for key in sorted(keys): if iinfo.access_level == "public": if key.startswith("_"): continue elif iinfo.access_level == "private": if key.startswith("__") and key.endswith("__"): continue try: with warnings.catch_warnings(): warnings.simplefilter("ignore") attr_value = getattr(value, key) if inspect.isroutine(attr_value) and not iinfo.show_methods: continue except Exception: attr_value = WatchEvalError() self.walk_value(parent, ".%s" % key, attr_value, f"{id_path}.{key}") def walk_value(self, parent, label, value, id_path=None, attr_prefix=None): if id_path is None: id_path = label assert isinstance(id_path, str) iinfo = self.frame_var_info.get_inspect_info(id_path, read_only=True) try: displayed_value = get_stringifier(iinfo)(value) except Exception: # Unfortunately, anything can happen when calling str() or # repr() on a random object. displayed_value = type_stringifier(value) \ + " (!! %s error !!)" % iinfo.display_type ui_log.exception("stringifier failed") if iinfo.show_detail: marker = iinfo.access_level[:3] if iinfo.show_methods: marker += "+()" displayed_value += " [%s]" % marker new_parent_item = self.add_item(parent, label, displayed_value, id_path, attr_prefix) if iinfo.show_detail: if isinstance(value, CONTAINER_CLASSES): self.walk_container(new_parent_item, label, value, id_path) self.walk_attributes(new_parent_item, label, value, id_path, iinfo) class BasicValueWalker(ValueWalker): def __init__(self, frame_var_info): ValueWalker.__init__(self, frame_var_info) self.widget_list = [] def add_item(self, parent, var_label, value_str, id_path, attr_prefix=None): iinfo = self.frame_var_info.get_inspect_info(id_path, read_only=True) if iinfo.highlighted: attr_prefix = "highlighted var" new_item = VariableWidget(parent, var_label, value_str, id_path, attr_prefix, iinfo=iinfo) self.widget_list.append(new_item) return new_item class WatchValueWalker(ValueWalker): def __init__(self, frame_var_info, widget_list, watch_expr): ValueWalker.__init__(self, frame_var_info) self.widget_list = widget_list self.watch_expr = watch_expr def add_item(self, parent, var_label, value_str, id_path, attr_prefix=None): iinfo = self.frame_var_info.get_inspect_info(id_path, read_only=True) if iinfo.highlighted: attr_prefix = "highlighted var" new_item = VariableWidget(parent, var_label, value_str, id_path, attr_prefix, watch_expr=self.watch_expr, iinfo=iinfo) self.widget_list.append(new_item) return new_item class TopAndMainVariableWalker(ValueWalker): def __init__(self, frame_var_info): ValueWalker.__init__(self, frame_var_info) self.main_widget_list = [] self.top_widget_list = [] self.top_id_path_prefixes = [] @staticmethod def _should_repeat_at_top(id_path, tipp) -> bool: """ :return: True if the id_path is a child path of tipp """ if id_path is None: return False if id_path == tipp: return True # Perhaps it's a child of the top-level path before, sep, after = id_path.partition(tipp) return (before == "" and sep == tipp and len(after) > 0 and after[0] in ".<[") def add_item(self, parent, var_label, value_str, id_path, attr_prefix=None): iinfo = self.frame_var_info.get_inspect_info(id_path, read_only=True) if iinfo.highlighted: attr_prefix = "highlighted var" repeated_at_top = iinfo.repeated_at_top if repeated_at_top and id_path is not None: self.top_id_path_prefixes.append(id_path) for tipp in self.top_id_path_prefixes: if self._should_repeat_at_top(id_path, tipp): repeated_at_top = True if repeated_at_top: self.top_widget_list.append(VariableWidget(parent, var_label, value_str, id_path, attr_prefix, iinfo=iinfo)) new_item = VariableWidget(parent, var_label, value_str, id_path, attr_prefix, iinfo=iinfo) self.main_widget_list.append(new_item) return new_item # }}} # {{{ top level SEPARATOR = urwid.AttrMap(urwid.Text(""), "variable separator") def make_var_view(frame_var_info, locals, globals): vars = list(locals.keys()) vars.sort(key=str.lower) tmv_walker = TopAndMainVariableWalker(frame_var_info) ret_walker = BasicValueWalker(frame_var_info) watch_widget_list = [] for watch_expr in frame_var_info.watches: try: value = eval(watch_expr.expression, globals, locals) except Exception: value = WatchEvalError() WatchValueWalker(frame_var_info, watch_widget_list, watch_expr) \ .walk_value(None, watch_expr.expression, value) if "__return__" in vars: ret_walker.walk_value(None, "Return", locals["__return__"], attr_prefix="return") for var in vars: if not (var.startswith("__") and var.endswith("__")): tmv_walker.walk_value(None, var, locals[var]) result = tmv_walker.main_widget_list if watch_widget_list: result = (watch_widget_list + [SEPARATOR] + result) if tmv_walker.top_widget_list: result = (tmv_walker.top_widget_list + [SEPARATOR] + result) if ret_walker.widget_list: result = (ret_walker.widget_list + result) return result class FrameVarInfoKeeper: def __init__(self): self.frame_var_info = {} def get_frame_var_info(self, read_only, ssid=None): if ssid is None: # self.debugger set by subclass ssid = self.debugger.get_stack_situation_id() # noqa: E501 # pylint: disable=no-member if read_only: return self.frame_var_info.get(ssid, FrameVarInfo()) else: return self.frame_var_info.setdefault(ssid, FrameVarInfo()) # }}} # vim: foldmethod=marker pudb-2024.1.3/pyproject.toml000066400000000000000000000047701470451231400156220ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "pudb" version = "2024.1.3" description = "A full-screen, console-based Python debugger" readme = "README.rst" license = "MIT" requires-python = "~=3.8" authors = [ { name = "Andreas Kloeckner", email = "inform@tiker.net" }, ] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Console :: Curses", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development", "Topic :: Software Development :: Debuggers", "Topic :: Software Development :: Quality Assurance", "Topic :: System :: Recovery Tools", "Topic :: System :: Software Distribution", "Topic :: Terminals", "Topic :: Utilities", ] dependencies = [ "jedi>=0.18,<1", "packaging>=20.0", "pygments>=2.7.4", "urwid>=2.4", "urwid_readline", ] [project.optional-dependencies] completion = [ "shtab", ] [project.scripts] pudb = "pudb.run:main" [tool.hatch.build.targets.sdist] include = [ "/pudb", "/doc", "/try-the-debugger.sh", "/debug_me.py", "/examples", ] [tool.ruff] preview = true [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear "C", # flake8-comprehensions "E", # pycodestyle "F", # pyflakes "I", # flake8-isort "N", # pep8-naming "NPY", # numpy "Q", # flake8-quotes "W", # pycodestyle ] extend-ignore = [ "C90", # McCabe complexity "E221", # multiple spaces before operator "E226", # missing whitespace around arithmetic operator "E241", # multiple spaces after comma "E242", # tab after comma "E402", # module level import not at the top of file "N818", # error suffix in exception names ] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double" multiline-quotes = "double" [tool.ruff.lint.isort] combine-as-imports = true known-local-folder = [ "pudb", ] lines-after-imports = 2 [tool.pytest.ini_options] addopts = [ "--ignore=pudb/b.py", "--ignore=pudb/ipython.py", ] [tool.typos.default] extend-ignore-re = [ "(?Rm)^.*(#|//)\\s*spellchecker:\\s*disable-line$" ] [tool.typos.default.extend-words] # Ned Batchelder, person Ned = "Ned" [tool.typos.files] extend-exclude = [ ] pudb-2024.1.3/requirements.dev.txt000066400000000000000000000000741470451231400167400ustar00rootroot00000000000000coverage Pygments pytest pytest-cov pytest-mock urwid numpy pudb-2024.1.3/try-the-debugger.sh000077500000000000000000000001641470451231400164140ustar00rootroot00000000000000#! /bin/sh if test "$1" = ""; then PYINTERP="python3" else PYINTERP="$1" fi $PYINTERP -m pudb.run debug_me.py