thonny-2.1.16/0000777000000000000000000000000013201324660011247 5ustar 00000000000000thonny-2.1.16/CHANGELOG.rst0000666000000000000000000003314713201322756013304 0ustar 00000000000000=============== Version history =============== 2.1.16 (2017-11-10) =================== * Tests moved under thonny package * Tests included in the source distribution * More icons included in the source distribution 2.1.15 (2017-11-07) =================== * Removed StartupNotify from Linux .desktop file (StartupNotify=true leaves cursor spinning in Debian) 2.1.14 (2017-11-02) =================== * Added some Linux-specific files to source distribution. No new features or fixes. 2.1.13 (2017-10-29) =================== * Temporary workaround for #351: Locals and name highlighter occasionally make Thonny freeze * Include only required licenses in source dist 2.1.12 (2017-10-13) =================== * FIXED #303: Allow specifying same interpreter for backend as frontend uses * FIXED #304: Allow specifying backend interpreter by relative path * FIXED #312: Closing unsaved tab causes error * FIXED #319: Linux install script needs quoting around the path(s) * FIXED #320: Install gets recursive if trying to install within extracted tarball * FIXED #321: Linux installer fails if invoked with relative, local user path * FIXED #334: init.tcl not found (Better control over back-end environment variables) * FIXED #343: Thonny now also works with jedi 0.11 2.1.11 (2017-07-22) =================== * FIXED #31: Infinite print loop freezes Thonny * FIXED #285: Previous used interpreters are not shown in options dialog * FIXED #296: Make it more explicit that pip GUI search box needs exact package name * FIXED #298: Python crashes keep backend hanging * FIXED #305: Variables table doesn't get updated, if it's blocked by another view 2.1.10 (2017-06-09) =================== * NEW: More flexibility for classroom setups (see https://bitbucket.org/plas/thonny/wiki/ClassroomSetup) * FIXED #276: Copy with Ctrl+C causes bell * FIXED #277: Triple-quoted strings keep keyword coloring * FIXED #278: Paste in shell causes bell * FIXED #281: Wrong unindentation with SHIFT+TAB when last line does not end with linebreak * FIXED #283: backend.log path doesn't take THONNY_USER_DIR into account * FIXED #284: Internal error when saving to a read-only folder/file (now proposes to choose another name) 2.1.9 (2017-06-01) ================== * FIXED #273: Memory leak in editor margin because of undo log * FIXED #275: Updating line numbers is very inefficient * FIXED: Pasted text occasionally was hidden below bottom edge of the editor * FIXED: sys.exit() didn't really close the backend 2.1.8 (2017-05-28) ================== * ENHANCEMENT: Code completion with Tab-key is now optional (see Tools => Options => Editor) * ENHANCEMENT: Clicking on the editor now closes code completion box * CHANGED: Code completion box doesn't offer names starting with double underscore anymore. * FIXED: Error caused by too fast typing with open code completions box * ENHANCEMENT: Find/Replace dialog can now be operated with F3 * ENHANCEMENT: Find/Replace pre-selects previously used search string * ENHANCEMENT: Find/Replace dialog doesn't block main window anymore * FIXED: Find/Replace doesn't ignore spaces in search string anymore * FIXED: Closed views reappeared after restart if they were only views in that notebook * FIXED #264: Debugger fails with with conditional list comprehension * FIXED #265: Error when using two word search string in pip GUI * FIXED #266: Occasional incorrect line numbering * FIXED #267: Kivy application main window didn't show in Windows * TECHNICAL: Better diagnostic logging 2.1.7 (2017-05-13) ================== * CHANGED: pip GUI now works in read-only mode unless backend is a virtual environment * FIXED: Error when non-default backend was used without previously generated Thonny-private virtual environment 2.1.6 (2017-05-12) ================== * FIXED #260: Strange behaviour when indenting with TAB * FIXED #261: Editing a triple-quoted string breaks coloring in following lines * FIXED: Made outdated pip detection more general 2.1.5 (2017-05-09) ================== * FIXED: Jedi version checking problem 2.1.4 (2017-05-09) ================== (This release is meant for making Thonny work better with system Python 3.4 in Debian Jessie) * FIXED #254: "Manage plug-ins" now gives instructions for installing pip if system is missing it or it's too old * FIXED #255: Name highlighter and locals marker are now quietly disabled when system has too old jedi * FIXED: Virtual env dialog now closes properly * TECHNICAL: SubprocessDialog now has more robust returncode checking in Linux 2.1.3 (2017-05-09) ================== * FIXED #250: Debugger focus was off by one line in function frames * FIXED #251: Debugger timing issue (wrong command type in the backend) * FIXED #252: Debugger timing issue (get_globals and debugger commands interfere) * FIXED #253: Creating default virtual env does not work when using Debian python3 without ensurepip 2.1.2 (2017-05-08) ================== * FIXED #220 and #237: Icon problems in Linux tasbar. * FIXED #245: Tooltips not working in Mac * FIXED #246: Current script did not get executed if cursor was not in the end of the shell * FIXED #249: Reset, Run and Debug caused double prompt 2.1.1 (2017-05-03) ================== * FIXED #241: Some menu items gave errors with micro:bit backend. * FIXED #242: Focus got stuck on first run (no entry was possible neither in shell nor editor when initialization dialog closed) 2.1.0 (2017-05-02) ================== * TECHNICAL: Changes in diagnostic logging 2.1.0b11 (2017-04-29) ===================== * TECHNICAL: Implemented more robust approach for installing Thonny plugins 2.1.0b10 (2017-04-29) ===================== * CHANGED: Installed plugins now end up under ~/.thonny/plugins * TECHNICAL: Backend preparation now occurs when main window has been opened 2.1.0b9 (2017-04-28) ==================== * FIXED: Backend related regression introduced in b8 2.1.0b8 (2017-04-27) ==================== * CHANGED: (FIXED #231) Stop/Reset button is now Interrupt/Reset button (tries to interrupt a running command instead of reseting. Resets if pressed in idle state) * FIXED #232: Ubuntu showed pip GUI captions with too big font * FIXED #233: Thonny now remembers which view was on top in a panel. * FIXED #234: Multiline support problems in shell (trailing whitespace was causing trouble) * FIXED: pip GUI shows latest version number when there is no stable version. * FIXED: pip GUI now can handle also packages without PyPI presence * TECHNICAL: Backends are not sent Reset command for initialization anymore. 2.1.0b7 (2017-04-25) ================== * FIXED: Removed some circular import to support Python 3.4 * FIXED: pip GUI now also lists installed pre-releases * EXPERIMENTAL: GUI for installing Thonny plug-ins (Tools => Manage plug-ins...) * TECHNICAL: Thonny+Python bundles again include pip (needed for installing plug-ins) * TECHNICAL: Refactored creation of several widgets to support theming * TECHNICAL: THONNY_USER_DIR environment variable can now specify where Thonny stores user data (conf files, default virtual env, ...) 2.1.0b6 (2017-04-19) ================== * ENHANCEMENT: Shell now shows location of external interpreter as welcome text * FIXED #224: Tab-indentation didn't work if tail of the text was selected and text didn't end with empty line * FIXED: Tab with selected text occasionally invoked code-completion * TECHNICAL: Tweaks in Windows console allocation * TECHNICAL: Thonny+Python bundles don't include pip anymore (venv gets pip via ensurepip) 2.1.0b5 (2017-04-18) ================== * FIXED: Typo in pipGUI (regression introduced in b4) 2.1.0b4 (2017-04-18) ==================== * CHANGED: If you want to use Thonny with external Python interpreter, then now you should select python.exe instead of pythonw.exe. * FIXED #223: Can't interrupt subprocess when Thonny is run via thonny.exe * FIXED: Private venv didn't find Tcl/Tk in ubuntu (commit 33eabff) * FIXED: Right-click on editor tabs now also works on macOS. 2.1.0b3 (2017-04-17) ==================== * NEW: Dialog for managing 3rd party packages / a simple pip GUI. Check it out: "Tools => Manage packages" * NEW: Shell now supports multiline commands * ENHANCEMENT: Window title now shows full path and cursor location of current file. * ENHANCEMENT: Editor lines can be selected by clicking and/or dragging on line-number margin (thanks to Sven). * ENHANCEMENT: Most programs can now be interrupted by Ctrl+C without restarting the process. * ENHANCEMENT: You can start editing the code that is still running (the process gets interrupted automatically). This is handy when developing tkinter applications. * ENHANCEMENT: Tab can be used as alternative code-completion shortcut. * ENHANCEMENT: Recommended pip-command now appears faster in System Shell. * ENHANCEMENT: Alternative interpreter doesn't need to have jedi installed in order to provide code-completions (see #171: Code auto-complete error) * ENHANCEMENT: Double-click on autocomplete list inserts the completion * EXPERIMENTAL: Ctrl-click on a name in code tries to locate its definition. NB! Not finished yet! * CHANGED: Bundled Python version has been upgraded to 3.6.1 * CHANGED: Bundled Python in Mac and Linux now uses SSL certs from certifi project (https://pypi.python.org/pypi/certifi). * REMOVED: Moved incomplete Exercise system to a separate plugin (https://bitbucket.org/plas/thonny-exersys). With this got rid of tkinterhtml, requests and beautifulsoup4 dependencies. * FIXED #16: Run doesn't clear variables (again?) * FIXED #98: Nested functions crashed the debugger. * FIXED #114: Crash when trying to change interpreter in macOS. * FIXED #142: "Open system shell" failed when Thonny path had spaces in it. Paths are now properly quoted. * FIXED #154: Problems with Notebook tabs' context menus * FIXED #159: Debugging list or set comprehension caused crash * FIXED #166: Can't delete one of two spaces with backspace * FIXED #180: Right-click doesn't focus editor * FIXED #187: Main modules launched by Thonny were missing ``__spec__`` attribute. * FIXED #195: Debugger crashes when using generators. * FIXED #201: "Tools => Open Thonny data folder" now works also in macOS. * FIXED #211: Linux installer was failing when using ``xdg-user-dir`` (thanks to Ryan McQuen) * FIXED #213: In single instance mode new Window doesn't get focus * FIXED #217: Debugger on Python 3.5 and later can't handle splat operator * FIXED #221: Context menus in Linux can now be closed by clicking elsewhere * FIXED: Event logger did not save filenames (eb34c5d). * FIXED: Problem in replayer (db78855). * TECHNICAL: Bundled Jedi version has been upgraded to 0.10.2. * TECHNICAL: 3rd party Thonny plugins must now be under ``thonnycontrib`` namespace package. * TECHNICAL: Introduced the concept of "eary plugins" (plugins, which get loaded before initializing the runner). * TECHNICAL: Refactored the interface between GUI and backend to allow different backend implementations * TECHNICAL: Previously, with bundled Python, Thonny was using nasty tricks to force pip install packages install under ~/.thonny. Now it creates a proper virtual environment under ~/.thonny and uses this as the backend by default (instead of using interpreter running the GUI directly). * TECHNICAL: Automatic tkinter updates on the backend are now less invasive 2.0.7 (2017-01-06) ================== * FIXED: Making font size too small would crash Thonny. * FIXED: Another take on configuration file corruption. * FIXED: Shift-Tab wasn’t working in some cases. * FIXED #165: "Open system shell" did not add Scripts dir to PATH in Windows. * FIXED #183: ``from __future__ import`` crashed the debugger. 2.0.6 (2017-01-06) ================== * FIXED: a bug in Linux installer (configuration file wasn’t created in new installations) 2.0.5 (2016-11-30) ================== * FIXED: Corrected shift key detection (a82bd4d) 2.0.4 (2016-10-26) ================== * FIXED: Configuration file was occasionally getting corrupted (for mysterious reasons, maybe a bug in Python’s configparser) * FIXED #104: Negative font size crashed Thonny * FIXED #143: Linux installer fails if desktop isn't named "Desktop". (Later turned out this wasn't fixed for all cases). * FIXED #134: "Open system shell" doesn't work in Centos 7 KDE 2.0.3 (2016-09-30) ================== * FIXED: Quoting in "Open system shell" in Mac. Again. 2.0.2 (2016-09-30) ================== * FIXED: Quoting in "Open system shell" in Mac. 2.0.1 (2016-09-30) ================== * FIXED #106: Don't let user logs grow too big 2.0.0 (2016-09-29) ================== * NEW: Added code completion (powered by Jedi: https://github.com/davidhalter/jedi) * NEW: Added new command "Tools => Open system shell" which opens terminal where current Python is in PATH. * CHANGED: Single instance mode is now optional (Tools => Options => General) * FIXED: Many bugs 1.2.0b2 (2016-02-10) ==================== * NEW: Thonny now runs in single instance mode. Previously, when you opened a py file with Thonny, a new Thonny instance (window) was created even if an instance existed already. This became nuisance if you opened several files. Now Thonny works as single instance program, meaning only one instance of Thonny runs at the time. When you open another file, it is opened in existing window. * NEW: Editor enhancements. Added option to show line numbers and right margin in the editor. In order to keep first impression cleaner, they are disabled by default. See Tools => Options => Editor. Don't forget that you don't need line numbers for locating lines mentioned in error messages -- you can click them and Thonny shows you the line. * FIXED: Some bugs where Thonny couldn't prepare some programs for debugging. Older versions ============== See https://bitbucket.org/plas/thonny/issues/ and https://bitbucket.org/plas/thonny/commits/ for details thonny-2.1.16/CREDITS.rst0000666000000000000000000000206013201264465013102 0ustar 00000000000000======= Credits ======= Thonny is thankful to: Its home -------- Thonny was born in University of Tartu (https://www.ut.ee), Institute of Computer Science (https://www.cs.ut.ee) and the main development is being done here. Python ------ It's a really nice language for teaching programming. It also has some nice technical properties, that made Thonny's program animation features pleasure to implement. Libraries -------------- * jedi (http://jedi.readthedocs.io) is used for code completion, go to definition, etc. * certifi (https://pypi.python.org/pypi/certifi) provides SSL certs for bundled Python in Linux and Mac. * distro (https://pypi.python.org/pypi/distro) is optionally used for detecting Linux version in About dialog. Source contributors and frequent bug-reporters ---------------------------------------------- * Aivar Annamaa * Rene Lehtma * Filip Schouwenaars * Fizban * Sven (s_v_e_n) * Taavi Ilp * Toomas Mölder * Xin Rong Please let us know if we have forgotten to add your name to this list! Also, let us know if you want to remove your name.thonny-2.1.16/LICENSE.txt0000666000000000000000000000207113172664305013104 0ustar 00000000000000The MIT License (MIT) Copyright (c) 2017 Aivar Annamaa 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. thonny-2.1.16/licenses/0000777000000000000000000000000013201324657013062 5ustar 00000000000000thonny-2.1.16/licenses/ECLIPSE-ICONS-LICENSE.txt0000666000000000000000000002576213201264465016654 0ustar 00000000000000 Eclipse Public License - v 1.0 THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 1. DEFINITIONS "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 2. GRANT OF RIGHTS a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 3. REQUIREMENTS A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 4. COMMERCIAL DISTRIBUTION Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 5. NO WARRANTY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 6. DISCLAIMER OF LIABILITY EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. GENERAL If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. thonny-2.1.16/MANIFEST.in0000666000000000000000000000047513201322223013004 0ustar 00000000000000include CHANGELOG.rst include CREDITS.rst include LICENSE.txt include README.rst include requirements.txt include licenses/ECLIPSE-ICONS-LICENSE.txt include packaging/linux/thonny.1 include packaging/icons/*.png include packaging/linux/org.thonny.Thonny.appdata.xml include packaging/linux/org.thonny.Thonny.desktop thonny-2.1.16/packaging/0000777000000000000000000000000013201324657013201 5ustar 00000000000000thonny-2.1.16/packaging/icons/0000777000000000000000000000000013201324660014306 5ustar 00000000000000thonny-2.1.16/packaging/icons/thonny-128x128.png0000666000000000000000000000306713201264465017302 0ustar 00000000000000PNG  IHDRi7@bKGD̿ pHYs  tIME 6%IDATx_TU?Y+"kIX| &FaJQC KCж&Bpgfϝ{g9zgGs<}s~;      sfe0&I /nZ SR~ҌVf_[Zyʹf0"'y&_ Nw||ɰ^;h4/6fd\ Nt)-K/k23㬺gztі|dձ[8o6Ǩx݁Koy&~'E>Å):E+928D[gOC&:,)q++e8mn\"zyeЄ1q5ԡK|ŕ=hs wMZsoDNPr|!ew&ieW ٸHg 5i@.@j:&3lleDf\ zyZzJ{ 0V-gXS䛞iGk] 8╴(;xً^YGb&A6Ag=Xֳ0x77 *f6 A4$F706}4: uHMBE*SY_5-xhJS$ yL>Oՙ`"{RaICsh׆N}a0QgѮJ[eRLm"gD}1\~; fu @|>zZ~кeǰ+|گzS) B6wGcP=N~@KzhvG1|gBe2hӥԳQQߡ\J3 e'۾CAo`wle3%Z }&      ? M7BIENDB`thonny-2.1.16/packaging/icons/thonny-16x16.png0000666000000000000000000000032613201322022017105 0ustar 00000000000000PNG  IHDR7bKGD̿ pHYs  tIME agIDAT(ϭ (CMRD"Ag|}|ۖ@n uYjPHӐ6ɚĆn:hVZȺGNOBTt#5IENDB`thonny-2.1.16/packaging/icons/thonny-2.png0000666000000000000000000000050413201264465016477 0ustar 00000000000000PNG  IHDR7gAMA a cHRMz&u0`:pQ<bKGD̿tIME \MLIDAT(ϭO90Mo2PPm,^U$ܲ!&T'5v91'IJ:%tEXtdate:create2017-11-09T04:26:24-05:00xG%tEXtdate:modify2017-11-09T04:26:24-05:00%IENDB`thonny-2.1.16/packaging/icons/thonny-22x22.png0000666000000000000000000000103613201320532017102 0ustar 00000000000000PNG  IHDRnbKGD̿ pHYs  tIME 5.~\IDAT(ϕ=kA3r7#FP0`΀_"E"`(Qbr{Oݽܞoy? uʊELz2eXH$ʔQ4Pwjl]C0N9KX$Q;zvFzhem#z8$DDH )ښl r8Gb{K`ϹKg2,F\Xǝ .H6! kmH ,j +Q򐔀}aaRe!0{,˰0} Ժz~?PNNɻ? =|9rj nH2yGFX^!) ?*W_.[M|7E0 ͼ3=X%zguc i~+kKIENDB`thonny-2.1.16/packaging/icons/thonny-256x256.png0000666000000000000000000000735713201264465017314 0ustar 00000000000000PNG  IHDR{`gAMA a cHRMz&u0`:pQ<bKGD̿tIME \M IDATx_]EǿK[!Զ0& b "ARu'%(AM|4@"_[S&F16h`?»P] 53,:7??o؁&6y|B?(FAFn6Cs6[a|;df]F<:){/RMĥ86E.b;~Z)r-ފTMX+PR4#cfq3as?թ#Q\ʠޭ7,~يp--x ٟJ55?k)H8/M\Id]kx zڸfOxw r>"޽?IY]m w('I\UVDpM?C@1._[1oCK1Nh$#}/Ÿ0`.L"8t[E<ݘmyQHK#phh xO 5`mt꾾dOr i6_2U'U7 UFA|S|?`K,(f;zV)d6#IsG;CF$U^Wp-u:1%vGs$[>v? [, Tw\yaYEcUAR]׉CXU-hFqm[Uɱ [ jw\تYH||SJ{ ߱lt<2M|9BԗI46.x'8T`d `wch~P ھTC I/X[/O!Hz錍t~.m Dnlh/$ y;MKf $ !Ljht[8'l @U<#+{$>(ظN*03@nHb. ZN*o ~iV?E+H@|Db?@ a,o熏C&Y@}%,q9>&ĜdȲrb2`qͰʃAWȄz^N/B(,?2!6}bNrI>4#9 f:_'p-nB?a1JNBtdV܃pI)_c|o ~Iwf+# ԑn?X2(*:ȾZ)c]و'0:QlflM܆?︹d25,đN/ 1Z. ( Fy=}!N%* ,Up ;_9&",ݖǕhҨȖ@23RӑfU?-G <]eq,Y 9eMY.B*rOѾAJ)55WH脖J5BiYaI.D04MJ4fݚz;m՜<$>)7KRޅMIL\fMp\aW15;-\ ,[ΪezZH .-L@gJm-pRn 0 8nR`rUZ\,FSPTLe3t A@ި)ϕRh53E*I_E3˚j #]L9EU[w8N+ %ۡ]hm7Fa$>G˸fpZΌ!Khiճ ~wp<}@lE& |&]T u)r`:GvQY^->A3fD9r#krX5V|T͖D|x|vv3QqH@_kgcYYmEݜ6$MkIpV $$*[ Ê+z&&)5QH{k5~C#d2}Ms2oM457]Llq2/~ogFLx''ӡo2/wnNvL 5 'SlU{7<6@8lJujm(B꫼ |j )Ժ:uP8' !SmZW':eՑR4c{G:n\zDgdӰH}O4wF%kg0e ^jD4w-Vөrix8(U)Kw,usq9@#P>ww~ r Qʒrn544 q2(H9K44nʟM\QY4MM/IR{OĘ-jDϲ;CݢL븭Y.Ty^Gn%B%2ӏc~'"!u+ #S>=N@40b\+Ihj"ie~q1|Roi~]GA#3Df& )՚ 2(ɸdd6~Q}3g}LLr?GCQIwPUJ)u{W'Q&Mͤ`Psp\u/ns5|RgiYqCds^?!9k$6$_BHr)\ uf?!$c8W6L%dRz3ɏ{&`'@pnU` 7E!B!B!B!B!B!B!B!B!B!B!B!B!A4Y~O%tEXtdate:create2017-11-09T04:26:24-05:00xG%tEXtdate:modify2017-11-09T04:26:24-05:00%IENDB`thonny-2.1.16/packaging/icons/thonny-32x32.png0000666000000000000000000000116013201320162017101 0ustar 00000000000000PNG  IHDR sbKGD̿ pHYs  tIME 16 \ؽIDATH;kAg/1 !,DC *BDڊ` L#bc!h!hBBl e^Zb9gμ3{3k U<6e, c.$ŕYڧG.J`ܓB3\tsưѪX ?C ^_{Y5c& #x'퇼Fa-/|}ʦB 4VxU$ _cQ0)ҴN(M;1LIxN~5 0V 6CJK+6@uB\̜;rB !Sa *WBlu;)Vc_bNo1f]D2Ge&9郉UWHW*)tcd\Й=Pi%VsPŒ9XXc϶'AV6@ד9TX KQ'KURxVqc_2BlnRfmhw)v)6V'n2?@/濰W\gcIENDB`thonny-2.1.16/packaging/icons/thonny-48x48.png0000666000000000000000000000162213201264465017137 0ustar 00000000000000PNG  IHDR00 1 bKGD̿ pHYs  tIME "6|*#IDATXkw?3of7$nҒڭ/$65 (xH< "|AD)-4BPH%P(%bj1m63;_YήH/wo} ; apy Iմ,]<3RHis_$pby0%7fyH]a! asw@]w໮0pĬܠ+(@ťQI1O!,b{X)C Nx:\&2ٿ`3IdN`)Kh94NひoF+8}oJ3&`qybcZs{6q6z-sCy.Zh:+ TW#*+z)jIENDB`thonny-2.1.16/packaging/icons/thonny-64x64.png0000666000000000000000000000164513201264465017140 0ustar 00000000000000PNG  IHDR@@`UbKGD̿ pHYs  tIME "AA6IDAThIhAN& ACxIP! x0FAE o^rxA=(j<\&f!HQb .A4.=T86m' O!@|أPlC {(Si!,f"f!{Ix!rTAGE;/i+bT;lS?;*vތܗq@7XXW:{91 "K7SBC7_xG/_1LSjYfSٔծT!bWӕI6h S;G\EgEcJ$"Vi&0L`[z(DYZ]s󰌃J hN $QsI{Kb d<d#IRP?{?i&3)"O rWN@)LE?UjL.K Kb-'D_ڞE $ nGtDUgNHMQnWO$IB|F[m B,~^$]t `\!6>) nrv1N"+I:,;II8BT&:JI~r',%[ VI~($ ,3FoB@KQ|t%jHLC5blWM@&aO2(%qsȯ @`5ם^8]X°ܨ^p!uqǓSLVxg5|J^w7 CʈSX8{֪]u7Ѱyc_CݰK_O2=IeIENDB`thonny-2.1.16/packaging/linux/0000777000000000000000000000000013201324660014332 5ustar 00000000000000thonny-2.1.16/packaging/linux/org.thonny.Thonny.appdata.xml0000666000000000000000000000307113201264334022052 0ustar 00000000000000 org.thonny.Thonny.desktop MIT MIT Thonny Python IDE for beginners

Thonny is a simple Python IDE with features useful for learning programming. It comes with a debugger which is able to visualize all the conceptual steps taken to run a Python program (executing statements, evaluating expressions, maintaining the call stack). There is a GUI for installing 3rd party packages and special mode for learning about references.

See the homepage for more information, screenshots and a walk-through video.

Development Education Debugger IDE ComputerScience org.thonny.Thonny.desktop http://thonny.org https://bitbucket.org/plas/thonny/issues/ https://bitbucket.org/plas/thonny/wiki/Home http://thonny.org/img/screenshot.png Thonny stepping through a recursive function thonny
thonny-2.1.16/packaging/linux/org.thonny.Thonny.desktop0000666000000000000000000000053113201264334021310 0ustar 00000000000000[Desktop Entry] Type=Application Name=Thonny GenericName=Python IDE Exec=/usr/bin/thonny %F Comment=Python IDE for beginners Icon=thonny StartupWMClass=Thonny Terminal=false Categories=Education;Development Keywords=programming;education MimeType=text/x-python; Actions=Edit; [Desktop Action Edit] Exec=/usr/bin/thonny %F Name=Edit with Thonny thonny-2.1.16/packaging/linux/thonny.10000666000000000000000000000171713201264334015742 0ustar 00000000000000.TH THONNY 1 .SH NAME thonny \- Python IDE for beginners .SH SYNOPSIS .B thonny [\fIFILE...\fR] .SH DESCRIPTION Thonny is a Python IDE for learning and teaching programming. .SH BASIC USAGE On the first run you see a code editor and the Python shell. .PP Enter some Python code (eg. .B print("Hello world") ) into the editor and save the file with Ctrl+S. .PP Now run the code by pressing F5. You should see the output of the program in the Python shell. .PP You can also enter Python code directly into the shell. .SH USING THE DEBUGGER You can see the steps Python takes to run your code. For this you need to press Ctrl+F5 to run the program in debug mode. In this mode you can advance the program either with big steps (F6) or small steps (F7). If you want to see how the steps affect program variables, then open global variables pane (View => Variables). .SH MORE INFORMATION You can find more information, screenshots and a walk-through video at http://thonny.org. thonny-2.1.16/PKG-INFO0000666000000000000000000000301213201324660012340 0ustar 00000000000000Metadata-Version: 1.2 Name: thonny Version: 2.1.16 Summary: Python IDE for beginners Home-page: http://thonny.org Author: Aivar Annamaa and others Author-email: thonny@googlegroups.com License: MIT Description: Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info. Keywords: IDE education debugger Platform: Windows Platform: macOS Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: MacOS X Classifier: Environment :: Win32 (MS Windows) Classifier: Environment :: X11 Applications Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: License :: Freeware Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Education Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Debuggers Classifier: Topic :: Text Editors Requires-Python: >=3.4 thonny-2.1.16/README.rst0000666000000000000000000000013413201264465012742 0ustar 00000000000000Thonny is a Python IDE meant for learning programming. See http://thonny.org for more info.thonny-2.1.16/requirements.txt0000666000000000000000000000001213172664305014536 0ustar 00000000000000jedi>=0.9 thonny-2.1.16/setup.cfg0000666000000000000000000000010013201324660013057 0ustar 00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 thonny-2.1.16/setup.py0000666000000000000000000000446213201273725012774 0ustar 00000000000000from setuptools import setup, find_packages import os.path import sys if sys.version_info < (3,4): raise RuntimeError("Thonny requires Python 3.4 or later") setupdir = os.path.dirname(__file__) with open(os.path.join(setupdir, 'thonny', 'VERSION'), encoding="ASCII") as f: version = f.read().strip() requirements = [] for line in open(os.path.join(setupdir, 'requirements.txt'), encoding="ASCII"): if line.strip() and not line.startswith('#'): requirements.append(line) setup( name="thonny", version=version, description="Python IDE for beginners", long_description="Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info.", url="http://thonny.org", author="Aivar Annamaa and others", author_email="thonny@googlegroups.com", license="MIT", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: MacOS X", "Environment :: Win32 (MS Windows)", "Environment :: X11 Applications", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: End Users/Desktop", "License :: Freeware", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Education", "Topic :: Software Development", "Topic :: Software Development :: Debuggers", "Topic :: Text Editors", ], keywords="IDE education debugger", platforms=["Windows", "macOS", "Linux"], install_requires=requirements, python_requires=">=3.4", packages=find_packages(), package_data={'': ['VERSION', 'res/*'], 'thonny.plugins.help' : ['*.rst']}, entry_points={ 'gui_scripts': [ 'thonny = thonny:launch', ] }, )thonny-2.1.16/thonny/0000777000000000000000000000000013201324660012566 5ustar 00000000000000thonny-2.1.16/thonny/ast_utils.py0000666000000000000000000000032013172664305015154 0ustar 00000000000000# This is a proxy module which gives frontend the illusion # that ast_utils lives directly in thonny package # (this is the case for backend) from thonny.shared.thonny.ast_utils import * # @UnusedWildImportthonny-2.1.16/thonny/base_file_browser.py0000666000000000000000000002040213172664305016624 0ustar 00000000000000import os.path import tkinter as tk from tkinter import ttk from thonny.ui_utils import TreeFrame from thonny import misc_utils from thonny.globals import get_workbench _dummy_node_text = "..." class BaseFileBrowser(TreeFrame): def __init__(self, master, show_hidden_files=False, last_folder_setting_name=None): TreeFrame.__init__(self, master, ["#0", "kind", "path"], displaycolumns=(0,)) #print(self.get_toplevel_items()) self.show_hidden_files = show_hidden_files self.tree['show'] = ('tree',) self.hor_scrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL) self.tree.config(xscrollcommand=self.hor_scrollbar.set) self.hor_scrollbar['command'] = self.tree.xview self.hor_scrollbar.grid(row=1, column=0, sticky="nsew") wb = get_workbench() self.folder_icon = wb.get_image("folder.gif") self.python_file_icon = wb.get_image("python_file.gif") self.text_file_icon = wb.get_image("text_file.gif") self.generic_file_icon = wb.get_image("generic_file.gif") self.hard_drive_icon = wb.get_image("hard_drive2.gif") self.tree.column('#0', width=500, anchor=tk.W) # set-up root node self.tree.set("", "kind", "root") self.tree.set("", "path", "") self.refresh_tree() self.tree.bind("<>", self.on_open_node) self._last_folder_setting_name = last_folder_setting_name self.open_initial_folder() def open_initial_folder(self): if self._last_folder_setting_name: path = get_workbench().get_option(self._last_folder_setting_name) if path: self.open_path_in_browser(path, True) def save_current_folder(self): if not self._last_folder_setting_name: return path = self.get_selected_path() if not path: return if os.path.isfile(path): path = os.path.dirname(path) get_workbench().set_option(self._last_folder_setting_name, path) def on_open_node(self, event): node_id = self.get_selected_node() if node_id: self.refresh_tree(node_id, True) def get_selected_node(self): nodes = self.tree.selection() assert len(nodes) <= 1 if len(nodes) == 1: return nodes[0] else: return None def get_selected_path(self): node_id = self.get_selected_node() if node_id: return self.tree.set(node_id, "path") else: return None def open_path_in_browser(self, path, see=True): # unfortunately os.path.split splits from the wrong end (for this case) def split(path): head, tail = os.path.split(path) if head == "" and tail == "": return [] elif head == path or tail == path: return [path] elif head == "": return [tail] elif tail == "": return split(head) else: return split(head) + [tail] parts = split(path) current_node_id = "" current_path = "" while parts != []: current_path = os.path.join(current_path, parts.pop(0)) for child_id in self.tree.get_children(current_node_id): child_path = self.tree.set(child_id, "path") if child_path == current_path: self.tree.item(child_id, open=True) self.refresh_tree(child_id) current_node_id = child_id break if see: self.tree.selection_set(current_node_id) self.tree.focus(current_node_id) if self.tree.set(current_node_id, "kind") == "file": self.tree.see(self.tree.parent(current_node_id)) else: self.tree.see(current_node_id) def populate_tree(self): for path in self.get_toplevel_items(): self.show_item(path, path, "", 2) def refresh_tree(self, node_id="", opening=None): path = self.tree.set(node_id, "path") #print("REFRESH", path) if os.path.isfile(path): self.tree.set_children(node_id) self.tree.item(node_id, open=False) else: # either root or directory if node_id == "" or self.tree.item(node_id, "open") or opening == True: fs_children_names = self.listdir(path, self.show_hidden_files) tree_children_ids = self.tree.get_children(node_id) # recollect children children = {} # first the ones, which are present already in tree for child_id in tree_children_ids: name = self.tree.item(child_id, "text") if name in fs_children_names: children[name] = child_id # add missing children for name in fs_children_names: if name not in children: children[name] = self.tree.insert(node_id, "end") self.tree.set(children[name], "path", os.path.join(path, name)) def file_order(name): # items in a folder should be ordered so that # folders come first and names are ordered case insensitively return (os.path.isfile(os.path.join(path, name)), name.upper()) # update tree ids_sorted_by_name = list(map(lambda key: children[key], sorted(children.keys(), key=file_order))) self.tree.set_children(node_id, *ids_sorted_by_name) for child_id in ids_sorted_by_name: self.update_node_format(child_id) self.refresh_tree(child_id) else: # closed dir # Don't fetch children yet, but ensure that expand button is visible children_ids = self.tree.get_children(node_id) if len(children_ids) == 0: self.tree.insert(node_id, "end", text=_dummy_node_text) def update_node_format(self, node_id): assert node_id != "" path = self.tree.set(node_id, "path") if os.path.isdir(path) or path.endswith(":") or path.endswith(":\\"): self.tree.set(node_id, "kind", "dir") if path.endswith(":") or path.endswith(":\\"): img = self.hard_drive_icon else: img = self.folder_icon else: self.tree.set(node_id, "kind", "file") if path.lower().endswith(".py"): img = self.python_file_icon elif path.lower().endswith(".txt") or path.lower().endswith(".csv"): img = self.text_file_icon else: img = self.generic_file_icon # compute caption text = os.path.basename(path) if text == "": # in case of drive roots text = path self.tree.item(node_id, text=" " + text, image=img) self.tree.set(node_id, "path", path) def listdir(self, path="", include_hidden_files=False): if path == "" and misc_utils.running_on_windows(): result = misc_utils.get_win_drives() else: if path == "": first_level = True path = "/" else: first_level = False result = [x for x in os.listdir(path) if include_hidden_files or not misc_utils.is_hidden_or_system_file(os.path.join(path, x))] if first_level: result = ["/" + x for x in result] return sorted(result, key=str.upper) thonny-2.1.16/thonny/code.py0000666000000000000000000005743713201264465014100 0ustar 00000000000000# -*- coding: utf-8 -*- import sys import os.path import tkinter as tk from tkinter import ttk, messagebox from tkinter.filedialog import asksaveasfilename from tkinter.filedialog import askopenfilename from thonny.misc_utils import eqfn, running_on_mac_os from thonny.codeview import CodeView from thonny.globals import get_workbench, get_runner from logging import exception from thonny.ui_utils import get_current_notebook_tab_widget, select_sequence from thonny.common import parse_shell_command from thonny.tktextext import rebind_control_a import tokenize from thonny.shared.thonny.common import ToplevelCommand, DebuggerCommand from tkinter.messagebox import askyesno import traceback _dialog_filetypes = [('Python files', '.py .pyw'), ('text files', '.txt'), ('all files', '.*')] class Editor(ttk.Frame): def __init__(self, master, filename=None): ttk.Frame.__init__(self, master) assert isinstance(master, EditorNotebook) # parent of codeview will be workbench so that it can be maximized self._code_view = CodeView(get_workbench(), propose_remove_line_numbers=True, font=get_workbench().get_font("EditorFont")) get_workbench().event_generate("EditorTextCreated", editor=self, text_widget=self.get_text_widget()) self._code_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self) self._code_view.home_widget = self # don't forget home self.maximizable_widget = self._code_view self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._filename = None if filename is not None: self._load_file(filename) self._code_view.text.edit_modified(False) self._code_view.text.bind("<>", self._on_text_modified, True) self._code_view.text.bind("<>", self._on_text_change, True) self._code_view.text.bind("", self._control_tab, True) get_workbench().bind("AfterKnownMagicCommand", self._listen_for_execute, True) get_workbench().bind("ToplevelResult", self._listen_for_toplevel_result, True) self.update_appearance() def get_text_widget(self): return self._code_view.text def get_code_view(self): # TODO: try to get rid of this return self._code_view def get_filename(self, try_hard=False): if self._filename is None and try_hard: self.save_file() return self._filename def get_long_description(self): if self._filename is None: result = "" else: result = self._filename try: index = self._code_view.text.index("insert") if index and "." in index: line, col = index.split(".") result += " @ {} : {}".format(line, int(col)+1) except: exception("Finding cursor location") return result def _load_file(self, filename): with tokenize.open(filename) as fp: # TODO: support also text files source = fp.read() self._filename = filename get_workbench().event_generate("Open", editor=self, filename=filename) self._code_view.set_content(source) self._code_view.focus_set() self.master.remember_recent_file(filename) def is_modified(self): return self._code_view.text.edit_modified() def save_file_enabled(self): return self.is_modified() or not self.get_filename() def save_file(self, ask_filename=False): if self._filename is not None and not ask_filename: filename = self._filename get_workbench().event_generate("Save", editor=self, filename=filename) else: # http://tkinter.unpythonic.net/wiki/tkFileDialog filename = asksaveasfilename ( filetypes = _dialog_filetypes, defaultextension = ".py", initialdir = get_workbench().get_option("run.working_directory") ) if filename in ["", (), None]: # Different tkinter versions may return different values return None # Seems that in some Python versions defaultextension # acts funny if filename.lower().endswith(".py.py"): filename = filename[:-3] get_workbench().event_generate("SaveAs", editor=self, filename=filename) content = self._code_view.get_content() encoding = "UTF-8" # TODO: check for marker in the head of the code try: f = open(filename, mode="wb", ) f.write(content.encode(encoding)) f.close() except PermissionError: if askyesno("Permission Error", "Looks like this file or folder is not writable.\n\n" + "Do you want to save under another folder and/or filename?"): return self.save_file(True) else: return None self._filename = filename self.master.remember_recent_file(filename) self._code_view.text.edit_modified(False) return self._filename def show(self): self.master.select(self) def update_appearance(self): self._code_view.set_line_numbers(get_workbench().get_option("view.show_line_numbers")) self._code_view.set_line_length_margin(get_workbench().get_option("view.recommended_line_length")) self._code_view.text.event_generate("<>") def _listen_for_execute(self, event): command, args = parse_shell_command(event.cmd_line) # Go read-only if command.lower() == "debug": if len(args) == 0: return filename = args[0] self_filename = self.get_filename() if self_filename is not None and os.path.basename(self_filename) == filename: # Not that command has only basename # so this solution may make more editors read-only than necessary self._code_view.text.set_read_only(True) def _listen_for_toplevel_result(self, event): self._code_view.text.set_read_only(False) def _control_tab(self, event): if event.state & 1: # shift was pressed direction = -1 else: direction = 1 self.master.select_next_prev_editor(direction) return "break" def _shift_control_tab(self, event): self.master.select_next_prev_editor(-1) return "break" def select_range(self, text_range): self._code_view.select_range(text_range) def focus_set(self): self._code_view.focus_set() def is_focused(self): return self.focus_displayof() == self._code_view.text def _on_text_modified(self, event): try: self.master.update_editor_title(self) except: traceback.print_exc() def _on_text_change(self, event): self.master.update_editor_title(self) runner = get_runner() if (runner.get_state() in ["running", "waiting_input", "waiting_debugger_command"] and isinstance(runner.get_current_command(), (ToplevelCommand, DebuggerCommand))): # exclude running InlineCommands runner.interrupt_backend() def destroy(self): get_workbench().unbind("AfterKnownMagicCommand", self._listen_for_execute) get_workbench().unbind("ToplevelResult", self._listen_for_toplevel_result) ttk.Frame.destroy(self) class EditorNotebook(ttk.Notebook): """ Manages opened files / modules """ def __init__(self, master): _check_create_ButtonNotebook_style() ttk.Notebook.__init__(self, master, padding=0, style="ButtonNotebook") get_workbench().set_default("file.reopen_all_files", False) get_workbench().set_default("file.open_files", []) get_workbench().set_default("file.current_file", None) get_workbench().set_default("file.recent_files", []) get_workbench().set_default("view.show_line_numbers", False) get_workbench().set_default("view.recommended_line_length", 0) self._init_commands() self.enable_traversal() # open files from last session """ TODO: they should go only to recent files for filename in prefs["open_files"].split(";"): if os.path.exists(filename): self._open_file(filename) """ self.update_appearance() def _list_recent_files(self): pass # TODO: def _init_commands(self): # TODO: do these commands have to be in EditorNotebook ?? # Create a module level function install_editor_notebook ?? # Maybe add them separately, when notebook has been installed ?? get_workbench().add_command("new_file", "file", "New", self._cmd_new_file, default_sequence=select_sequence("", ""), group=10, image_filename="file.new_file.gif", include_in_toolbar=True) get_workbench().add_command("open_file", "file", "Open...", self._cmd_open_file, default_sequence=select_sequence("", ""), group=10, image_filename="file.open_file.gif", include_in_toolbar=True) # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter get_workbench().bind_class("Text", "", self._control_o) rebind_control_a(get_workbench()) get_workbench().add_command("close_file", "file", "Close", self._cmd_close_file, default_sequence=select_sequence("", ""), tester=lambda: self.get_current_editor() is not None, group=10) get_workbench().add_command("close_files", "file", "Close all", self._cmd_close_files, tester=lambda: self.get_current_editor() is not None, group=10) get_workbench().add_command("save_file", "file", "Save", self._cmd_save_file, default_sequence=select_sequence("", ""), tester=self._cmd_save_file_enabled, group=10, image_filename="file.save_file.gif", include_in_toolbar=True) get_workbench().add_command("save_file_as", "file", "Save as...", self._cmd_save_file_as, default_sequence=select_sequence("", ""), tester=lambda: self.get_current_editor() is not None, group=10) get_workbench().createcommand("::tk::mac::OpenDocument", self._mac_open_document) def load_startup_files(self): """If no filename was sent from command line then load previous files (if setting allows)""" cmd_line_filenames = [name for name in sys.argv[1:] if os.path.exists(name)] if len(cmd_line_filenames) > 0: filenames = cmd_line_filenames elif get_workbench().get_option("file.reopen_all_files"): filenames = get_workbench().get_option("file.open_files") elif get_workbench().get_option("file.current_file"): filenames = [get_workbench().get_option("file.current_file")] else: filenames = [] if len(filenames) > 0: for filename in filenames: if os.path.exists(filename): self.show_file(filename) cur_file = get_workbench().get_option("file.current_file") # choose correct active file if len(cmd_line_filenames) > 0: self.show_file(cmd_line_filenames[0]) elif cur_file and os.path.exists(cur_file): self.show_file(cur_file) else: self._cmd_new_file() else: self._cmd_new_file() self._remember_open_files() def save_all_named_editors(self): all_saved = True for editor in self.winfo_children(): if editor.get_filename() and editor.is_modified(): success = editor.save_file() all_saved = all_saved and success return all_saved def remember_recent_file(self, filename): recents = get_workbench().get_option("file.recent_files") if filename in recents: recents.remove(filename) recents.insert(0, filename) existing_recents = [name for name in recents if os.path.exists(name)] get_workbench().set_option("file.recent_files", existing_recents[:10]) def _remember_open_files(self): if (self.get_current_editor() is not None and self.get_current_editor().get_filename() is not None): get_workbench().set_option("file.current_file", self.get_current_editor().get_filename()) open_files = [editor.get_filename() for editor in self.winfo_children() if editor.get_filename()] get_workbench().set_option("file.open_files", open_files) if len(open_files) == 0: get_workbench().set_option("file.current_file", None) def _cmd_new_file(self): new_editor = Editor(self) get_workbench().event_generate("NewFile", editor=new_editor) self.add(new_editor, text=self._generate_editor_title(None)) self.select(new_editor) new_editor.focus_set() def _cmd_open_file(self): filename = askopenfilename ( filetypes = _dialog_filetypes, initialdir = get_workbench().get_option("run.working_directory") ) if filename: # Note that missing filename may be "" or () depending on tkinter version #self.close_single_untitled_unmodified_editor() self.show_file(filename) self._remember_open_files() def _control_o(self, event): # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter self._cmd_open_file() return "break" def _cmd_close_files(self): self._close_files(None) def _close_files(self, except_index=None): for tab_index in reversed(range(len(self.winfo_children()))): if except_index is not None and tab_index == except_index: continue else: editor = self._get_editor_by_index(tab_index) if self.check_allow_closing(editor): self.forget(editor) editor.destroy() self._remember_open_files() def _cmd_close_file(self): self._close_file(None) def _close_file(self, index=None): if index is None: editor = self.get_current_editor() else: editor = self._get_editor_by_index(index) if editor: if not self.check_allow_closing(editor): return self.forget(editor) editor.destroy() self._remember_open_files() def _cmd_save_file(self): if self.get_current_editor(): self.get_current_editor().save_file() self.update_editor_title(self.get_current_editor()) self._remember_open_files() def _cmd_save_file_enabled(self): return (self.get_current_editor() and self.get_current_editor().save_file_enabled()) def _cmd_save_file_as(self): if self.get_current_editor(): self.get_current_editor().save_file(ask_filename=True) self.update_editor_title(self.get_current_editor()) self._remember_open_files() def _cmd_save_file_as_enabled(self): return self.get_current_editor() is not None def close_single_untitled_unmodified_editor(self): editors = self.winfo_children() if (len(editors) == 1 and not editors[0].is_modified() and not editors[0].get_filename()): self._cmd_close_file() def _mac_open_document(self, *args): for arg in args: if isinstance(arg, str) and os.path.exists(arg): self.show_file(arg) get_workbench().become_topmost_window() def get_current_editor(self): return get_current_notebook_tab_widget(self) def select_next_prev_editor(self, direction): cur_index = self.index(self.select()) next_index = (cur_index + direction) % len(self.tabs()) self.select(self._get_editor_by_index(next_index)) def _get_editor_by_index(self, index): tab_id = self.tabs()[index] for child in self.winfo_children(): if str(child) == tab_id: return child return None def show_file(self, filename, text_range=None): #self.close_single_untitled_unmodified_editor() editor = self.get_editor(filename, True) assert editor is not None self.select(editor) editor.focus_set() if text_range is not None: editor.select_range(text_range) return editor def update_appearance(self): for editor in self.winfo_children(): editor.update_appearance() def update_editor_title(self, editor): self.tab(editor, text=self._generate_editor_title(editor.get_filename(), editor.is_modified())) def _generate_editor_title(self, filename, is_modified=False): if filename is None: result = "" else: result = os.path.basename(filename) if is_modified: result += " *" return result def _open_file(self, filename): editor = Editor(self, filename) self.add(editor, text=self._generate_editor_title(filename)) return editor def get_editor(self, filename, open_when_necessary=False): for child in self.winfo_children(): child_filename = child.get_filename(False) if child_filename and eqfn(child.get_filename(), filename): return child if open_when_necessary: return self._open_file(filename) else: return None def focus_set(self): editor = self.get_current_editor() if editor: editor.focus_set() else: super().focus_set() def current_editor_is_focused(self): editor = self.get_current_editor() return editor.is_focused() def check_allow_closing(self, editor=None): if not editor: modified_editors = [e for e in self.winfo_children() if e.is_modified()] else: if not editor.is_modified(): return True else: modified_editors = [editor] if len(modified_editors) == 0: return True message = "Do you want to save files before closing?" if editor: message = "Do you want to save file before closing?" confirm = messagebox.askyesnocancel( title="Save On Close", message=message, default=messagebox.YES, master=self) if confirm: for editor in modified_editors: if editor.get_filename(True): editor.save_file() else: return False return True elif confirm is None: return False else: return True def _check_create_ButtonNotebook_style(): """Taken from http://svn.python.org/projects/python/trunk/Demo/tkinter/ttk/notebook_closebtn.py""" style = ttk.Style() if "closebutton" in style.element_names(): # It's done already return get_workbench().get_image('tab_close.gif', "img_close") get_workbench().get_image('tab_close_active.gif', "img_close_active") style.element_create("closebutton", "image", "img_close", ("active", "pressed", "!disabled", "img_close_active"), ("active", "!disabled", "img_close_active"), border=8, sticky='') style.layout("ButtonNotebook", [("Notebook.client", {"sticky": "nswe"})]) style.layout("ButtonNotebook.Tab", [ ("ButtonNotebook.tab", {"sticky": "nswe", "children": [("ButtonNotebook.padding", {"side": "top", "sticky": "nswe", "children": [("ButtonNotebook.focus", {"side": "top", "sticky": "nswe", "children": [("ButtonNotebook.label", {"side": "left", "sticky": ''}), ("ButtonNotebook.closebutton", {"side": "left", "sticky": ''}) ] })] })] })] ) menu = tk.Menu(get_workbench(), tearoff=False) menu.popup_index = None menu.add_command(label="Close", command=lambda:get_workbench().get_editor_notebook()._close_file(menu.popup_index)) menu.add_command(label="Close others", command=lambda:get_workbench().get_editor_notebook()._close_files(menu.popup_index)) menu.add_command(label="Close all", command=lambda:get_workbench().get_editor_notebook()._close_files()) def letf_btn_press(event): try: x, y, widget = event.x, event.y, event.widget elem = widget.identify(x, y) index = widget.index("@%d,%d" % (x, y)) if "closebutton" in elem: widget.state(['pressed']) widget.pressed_index = index except: # may fail, if clicked outside of tab pass def left_btn_release(event): x, y, widget = event.x, event.y, event.widget if not widget.instate(['pressed']): return try: elem = widget.identify(x, y) index = widget.index("@%d,%d" % (x, y)) if "closebutton" in elem and widget.pressed_index == index: if isinstance(widget, EditorNotebook): widget._cmd_close_file() else: widget.forget(index) widget.event_generate("<>") widget.state(["!pressed"]) widget.pressed_index = None except: # may fail, when mouse is dragged exception("Closing tab") def right_btn_press(event): x, y, widget = event.x, event.y, event.widget try: if "ButtonNotebook" in widget["style"]: index = widget.index("@%d,%d" % (x, y)) menu.popup_index = index menu.tk_popup(*get_workbench().winfo_pointerxy()) except: pass get_workbench().bind_class("TNotebook", "", letf_btn_press, True) get_workbench().bind_class("TNotebook", "", left_btn_release, True) if running_on_mac_os(): get_workbench().bind_class("TNotebook", "", right_btn_press, True) get_workbench().bind_class("TNotebook", "", right_btn_press, True) else: get_workbench().bind_class("TNotebook", "", right_btn_press, True) thonny-2.1.16/thonny/codeview.py0000666000000000000000000002060013201264465014751 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk from thonny.common import TextRange from thonny.globals import get_workbench from thonny import tktextext, roughparse from thonny.ui_utils import EnhancedTextWithLogging from thonny.tktextext import EnhancedText EDIT_BACKGROUND="white" READ_ONLY_BACKGROUND="LightYellow" class PythonText(EnhancedText): def perform_return(self, event): # copied from idlelib.EditorWindow (Python 3.4.2) # slightly modified text = event.widget assert text is self try: # delete selection first, last = text.get_selection_indices() if first and last: text.delete(first, last) text.mark_set("insert", first) # Strip whitespace after insert point # (ie. don't carry whitespace from the right of the cursor over to the new line) while text.get("insert") in [" ", "\t"]: text.delete("insert") left_part = text.get("insert linestart", "insert") # locate first non-white character i = 0 n = len(left_part) while i < n and left_part[i] in " \t": i = i+1 # is it only whitespace? if i == n: # start the new line with the same whitespace text.insert("insert", '\n' + left_part) return "break" # Turned out the left part contains visible chars # Remember the indent indent = left_part[:i] # Strip whitespace before insert point # (ie. after inserting the linebreak this line doesn't have trailing whitespace) while text.get("insert-1c", "insert") in [" ", "\t"]: text.delete("insert-1c", "insert") # start new line text.insert("insert", '\n') # adjust indentation for continuations and block # open/close first need to find the last stmt lno = tktextext.index2line(text.index('insert')) y = roughparse.RoughParser(text.indentwidth, text.tabwidth) for context in roughparse.NUM_CONTEXT_LINES: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" rawtext = text.get(startatindex, "insert") y.set_str(rawtext) bod = y.find_good_parse_start( False, roughparse._build_char_in_string_func(startatindex)) if bod is not None or startat == 1: break y.set_lo(bod or 0) c = y.get_continuation_type() if c != roughparse.C_NONE: # The current stmt hasn't ended yet. if c == roughparse.C_STRING_FIRST_LINE: # after the first line of a string; do not indent at all pass elif c == roughparse.C_STRING_NEXT_LINES: # inside a string which started before this line; # just mimic the current indent text.insert("insert", indent) elif c == roughparse.C_BRACKET: # line up with the first (if any) element of the # last open bracket structure; else indent one # level beyond the indent of the line with the # last open bracket text._reindent_to(y.compute_bracket_indent()) elif c == roughparse.C_BACKSLASH: # if more than one line in this stmt already, just # mimic the current indent; else if initial line # has a start on an assignment stmt, indent to # beyond leftmost =; else to beyond first chunk of # non-whitespace on initial line if y.get_num_lines_in_stmt() > 1: text.insert("insert", indent) else: text._reindent_to(y.compute_backslash_indent()) else: assert 0, "bogus continuation type %r" % (c,) return "break" # This line starts a brand new stmt; indent relative to # indentation of initial line of closest preceding # interesting stmt. indent = y.get_base_indent_string() text.insert("insert", indent) if y.is_block_opener(): text.perform_smart_tab(event) elif indent and y.is_block_closer(): text.perform_smart_backspace(event) return "break" finally: text.see("insert") text.event_generate("<>") return "break" class CodeViewText(EnhancedTextWithLogging, PythonText): """Provides opportunities for monkey-patching by plugins""" def __init__(self, master=None, cnf={}, **kw): if not "background" in kw: kw["background"] = EDIT_BACKGROUND EnhancedTextWithLogging.__init__(self, master=master, cnf=cnf, **kw) self._original_background = kw["background"] # Allow binding to events of all CodeView texts self.bindtags(self.bindtags() + ('CodeViewText',)) tktextext.fixwordbreaks(tk._default_root) def set_read_only(self, value): EnhancedTextWithLogging.set_read_only(self, value) if value: self.configure(background=READ_ONLY_BACKGROUND) else: self.configure(background=self._original_background) def on_secondary_click(self, event): super().on_secondary_click(event) get_workbench().get_menu("edit").tk_popup(event.x_root, event.y_root) class CodeView(tktextext.TextFrame): def __init__(self, master, propose_remove_line_numbers=False, **text_frame_args): tktextext.TextFrame.__init__(self, master, text_class=CodeViewText, undo=True, wrap=tk.NONE, background=EDIT_BACKGROUND, **text_frame_args) # TODO: propose_remove_line_numbers on paste?? self.text.bind("<>", self._on_text_changed, True) def get_content(self): return self.text.get("1.0", "end-1c") # -1c because Text always adds a newline itself def set_content(self, content): self.text.direct_delete("1.0", tk.END) self.text.direct_insert("1.0", content) self.update_line_numbers() self.text.edit_reset(); self.text.event_generate("<>") def _on_text_changed(self, event): self.update_line_numbers() self.update_margin_line() def select_lines(self, first_line, last_line): self.text.tag_remove("sel", "1.0", tk.END) self.text.tag_add("sel", "%s.0" % first_line, "%s.end" % last_line) def select_range(self, text_range): self.text.tag_remove("sel", "1.0", tk.END) if text_range: if isinstance(text_range, int): # it's line number start = str(text_range - self._first_line_number + 1) + ".0" end = str(text_range - self._first_line_number + 1) + ".end" elif isinstance(text_range, TextRange): start = "%s.%s" % (text_range.lineno - self._first_line_number + 1, text_range.col_offset) end = "%s.%s" % (text_range.end_lineno - self._first_line_number + 1, text_range.end_col_offset) else: assert isinstance(text_range, tuple) start, end = text_range self.text.tag_add("sel", start, end) if isinstance(text_range, int): self.text.mark_set("insert", end) self.text.see("%s -1 lines" % start) def get_selected_range(self): if self.text.has_selection(): lineno, col_offset = map(int, self.text.index(tk.SEL_FIRST).split(".")) end_lineno, end_col_offset = map(int, self.text.index(tk.SEL_LAST).split(".")) else: lineno, col_offset = map(int, self.text.index(tk.INSERT).split(".")) end_lineno, end_col_offset = lineno, col_offset return TextRange(lineno, col_offset, end_lineno, end_col_offset) thonny-2.1.16/thonny/common.py0000666000000000000000000000031213172664305014436 0ustar 00000000000000# This is a proxy module which gives frontend the illusion # that common lives directly in thonny package # (this is the case for backend) from thonny.shared.thonny.common import * # @UnusedWildImportthonny-2.1.16/thonny/config.py0000666000000000000000000001115113201264465014412 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk import os.path import ast from configparser import ConfigParser import configparser from logging import exception from tkinter import messagebox def try_load_configuration(filename): try: return ConfigurationManager(filename) except configparser.Error: if (os.path.exists(filename) and messagebox.askyesno("Problem", "Thonny's configuration file can't be read. It may be corrupt.\n\n" + "Do you want to discard the file and open Thonny with default settings?")): os.replace(filename, filename + "_corrupt") # For some reasons Thonny styles are not loaded properly once messagebox has been shown before main window (At least Windows Py 3.5) raise SystemExit("Configuration file has been discarded. Please restart Thonny!") else: raise class ConfigurationManager: def __init__(self, filename): self._ini = ConfigParser() self._filename = filename self._defaults = {} self._variables = {} # Tk variables if os.path.exists(self._filename): with open(self._filename, 'r', encoding="UTF-8") as fp: self._ini.read_file(fp) #print(prefs_filename, self.sections()) def get_option(self, name, secondary_default=None): section, option = self._parse_name(name) name = section + "." + option # variable may have more recent value if name in self._variables: return self._variables[name].get() try: val = self._ini.get(section, option) try: return ast.literal_eval(val) except: return val except: if name in self._defaults: return self._defaults[name] else: return secondary_default def has_option(self, name): return name in self._defaults def set_option(self, name, value): section, option = self._parse_name(name) name = section + "." + option if not self._ini.has_section(section): self._ini.add_section(section) if isinstance(value, str): self._ini.set(section, option, value) else: self._ini.set(section, option, repr(value)) # update variable if name in self._variables: self._variables[name].set(value) def set_default(self, name, primary_default_value): section, option = self._parse_name(name) name = section + "." + option self._defaults[name] = primary_default_value def get_variable(self, name): section, option = self._parse_name(name) name = section + "." + option if name in self._variables: return self._variables[name] else: value = self.get_option(name) if isinstance(value, bool): var = tk.BooleanVar(value=value) elif isinstance(value, int): var = tk.IntVar(value=value) elif isinstance(value, str): var = tk.StringVar(value=value) else: raise KeyError("Can't create Tk Variable for " + name) self._variables[name] = var return var def save(self): # save all tk variables for name in self._variables: self.set_option(name, self._variables[name].get()) # store if not os.path.exists(self._filename): os.makedirs(os.path.dirname(self._filename), mode=0o700, exist_ok=True) # Normal saving occasionally creates corrupted file: # https://bitbucket.org/plas/thonny/issues/167/configuration-file-occasionally-gets # Now I'm saving the configuration to a temp file # and if the save is successful, I replace configuration file with it temp_filename = self._filename + ".temp" with open(temp_filename, 'w', encoding="UTF-8") as fp: self._ini.write(fp) try: ConfigurationManager(temp_filename) # temp file was created successfully os.chmod(temp_filename, 0o600) os.replace(temp_filename, self._filename) os.chmod(self._filename, 0o600) except: exception("Could not save configuration file. Reverting to previous file.") def _parse_name(self, name): if "." in name: return name.split(".", 1) else: return "general", name thonny-2.1.16/thonny/config_ui.py0000666000000000000000000000624013172664305015116 0ustar 00000000000000import tkinter as tk from tkinter import ttk from thonny.globals import get_workbench class ConfigurationDialog(tk.Toplevel): def __init__(self, master, page_records): tk.Toplevel.__init__(self, master) width = 400 height = 400 left = max(int(get_workbench().winfo_x() + get_workbench().winfo_width()/2 - width/2), 0) top = max(int(get_workbench().winfo_y() + get_workbench().winfo_height()/2 - height/2), 0) self.geometry("%dx%d+%d+%d" % (width, height, left, top)) self.title("Thonny options") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) main_frame = ttk.Frame(self) # otherwise there is wrong color background with clam main_frame.grid(row=0, column=0, sticky=tk.NSEW) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(0, weight=1) self._notebook = ttk.Notebook(main_frame) self._notebook.grid(row=0, column=0, columnspan=3, sticky=tk.NSEW, padx=10, pady=10) self._ok_button = ttk.Button(main_frame, text="OK", command=self._ok, default="active") self._cancel_button = ttk.Button(main_frame, text="Cancel", command=self._cancel) self._ok_button.grid(row=1, column=1, padx=(0,11), pady=(0,10)) self._cancel_button.grid(row=1, column=2, padx=(0,11), pady=(0,10)) self._pages = {} for title in sorted(page_records): page_class = page_records[title] spacer = ttk.Frame(self) spacer.rowconfigure(0, weight=1) spacer.columnconfigure(0, weight=1) page = page_class(spacer) self._pages[title] = page page.grid(sticky=tk.NSEW, pady=10, padx=15) self._notebook.add(spacer, text=title) self.bind("", self._ok, True) self.bind("", self._cancel, True) def _ok(self, event=None): for title in sorted(self._pages): try: page = self._pages[title] if page.apply() == False: return except: get_workbench().report_exception("Error when applying options in " + title) self.destroy() def _cancel(self, event=None): self.destroy() class ConfigurationPage(ttk.Frame): """This is an example dummy implementation of a configuration page. It's not required that configuration pages inherit from this class (can be any widget), but the class must have constructor with single parameter for getting the master.""" def __init__(self, master): ttk.Frame.__init__(self, master) def add_checkbox(self, flag_name, description, row=None, pady=0, columnspan=1): variable = get_workbench().get_variable(flag_name) checkbox = ttk.Checkbutton(self, text=description, variable=variable) checkbox.grid(row=row, column=0, sticky=tk.W, pady=pady, columnspan=columnspan) def apply(self): """Apply method should return False, when page contains invalid input and configuration dialog should not be closed.""" pass thonny-2.1.16/thonny/globals.py0000666000000000000000000000046313172664305014600 0ustar 00000000000000# -*- coding: utf-8 -*- _worbench = None _runner = None def register_workbench(workbench): global _workbench _workbench = workbench def register_runner(runner): global _runner _runner = runner def get_workbench(): return _workbench def get_runner(): return _runner thonny-2.1.16/thonny/jedi_utils.py0000666000000000000000000000404113172664305015304 0ustar 00000000000000# Utils to handle different jedi versions def import_tree(): try: # jedi 0.11 from parso.python import tree except ImportError: try: # jedi 0.10 from jedi.parser.python import tree except ImportError: # jedi 0.9 try: from jedi.parser import tree except: # older versions tree = None return tree def get_params(func_node): if hasattr(func_node, "get_params"): # parso return func_node.get_params() else: # older jedi return func_node.params def get_parent_scope(node): try: # jedi 0.11 from jedi import parser_utils return parser_utils.get_parent_scope(node) except ImportError: # Older versions return node.get_parent_scope() def get_statement_of_position(node, pos): try: # jedi 0.11 from jedi.parser_utils import get_statement_of_position return get_statement_of_position(node, pos) except ImportError: # Older versions return node.get_statement_for_position(pos) def get_module_node(script): if hasattr(script, "_get_module_node"): return script._get_module_node() elif hasattr(script, "_get_module"): return script._get_module() else: return script._parser.module() def is_scope(node): try: # jedi 0.11 from jedi import parser_utils return parser_utils.is_scope(node) except ImportError: # Older versions return node.is_scope() def get_name_of_position(obj, position): if hasattr(obj, "get_name_of_position"): # parso return obj.get_name_of_position(position) else: # older jedi return obj.name_for_position(position) def get_version_tuple(): import jedi nums = [] for item in jedi.__version__.split("."): try: nums.append(int(item)) except: nums.append(0) return tuple(nums)thonny-2.1.16/thonny/memory.py0000666000000000000000000000717313201264465014466 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk import tkinter.font as tk_font from thonny.ui_utils import TreeFrame from thonny.misc_utils import shorten_repr from thonny.globals import get_workbench MAX_REPR_LENGTH_IN_GRID = 100 def format_object_id(object_id): # this format aligns with how Python shows memory addresses if object_id is None: return None else: return "0x" + hex(object_id)[2:].upper() #.rjust(8,'0') def parse_object_id(object_id_repr): return int(object_id_repr, base=16) class MemoryFrame(TreeFrame): def __init__(self, master, columns): TreeFrame.__init__(self, master, columns) font = tk_font.nametofont("TkDefaultFont").copy() font.configure(underline=True) self.tree.tag_configure("hovered", font=font) def stop_debugging(self): self._clear_tree() def show_selected_object_info(self): iid = self.tree.focus() if iid != '': # NB! Assuming id is second column! id_str = self.tree.item(iid)['values'][1] if id_str in ["", None, "None"]: return object_id = parse_object_id(id_str) get_workbench().event_generate("ObjectSelect", object_id=object_id) class VariablesFrame(MemoryFrame): def __init__(self, master): MemoryFrame.__init__(self, master, ('name', 'id', 'value')) self.tree.column('name', width=120, anchor=tk.W, stretch=False) self.tree.column('id', width=450, anchor=tk.W, stretch=True) self.tree.column('value', width=450, anchor=tk.W, stretch=True) self.tree.heading('name', text='Name', anchor=tk.W) self.tree.heading('id', text='Value ID', anchor=tk.W) self.tree.heading('value', text='Value', anchor=tk.W) get_workbench().bind("ShowView", self._update_memory_model, True) get_workbench().bind("HideView", self._update_memory_model, True) self._update_memory_model() #self.tree.tag_configure("item", font=ui_utils.TREE_FONT) def destroy(self): MemoryFrame.destroy(self) get_workbench().unbind("ShowView", self._update_memory_model) get_workbench().unbind("HideView", self._update_memory_model) def _update_memory_model(self, event=None): if get_workbench().in_heap_mode(): self.tree.configure(displaycolumns=("name", "id")) #self.tree.columnconfigure(1, weight=1, width=400) #self.tree.columnconfigure(2, weight=0) else: self.tree.configure(displaycolumns=("name", "value")) #self.tree.columnconfigure(1, weight=0) #self.tree.columnconfigure(2, weight=1, width=400) def update_variables(self, variables): self._clear_tree() if variables: for name in sorted(variables.keys()): if not name.startswith("__"): node_id = self.tree.insert("", "end", tags="item") self.tree.set(node_id, "name", name) if isinstance(variables[name], dict): repr_str = variables[name]["repr"] id_str = variables[name]["id"] else: repr_str = variables[name] id_str = None self.tree.set(node_id, "id", format_object_id(id_str)) self.tree.set(node_id, "value", shorten_repr(repr_str, MAX_REPR_LENGTH_IN_GRID)) def on_select(self, event): self.show_selected_object_info() thonny-2.1.16/thonny/misc_utils.py0000666000000000000000000000772613172664305015341 0ustar 00000000000000# -*- coding: utf-8 -*- import os.path import platform import sys import shutil import time def eqfn(name1, name2): return os.path.normcase(name1) == os.path.normcase(name2) def delete_dir_try_hard(path, hardness=5): # Deleting the folder on Windows is not so easy task # http://bugs.python.org/issue15496 for i in range(hardness): if os.path.exists(path): time.sleep(i * 0.5) shutil.rmtree(path, True) else: break if os.path.exists(path): # try once more but now without ignoring errors shutil.rmtree(path, False) def running_on_windows(): return platform.system() == "Windows" def running_on_mac_os(): return platform.system() == "Darwin" def running_on_linux(): return platform.system() == "Linux" def is_hidden_or_system_file(path): if os.path.basename(path).startswith("."): return True elif running_on_windows(): from ctypes import windll FILE_ATTRIBUTE_HIDDEN = 0x2 FILE_ATTRIBUTE_SYSTEM = 0x4 return bool(windll.kernel32.GetFileAttributesW(path) # @UndefinedVariable & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) else: return False def get_win_drives(): # http://stackoverflow.com/a/2288225/261181 # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364939%28v=vs.85%29.aspx import string from ctypes import windll all_drive_types = ['DRIVE_UNKNOWN', 'DRIVE_NO_ROOT_DIR', 'DRIVE_REMOVABLE', 'DRIVE_FIXED', 'DRIVE_REMOTE', 'DRIVE_CDROM', 'DRIVE_RAMDISK'] required_drive_types = ['DRIVE_REMOVABLE', 'DRIVE_FIXED', 'DRIVE_REMOTE', 'DRIVE_RAMDISK'] drives = [] bitmask = windll.kernel32.GetLogicalDrives() # @UndefinedVariable for letter in string.ascii_uppercase: drive_type = all_drive_types[windll.kernel32.GetDriveTypeW("%s:\\" % letter)] # @UndefinedVariable if bitmask & 1 and drive_type in required_drive_types: drives.append(letter + ":\\") bitmask >>= 1 return drives def shorten_repr(original_repr, max_len=1000): if len(original_repr) > max_len: return original_repr[:max_len] + " ... [{} chars truncated]".format(len(original_repr) - max_len) else: return original_repr def __maybe_later_get_thonny_data_folder(): if running_on_windows(): # CSIDL_LOCAL_APPDATA # http://www.installmate.com/support/im9/using/symbols/functions/csidls.htm return os.path.join(__maybe_later_get_windows_special_folder(28), "Thonny") elif running_on_linux(): # https://specifications.freedesktop.org/basedir-spec/latest/ar01s02.html # $XDG_DATA_HOME or $HOME/.local/share data_home = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) return os.path.join(data_home, "Thonny") elif running_on_mac_os(): return os.path.expanduser("~/Library/Thonny") else: return os.path.expanduser("~/.thonny") def __maybe_later_get_windows_special_folder(code): # http://stackoverflow.com/a/3859336/261181 # http://www.installmate.com/support/im9/using/symbols/functions/csidls.htm import ctypes.wintypes SHGFP_TYPE_CURRENT= 0 buf= ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(0, code, 0, SHGFP_TYPE_CURRENT, buf) return buf.value def get_python_version_string(version_info=None): if version_info == None: version_info = sys.version_info result = ".".join(map(str, version_info[:3])) if version_info[3] != "final": result += "-" + version_info[3] result += " (" + ("64" if sys.maxsize > 2**32 else "32")+ " bit)\n" return result thonny-2.1.16/thonny/plugins/0000777000000000000000000000000013201324660014247 5ustar 00000000000000thonny-2.1.16/thonny/plugins/about.py0000666000000000000000000001225113201264465015742 0ustar 00000000000000# -*- coding: utf-8 -*- import datetime import webbrowser import platform import tkinter as tk from tkinter import ttk import tkinter.font as font import thonny from thonny import misc_utils, ui_utils from thonny.misc_utils import get_python_version_string from thonny.globals import get_workbench class AboutDialog(tk.Toplevel): def __init__(self, master): tk.Toplevel.__init__(self, master) main_frame = ttk.Frame(self) main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) self.title("About Thonny") if misc_utils.running_on_mac_os(): self.configure(background="systemSheetBackground") self.resizable(height=tk.FALSE, width=tk.FALSE) self.transient(master) self.grab_set() self.protocol("WM_DELETE_WINDOW", self._ok) #bg_frame = ttk.Frame(self) # gives proper color in aqua #bg_frame.grid() heading_font = font.nametofont("TkHeadingFont").copy() heading_font.configure(size=19, weight="bold") heading_label = ttk.Label(main_frame, text="Thonny " + thonny.get_version(), font=heading_font) heading_label.grid() url = "http://thonny.org" url_font = font.nametofont("TkDefaultFont").copy() url_font.configure(underline=1) url_label = ttk.Label(main_frame, text=url, cursor="hand2", foreground="blue", font=url_font,) url_label.grid() url_label.bind("", lambda _:webbrowser.open(url)) if platform.system() == "Linux": try: import distro # distro don't need to be installed system_desc = distro.name(True) except ImportError: system_desc = "Linux" if "32" not in system_desc and "64" not in system_desc: system_desc += " " + self.get_os_word_size_guess() else: system_desc = (platform.system() + " " + platform.release() + " " + self.get_os_word_size_guess()) platform_label = ttk.Label(main_frame, justify=tk.CENTER, text= system_desc + "\n" + "Python " + get_python_version_string() + "Tk " + ui_utils.get_tk_version_str()) platform_label.grid(pady=20) credits_label = ttk.Label(main_frame, text="Made in\nUniversity of Tartu, Estonia\n" + "with the help from\nopen-source community", cursor="hand2", foreground="blue", font=url_font, justify=tk.CENTER) credits_label.grid() credits_label.bind("", lambda _:webbrowser.open("https://bitbucket.org/plas/thonny/src/master/CREDITS.rst")) license_font = font.nametofont("TkDefaultFont").copy() license_font.configure(size=7) license_label = ttk.Label(main_frame, text="Copyright (©) " + str(datetime.datetime.now().year) + " Aivar Annamaa\n" + "This program comes with\n" + "ABSOLUTELY NO WARRANTY!\n" + "It is free software, and you are welcome to\n" + "redistribute it under certain conditions, see\n" + "https://opensource.org/licenses/MIT\n" + "for details", justify=tk.CENTER, font=license_font) license_label.grid(pady=20) ok_button = ttk.Button(main_frame, text="OK", command=self._ok, default="active") ok_button.grid(pady=(0,15)) ok_button.focus_set() self.bind('', self._ok, True) self.bind('', self._ok, True) ui_utils.center_window(self, master) self.wait_window() def _ok(self, event=None): self.destroy() def get_os_word_size_guess(self): if "32" in platform.machine() and "64" not in platform.machine(): return "(32-bit)" elif "64" in platform.machine() and "32" not in platform.machine(): return "(64-bit)" else: return "" def load_plugin(): def open_about(*args): AboutDialog(get_workbench()) get_workbench().add_command("changelog", "help", "Version history", lambda: webbrowser.open("https://bitbucket.org/plas/thonny/src/master/CHANGELOG.rst"), group=60) get_workbench().add_command("about", "help", "About Thonny", open_about, group=61) # For Mac get_workbench().createcommand("tkAboutDialog", open_about) thonny-2.1.16/thonny/plugins/ast_view.py0000666000000000000000000001330513172664305016456 0ustar 00000000000000# -*- coding: utf-8 -*- import ast import traceback import tkinter as tk from thonny import ast_utils from thonny import ui_utils from thonny.common import TextRange from thonny.globals import get_workbench class AstView(ui_utils.TreeFrame): def __init__(self, master): ui_utils.TreeFrame.__init__(self, master, columns=('range', 'lineno', 'col_offset', 'end_lineno', 'end_col_offset'), displaycolumns=(0,) ) self._current_code_view = None self.tree.bind("<>", self._locate_code) self.tree.bind("<>", self._copy_to_clipboard) get_workbench().get_editor_notebook().bind("<>", self._update) get_workbench().bind("Save", self._update, True) get_workbench().bind("SaveAs", self._update, True) get_workbench().bind_class("Text", "", self._update, True) self.tree.column('#0', width=550, anchor=tk.W) self.tree.column('range', width=100, anchor=tk.W) self.tree.column('lineno', width=30, anchor=tk.W) self.tree.column('col_offset', width=30, anchor=tk.W) self.tree.column('end_lineno', width=30, anchor=tk.W) self.tree.column('end_col_offset', width=30, anchor=tk.W) self.tree.heading('#0', text="Node", anchor=tk.W) self.tree.heading('range', text='Code range', anchor=tk.W) self.tree['show'] = ('headings', 'tree') self._current_source = None self._update(None) def _update(self, event): editor = get_workbench().get_editor_notebook().get_current_editor() if not editor: self._current_code_view = None return self._current_code_view = editor.get_code_view() self._current_source = self._current_code_view.get_content() selection = self._current_code_view.get_selected_range() self._clear_tree() try: root = ast_utils.parse_source(self._current_source) selected_ast_node = _find_closest_containing_node(root, selection) except Exception as e: self.tree.insert("", "end", text=str(e), open=True) traceback.print_exc() return def _format(key, node, parent_id): if isinstance(node, ast.AST): fields = [(key, val) for key, val in ast.iter_fields(node)] value_label = node.__class__.__name__ elif isinstance(node, list): fields = list(enumerate(node)) if len(node) == 0: value_label = "[]" else: value_label = "[...]" else: fields = [] value_label = repr(node) item_text = str(key) + "=" + value_label node_id = self.tree.insert(parent_id, "end", text=item_text, open=True) if node == selected_ast_node: self.tree.see(node_id) self.tree.selection_add(node_id) if hasattr(node, "lineno") and hasattr(node, "col_offset"): self.tree.set(node_id, "lineno", node.lineno) self.tree.set(node_id, "col_offset", node.col_offset) range_str = str(node.lineno) + '.' + str(node.col_offset) if hasattr(node, "end_lineno") and hasattr(node, "end_col_offset"): self.tree.set(node_id, "end_lineno", node.end_lineno) self.tree.set(node_id, "end_col_offset", node.end_col_offset) range_str += " - " + str(node.end_lineno) + '.' + str(node.end_col_offset) else: # fallback self.tree.set(node_id, "end_lineno", node.lineno) self.tree.set(node_id, "end_col_offset", node.col_offset + 1) self.tree.set(node_id, "range", range_str) for field_key, field_value in fields: _format(field_key, field_value, node_id) _format("root", root, "") def _locate_code(self, event): if self._current_code_view is None: return iid = self.tree.focus() if iid != '': values = self.tree.item(iid)['values'] if isinstance(values, list) and len(values) >= 5: start_line, start_col, end_line, end_col = values[1:5] self._current_code_view.select_range(TextRange(start_line, start_col, end_line, end_col)) def _clear_tree(self): for child_id in self.tree.get_children(): self.tree.delete(child_id) def _copy_to_clipboard(self, event): self.clipboard_clear() if self._current_source is not None: pretty_ast = ast_utils.pretty(ast_utils.parse_source(self._current_source)) self.clipboard_append(pretty_ast) def _find_closest_containing_node(tree, text_range): # first look among children for child in ast.iter_child_nodes(tree): result = _find_closest_containing_node(child, text_range) if result is not None: return result # no suitable child was found if (hasattr(tree, "lineno") and TextRange(tree.lineno, tree.col_offset, tree.end_lineno, tree.end_col_offset) .contains_smaller_eq(text_range)): return tree # nope else: return None def load_plugin(): get_workbench().add_view(AstView, "AST", "s") thonny-2.1.16/thonny/plugins/autocomplete.py0000666000000000000000000002574413172664305017350 0ustar 00000000000000import tkinter as tk from thonny.globals import get_workbench, get_runner from thonny.codeview import CodeViewText from thonny.shell import ShellText from thonny.common import InlineCommand from tkinter import messagebox # TODO: adjust the window position in cases where it's too close to bottom or right edge - but make sure the current line is shown """Completions get computed on the backend, therefore getting the completions is asynchronous. """ class Completer(tk.Listbox): def __init__(self, text): self.font = get_workbench().get_font("EditorFont").copy() tk.Listbox.__init__(self, master=text, font=self.font, activestyle="dotbox", exportselection=False) self.text = text self.completions = [] self.doc_label = tk.Label(master=text, text="Aaappiiiii", bg="#ffffe0", justify="left", anchor="nw") # Auto indenter will eat up returns, therefore I need to raise the priority # of this binding self.text_priority_bindtag = "completable" + str(self.text.winfo_id()) self.text.bindtags((self.text_priority_bindtag,) + self.text.bindtags()) self.text.bind_class(self.text_priority_bindtag, "", self._on_text_keypress, True) self.text.bind("<>", self._on_text_change, True) # Assuming TweakableText # for cases when Listbox gets focus self.bind("", self._close) self.bind("", self._insert_current_selection) self.bind("", self._insert_current_selection) self._bind_result_event() def _bind_result_event(self): # TODO: remove binding when editor gets closed get_workbench().bind("EditorCompletions", self._handle_backend_response, True) def handle_autocomplete_request(self): row, column = self._get_position() source = self.text.get("1.0", "end-1c") get_runner().send_command(InlineCommand(command="editor_autocomplete", source=source, row=row, column=column, filename=self._get_filename())) def _handle_backend_response(self, msg): row, column = self._get_position() source = self.text.get("1.0", "end-1c") if msg.source != source or msg.row != row or msg.column != column: # situation has changed, information is obsolete self._close() elif msg.error: self._close() messagebox.showerror("Autocomplete error", msg.error) else: self._present_completions(msg.completions) def _present_completions(self, completions): self.completions = completions # broadcast logging info row, column = self._get_position() get_workbench().event_generate("AutocompleteProposal", text_widget=self.text, row=row, column=column, proposal_count=len(completions)) # present if len(completions) == 0: self._close() elif len(completions) == 1: self._insert_completion(completions[0]) #insert the only completion self._close() else: self._show_box(completions) def _show_box(self, completions): self.delete(0, self.size()) self.insert(0, *[c["name"] for c in completions]) self.activate(0) self.selection_set(0) # place box if not self._is_visible(): self.font.configure(size=get_workbench().get_font("EditorFont")["size"]-2) #_, _, _, list_box_height = self.bbox(0) height = 100 #min(150, list_box_height * len(completions) * 1.15) typed_name_length = len(completions[0]["name"]) - len(completions[0]["complete"]) text_box_x, text_box_y, _, text_box_height = self.text.bbox('insert-%dc' % typed_name_length); # should the box appear below or above cursor? space_below = self.master.winfo_height() - text_box_y - text_box_height space_above = text_box_y if space_below >= height or space_below > space_above: height = min(height, space_below) y = text_box_y + text_box_height else: height = min(height, space_above) y = text_box_y - height width = 400 self.place(x=text_box_x, y=y, width=width, height=height) self._update_doc() def _update_doc(self): c = self._get_selected_completion() if c is None: self.doc_label["text"] = "" self.doc_label.place_forget() else: docstring = c.get("docstring", None) if docstring: self.doc_label["text"] = docstring self.doc_label.place(x=self.winfo_x() + self.winfo_width(), y=self.winfo_y(), width=400, height=self.winfo_height()) else: self.doc_label["text"] = "" self.doc_label.place_forget() def _is_visible(self): return self.winfo_ismapped() def _insert_completion(self, completion): typed_len = len(completion["name"]) - len(completion["complete"]) typed_prefix = self.text.get("insert-{}c".format(typed_len), "insert") get_workbench().event_generate("AutocompleteInsertion", text_widget=self.text, typed_prefix=typed_prefix, completed_name=completion["name"]) if self._is_visible(): self._close() if not completion["name"].startswith(typed_prefix): # eg. case of the prefix was not correct self.text.delete("insert-{}c".format(typed_len), "insert") self.text.insert('insert', completion["name"]) else: self.text.insert('insert', completion["complete"]) def _get_filename(self): # TODO: allow completing in shell if not isinstance(self.text, CodeViewText): return None codeview = self.text.master editor = get_workbench().get_editor_notebook().get_current_editor() if editor.get_code_view() is codeview: return editor.get_filename() else: return None def _move_selection(self, delta): selected = self.curselection() if len(selected) == 0: index = 0 else: index = selected[0] index += delta index = max(0, min(self.size()-1, index)) self.selection_clear(0, self.size()-1) self.selection_set(index) self.activate(index) self.see(index) self._update_doc() def _get_request_id(self): return "autocomplete_" + str(self.text.winfo_id()) def _get_position(self): return map(int, self.text.index("insert").split(".")) def _on_text_keypress(self, event=None): if not self._is_visible(): return if event.keysym == "Escape": self._close() return "break" elif event.keysym in ["Up", "KP_Up"]: self._move_selection(-1) return "break" elif event.keysym in ["Down", "KP_Down"]: self._move_selection(1) return "break" elif event.keysym in ["Return", "KP_Enter"]: assert self.size() > 0 self._insert_current_selection() return "break" def _insert_current_selection(self, event=None): self._insert_completion(self._get_selected_completion()) def _get_selected_completion(self): sel = self.curselection() if len(sel) != 1: return None return self.completions[sel[0]] def _on_text_change(self, event=None): if self._is_visible(): self.handle_autocomplete_request() def _close(self, event=None): self.place_forget() self.doc_label.place_forget() self.text.focus_set() def on_text_click(self, event=None): if self._is_visible(): self._close() class ShellCompleter(Completer): def _bind_result_event(self): # TODO: remove binding when editor gets closed get_workbench().bind("ShellCompletions", self._handle_backend_response, True) def handle_autocomplete_request(self): source=self._get_prefix() get_runner().send_command(InlineCommand(command="shell_autocomplete", source=source)) def _handle_backend_response(self, msg): # check if the response is relevant for current state if msg.source != self._get_prefix(): self._close() else: self._present_completions(msg.completions) def _get_prefix(self): return self.text.get("insert linestart", "insert") # TODO: allow multiple line input def handle_autocomplete_request(event=None): if event is None: text = get_workbench().focus_get() else: text = event.widget _handle_autocomplete_request_for_text(text) def _handle_autocomplete_request_for_text(text): if not hasattr(text, "autocompleter"): if isinstance(text, (CodeViewText, ShellText)): if isinstance(text, CodeViewText): text.autocompleter = Completer(text) elif isinstance(text, ShellText): text.autocompleter = ShellCompleter(text) text.bind("<1>", text.autocompleter.on_text_click) else: return text.autocompleter.handle_autocomplete_request() def patched_perform_midline_tab(text, event): if isinstance(text, ShellText): option_name = "edit.tab_complete_in_shell" else: option_name = "edit.tab_complete_in_editor" if get_workbench().get_option(option_name): if not text.has_selection(): _handle_autocomplete_request_for_text(text) return "break" else: return text.perform_smart_tab(event) def load_plugin(): get_workbench().add_command("autocomplete", "edit", "Auto-complete", handle_autocomplete_request, default_sequence="" # TODO: tester ) get_workbench().set_default("edit.tab_complete_in_editor", True) get_workbench().set_default("edit.tab_complete_in_shell", True) CodeViewText.perform_midline_tab = patched_perform_midline_tab ShellText.perform_midline_tab = patched_perform_midline_tab thonny-2.1.16/thonny/plugins/coloring.py0000666000000000000000000002475713201276204016454 0ustar 00000000000000""" Each text will get its on SyntaxColorer. For performance reasons, coloring is updated in 2 phases: 1. recolor single-line tokens on the modified line(s) 2. recolor multi-line tokens (triple-quoted strings) in the whole text First phase may insert wrong tokens inside triple-quoted strings, but the priorities of triple-quoted-string tags are higher and therefore user doesn't see these wrong taggings. In Shell only current command entry is colored Regexes are adapted from idlelib """ import re from thonny.globals import get_workbench from thonny.shell import ShellText from thonny.codeview import CodeViewText class SyntaxColorer: def __init__(self, text, main_font, bold_font): self.text = text self._compile_regexes() self._config_colors(main_font, bold_font) self._update_scheduled = False self._dirty_ranges = set() self._use_coloring = True def _compile_regexes(self): from thonny.token_utils import BUILTIN, COMMENT, MAGIC_COMMAND, STRING3,\ STRING3_DELIMITER, STRING_OPEN, KW, STRING_CLOSED self.uniline_regex = re.compile( KW + "|" + BUILTIN + "|" + COMMENT + "|" + MAGIC_COMMAND + "|" + STRING3_DELIMITER # to avoid marking """ and ''' as single line string in uniline mode + "|" + STRING_CLOSED + "|" + STRING_OPEN , re.S) self.multiline_regex = re.compile( STRING3 + "|" + COMMENT + "|" + MAGIC_COMMAND #+ "|" + STRING_CLOSED # need to include single line strings otherwise '"""' ... '""""' will give wrong result + "|" + STRING_OPEN # (seems that it works faster and also correctly with only open strings) , re.S) self.id_regex = re.compile(r"\s+(\w+)", re.S) def _config_colors(self, main_font, bold_font): string_foreground = "DarkGreen" open_string_background = "#c3f9d3" self.uniline_tagdefs = { "COMMENT" : {"font":main_font, 'background':None, 'foreground':"DarkGray", }, "MAGIC_COMMAND" : {"font":main_font, 'background':None, 'foreground':"DarkGray", }, "STRING_CLOSED" : {"font":main_font, 'background':None, 'foreground':string_foreground, }, "STRING_OPEN" : {"font":main_font, 'background': open_string_background, "foreground": string_foreground}, "KEYWORD" : {"font":bold_font, 'background':None, 'foreground':"#7f0055", }, "BUILTIN" : {"font":main_font, 'background':None, 'foreground':None}, #"DEFINITION" : {}, } self.multiline_tagdefs = { "STRING_CLOSED3": self.uniline_tagdefs["STRING_CLOSED"], "STRING_OPEN3" : self.uniline_tagdefs["STRING_OPEN"], } for tagdefs in [self.multiline_tagdefs, self.uniline_tagdefs]: for tag, cnf in tagdefs.items(): if cnf: self.text.tag_configure(tag, **cnf) self.text.tag_raise('sel') self.text.tag_raise('STRING_CLOSED3') self.text.tag_raise('STRING_OPEN3') def schedule_update(self, event, use_coloring=True): self._use_coloring = use_coloring # Allow reducing work by remembering only changed lines if hasattr(event, "sequence"): if event.sequence == "TextInsert": index = self.text.index(event.index) start_row = int(index.split(".")[0]) end_row = start_row + event.text.count("\n") start_index = "%d.%d" % (start_row, 0) end_index = "%d.%d" % (end_row + 1, 0) elif event.sequence == "TextDelete": index = self.text.index(event.index1) start_row = int(index.split(".")[0]) start_index = "%d.%d" % (start_row, 0) end_index = "%d.%d" % (start_row + 1, 0) else: start_index = "1.0" end_index = "end" self._dirty_ranges.add((start_index, end_index)) def perform_update(): try: self._update_coloring() finally: self._update_scheduled = False self._dirty_ranges = set() if not self._update_scheduled: self._update_scheduled = True self.text.after_idle(perform_update) def _update_coloring(self): self._update_uniline_tokens("1.0", "end") self._update_multiline_tokens("1.0", "end") def _update_uniline_tokens(self, start, end): chars = self.text.get(start, end) # clear old tags for tag in self.uniline_tagdefs: self.text.tag_remove(tag, start, end) if not self._use_coloring: return for match in self.uniline_regex.finditer(chars): for token_type, token_text in match.groupdict().items(): if token_text and token_type in self.uniline_tagdefs: token_text = token_text.strip() match_start, match_end = match.span(token_type) self.text.tag_add(token_type, start + "+%dc" % match_start, start + "+%dc" % match_end) # Mark also the word following def or class if token_text in ("def", "class"): id_match = self.id_regex.match(chars, match_end) if id_match: id_match_start, id_match_end = id_match.span(1) self.text.tag_add("DEFINITION", start + "+%dc" % id_match_start, start + "+%dc" % id_match_end) def _update_multiline_tokens(self, start, end): chars = self.text.get(start, end) # clear old tags for tag in self.multiline_tagdefs: self.text.tag_remove(tag, start, end) if not self._use_coloring: return # Count number of open multiline strings to be able to detect when string gets closed self.text.number_of_open_multiline_strings = 0 interesting_token_types = list(self.multiline_tagdefs.keys()) + ["STRING3"] for match in self.multiline_regex.finditer(chars): for token_type, token_text in match.groupdict().items(): if token_text and token_type in interesting_token_types: token_text = token_text.strip() match_start, match_end = match.span(token_type) if token_type == "STRING3": if (token_text.startswith('"""') and not token_text.endswith('"""') or token_text.startswith("'''") and not token_text.endswith("'''") or len(token_text) == 3): str_end = int(float(self.text.index(start + "+%dc" % match_end))) file_end = int(float(self.text.index("end"))) if str_end == file_end: token_type = "STRING_OPEN3" self.text.number_of_open_multiline_strings += 1 else: token_type = None elif len(token_text) >= 4 and token_text[-4] == "\\": token_type = "STRING_OPEN3" self.text.number_of_open_multiline_strings += 1 else: token_type = "STRING_CLOSED3" token_start = start + "+%dc" % match_start token_end = start + "+%dc" % match_end # clear uniline tags for tag in self.uniline_tagdefs: self.text.tag_remove(tag, token_start, token_end) # add tag self.text.tag_add(token_type, token_start, token_end) class CodeViewSyntaxColorer(SyntaxColorer): def _update_coloring(self): for dirty_range in self._dirty_ranges: self._update_uniline_tokens(*dirty_range) # Multiline tokens need to be searched from the whole source open_before = getattr(self.text, "number_of_open_multiline_strings", 0) self._update_multiline_tokens("1.0", "end") open_after = getattr(self.text, "number_of_open_multiline_strings", 0) if open_after == 0 and open_before != 0: # recolor uniline tokens after closing last open multiline string self._update_uniline_tokens("1.0", "end") class ShellSyntaxColorer(SyntaxColorer): def _update_coloring(self): parts = self.text.tag_prevrange("command", "end") if parts: end_row, end_col = map(int, self.text.index(parts[1]).split(".")) if end_col != 0: # if not just after the last linebreak end_row += 1 # then extend the range to the beginning of next line end_col = 0 # (otherwise open strings are not displayed correctly) start_index = parts[0] end_index = "%d.%d" % (end_row, end_col) self._update_uniline_tokens(start_index, end_index) self._update_multiline_tokens(start_index, end_index) def update_coloring(event): if hasattr(event, "text_widget"): text = event.text_widget else: text = event.widget if not hasattr(text, "syntax_colorer"): if isinstance(text, ShellText): class_ = ShellSyntaxColorer elif isinstance(text, CodeViewText): class_ = CodeViewSyntaxColorer else: return text.syntax_colorer = class_(text, get_workbench().get_font("EditorFont"), get_workbench().get_font("BoldEditorFont")) text.syntax_colorer.schedule_update(event, get_workbench().get_option("view.syntax_coloring")) def load_plugin(): wb = get_workbench() wb.set_default("view.syntax_coloring", True) wb.bind("TextInsert", update_coloring, True) wb.bind("TextDelete", update_coloring, True) wb.bind("<>", update_coloring, True) thonny-2.1.16/thonny/plugins/commenting.py0000666000000000000000000000764213201264465017000 0ustar 00000000000000import tkinter as tk from thonny.globals import get_workbench from thonny.ui_utils import select_sequence from thonny.common import TextRange BLOCK_COMMENT_PREFIX = "##" def _get_focused_writable_text(): widget = get_workbench().focus_get() # In Ubuntu when moving from one menu to another, this may give None when text is actually focused if (isinstance(widget, tk.Text) and (not hasattr(widget, "is_read_only") or not widget.is_read_only())): return widget else: return None def _writable_text_is_focused(): return _get_focused_writable_text() is not None def _selection_is_line_commented(text): sel_range = _get_focused_code_range(text) for lineno in range(sel_range.lineno, sel_range.end_lineno+1): line = text.get(str(lineno) + '.0', str(lineno) + '.end') if not line.startswith(BLOCK_COMMENT_PREFIX): return False return True def _select_lines(text, first_line, last_line): text.tag_remove("sel", "1.0", tk.END) text.tag_add("sel", str(first_line) + ".0", str(last_line) + ".end") def _toggle_selection_comment(text): if _selection_is_line_commented(text): _uncomment_selection(text) else: _comment_selection(text) def _comment_selection(text): """Adds ## in front of all selected lines if any lines are selected, or just the current line otherwise""" sel_range = _get_focused_code_range(text) for lineno in range(sel_range.lineno, sel_range.end_lineno+1): text.insert(str(lineno) + '.0', BLOCK_COMMENT_PREFIX) if sel_range.end_lineno > sel_range.lineno: _select_lines(text, sel_range.lineno, sel_range.end_lineno) text.edit_separator() def _uncomment_selection(text): sel_range = _get_focused_code_range(text) for lineno in range(sel_range.lineno, sel_range.end_lineno+1): line = text.get(str(lineno) + '.0', str(lineno) + '.end') if line.startswith(BLOCK_COMMENT_PREFIX): text.delete(str(lineno) + ".0", str(lineno) + "." + str(len(BLOCK_COMMENT_PREFIX))) def _get_focused_code_range(text): if len(text.tag_ranges("sel")) > 0: lineno, col_offset = map(int, text.index(tk.SEL_FIRST).split(".")) end_lineno, end_col_offset = map(int, text.index(tk.SEL_LAST).split(".")) if end_lineno > lineno and end_col_offset == 0: # SelectAll includes nonexisting extra line end_lineno -= 1 end_col_offset = int(text.index(str(end_lineno) + ".end").split(".")[1]) else: lineno, col_offset = map(int, text.index(tk.INSERT).split(".")) end_lineno, end_col_offset = lineno, col_offset return TextRange(lineno, col_offset, end_lineno, end_col_offset) def _cmd_toggle_selection_comment(): text = _get_focused_writable_text() if text is not None: _toggle_selection_comment(text) def _cmd_comment_selection(): text = _get_focused_writable_text() if text is not None: _comment_selection(text) def _cmd_uncomment_selection(): text = _get_focused_writable_text() if text is not None: _uncomment_selection(text) def load_plugin(): get_workbench().add_command("toggle_comment", "edit", "Toggle comment", _cmd_toggle_selection_comment, default_sequence=select_sequence("", ""), tester=_writable_text_is_focused, group=50) get_workbench().add_command("comment_selection", "edit", "Comment out", _cmd_comment_selection, default_sequence="", tester=_writable_text_is_focused, group=50) get_workbench().add_command("uncomment_selection", "edit", "Uncomment", _cmd_uncomment_selection, default_sequence="", tester=_writable_text_is_focused, group=50) thonny-2.1.16/thonny/plugins/common_editing_commands.py0000666000000000000000000000515313172664305021513 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk from tkinter import ttk from thonny.globals import get_workbench from thonny.ui_utils import select_sequence def load_plugin(): def create_edit_command_handler(virtual_event_sequence): def handler(event=None): widget = get_workbench().focus_get() if widget: return widget.event_generate(virtual_event_sequence) return handler def select_all(event=None): # Tk 8.6 has <> virtual event, but 8.5 doesn't widget = get_workbench().focus_get() if isinstance(widget, tk.Text): widget.tag_remove("sel","1.0","end") widget.tag_add("sel","1.0","end") elif isinstance(widget, ttk.Entry) or isinstance(widget, tk.Entry): widget.select_range(0, tk.END) get_workbench().add_command("undo", "edit", "Undo", create_edit_command_handler("<>"), tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=10) get_workbench().add_command("redo", "edit", "Redo", create_edit_command_handler("<>"), tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=10) # Ctrl+Shift+Z as alternative shortcut for redo get_workbench().bind_class("Text", select_sequence("", ""), create_edit_command_handler("<>"), True) get_workbench().add_command("Cut", "edit", "Cut", create_edit_command_handler("<>"), tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=20) get_workbench().add_command("Copy", "edit", "Copy", create_edit_command_handler("<>"), tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=20) get_workbench().add_command("Paste", "edit", "Paste", create_edit_command_handler("<>"), tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=20) get_workbench().add_command("SelectAll", "edit", "Select all", select_all, tester=None, # TODO: default_sequence=select_sequence("", ""), skip_sequence_binding=True, group=20) thonny-2.1.16/thonny/plugins/debugger.py0000666000000000000000000006444513201264465016430 0ustar 00000000000000# -*- coding: utf-8 -*- """ Adds debugging commands and features. """ import tkinter as tk from tkinter import ttk from thonny.common import DebuggerCommand from thonny.memory import VariablesFrame from thonny import ast_utils, memory, misc_utils, ui_utils from thonny.misc_utils import shorten_repr import ast from thonny.codeview import CodeView, READ_ONLY_BACKGROUND from tkinter.messagebox import showinfo, showerror from thonny.globals import get_workbench, get_runner from thonny.ui_utils import select_sequence import tokenize import logging _SUSPENDED_FOCUS_BACKGROUND = "#DCEDF2" _ACTIVE_FOCUS_BACKGROUND = "#F8FC9A" class Debugger: def __init__(self): self._init_commands() self._main_frame_visualizer = None self._last_progress_message = None get_workbench().bind("DebuggerProgress", self._handle_debugger_progress, True) get_workbench().bind("ToplevelResult", self._handle_toplevel_result, True) get_workbench().get_view("ShellView").add_command("Debug", get_runner().handle_execute_from_shell) def _init_commands(self): get_workbench().add_command("debug", "run", "Debug current script", self._cmd_debug_current_script, tester=self._cmd_debug_current_script_enabled, default_sequence="", group=10, image_filename="run.debug_current_script.gif", include_in_toolbar=True) get_workbench().add_command("step_over", "run", "Step over", self._cmd_step_over, tester=self._cmd_stepping_commands_enabled, default_sequence="", group=30, image_filename="run.step_over.gif", include_in_toolbar=True) get_workbench().add_command("step_into", "run", "Step into", self._cmd_step_into, tester=self._cmd_stepping_commands_enabled, default_sequence="", group=30, image_filename="run.step_into.gif", include_in_toolbar=True) get_workbench().add_command("step_out", "run", "Step out", self._cmd_step_out, tester=self._cmd_stepping_commands_enabled, default_sequence="", group=30, image_filename="run.step_out.gif", include_in_toolbar=True) get_workbench().add_command("run_to_cursor", "run", "Run to cursor", self._cmd_run_to_cursor, tester=self._cmd_run_to_cursor_enabled, default_sequence=select_sequence("", ""), group=30, image_filename="run.run_to_cursor.gif", include_in_toolbar=False) def _cmd_debug_current_script(self): get_runner().execute_current("Debug") def _cmd_debug_current_script_enabled(self): return (get_workbench().get_editor_notebook().get_current_editor() is not None and get_runner().get_state() == "waiting_toplevel_command" and "debug" in get_runner().supported_features()) def _check_issue_debugger_command(self, command, **kwargs): cmd = DebuggerCommand(command=command, **kwargs) self._last_debugger_command = cmd state = get_runner().get_state() if (state == "waiting_debugger_command" or getattr(cmd, "automatic", False) and state == "running"): logging.debug("_check_issue_debugger_command: %s", cmd) # tell VM the state we are seeing cmd.setdefault ( frame_id=self._last_progress_message.stack[-1].id, state=self._last_progress_message.stack[-1].last_event, focus=self._last_progress_message.stack[-1].last_event_focus ) get_runner().send_command(cmd) else: logging.debug("Bad state for sending debugger command " + str(command)) def _cmd_stepping_commands_enabled(self): return get_runner().get_state() == "waiting_debugger_command" def _cmd_step_into(self): self._check_issue_debugger_command("step") def _cmd_step_over(self): # Step over should stop when new statement or expression is selected. # At the same time, I need to get value from after_expression event. # Therefore I ask backend to stop first after the focus # and later I ask it to run to the beginning of new statement/expression. self._check_issue_debugger_command("exec") def _cmd_step_out(self): self._check_issue_debugger_command("out") def _cmd_run_to_cursor(self): visualizer = self._get_topmost_selected_visualizer() if visualizer: assert isinstance(visualizer._text_frame, CodeView) code_view = visualizer._text_frame selection = code_view.get_selected_range() target_lineno = visualizer._firstlineno-1 + selection.lineno self._check_issue_debugger_command("line", target_filename=visualizer._filename, target_lineno=target_lineno, ) def _cmd_run_to_cursor_enabled(self): return (self._cmd_stepping_commands_enabled() and self._get_topmost_selected_visualizer() is not None ) def _get_topmost_selected_visualizer(self): visualizer = self._main_frame_visualizer if visualizer is None: return None while visualizer._next_frame_visualizer is not None: visualizer = visualizer._next_frame_visualizer topmost_text_widget = visualizer._text focused_widget = get_workbench().focus_get() if focused_widget is None: return None elif focused_widget == topmost_text_widget: return visualizer else: return None def _handle_debugger_progress(self, msg): self._last_progress_message = msg if self._should_skip_event(msg): self._check_issue_debugger_command("run_to_before", automatic=True) else: main_frame_id = msg.stack[0].id # clear obsolete main frame visualizer if (self._main_frame_visualizer and self._main_frame_visualizer.get_frame_id() != main_frame_id): self._main_frame_visualizer.close() self._main_frame_visualizer = None if not self._main_frame_visualizer: self._main_frame_visualizer = MainFrameVisualizer(msg.stack[0]) self._main_frame_visualizer.update_this_and_next_frames(msg) # advance automatically in some cases event = msg.stack[-1].last_event args = msg.stack[-1].last_event_args if msg.exception: showerror("Exception", # Following is clever but noisy msg.exception_lower_stack_description.lstrip() + msg.exception["type_name"] + ": " + msg.exception_msg) self._check_issue_debugger_command("step", automatic=True) elif (event == "after_expression" and "last_child" in args["node_tags"] and "child_of_statement" in args["node_tags"]): # This means we're done with the expression, so let's speed up a bit. self._check_issue_debugger_command("step", automatic=True) # Next event will be before_statement_again def _should_skip_event(self, msg): frame_info = msg.stack[-1] event = frame_info.last_event tags = frame_info.last_event_args["node_tags"] if event == "after_statement": return True # TODO: consult also configuration if "call_function" in tags: return True else: return False def _handle_toplevel_result(self, msg): if self._main_frame_visualizer is not None: self._main_frame_visualizer.close() self._main_frame_visualizer = None class FrameVisualizer: """ Is responsible for stepping through statements and updating corresponding UI in Editor-s, FunctionCallDialog-s, ModuleDialog-s """ def __init__(self, text_frame, frame_info): self._text_frame = text_frame self._text = text_frame.text self._frame_id = frame_info.id self._filename = frame_info.filename self._firstlineno = frame_info.firstlineno self._source = frame_info.source self._expression_box = ExpressionBox(text_frame) self._next_frame_visualizer = None self._text.tag_configure('focus', background=_ACTIVE_FOCUS_BACKGROUND, borderwidth=1, relief=tk.SOLID) self._text.tag_configure('exception', background="#FFBFD6") self._text.tag_raise("exception", "focus") self._text.set_read_only(True) def close(self): if self._next_frame_visualizer: self._next_frame_visualizer.close() self._next_frame_visualizer = None self._text.set_read_only(False) self._remove_focus_tags() self._expression_box.clear_debug_view() def get_frame_id(self): return self._frame_id def update_this_and_next_frames(self, msg): """Must not be used on obsolete frame""" #debug("State: %s, focus: %s", msg.state, msg.focus) frame_info, next_frame_info = self._find_this_and_next_frame(msg.stack) self._update_this_frame(msg, frame_info) # clear obsolete next frame visualizer if (self._next_frame_visualizer and (not next_frame_info or self._next_frame_visualizer.get_frame_id() != next_frame_info.id)): self._next_frame_visualizer.close() self._next_frame_visualizer = None if next_frame_info and not self._next_frame_visualizer: self._next_frame_visualizer = self._create_next_frame_visualizer(next_frame_info) if self._next_frame_visualizer: self._next_frame_visualizer.update_this_and_next_frames(msg) def _remove_focus_tags(self): self._text.tag_remove("focus", "0.0", "end") self._text.tag_remove("exception", "0.0", "end") def _update_this_frame(self, msg, frame_info): self._frame_info = frame_info # TODO: if focus is in expression, then find and highlight closest # statement if "statement" in frame_info.last_event: self._remove_focus_tags() self._tag_range(frame_info.last_event_focus, "focus", True) if msg.exception is not None: self._tag_range(frame_info.last_event_focus, "exception", True) self._text.tag_configure('focus', background=_ACTIVE_FOCUS_BACKGROUND, borderwidth=1, relief=tk.SOLID) else: self._text.tag_configure('focus', background=READ_ONLY_BACKGROUND, borderwidth=1, relief=tk.SOLID) self._expression_box.update_expression(msg, frame_info) def _find_this_and_next_frame(self, stack): for i in range(len(stack)): if stack[i].id == self._frame_id: if i == len(stack)-1: # last frame return stack[i], None else: return stack[i], stack[i+1] else: raise AssertionError("Frame doesn't exist anymore") def _tag_range(self, text_range, tag, see=False): first_line, first_col, last_line = self._get_text_range_block(text_range) for lineno in range(first_line, last_line+1): self._text.tag_add(tag, "%d.%d" % (lineno, first_col), "%d.0" % (lineno+1)) self._text.update_idletasks() self._text.see("%d.0" % (first_line)) if last_line - first_line < 3: # if it's safe to assume that whole code fits into screen # then scroll it down a bit so that expression view doesn't hide behind # lower edge of the editor self._text.update_idletasks() self._text.see("%d.0" % (first_line+3)) def _get_text_range_block(self, text_range): first_line = text_range.lineno - self._frame_info.firstlineno + 1 last_line = text_range.end_lineno - self._frame_info.firstlineno + 1 first_line_content = self._text.get("%d.0" % first_line, "%d.end" % first_line) if first_line_content.strip().startswith("elif "): first_col = first_line_content.find("elif ") else: first_col = text_range.col_offset return (first_line, first_col, last_line) def _create_next_frame_visualizer(self, next_frame_info): if next_frame_info.code_name == "": return ModuleLoadDialog(self._text, next_frame_info) else: dialog = FunctionCallDialog(self._text.master, next_frame_info) if self._expression_box.winfo_ismapped(): dialog.title(self._expression_box.get_focused_text()) else: dialog.title("Function call at " + hex(self._frame_id)) return dialog class MainFrameVisualizer(FrameVisualizer): """ Takes care of stepping in the main module """ def __init__(self, frame_info): editor = get_workbench().get_editor_notebook().show_file(frame_info.filename) FrameVisualizer.__init__(self, editor.get_code_view(), frame_info) class CallFrameVisualizer(FrameVisualizer): def __init__(self, text_frame, frame_id): self._dialog = FunctionCallDialog(text_frame) FrameVisualizer.__init__(self, self._dialog.get_code_view(), frame_id) def close(self): super().close() self._dialog.destroy() class ExpressionBox(tk.Text): def __init__(self, codeview): tk.Text.__init__(self, codeview.winfo_toplevel(), #codeview.text, height=1, width=1, relief=tk.RAISED, background="#DCEDF2", borderwidth=1, highlightthickness=0, padx=7, pady=7, wrap=tk.NONE, font=get_workbench().get_font("EditorFont")) self._codeview = codeview self._main_range = None self._last_focus = None self.tag_config("value", foreground="Blue") self.tag_configure('before', background="#F8FC9A", borderwidth=1, relief=tk.SOLID) self.tag_configure('after', background="#D7EDD3", borderwidth=1, relief=tk.FLAT) self.tag_configure('exception', background="#FFBFD6", borderwidth=1, relief=tk.SOLID) self.tag_raise("exception", "before") self.tag_raise("exception", "after") def update_expression(self, msg, frame_info): focus = frame_info.last_event_focus event = frame_info.last_event if event in ("before_expression", "before_expression_again"): # (re)load stuff if self._main_range is None or focus.not_smaller_eq_in(self._main_range): self._load_expression(frame_info.filename, focus) self._update_position(focus) self._update_size() self._highlight_range(focus, event, msg.exception) elif event == "after_expression": logging.debug("EV: after_expression %s", msg) self.tag_configure('after', background="#BBEDB2", borderwidth=1, relief=tk.FLAT) start_mark = self._get_mark_name(focus.lineno, focus.col_offset) end_mark = self._get_mark_name(focus.end_lineno, focus.end_col_offset) assert hasattr(msg, "value") logging.debug("EV: replacing expression with value") original_focus_text = self.get(start_mark, end_mark) self.delete(start_mark, end_mark) id_str = memory.format_object_id(msg.value["id"]) if get_workbench().in_heap_mode(): value_str = id_str elif "StringLiteral" in frame_info.last_event_args["node_tags"]: # No need to show Python replacing double quotes with single quotes value_str = original_focus_text else: value_str = shorten_repr(msg.value["repr"], 100) object_tag = "object_" + str(msg.value["id"]) self.insert(start_mark, value_str, ('value', 'after', object_tag)) if misc_utils.running_on_mac_os(): sequence = "" else: sequence = "" self.tag_bind(object_tag, sequence, lambda _: get_workbench().event_generate("ObjectSelect", object_id=msg.value["id"])) self._update_size() elif (event == "before_statement_again" and self._main_range is not None # TODO: shouldn't need this and self._main_range.is_smaller_eq_in(focus)): # we're at final stage of executing parent statement # (eg. assignment after the LHS has been evaluated) # don't close yet self.tag_configure('after', background="#DCEDF2", borderwidth=1, relief=tk.FLAT) elif event == "exception": "TODO:" else: # hide and clear on non-expression events self.clear_debug_view() self._last_focus = focus def get_focused_text(self): if self._last_focus: start_mark = self._get_mark_name(self._last_focus.lineno, self._last_focus.col_offset) end_mark = self._get_mark_name(self._last_focus.end_lineno, self._last_focus.end_col_offset) return self.get(start_mark, end_mark) else: return "" def clear_debug_view(self): self.place_forget() self._main_range = None self._last_focus = None for tag in self.tag_names(): self.tag_remove(tag, "1.0", "end") for mark in self.mark_names(): self.mark_unset(mark) def _load_expression(self, filename, text_range): with tokenize.open(filename) as fp: whole_source = fp.read() root = ast_utils.parse_source(whole_source, filename) self._main_range = text_range assert self._main_range is not None main_node = ast_utils.find_expression(root, text_range) source = ast_utils.extract_text_range(whole_source, text_range) logging.debug("EV.load_exp: %s", (text_range, main_node, source)) self.delete("1.0", "end") self.insert("1.0", source) # create node marks def _create_index(lineno, col_offset): local_lineno = lineno - main_node.lineno + 1 if lineno == main_node.lineno: local_col_offset = col_offset - main_node.col_offset else: local_col_offset = col_offset return str(local_lineno) + "." + str(local_col_offset) for node in ast.walk(main_node): if "lineno" in node._attributes: index1 = _create_index(node.lineno, node.col_offset) index2 = _create_index(node.end_lineno, node.end_col_offset) start_mark = self._get_mark_name(node.lineno, node.col_offset) if not start_mark in self.mark_names(): self.mark_set(start_mark, index1) self.mark_gravity(start_mark, tk.LEFT) end_mark = self._get_mark_name(node.end_lineno, node.end_col_offset) if not end_mark in self.mark_names(): self.mark_set(end_mark, index2) self.mark_gravity(end_mark, tk.RIGHT) def _get_mark_name(self, lineno, col_offset): return str(lineno) + "_" + str(col_offset) def _get_tag_name(self, node_or_text_range): return (str(node_or_text_range.lineno) + "_" + str(node_or_text_range.col_offset) + "_" + str(node_or_text_range.end_lineno) + "_" + str(node_or_text_range.end_col_offset)) def _highlight_range(self, text_range, state, exception): logging.debug("EV._highlight_range: %s", text_range) self.tag_remove("after", "1.0", "end") self.tag_remove("before", "1.0", "end") self.tag_remove("exception", "1.0", "end") if state.startswith("after"): tag = "after" elif state.startswith("before"): tag = "before" else: return start_index = self._get_mark_name(text_range.lineno, text_range.col_offset) end_index = self._get_mark_name(text_range.end_lineno, text_range.end_col_offset) self.tag_add(tag, start_index, end_index) if exception: self.tag_add("exception", start_index, end_index) def _update_position(self, text_range): self._codeview.update_idletasks() text_line_number = text_range.lineno - self._codeview._first_line_number + 1 bbox = self._codeview.text.bbox(str(text_line_number) + "." + str(text_range.col_offset)) if isinstance(bbox, tuple): x = bbox[0] - self._codeview.text.cget("padx") + 6 y = bbox[1] - self._codeview.text.cget("pady") - 6 else: x = 30 y = 30 widget = self._codeview.text while widget != self.winfo_toplevel(): x += widget.winfo_x() y += widget.winfo_y() widget = widget.master self.place(x=x, y=y, anchor=tk.NW) self.update_idletasks() def _update_size(self): content = self.get("1.0", tk.END) lines = content.splitlines() self["height"] = len(lines) self["width"] = max(map(len, lines)) class FrameDialog(tk.Toplevel, FrameVisualizer): def __init__(self, master, frame_info): tk.Toplevel.__init__(self, master) self.transient(master) if misc_utils.running_on_windows(): self.wm_attributes('-toolwindow', 1) # TODO: take size from prefs editor_notebook = get_workbench().get_editor_notebook() if master.winfo_toplevel() == get_workbench(): position_reference = editor_notebook else: # align to previous frame position_reference = master.winfo_toplevel() self.geometry("{}x{}+{}+{}".format(editor_notebook.winfo_width(), editor_notebook.winfo_height()-20, position_reference.winfo_rootx(), position_reference.winfo_rooty())) self.protocol("WM_DELETE_WINDOW", self._on_close) self._init_layout_widgets(master, frame_info) FrameVisualizer.__init__(self, self._text_frame, frame_info) self._load_code(frame_info) self._text_frame.text.focus() def _init_layout_widgets(self, master, frame_info): self.main_frame= ttk.Frame(self) # just a backgroud behind padding of main_pw, without this OS X leaves white border self.main_frame.grid(sticky=tk.NSEW) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.main_pw = ui_utils.AutomaticPanedWindow(self.main_frame, orient=tk.VERTICAL) self.main_pw.grid(sticky=tk.NSEW, padx=10, pady=10) self.main_frame.rowconfigure(0, weight=1) self.main_frame.columnconfigure(0, weight=1) self._code_book = ttk.Notebook(self.main_pw) self._text_frame = CodeView(self._code_book, first_line_number=frame_info.firstlineno, font=get_workbench().get_font("EditorFont")) self._code_book.add(self._text_frame, text="Source") self.main_pw.add(self._code_book, minsize=100) def _load_code(self, frame_info): self._text_frame.set_content(frame_info.source) def _update_this_frame(self, msg, frame_info): FrameVisualizer._update_this_frame(self, msg, frame_info) def _on_close(self): showinfo("Can't close yet", 'Use "Stop" command if you want to cancel debugging') def close(self): FrameVisualizer.close(self) self.destroy() class FunctionCallDialog(FrameDialog): def __init__(self, master, frame_info): FrameDialog.__init__(self, master, frame_info) def _init_layout_widgets(self, master, frame_info): FrameDialog._init_layout_widgets(self, master, frame_info) self._locals_book = ttk.Notebook(self.main_pw) self._locals_frame = VariablesFrame(self._locals_book) self._locals_book.add(self._locals_frame, text="Local variables") self.main_pw.add(self._locals_book, minsize=100) def _load_code(self, frame_info): FrameDialog._load_code(self, frame_info) if hasattr(frame_info, "function"): function_label = frame_info.function["repr"] else: function_label = frame_info.code_name # change tab label self._code_book.tab(self._text_frame, text=function_label) def _update_this_frame(self, msg, frame_info): FrameDialog._update_this_frame(self, msg, frame_info) self._locals_frame.update_variables(frame_info.locals) class ModuleLoadDialog(FrameDialog): def __init__(self, text_frame, frame_info): FrameDialog.__init__(self, text_frame) def load_plugin(): Debugger() thonny-2.1.16/thonny/plugins/editor_config_page.py0000666000000000000000000000440513172664305020445 0ustar 00000000000000import tkinter as tk from tkinter import ttk from thonny.config_ui import ConfigurationPage from thonny.globals import get_workbench import logging class EditorConfigurationPage(ConfigurationPage): def __init__(self, master): ConfigurationPage.__init__(self, master) try: self.add_checkbox("view.name_highlighting", "Highlight matching names") except: # name matcher may have been disabled logging.warning("Couldn't create name matcher checkbox") try: self.add_checkbox("view.locals_highlighting", "Highlight local variables") except: # locals highlighter may have been disabled logging.warning("Couldn't create name locals highlighter checkbox") self.add_checkbox("view.paren_highlighting", "Highlight parentheses") self.add_checkbox("view.syntax_coloring", "Highlight syntax elements") self.add_checkbox("edit.tab_complete_in_editor", "Allow code completion with Tab-key in editors", columnspan=2, pady=(20,0), ) self.add_checkbox("edit.tab_complete_in_shell", "Allow code completion with Tab-key in Shell", columnspan=2) self.add_checkbox("view.show_line_numbers", "Show line numbers", pady=(20,0)) self._line_length_var = get_workbench().get_variable("view.recommended_line_length") label = ttk.Label(self, text="Recommended maximum line length\n(Set to 0 to turn off margin line)") label.grid(row=7, column=0, sticky=tk.W) self._line_length_combo = ttk.Combobox(self, width=4, exportselection=False, textvariable=self._line_length_var, state='readonly', values=[0,60,70,80,90,100,110,120]) self._line_length_combo.grid(row=7, column=1, sticky=tk.E) self.columnconfigure(0, weight=1) def apply(self): ConfigurationPage.apply(self) get_workbench().get_editor_notebook().update_appearance() def load_plugin(): get_workbench().add_configuration_page("Editor", EditorConfigurationPage) thonny-2.1.16/thonny/plugins/event_logging.py0000666000000000000000000001626413172664305017473 0ustar 00000000000000import os.path import tkinter as tk import time from thonny.globals import get_workbench from thonny.workbench import WorkbenchEvent from datetime import datetime import zipfile from tkinter.filedialog import asksaveasfilename import json from thonny.shell import ShellView from thonny import THONNY_USER_DIR class EventLogger: def __init__(self, filename): self._filename = filename self._init_logging() self._init_commands() def _init_commands(self): get_workbench().add_command( "export_usage_logs", "tools", "Export usage logs...", self._cmd_export, group=110 ) def _init_logging(self): self._events = [] wb = get_workbench() wb.bind("WorkbenchClose", self._on_worbench_close, True) for sequence in ["<>", "<>", "<>", "<>", "<>", #"<>", #"", #"", "", "", "" ]: self._bind_all(sequence) for sequence in ["Command", "MagicCommand", "Open", "Save", "SaveAs", "NewFile", "EditorTextCreated", #"ShellTextCreated", # Too bad, this event happens before event_logging is loaded "ShellCommand", "ShellInput", "ShowView", "HideView", "TextInsert", "TextDelete", ]: self._bind_workbench(sequence) self._bind_workbench("", True) self._bind_workbench("", True) ### log_user_event(KeyPressEvent(self, e.char, e.keysym, self.text.index(tk.INSERT))) # TODO: if event data includes an Editor, then look up also text id def _bind_workbench(self, sequence, only_workbench_widget=False): def handle(event): if not only_workbench_widget or event.widget == get_workbench(): self._log_event(sequence, event) get_workbench().bind(sequence, handle, True) def _bind_all(self, sequence): def handle(event): self._log_event(sequence, event) tk._default_root.bind_all(sequence, handle, True) def _extract_interesting_data(self, event, sequence): attributes = vars(event) # generate some new attributes if "text_widget" not in attributes: if "editor" in attributes: attributes["text_widget"] = attributes["editor"].get_text_widget() if "widget" in attributes and isinstance(attributes["widget"], tk.Text): attributes["text_widget"] = attributes["widget"] if "text_widget" in attributes: widget = attributes["text_widget"] if isinstance(widget.master, ShellView): attributes["text_widget_context"] = "shell" # select attributes data = {} for name in attributes: # skip some attributes if (name.startswith("_") or isinstance(event, WorkbenchEvent) and name in ["update", "setdefault"] or isinstance(event, tk.Event) and name not in ["widget", "text_widget", "text_widget_context"]): continue value = attributes[name] if isinstance(value, tk.BaseWidget) or isinstance(value, tk.Tk): data[name + "_id"] = id(value) data[name + "_class"] = value.__class__.__name__ elif type(value) in [str, int, float]: data[name] = value else: data[name] = repr(value) return data def _get_log_dir(self): return os.path.dirname(self._filename) def _cmd_export(self): filename = asksaveasfilename ( filetypes = [('Zip-files', '.zip'), ('all files', '.*')], defaultextension = ".zip", initialdir = get_workbench().get_option("run.working_directory"), initialfile = time.strftime("ThonnyUsageLogs_%Y-%m-%d.zip") ) if not filename: return log_dir = self._get_log_dir() with zipfile.ZipFile(filename, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: for item in os.listdir(log_dir): if item.endswith(".txt") or item.endswith(".zip"): zipf.write(os.path.join(log_dir, item), arcname=item) def _log_event(self, sequence, event): data = self._extract_interesting_data(event, sequence) data["sequence"] = sequence data["time"] = datetime.now().isoformat() self._events.append(data) def _on_worbench_close(self, event=None): with open(self._filename, encoding="UTF-8", mode="w") as fp: json.dump(self._events, fp, indent=" ") self._check_compress_logs() def _check_compress_logs(self): # if uncompressed logs have grown over 10MB, # compress these into new zipfile log_dir = self._get_log_dir() total_size = 0 uncompressed_files = [] for item in os.listdir(log_dir): if item.endswith(".txt"): full_name = os.path.join(log_dir, item) total_size += os.stat(full_name).st_size uncompressed_files.append((item, full_name)) if total_size > 10*1024*1024: zip_filename = _generate_timestamp_file_name("zip") with zipfile.ZipFile(zip_filename, 'w', compression=zipfile.ZIP_DEFLATED) as zipf: for item, full_name in uncompressed_files: zipf.write(full_name, arcname=item) for _, full_name in uncompressed_files: os.remove(full_name) def _generate_timestamp_file_name(extension): # generate log filename folder = os.path.expanduser(os.path.join(THONNY_USER_DIR, "user_logs")) if not os.path.exists(folder): os.makedirs(folder) for i in range(100): filename = os.path.join(folder, time.strftime("%Y-%m-%d_%H-%M-%S_{}.{}".format(i, extension))); if not os.path.exists(filename): return filename raise RuntimeError() def load_plugin(): filename = _generate_timestamp_file_name("txt") # create logger EventLogger(filename) thonny-2.1.16/thonny/plugins/event_view.py0000666000000000000000000000300213172664305017001 0ustar 00000000000000""" Helper view for Thonny developers """ from thonny.tktextext import TextFrame from thonny.globals import get_workbench class EventsView(TextFrame): def __init__(self, master): TextFrame.__init__(self, master) #self.text.config(wrap=tk.WORD) get_workbench().bind("ShowView", self._log_event, True) get_workbench().bind("HideView", self._log_event, True) get_workbench().bind("ToplevelResult", self._log_event, True) get_workbench().bind("DebuggerProgress", self._log_event, True) get_workbench().bind("ProgramOutput", self._log_event, True) get_workbench().bind("InputRequest", self._log_event, True) def _log_event(self, event): self.text.insert("end", event.sequence + "\n") for name in dir(event): if name not in ["sequence", "setdefault", "update"] and not name.startswith("_"): self.text.insert("end", " " + name + ": " + repr(getattr(event, name))[:100] + "\n") if event.sequence == "DebuggerProgress": frame = event.stack[-1] self.text.insert("end", " " + "event" + ": " + frame.last_event + "\n") self.text.insert("end", " " + "focus" + ": " + str(frame.last_event_focus) + "\n") self.text.insert("end", " " + "args" + ": " + str(frame.last_event_args) + "\n") self.text.see("end") def load_plugin(): if get_workbench().get_option("debug_mode"): get_workbench().add_view(EventsView, "Events", "se")thonny-2.1.16/thonny/plugins/find_replace.py0000666000000000000000000004117313172664305017254 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk from tkinter import ttk from thonny import misc_utils from thonny.globals import get_workbench from thonny.ui_utils import select_sequence #TODO - consider moving the cmd_find method to main class in order to pass the editornotebook reference #TODO - logging #TODO - instead of text.see method create another one which attempts to center the line where the text was found #TODO - test on mac and linux # Handles the find dialog display and the logic of searching. #Communicates with the codeview that is passed to the constructor as a parameter. _active_find_dialog = None class FindDialog(tk.Toplevel): def __init__(self, master): padx=15 pady=15 tk.Toplevel.__init__(self, master, takefocus=1, background="pink") main_frame = ttk.Frame(self) main_frame.grid(row=1, column=1, sticky="nsew") self.columnconfigure(1, weight=1) self.rowconfigure(1, weight=1) self.codeview = master self.codeview.text.tag_configure("hit", background="Yellow", foreground=None) self._init_found_tag_styles() #sets up the styles used to highlight found strings #references to the current set of passive found tags e.g. all words that match the searched term but are not the active string self.passive_found_tags = set() self.active_found_tag = None #reference to the currently active (centered) found string #if find dialog was used earlier then put the previous search word to the Find entry field #TODO - refactor this, there must be a better way try: #if find dialog was used earlier then this is present FindDialog.last_searched_word = FindDialog.last_searched_word except: FindDialog.last_searched_word = None #if this variable does not exist then this is the first time find dialog has been launched #a tuple containing the start and indexes of the last processed string #if the last action was find, then the end index is start index + 1 #if the last action was replace, then the indexes correspond to the start #and end of the inserted word self.last_processed_indexes = None self.last_search_case = None #case sensitivity value used during the last search #set up window display self.geometry("+%d+%d" % (master.winfo_rootx() + master.winfo_width() // 2, master.winfo_rooty() + master.winfo_height() // 2 - 150)) self.title("Find & Replace") if misc_utils.running_on_mac_os(): self.configure(background="systemSheetBackground") self.resizable(height=tk.FALSE, width=tk.FALSE) self.transient(master) self.protocol("WM_DELETE_WINDOW", self._ok) #Find text label self.find_label = ttk.Label(main_frame, text="Find:"); self.find_label.grid(column=0, row=0, sticky="w", padx=(padx, 0), pady=(pady, 0)); #Find text field self.find_entry_var = tk.StringVar() self.find_entry = ttk.Entry(main_frame, textvariable=self.find_entry_var); self.find_entry.grid(column=1, row=0, columnspan=2, padx=(0, 10), pady=(pady, 0)); if FindDialog.last_searched_word is not None: self.find_entry.insert(0, FindDialog.last_searched_word) #Replace text label self.replace_label = ttk.Label(main_frame, text="Replace with:"); self.replace_label.grid(column=0, row=1, sticky="w", padx=(padx, 0)); #Replace text field self.replace_entry = ttk.Entry(main_frame); self.replace_entry.grid(column=1, row=1, columnspan=2, padx=(0, 10)); #Info text label (invisible by default, used to tell user that searched string was not found etc) self.infotext_label_var = tk.StringVar(); self.infotext_label_var.set(""); self.infotext_label = ttk.Label(main_frame, textvariable=self.infotext_label_var, foreground="red"); #TODO - style to conf self.infotext_label.grid(column=0, row=2, columnspan=3, pady=3, padx=(padx, 0)); #Case checkbox self.case_var = tk.IntVar() self.case_checkbutton = ttk.Checkbutton(main_frame,text="Case sensitive",variable=self.case_var) self.case_checkbutton.grid(column=0, row=3, padx=(padx, 0), pady=(0, pady)) #Direction radiobuttons self.direction_var = tk.IntVar() self.up_radiobutton = ttk.Radiobutton(main_frame, text="Up", variable=self.direction_var, value=1) self.up_radiobutton.grid(column=1, row=3, pady=(0, pady)) self.down_radiobutton = ttk.Radiobutton(main_frame, text="Down", variable=self.direction_var, value=2) self.down_radiobutton.grid(column=2, row=3, pady=(0, pady)) self.down_radiobutton.invoke() #Find button - goes to the next occurrence self.find_button = ttk.Button(main_frame, text="Find", command=self._perform_find, default="active") self.find_button.grid(column=3, row=0, sticky=tk.W + tk.E, pady=(pady, 0), padx=(0, padx)); self.find_button.config(state='disabled') #Replace button - replaces the current occurrence, if it exists self.replace_button = ttk.Button(main_frame, text="Replace", command=self._perform_replace) self.replace_button.grid(column=3, row=1, sticky=tk.W + tk.E, padx=(0, padx)); self.replace_button.config(state='disabled') #Replace + find button - replaces the current occurence and goes to next self.replace_and_find_button = ttk.Button(main_frame, text="Replace+Find", command=self._perform_replace_and_find) #TODO - text to resources self.replace_and_find_button.grid(column=3, row=2, sticky=tk.W + tk.E, padx=(0, padx)); self.replace_and_find_button.config(state='disabled') #Replace all button - replaces all occurrences self.replace_all_button = ttk.Button(main_frame, text="Replace all", command=self._perform_replace_all) #TODO - text to resources self.replace_all_button.grid(column=3, row=3, sticky=tk.W + tk.E, padx=(0, padx), pady=(0, pady)); if FindDialog.last_searched_word == None: self.replace_all_button.config(state='disabled') #create bindings self.bind('', self._ok) self.find_entry_var.trace('w', self._update_button_statuses) self.find_entry.bind("", self._perform_find, True) self.bind("", self._perform_find, True) self.find_entry.bind("", self._perform_find, True) self._update_button_statuses() global _active_find_dialog _active_find_dialog = self self.focus_set(); def focus_set(self): self.find_entry.focus_set() self.find_entry.selection_range(0, tk.END) #callback for text modifications on the find entry object, used to dynamically enable and disable buttons def _update_button_statuses(self, *args): find_text = self.find_entry_var.get() if len(find_text) == 0: self.find_button.config(state='disabled') self.replace_and_find_button.config(state='disabled') self.replace_all_button.config(state='disabled') else: self.find_button.config(state='normal') self.replace_all_button.config(state='normal') if self.active_found_tag is not None: self.replace_and_find_button.config(state='normal') #returns whether the next search is case sensitive based on the current value of the case sensitivity checkbox def _is_search_case_sensitive(self): return self.case_var.get() != 0 #returns whether the current search is a repeat of the last searched based on all significant values def _repeats_last_search(self, tofind): return tofind == FindDialog.last_searched_word and self.last_processed_indexes is not None and self.last_search_case == self._is_search_case_sensitive(); #performs the replace operation - replaces the currently active found word with what is entered in the replace field def _perform_replace(self): #nothing is currently in found status if self.active_found_tag == None: return #get the found word bounds del_start = self.active_found_tag[0] del_end = self.active_found_tag[1] #erase all tags - these would not be correct anyway after new word is inserted self._remove_all_tags() toreplace = self.replace_entry.get(); #get the text to replace #delete the found word self.codeview.text.delete(del_start, del_end) #insert the new word self.codeview.text.insert(del_start, toreplace) #mark the inserted word boundaries self.last_processed_indexes = (del_start, self.codeview.text.index("%s+%dc" % (del_start, len(toreplace)))) get_workbench().event_generate("Replace", widget=self.codeview.text, old_text=self.codeview.text.get(del_start, del_end), new_text=toreplace) #performs the replace operation followed by a new find def _perform_replace_and_find(self): if self.active_found_tag == None: return self._perform_replace() self._perform_find() #replaces all occurences of the search string with the replace string def _perform_replace_all(self): tofind = self.find_entry.get(); if len(tofind) == 0: self.infotext_label_var.set("Enter string to be replaced.") return toreplace = self.replace_entry.get(); self._remove_all_tags() currentpos = 1.0; end = self.codeview.text.index("end"); while True: currentpos = self.codeview.text.search(tofind, currentpos, end, nocase = not self._is_search_case_sensitive()); if currentpos == "": break endpos = self.codeview.text.index("%s+%dc" % (currentpos, len(tofind))) self.codeview.text.delete(currentpos, endpos) if toreplace != "": self.codeview.text.insert(currentpos, toreplace) currentpos = self.codeview.text.index("%s+%dc" % (currentpos, len(toreplace))) get_workbench().event_generate("ReplaceAll", widget=self.codeview.text, old_text=tofind, new_text=toreplace) def _perform_find(self, event=None): self.infotext_label_var.set(""); #reset the info label text tofind = self.find_entry.get(); #get the text to find if len(tofind) == 0: #in the case of empty string, cancel return #TODO - set warning text to info label? search_backwards = self.direction_var.get() == 1 #True - search backwards ('up'), False - forwards ('down') if self._repeats_last_search(tofind): #continuing previous search, find the next occurrence if search_backwards: search_start_index = self.last_processed_indexes[0]; else: search_start_index = self.last_processed_indexes[1]; if self.active_found_tag is not None: self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]); #remove the active tag from the previously found string self.passive_found_tags.add((self.active_found_tag[0], self.active_found_tag[1])) #..and set it to passive instead self.codeview.text.tag_add("found", self.active_found_tag[0], self.active_found_tag[1]); else: #start a new search, start from the current insert line position if self.active_found_tag is not None: self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]); #remove the previous active tag if it was present for tag in self.passive_found_tags: self.codeview.text.tag_remove("found", tag[0], tag[1]); #and remove all the previous passive tags that were present search_start_index = self.codeview.text.index("insert"); #start searching from the current insert position self._find_and_tag_all(tofind); #set the passive tag to ALL found occurences FindDialog.last_searched_word = tofind; #set the data about last search self.last_search_case = self._is_search_case_sensitive(); wordstart = self.codeview.text.search(tofind, search_start_index, backwards = search_backwards, forwards = not search_backwards, nocase = not self._is_search_case_sensitive()); #performs the search and sets the start index of the found string if len(wordstart) == 0: self.infotext_label_var.set("The specified text was not found!"); #TODO - better text, also move it to the texts resources list self.replace_and_find_button.config(state='disabled') self.replace_button.config(state='disabled') return self.last_processed_indexes = (wordstart, self.codeview.text.index("%s+1c" % wordstart)); #sets the data about last search self.codeview.text.see(wordstart); #moves the view to the found index wordend = self.codeview.text.index("%s+%dc" % (wordstart, len(tofind))); #calculates the end index of the found string self.codeview.text.tag_add("currentfound", wordstart, wordend); #tags the found word as active self.active_found_tag = (wordstart, wordend); self.replace_and_find_button.config(state='normal') self.replace_button.config(state='normal') get_workbench().event_generate("Find", widget=self.codeview.text, text=tofind, backwards=search_backwards, case_sensitive=self._is_search_case_sensitive()) def _ok(self, event=None): """Called when the window is closed. responsible for handling all cleanup.""" self._remove_all_tags() self.destroy() global _active_find_dialog _active_find_dialog = None #removes the active tag and all passive tags def _remove_all_tags(self): for tag in self.passive_found_tags: self.codeview.text.tag_remove("found", tag[0], tag[1]); #removes the passive tags if self.active_found_tag is not None: self.codeview.text.tag_remove("currentfound", self.active_found_tag[0], self.active_found_tag[1]); #removes the currently active tag self.active_found_tag = None self.replace_and_find_button.config(state='disabled') self.replace_button.config(state='disabled') #finds and tags all occurences of the searched term def _find_and_tag_all(self, tofind, force=False): #TODO - to be improved so only whole words are matched - surrounded by whitespace, parentheses, brackets, colons, semicolons, points, plus, minus if self._repeats_last_search(tofind) and not force: #nothing to do, all passive tags already set return currentpos = 1.0; end = self.codeview.text.index("end"); #searches and tags until the end of codeview while True: currentpos = self.codeview.text.search(tofind, currentpos, end, nocase = not self._is_search_case_sensitive()); if currentpos == "": break endpos = self.codeview.text.index("%s+%dc" % (currentpos, len(tofind))) self.passive_found_tags.add((currentpos, endpos)) self.codeview.text.tag_add("found", currentpos, endpos); currentpos = self.codeview.text.index("%s+1c" % currentpos); #initializes the tagging styles def _init_found_tag_styles(self): self.codeview.text.tag_configure("found", foreground="blue", underline=True) #TODO - style self.codeview.text.tag_configure("currentfound", foreground="white", background="red") #TODO - style def load_plugin(): def cmd_open_find_dialog(): if _active_find_dialog is not None: _active_find_dialog.focus_set() else: editor = get_workbench().get_editor_notebook().get_current_editor() if editor: FindDialog(editor._code_view) def find_f3(event): if _active_find_dialog is None: cmd_open_find_dialog() else: _active_find_dialog._perform_find(event) get_workbench().add_command("OpenFindDialog", "edit", 'Find & Replace', cmd_open_find_dialog, default_sequence=select_sequence("", "")) get_workbench().bind("", find_f3, True) thonny-2.1.16/thonny/plugins/font_config_page.py0000666000000000000000000000661013172664305020125 0ustar 00000000000000import tkinter as tk from tkinter import font as tk_font from tkinter import ttk from thonny.config_ui import ConfigurationPage from thonny.globals import get_workbench from thonny.ui_utils import create_string_var import textwrap class FontConfigurationPage(ConfigurationPage): def __init__(self, master): ConfigurationPage.__init__(self, master) self._family_variable = create_string_var( get_workbench().get_option("view.editor_font_family"), modification_listener=self._update_preview_font) self._size_variable = create_string_var( get_workbench().get_option("view.editor_font_size"), modification_listener=self._update_preview_font) ttk.Label(self, text="Editor font").grid(row=0, column=0, sticky="w") self._family_combo = ttk.Combobox(self, exportselection=False, state='readonly', textvariable=self._family_variable, values=self._get_families_to_show()) self._family_combo.grid(row=1, column=0, sticky=tk.NSEW, padx=(0,10)) ttk.Label(self, text="Size").grid(row=0, column=1, sticky="w") self._size_combo = ttk.Combobox(self, width=4, exportselection=False, textvariable=self._size_variable, state='readonly', values=[str(x) for x in range(3,73)]) self._size_combo.grid(row=1, column=1) ttk.Label(self, text="Preview").grid(row=2, column=0, sticky="w", pady=(10,0)) self._preview_font = tk_font.Font() self._preview_text = tk.Text(self, height=10, borderwidth=1, font=self._preview_font, wrap=tk.WORD) self._preview_text.insert("1.0", textwrap.dedent(""" The quick brown fox jumps over the lazy dog ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 1234567890 @$%()[]{}/\_-+ "Hello " + 'world!'""").strip()) self._preview_text.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW, pady=(0,5)) self.columnconfigure(0, weight=1) self.rowconfigure(3, weight=1) self._update_preview_font() def apply(self): if (not self._family_variable.modified and not self._size_variable.modified): return get_workbench().set_option("view.editor_font_size", int(self._size_variable.get())) get_workbench().set_option("view.editor_font_family", self._family_variable.get()) get_workbench().update_fonts() def _update_preview_font(self): self._preview_font.configure(family=self._family_variable.get(), size=int(self._size_variable.get())) def _get_families_to_show(self): return sorted(filter( lambda name : name[0].isalpha(), tk_font.families() )) def load_plugin(): get_workbench().add_configuration_page("Font", FontConfigurationPage)thonny-2.1.16/thonny/plugins/general_config_page.py0000666000000000000000000000312113174702145020563 0ustar 00000000000000import tkinter as tk from tkinter import ttk from thonny.config_ui import ConfigurationPage from thonny.globals import get_workbench class GeneralConfigurationPage(ConfigurationPage): def __init__(self, master): ConfigurationPage.__init__(self, master) self._single_instance_var = get_workbench().get_variable("general.single_instance") self._single_instance_checkbox = ttk.Checkbutton(self, text="Allow only single Thonny instance", variable=self._single_instance_var) self._single_instance_checkbox.grid(row=1, column=0, sticky=tk.W) self._expert_var = get_workbench().get_variable("general.expert_mode") self._expert_checkbox = ttk.Checkbutton(self, text="Expert mode", variable=self._expert_var) self._expert_checkbox.grid(row=2, column=0, sticky=tk.W) self._debug_var = get_workbench().get_variable("general.debug_mode") self._debug_checkbox = ttk.Checkbutton(self, text="Debug mode", variable=self._debug_var) self._debug_checkbox.grid(row=3, column=0, sticky=tk.W) reopen_label = ttk.Label(self, text="NB! Restart Thonny after changing these options" + "\nin order to see the effect") reopen_label.grid(row=4, column=0, sticky=tk.W, pady=20) self.columnconfigure(0, weight=1) def load_plugin(): get_workbench().add_configuration_page("General", GeneralConfigurationPage) thonny-2.1.16/thonny/plugins/goto_definition.py0000666000000000000000000000220213172664305020007 0ustar 00000000000000import tkinter as tk from jedi import Script from thonny.globals import get_workbench, get_runner from thonny.ui_utils import control_is_pressed def goto_definition(event): if not control_is_pressed(event.state): return assert isinstance(event.widget, tk.Text) text = event.widget source = text.get("1.0", "end") index = text.index("insert") index_parts = index.split('.') line, column = int(index_parts[0]), int(index_parts[1]) # TODO: find current editor filename script = Script(source, line=line, column=column, path="") defs = script.goto_definitions() if len(defs) > 0: module_path = defs[0].module_path module_name = defs[0].module_name line = defs[0].line if module_path and line is not None: get_workbench().get_editor_notebook().show_file(module_path, line) elif module_name == "" and line is not None: # current editor get_workbench().get_editor_notebook().get_current_editor().select_range(line) def load_plugin(): wb = get_workbench() wb.bind_class("CodeViewText", "<1>", goto_definition, True) thonny-2.1.16/thonny/plugins/heap.py0000666000000000000000000000431113172664305015547 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk from thonny.memory import MemoryFrame, format_object_id, MAX_REPR_LENGTH_IN_GRID,\ parse_object_id from thonny.misc_utils import shorten_repr from thonny.globals import get_workbench, get_runner from thonny.common import InlineCommand class HeapView(MemoryFrame): def __init__(self, master): MemoryFrame.__init__(self, master, ("id", "value")) self.tree.column('id', width=100, anchor=tk.W, stretch=False) self.tree.column('value', width=150, anchor=tk.W, stretch=True) self.tree.heading('id', text='ID', anchor=tk.W) self.tree.heading('value', text='Value', anchor=tk.W) get_workbench().bind("Heap", self._handle_heap_event, True) get_workbench().bind("DebuggerProgress", self._request_heap_data, True) get_workbench().bind("ToplevelResult", self._request_heap_data, True) # Showing new globals may introduce new interesting objects get_workbench().bind("Globals", self._request_heap_data, True) def _update_data(self, data): self._clear_tree() for value_id in sorted(data.keys()): node_id = self.tree.insert("", "end") self.tree.set(node_id, "id", format_object_id(value_id)) self.tree.set(node_id, "value", shorten_repr(data[value_id]["repr"], MAX_REPR_LENGTH_IN_GRID)) def before_show(self): self._request_heap_data(even_when_hidden=True) def on_select(self, event): iid = self.tree.focus() if iid != '': object_id = parse_object_id(self.tree.item(iid)['values'][0]) get_workbench().event_generate("ObjectSelect", object_id=object_id) def _request_heap_data(self, msg=None, even_when_hidden=False): if self.winfo_ismapped() or even_when_hidden: # TODO: update itself also when it becomes visible get_runner().send_command(InlineCommand("get_heap")) def _handle_heap_event(self, msg): if self.winfo_ismapped(): if hasattr(msg, "heap"): self._update_data(msg.heap) def load_plugin(): get_workbench().add_view(HeapView, "Heap", "e")thonny-2.1.16/thonny/plugins/help/0000777000000000000000000000000013201324660015177 5ustar 00000000000000thonny-2.1.16/thonny/plugins/help/help.rst0000666000000000000000000001015713172664305016677 0ustar 00000000000000=========== Thonny help =========== Running programs step-wise ========================== If you want to see how Python executes your program step-by-step then you should run it in *debug-mode*. Start by selecting *Debug current script* from the *Run* menu or by pressing Ctrl+F5. You'll see that first statement of the program gets highlighted and nothing more happens. In this mode you need to notify Thonny that you're ready to let Python make the next step. For this you have two main options: * *Run → Step over* (or F6) makes big steps, ie. it executes the highlighted code and highlights the next part of the code. * *Run → Step into* (or F7) tries to make smaller steps. If the highlighted code is made of smaller parts (statements or expressions), then first of these gets highlighted and Thonny waits for next command. If you have reached to a program component which doesn't have any sub-parts (eg. variable name) then *Step into* works like *Step over*, ie. executes (or evaluates) the code. If you have stepped into the depths of a statement or expression and want to move on faster, then you can use *Run → Step out* (or F8), which executes currently highlighted code and all following program parts on the same level. If you want to reach a specific part of the code, then you can speed up the process by placing your cursor on that line and selecting *Run → Run to cursor*. This makes Thonny automatically step until this line. You can take the command from there. Installing 3rd party packages ============================== Thonny has two options for installing 3rd party libraries. With pip-GUI ------------- From "Tools" menu select "Manage packages..." and follow the instructions. .. image:: https://bitbucket.org/repo/gXnbod/images/2226680569-pipgui_big.png :alt: pipgui_big.png With pip on command line ------------------------ #. From "Tools" menu select "Open system shell...". You should get a new terminal window stating the correct name of *pip* command (usually ``pip`` or ``pip3``). In the following I've assumed the command name is ``pip``. #. Enter ``pip install `` (eg. ``pip install pygame``) and press ENTER. You should see *pip* downloading and installing the package and printing a success message. #. Close the terminal (optional) #. Return to Thonny #. Reset interpreter by selecting "Stop/Reset" from "Run menu" (this is required only first time you do pip install) #. Start using the package .. image:: https://bitbucket.org/repo/gXnbod/images/1183520217-pipinstall_cmdline.png :alt: pipinstall_cmdline.png Using scientific Python packages ================================ Python distribution coming with Thonny doesn't contain scientific programming libraries (eg. `NumPy `_ and `Matplotlib `_). Recent versions of most popular scientific Python packages (eg. numpy, pandas and matplotlib) have wheels available for popular platforms so you can most likely install them with pip but in case you have troubles, you could try using Thonny with separate Python distribution meant for scientific computing (eg. `Anaconda `_, `Canopy `_ or `Pyzo `_). Example: Using Anaconda ------------------------------------ Go to https://www.continuum.io/downloads and download a suitable binary distribution for your platform. Most likely you want graphical installer and 64-bit version (you may need 32-bit version if you have very old system). **Note that Thonny supports only on Python 3, so make sure you choose Python 3 version of Anaconda.** Install it and find out where it puts Python executable (*pythonw.exe* in Windows and *python3* or *python* in Linux and Mac). For example in Windows the full path is by default ``c:\anaconda\pythonw.exe``. In Thonny open "Tools" menu and select "Options...". In the options dialog open "Intepreter" tab, click "Select executable" and show the location of Anaconda's Python executable. After you have done this, next time you run your program, it will be run through Anaconda's Python and all the libraries installed there are available.thonny-2.1.16/thonny/plugins/help/__init__.py0000666000000000000000000000663613172664305017335 0ustar 00000000000000import tkinter as tk import tkinter.font import os.path from tkinter import ttk from thonny import tktextext from thonny.globals import get_workbench class HelpView(ttk.Frame): def __init__(self, master): ttk.Frame.__init__(self, master) main_font = tkinter.font.nametofont("TkDefaultFont") bold_font = main_font.copy() bold_font.configure(weight="bold", size=main_font.cget("size")) h1_font = main_font.copy() h1_font.configure(size=main_font.cget("size") * 2, weight="bold") h2_font = main_font.copy() h2_font.configure(size=round(main_font.cget("size") * 1.5), weight="bold") h3_font = main_font.copy() h3_font.configure(size=main_font.cget("size"), weight="bold") self.text = tktextext.TweakableText(self, border=0, padx=15, pady=15, font=main_font, wrap="word") self.text.tag_configure("h1", font=h1_font) self.text.tag_configure("h2", font=h2_font) self.text.tag_configure("h3", font=h3_font) self.text.grid(row=0, column=0, sticky="nsew") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._vbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self._vbar.grid(row=0, column=1, sticky=tk.NSEW) self._vbar['command'] = self.text.yview self.text['yscrollcommand'] = self._vbar.set self.load_rst_file("help.rst") def clear(self): self.text.direct_delete("1.0", "end") def load_rst(self, source): def is_symbol_line(line, symbol, min_count=3): line = line.rstrip() return (line.startswith(symbol) and line.replace(symbol, "") == "" and len(line) >= min_count) self.clear() lines = source.splitlines(True) i = 0 while i < len(lines): if is_symbol_line(lines[i], "="): self.append_chars(lines[i+1], "h1") assert is_symbol_line(lines[i+2], "=") i += 3 elif (i < len(lines)-1 and is_symbol_line(lines[i+1], "=")): self.append_chars(lines[i], "h2") i += 2 elif (i < len(lines)-1 and is_symbol_line(lines[i+1], "-")): self.append_chars(lines[i], "h3") i += 2 else: self.append_rst_line(lines[i]) i += 1 def append_chars(self, chars, tag=None): if tag: self.text.direct_insert("end", chars, (tag,)) else: self.text.direct_insert("end", chars) def append_rst_line(self, source): self.append_chars(source) def load_rst_file(self, filename): if not os.path.isabs(filename): filename = os.path.join(os.path.dirname(__file__), filename) with open(filename, encoding="UTF-8") as fp: self.load_rst(fp.read()) def open_help(): get_workbench().show_view("HelpView") def load_plugin(): get_workbench().add_view(HelpView, "Help", "ne") get_workbench().add_command("help_contents", "help", "Help contents", open_help, group=30) thonny-2.1.16/thonny/plugins/highlight_names.py0000666000000000000000000002763113201264465017772 0ustar 00000000000000from jedi import Script import thonny.jedi_utils as jedi_utils import traceback tree = jedi_utils.import_tree() from thonny.globals import get_workbench import tkinter as tk import logging NAME_CONF = {'background' : '#e6ecfe'} class BaseNameHighlighter: def __init__(self, text): self.text = text self.text.tag_configure("NAME", NAME_CONF) self.text.tag_raise("sel") self._update_scheduled = False def get_positions_for_script(self, script): raise NotImplementedError(); def get_positions(self): index = self.text.index("insert") # ignore if cursor in STRING_OPEN if self.text.tag_prevrange("STRING_OPEN", index): return set() source = self.text.get("1.0", "end") index_parts = index.split('.') line, column = int(index_parts[0]), int(index_parts[1]) script = Script(source + ")", line=line, column=column, path="") # https://github.com/davidhalter/jedi/issues/897 return self.get_positions_for_script(script) def schedule_update(self): def perform_update(): try: self.update() finally: self._update_scheduled = False if not self._update_scheduled: self._update_scheduled = True self.text.after_idle(perform_update) def update(self): self.text.tag_remove("NAME", "1.0", "end") if get_workbench().get_option("view.name_highlighting"): try: for pos in self.get_positions(): start_index, end_index = pos[0], pos[1] self.text.tag_add("NAME", start_index, end_index) except: logging.exception("Problem when updating name highlighting") class VariablesHighlighter(BaseNameHighlighter): """This is heavy, but more correct solution for variables, than Script.usages provides (at least for Jedi 0.10)""" def _is_name_function_call_name(self, name): stmt = name.get_definition() return stmt.type == "power" and stmt.children[0] == name def _is_name_function_definition(self, name): scope = name.get_definition() return (isinstance(scope, tree.Function) and hasattr(scope.children[1], "value") and scope.children[1].value == name.value) def _get_def_from_function_params(self, func_node, name): params = jedi_utils.get_params(func_node) for param in params: if param.children[0].value == name.value: return param.children[0] return None # copied from jedi's tree.py with a few modifications def _get_statement_for_position(self, node, pos): for c in node.children: # sorted here, because the end_pos property depends on the last child having the last position, # there seems to be a problem with jedi, where the children of a node are not always in the right order if isinstance(c, tree.Class): c.children.sort(key=lambda x: x.end_pos) if c.start_pos <= pos <= c.end_pos: if c.type not in ('decorated', 'simple_stmt', 'suite') \ and not isinstance(c, (tree.Flow, tree.ClassOrFunc)): return c else: try: return jedi_utils.get_statement_of_position(c, pos) except AttributeError: traceback.print_exc() return None def _is_global_stmt_with_name(self, node, name_str): return isinstance(node, tree.BaseNode) and node.type == "simple_stmt" and \ isinstance(node.children[0], tree.GlobalStmt) and \ node.children[0].children[1].value == name_str def _find_definition(self, scope, name): # if the name is the name of a function definition if isinstance(scope, tree.Function): if scope.children[1] == name: return scope.children[1] # 0th child is keyword "def", 1st is name else: definition = self._get_def_from_function_params(scope, name) if definition: return definition for c in scope.children: if isinstance(c, tree.BaseNode) and c.type == "simple_stmt" and isinstance(c.children[0], tree.ImportName): for n in c.children[0].get_defined_names(): if n.value == name.value: return n # print(c.path_for_name(name.value)) if isinstance(c, tree.Function) and c.children[1].value == name.value and \ not isinstance(jedi_utils.get_parent_scope(c), tree.Class): return c.children[1] if isinstance(c, tree.BaseNode) and c.type == "suite": for x in c.children: if self._is_global_stmt_with_name(x, name.value): return self._find_definition(jedi_utils.get_parent_scope(scope), name) if isinstance(x, tree.Name) and x.is_definition() and x.value == name.value: return x def_candidate = self._find_def_in_simple_node(x, name) if def_candidate: return def_candidate if not isinstance(scope, tree.Module): return self._find_definition(jedi_utils.get_parent_scope(scope), name) # if name itself is the left side of an assignment statement, then the name is the definition if name.is_definition(): return name return None def _find_def_in_simple_node(self, node, name): if isinstance(node, tree.Name) and node.is_definition() and node.value == name.value: return name if not isinstance(node, tree.BaseNode): return None for c in node.children: return self._find_def_in_simple_node(c, name) def _get_dot_names(self, stmt): try: if stmt.children[1].children[0].value == ".": return stmt.children[0], stmt.children[1].children[1] except: return () return () def _find_usages(self, name, stmt, module): # check if stmt is dot qualified, disregard these dot_names = self._get_dot_names(stmt) if len(dot_names) > 1 and dot_names[1].value == name.value: return set() # search for definition definition = self._find_definition(jedi_utils.get_parent_scope(name), name) searched_scopes = set() is_function_definition = self._is_name_function_definition(definition) if definition else False def find_usages_in_node(node, global_encountered=False): names = [] if isinstance(node, tree.BaseNode): if jedi_utils.is_scope(node): global_encountered = False if node in searched_scopes: return names searched_scopes.add(node) if isinstance(node, tree.Function): d = self._get_def_from_function_params(node, name) if d and d != definition: return [] for c in node.children: dot_names = self._get_dot_names(c) if len(dot_names) > 1 and dot_names[1].value == name.value: continue sub_result = find_usages_in_node(c, global_encountered=global_encountered) if sub_result is None: if not jedi_utils.is_scope(node): return None if definition and node != jedi_utils.get_parent_scope(definition) else [definition] else: sub_result = [] names.extend(sub_result) if self._is_global_stmt_with_name(c, name.value): global_encountered = True elif isinstance(node, tree.Name) and node.value == name.value: if definition and definition != node: if self._is_name_function_definition(node): if isinstance(jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(node)), tree.Class): return [] else: return None if node.is_definition() and not global_encountered and \ (is_function_definition or jedi_utils.get_parent_scope(node) != jedi_utils.get_parent_scope(definition)): return None if self._is_name_function_definition(definition) and \ isinstance(jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(definition)), tree.Class): return None names.append(node) return names if definition: if self._is_name_function_definition(definition): scope = jedi_utils.get_parent_scope(jedi_utils.get_parent_scope(definition)) else: scope = jedi_utils.get_parent_scope(definition) else: scope = jedi_utils.get_parent_scope(name) usages = find_usages_in_node(scope) return usages def get_positions_for_script(self, script): name = None module_node = jedi_utils.get_module_node(script) stmt = self._get_statement_for_position(module_node, script._pos) if isinstance(stmt, tree.Name): name = stmt elif isinstance(stmt, tree.BaseNode): name = jedi_utils.get_name_of_position(stmt, script._pos) if not name: return set() # format usage positions as tkinter text widget indices return set(("%d.%d" % (usage.start_pos[0], usage.start_pos[1]), "%d.%d" % (usage.start_pos[0], usage.start_pos[1] + len(name.value))) for usage in self._find_usages(name, stmt, module_node)) class UsagesHighlighter(BaseNameHighlighter): """Script.usages looks tempting method to use for finding variable ocurrences, but it only returns last assignments to a variable, not really all usages (with Jedi 0.10). But it finds attribute usages quite nicely. TODO: check if this gets fixed in later versions of Jedi""" def get_positions_for_script(self, script): usages = script.usages() result = {("%d.%d" % (usage.line, usage.column), "%d.%d" % (usage.line, usage.column + len(usage.name))) for usage in usages if usage.module_name == ""} return result class CombinedHighlighter(VariablesHighlighter, UsagesHighlighter): def get_positions_for_script(self, script): usages = UsagesHighlighter.get_positions_for_script(self, script) variables = VariablesHighlighter.get_positions_for_script(self, script) return usages | variables def update_highlighting(event): assert isinstance(event.widget, tk.Text) text = event.widget if not hasattr(text, "name_highlighter"): text.name_highlighter = VariablesHighlighter(text) # Alternatives: # NB! usages() is too slow when used on library names #text.name_highlighter = CombinedHighlighter(text) #text.name_highlighter = UsagesHighlighter(text) text.name_highlighter.schedule_update() def load_plugin(): if jedi_utils.get_version_tuple() < (0, 9): logging.warning("Jedi version is too old. Disabling name highlighter") return wb = get_workbench() # type:Workbench wb.set_default("view.name_highlighting", False) wb.bind_class("CodeViewText", "<>", update_highlighting, True) wb.bind_class("CodeViewText", "<>", update_highlighting, True) wb.bind("<>", update_highlighting, True) thonny-2.1.16/thonny/plugins/interpreter_config_page.py0000666000000000000000000000641113172664305021521 0ustar 00000000000000import tkinter as tk import os.path from tkinter import filedialog from tkinter import ttk from thonny.config_ui import ConfigurationPage from thonny.globals import get_workbench, get_runner from thonny.ui_utils import create_string_var from thonny.misc_utils import running_on_windows from thonny.running import parse_configuration class InterpreterConfigurationPage(ConfigurationPage): def __init__(self, master): ConfigurationPage.__init__(self, master) self._configuration_variable = create_string_var( get_workbench().get_option("run.backend_configuration")) entry_label = ttk.Label(self, text="Which interpreter to use for running programs?") entry_label.grid(row=0, column=0, columnspan=2, sticky=tk.W) self._entry = ttk.Combobox(self, exportselection=False, textvariable=self._configuration_variable, values=self._get_configurations()) self._entry.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW) self._entry.state(['!disabled', 'readonly']) another_label = ttk.Label(self, text="Your interpreter isn't in the list?") another_label.grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(10,0)) self._select_button = ttk.Button(self, text="Locate another executable " + ("(python.exe) ..." if running_on_windows() else "(python3) ...") + "\nNB! Thonny only supports Python 3.4 and later", command=self._select_executable) self._select_button.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) def _get_configurations(self): result = [] backends = get_workbench().get_backends() for backend_name in sorted(backends.keys()): backend_class = backends[backend_name] for configuration_option in backend_class.get_configuration_options(): if configuration_option is None or configuration_option == "": result.append(backend_name) else: result.append("%s (%s)" % (backend_name, configuration_option)) return result def _select_executable(self): if running_on_windows(): options = {"filetypes" : [('Exe-files', '.exe'), ('all files', '.*')]} else: options = {} # TODO: get dir of current interpreter filename = filedialog.askopenfilename(**options) if filename: self._configuration_variable.set("Python (%s)" % os.path.realpath(filename)) def apply(self): if not self._configuration_variable.modified: return configuration = self._configuration_variable.get() get_workbench().set_option("run.backend_configuration", configuration) get_runner().reset_backend() def load_plugin(): get_workbench().add_configuration_page("Interpreter", InterpreterConfigurationPage) thonny-2.1.16/thonny/plugins/locals_marker.py0000666000000000000000000001301213201300104017417 0ustar 00000000000000import tkinter as tk from thonny.globals import get_workbench import logging import thonny.jedi_utils as jedi_utils class LocalsHighlighter: def __init__(self, text, local_variable_font=None): self.text = text if local_variable_font: self.local_variable_font=local_variable_font else: self.local_variable_font = self.text["font"] self._configure_tags() self._update_scheduled = False def get_positions(self): return self._get_positions_correct_but_using_private_parts() def _get_positions_simple_but_incorrect(self): # goto_assignments only gives you last assignment to given node import jedi defs = jedi.names(self.text.get('1.0', 'end'), path="", all_scopes=True, definitions=True, references=True) result = set() for definition in defs: if definition.parent().type == "function": # is located in a function ass = definition.goto_assignments() if len(ass) > 0 and ass[0].parent().type == "function": # is assigned to in a function pos = ("%d.%d" % (definition.line, definition.column), "%d.%d" % (definition.line, definition.column+len(definition.name))) result.add(pos) return result def _get_positions_correct_but_using_private_parts(self): from jedi import Script tree = jedi_utils.import_tree() locs = [] def process_scope(scope): if isinstance(scope, tree.Function): # process all children after name node, # (otherwise name of global function will be marked as local def) local_names = set() global_names = set() for child in scope.children[2:]: process_node(child, local_names, global_names) else: for child in scope.subscopes: process_scope(child) def process_node(node, local_names, global_names): if isinstance(node, tree.GlobalStmt): global_names.update([n.value for n in node.get_global_names()]) elif isinstance(node, tree.Name): if node.value in global_names: return if node.is_definition(): # local def locs.append(node) local_names.add(node.value) elif node.value in local_names: # use of local locs.append(node) elif isinstance(node, tree.BaseNode): # ref: jedi/parser/grammar*.txt if node.type == "trailer" and node.children[0].value == ".": # this is attribute return if isinstance(node, tree.Function): global_names = set() # outer global statement doesn't have effect anymore for child in node.children: process_node(child, local_names, global_names) source = self.text.get('1.0', 'end') script = Script(source + ")") # https://github.com/davidhalter/jedi/issues/897 module = jedi_utils.get_module_node(script) for child in module.children: if isinstance(child, tree.BaseNode) and jedi_utils.is_scope(child): process_scope(child) loc_pos = set(("%d.%d" % (usage.start_pos[0], usage.start_pos[1]), "%d.%d" % (usage.start_pos[0], usage.start_pos[1] + len(usage.value))) for usage in locs) return loc_pos def _configure_tags(self): self.text.tag_configure("LOCAL_NAME", font=self.local_variable_font, foreground="#000055") self.text.tag_raise("sel") def _highlight(self, pos_info): for pos in pos_info: start_index, end_index = pos[0], pos[1] self.text.tag_add("LOCAL_NAME", start_index, end_index) def schedule_update(self): def perform_update(): try: self.update() finally: self._update_scheduled = False if not self._update_scheduled: self._update_scheduled = True self.text.after_idle(perform_update) def update(self): self.text.tag_remove("LOCAL_NAME", "1.0", "end") if get_workbench().get_option("view.locals_highlighting"): try: highlight_positions = self.get_positions() self._highlight(highlight_positions) except: logging.exception("Problem when updating local variable tags") def update_highlighting(event): assert isinstance(event.widget, tk.Text) text = event.widget if not hasattr(text, "local_highlighter"): text.local_highlighter = LocalsHighlighter(text, get_workbench().get_font("ItalicEditorFont")) text.local_highlighter.schedule_update() def load_plugin(): if jedi_utils.get_version_tuple() < (0, 9): logging.warning("Jedi version is too old. Disabling locals marker") return wb = get_workbench() wb.set_default("view.locals_highlighting", False) wb.bind_class("CodeViewText", "<>", update_highlighting, True) wb.bind("<>", update_highlighting, True) thonny-2.1.16/thonny/plugins/main_file_browser.py0000666000000000000000000000621713172664305020327 0ustar 00000000000000# -*- coding: utf-8 -*- import os import tkinter as tk from tkinter.messagebox import showerror #from thonny.ui_utils import askstring TODO: doesn't work from tkinter.simpledialog import askstring from thonny import misc_utils from thonny.globals import get_workbench from thonny.base_file_browser import BaseFileBrowser class MainFileBrowser(BaseFileBrowser): def __init__(self, master, show_hidden_files=False): BaseFileBrowser.__init__(self, master, show_hidden_files, "file.last_browser_folder") self.menu = tk.Menu(tk._default_root, tearoff=False) self.menu.add_command(label="Create new file", command=self.create_new_file) self.tree.bind('<3>', self.on_secondary_click, True) if misc_utils.running_on_mac_os(): self.tree.bind('<2>', self.on_secondary_click, True) self.tree.bind('', self.on_secondary_click, True) def create_new_file(self): selected_path = self.get_selected_path() if not selected_path: return if os.path.isdir(selected_path): parent_path = selected_path else: parent_path = os.path.dirname(selected_path) initial_name = self.get_proposed_new_file_name(parent_path, ".py") name = askstring("File name", "Provide filename", initialvalue=initial_name, #selection_range=(0, len(initial_name)-3) ) if not name: return path = os.path.join(parent_path, name) if os.path.exists(path): showerror("Error", "The file '"+path+"' already exists") else: open(path, 'w').close() self.open_path_in_browser(path, True) get_workbench().get_editor_notebook().show_file(path) def get_proposed_new_file_name(self, folder, extension): base = "new_file" if os.path.exists(os.path.join(folder, base + extension)): i = 2 while True: name = base + "_" + str(i) + extension path = os.path.join(folder, name) if os.path.exists(path): i += 1 else: return name else: return base + extension def on_secondary_click(self, event): node_id = self.tree.identify_row(event.y) if node_id: self.tree.selection_set(node_id) self.tree.focus(node_id) self.menu.tk_popup(event.x_root, event.y_root) def on_double_click(self, event): path = self.get_selected_path() if os.path.isfile(path): get_workbench().get_editor_notebook().show_file(path) self.save_current_folder() elif os.path.isdir(path): self.refresh_tree(self.get_selected_node(), True) def load_plugin(): get_workbench().set_default("file.last_browser_folder", None) get_workbench().add_view(MainFileBrowser, "Files", "nw") thonny-2.1.16/thonny/plugins/object_inspector.py0000666000000000000000000004355313201264465020175 0ustar 00000000000000# -*- coding: utf-8 -*- import tkinter as tk from thonny.memory import format_object_id, VariablesFrame, MemoryFrame,\ MAX_REPR_LENGTH_IN_GRID from thonny.misc_utils import shorten_repr from thonny.ui_utils import ScrollableFrame, CALM_WHITE, update_entry_text from thonny.tktextext import TextFrame from thonny.common import InlineCommand import ast from thonny.globals import get_workbench, get_runner from logging import exception class AttributesFrame(VariablesFrame): def __init__(self, master): VariablesFrame.__init__(self, master) self.configure(border=1) self.vert_scrollbar.grid_remove() def on_select(self, event): pass def on_double_click(self, event): self.show_selected_object_info() class ObjectInspector(ScrollableFrame): def __init__(self, master): ScrollableFrame.__init__(self, master) self.object_id = None self.object_info = None get_workbench().bind("ObjectSelect", self.show_object, True) self.grid_frame = tk.Frame(self.interior, bg=CALM_WHITE) self.grid_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=(10,0), pady=15) self.grid_frame.columnconfigure(1, weight=1) def _add_main_attribute(row, caption): label = tk.Label(self.grid_frame, text=caption + ": ", background=CALM_WHITE, justify=tk.LEFT) label.grid(row=row, column=0, sticky=tk.NW) value = tk.Entry(self.grid_frame, background=CALM_WHITE, bd=0, readonlybackground=CALM_WHITE, highlightthickness = 0, state="readonly" ) if row > 0: value.grid(row=row, column=1, columnspan=3, sticky=tk.NSEW, pady=2) else: value.grid(row=row, column=1, sticky=tk.NSEW, pady=2) return value self.id_entry = _add_main_attribute(0, "id") self.repr_entry = _add_main_attribute(1, "repr") self.type_entry = _add_main_attribute(2, "type") self.type_entry.config(cursor="hand2", fg="dark blue") self.type_entry.bind("", self.goto_type) self._add_block_label(5, "Attributes") self.attributes_frame = AttributesFrame(self.grid_frame) self.attributes_frame.grid(row=6, column=0, columnspan=4, sticky=tk.NSEW, padx=(0,10)) self.grid_frame.grid_remove() # navigation self.back_label = self.create_navigation_link(2, " << ", self.navigate_back) self.forward_label = self.create_navigation_link(3, " >> ", self.navigate_forward, (0,10)) self.back_links = [] self.forward_links = [] # type-specific inspectors self.current_type_specific_inspector = None self.current_type_specific_label = None self.type_specific_inspectors = [ FileHandleInspector(self.grid_frame), FunctionInspector(self.grid_frame), StringInspector(self.grid_frame), ElementsInspector(self.grid_frame), DictInspector(self.grid_frame), ] get_workbench().bind("ObjectInfo", self._handle_object_info_event, True) get_workbench().bind("DebuggerProgress", self._handle_progress_event, True) get_workbench().bind("ToplevelResult", self._handle_progress_event, True) def create_navigation_link(self, col, text, action, padx=0): link = tk.Label(self.grid_frame, text=text, background=CALM_WHITE, foreground="blue", cursor="hand2") link.grid(row=0, column=col, sticky=tk.NE, padx=padx) link.bind("", action) return link def navigate_back(self, event): if len(self.back_links) == 0: return self.forward_links.append(self.object_id) self._show_object_by_id(self.back_links.pop(), True) def navigate_forward(self, event): if len(self.forward_links) == 0: return self.back_links.append(self.object_id) self._show_object_by_id(self.forward_links.pop(), True) def show_object(self, event): self._show_object_by_id(event.object_id) def _show_object_by_id(self, object_id, via_navigation=False): if self.winfo_ismapped() and self.object_id != object_id: if not via_navigation and self.object_id is not None: if self.object_id in self.back_links: self.back_links.remove(self.object_id) self.back_links.append(self.object_id) del self.forward_links[:] self.object_id = object_id update_entry_text(self.id_entry, format_object_id(object_id)) self.set_object_info(None) self.request_object_info() def _handle_object_info_event(self, msg): if self.winfo_ismapped(): if msg.info["id"] == self.object_id: if hasattr(msg, "not_found") and msg.not_found: self.object_id = None self.set_object_info(None) else: self.set_object_info(msg.info) def _handle_progress_event(self, event): if self.object_id is not None: self.request_object_info() def request_object_info(self): get_runner().send_command(InlineCommand("get_object_info", object_id=self.object_id, all_attributes=False)) def set_object_info(self, object_info): self.object_info = object_info if object_info is None: update_entry_text(self.repr_entry, "") update_entry_text(self.type_entry, "") self.grid_frame.grid_remove() else: update_entry_text(self.repr_entry, object_info["repr"]) update_entry_text(self.type_entry, object_info["type"]) self.attributes_frame.tree.configure(height=len(object_info["attributes"])) self.attributes_frame.update_variables(object_info["attributes"]) self.update_type_specific_info(object_info) # update layout self._expose(None) if not self.grid_frame.winfo_ismapped(): self.grid_frame.grid() if self.back_links == []: self.back_label.config(foreground="lightgray", cursor="arrow") else: self.back_label.config(foreground="blue", cursor="hand2") if self.forward_links == []: self.forward_label.config(foreground="lightgray", cursor="arrow") else: self.forward_label.config(foreground="blue", cursor="hand2") def update_type_specific_info(self, object_info): type_specific_inspector = None for insp in self.type_specific_inspectors: if insp.applies_to(object_info): type_specific_inspector = insp break if type_specific_inspector != self.current_type_specific_inspector: if self.current_type_specific_inspector is not None: self.current_type_specific_inspector.grid_remove() # TODO: or forget? self.current_type_specific_label.destroy() self.current_type_specific_inspector = None self.current_type_specific_label = None if type_specific_inspector is not None: self.current_type_specific_label = self._add_block_label (3, "") type_specific_inspector.grid(row=4, column=0, columnspan=4, sticky=tk.NSEW, padx=(0,10)) self.current_type_specific_inspector = type_specific_inspector if self.current_type_specific_inspector is not None: self.current_type_specific_inspector.set_object_info(object_info, self.current_type_specific_label) def goto_type(self, event): if self.object_info is not None: get_workbench().event_generate("ObjectSelect", object_id=self.object_info["type_id"]) def _add_block_label(self, row, caption): label = tk.Label(self.grid_frame, bg=CALM_WHITE, text=caption) label.grid(row=row, column=0, columnspan=4, sticky="nsew", pady=(20,0)) return label class TypeSpecificInspector: def __init__(self, master): pass def set_object_info(self, object_info, label): pass def applies_to(self, object_info): return False class FileHandleInspector(TextFrame, TypeSpecificInspector): def __init__(self, master): TypeSpecificInspector.__init__(self, master) TextFrame.__init__(self, master, read_only=True) self.cache = {} # stores file contents for handle id-s self.config(borderwidth=1) self.text.configure(background="white") self.text.tag_configure("read", foreground="lightgray") def applies_to(self, object_info): return ("file_content" in object_info or "file_error" in object_info) def set_object_info(self, object_info, label): if "file_content" not in object_info: exception("File error: " + object_info["file_error"]) return assert "file_content" in object_info content = object_info["file_content"] line_count_sep = len(content.split("\n")) line_count_term = len(content.splitlines()) char_count = len(content) self.text.configure(height=min(line_count_sep, 10)) self.text.set_content(content) assert "file_tell" in object_info # f.tell() gives num of bytes read (minus some magic with linebreaks) file_bytes = content.encode(encoding=object_info["file_encoding"]) bytes_read = file_bytes[0:object_info["file_tell"]] read_content = bytes_read.decode(encoding=object_info["file_encoding"]) read_char_count = len(read_content) read_line_count_term = (len(content.splitlines()) - len(content[read_char_count:].splitlines())) pos_index = "1.0+" + str(read_char_count) + "c" self.text.tag_add("read", "1.0", pos_index) self.text.see(pos_index) label.configure(text="Read %d/%d %s, %d/%d %s" % (read_char_count, char_count, "symbol" if char_count == 1 else "symbols", read_line_count_term, line_count_term, "line" if line_count_term == 1 else "lines")) class FunctionInspector(TextFrame, TypeSpecificInspector): def __init__(self, master): TypeSpecificInspector.__init__(self, master) TextFrame.__init__(self, master, read_only=True) self.config(borderwidth=1) self.text.configure(background="white") def applies_to(self, object_info): return "source" in object_info def set_object_info(self, object_info, label): line_count = len(object_info["source"].split("\n")) self.text.configure(height=min(line_count, 15)) self.text.set_content(object_info["source"]) label.configure(text="Code") class StringInspector(TextFrame, TypeSpecificInspector): def __init__(self, master): TypeSpecificInspector.__init__(self, master) TextFrame.__init__(self, master, read_only=True) self.config(borderwidth=1) self.text.configure(background="white") def applies_to(self, object_info): return object_info["type"] == repr(str) def set_object_info(self, object_info, label): # TODO: don't show too big string content = ast.literal_eval(object_info["repr"]) line_count_sep = len(content.split("\n")) line_count_term = len(content.splitlines()) self.text.configure(height=min(line_count_sep, 10)) self.text.set_content(content) label.configure(text="%d %s, %d %s" % (len(content), "symbol" if len(content) == 1 else "symbols", line_count_term, "line" if line_count_term == 1 else "lines")) class ElementsInspector(MemoryFrame, TypeSpecificInspector): def __init__(self, master): TypeSpecificInspector.__init__(self, master) MemoryFrame.__init__(self, master, ('index', 'id', 'value')) self.configure(border=1) #self.vert_scrollbar.grid_remove() self.tree.column('index', width=40, anchor=tk.W, stretch=False) self.tree.column('id', width=750, anchor=tk.W, stretch=True) self.tree.column('value', width=750, anchor=tk.W, stretch=True) self.tree.heading('index', text='Index', anchor=tk.W) self.tree.heading('id', text='Value ID', anchor=tk.W) self.tree.heading('value', text='Value', anchor=tk.W) self.elements_have_indices = None self.update_memory_model() get_workbench().bind("ShowView", self.update_memory_model, True) get_workbench().bind("HideView", self.update_memory_model, True) def update_memory_model(self, event=None): self._update_columns() def _update_columns(self): if get_workbench().in_heap_mode(): if self.elements_have_indices: self.tree.configure(displaycolumns=("index", "id")) else: self.tree.configure(displaycolumns=("id",)) else: if self.elements_have_indices: self.tree.configure(displaycolumns=("index", "value")) else: self.tree.configure(displaycolumns=("value")) def applies_to(self, object_info): return "elements" in object_info def on_select(self, event): pass def on_double_click(self, event): self.show_selected_object_info() def set_object_info(self, object_info, label): assert "elements" in object_info self.elements_have_indices = object_info["type"] in (repr(tuple), repr(list)) self._update_columns() self._clear_tree() index = 0 # TODO: don't show too big number of elements for element in object_info["elements"]: node_id = self.tree.insert("", "end") if self.elements_have_indices: self.tree.set(node_id, "index", index) else: self.tree.set(node_id, "index", "") self.tree.set(node_id, "id", format_object_id(element["id"])) self.tree.set(node_id, "value", shorten_repr(element["repr"], MAX_REPR_LENGTH_IN_GRID)) index += 1 count = len(object_info["elements"]) self.tree.config(height=min(count,10)) label.configure ( text=("%d element" if count == 1 else "%d elements") % count ) class DictInspector(MemoryFrame, TypeSpecificInspector): def __init__(self, master): TypeSpecificInspector.__init__(self, master) MemoryFrame.__init__(self, master, ('key_id', 'id', 'key', 'value')) self.configure(border=1) #self.vert_scrollbar.grid_remove() self.tree.column('key_id', width=100, anchor=tk.W, stretch=False) self.tree.column('key', width=100, anchor=tk.W, stretch=False) self.tree.column('id', width=750, anchor=tk.W, stretch=True) self.tree.column('value', width=750, anchor=tk.W, stretch=True) self.tree.heading('key_id', text='Key ID', anchor=tk.W) self.tree.heading('key', text='Key', anchor=tk.W) self.tree.heading('id', text='Value ID', anchor=tk.W) self.tree.heading('value', text='Value', anchor=tk.W) self.update_memory_model() def update_memory_model(self, event=None): if get_workbench().in_heap_mode(): self.tree.configure(displaycolumns=("key_id", "id")) else: self.tree.configure(displaycolumns=("key", "value")) def applies_to(self, object_info): return "entries" in object_info def on_select(self, event): pass def on_double_click(self, event): # NB! this selects value self.show_selected_object_info() def set_object_info(self, object_info, label): assert "entries" in object_info self._clear_tree() # TODO: don't show too big number of elements for key, value in object_info["entries"]: node_id = self.tree.insert("", "end") self.tree.set(node_id, "key_id", format_object_id(key["id"])) self.tree.set(node_id, "key", shorten_repr(key["repr"], MAX_REPR_LENGTH_IN_GRID)) self.tree.set(node_id, "id", format_object_id(value["id"])) self.tree.set(node_id, "value", shorten_repr(value["repr"], MAX_REPR_LENGTH_IN_GRID)) count = len(object_info["entries"]) self.tree.config(height=min(count,10)) label.configure ( text=("%d entry" if count == 1 else "%d entries") % count ) self.update_memory_model() def load_plugin(): get_workbench().add_view(ObjectInspector, "Object inspector", "se")thonny-2.1.16/thonny/plugins/outline.py0000666000000000000000000001137713172664305016323 0ustar 00000000000000import re import tkinter as tk from tkinter import ttk from thonny.globals import get_workbench from thonny.ui_utils import SafeScrollbar class OutlineView(ttk.Frame): def __init__(self, master): ttk.Frame.__init__(self, master) self._init_widgets() self._tab_changed_binding = get_workbench().get_editor_notebook().bind("<>", self._update_frame_contents ,True) get_workbench().bind("Save", self._update_frame_contents, True) get_workbench().bind("SaveAs", self._update_frame_contents, True) get_workbench().bind_class("Text", "<>", self._update_frame_contents, True) self._update_frame_contents() def destroy(self): try: # Not sure if editor notebook is still living get_workbench().get_editor_notebook().unbind("<>", self._tab_changed_binding) except: pass self.vert_scrollbar["command"] = None ttk.Frame.destroy(self) def _init_widgets(self): #init and place scrollbar self.vert_scrollbar = SafeScrollbar(self, orient=tk.VERTICAL) self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) #init and place tree self.tree = ttk.Treeview(self, yscrollcommand=self.vert_scrollbar.set) self.tree.grid(row=0, column=0, sticky=tk.NSEW) self.vert_scrollbar['command'] = self.tree.yview #set single-cell frame self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) #init tree events self.tree.bind("", self._on_double_click, "+") #configure the only tree column self.tree.column('#0', anchor=tk.W, stretch=True) # self.tree.heading('#0', text='Item (type @ line)', anchor=tk.W) self.tree['show'] = ('tree',) self._class_img = get_workbench().get_image("class.gif") self._method_img = get_workbench().get_image("method.gif") def _update_frame_contents(self, event=None): self._clear_tree() editor = get_workbench().get_editor_notebook().get_current_editor() if editor is None: return root = self._parse_source(editor.get_code_view().get_content()) for child in root[2]: self._add_item_to_tree('', child) def _parse_source(self, source): #all nodes in format (parent, node_indent, node_children, name, type, linenumber) root_node = (None, 0, [], None, None, None) #name, type and linenumber not needed for root active_node = root_node lineno = 0 for line in source.split('\n'): lineno += 1 m = re.match('[ ]*[\w]{1}', line) if m: indent = len(m.group(0)) while indent <= active_node[1]: active_node = active_node[0] t = re.match('[ ]*(?P(def|class){1})[ ]+(?P[\w]+)', line) if t: current = (active_node, indent, [], t.group('name'), t.group('type'), lineno) active_node[2].append(current) active_node = current return root_node #adds a single item to the tree, recursively calls itself to add any child nodes def _add_item_to_tree(self, parent, item): #create the text to be played for this item item_type = item[4] item_text = " " + item[3] if item_type == "class": image = self._class_img elif item_type == "def": image = self._method_img else: image = None #insert the item, set lineno as a 'hidden' value current = self.tree.insert(parent, 'end', text=item_text, values = item[5], image=image) for child in item[2]: self._add_item_to_tree(current, child) #clears the tree by deleting all items def _clear_tree(self): for child_id in self.tree.get_children(): self.tree.delete(child_id) #called when a double-click is performed on any items def _on_double_click(self, event): editor = get_workbench().get_editor_notebook().get_current_editor() if editor: code_view = editor.get_code_view() lineno = self.tree.item(self.tree.focus())['values'][0] index = code_view.text.index(str(lineno) + '.0') code_view.text.see(index) #make sure that the double-clicked item is visible code_view.select_lines(lineno, lineno) get_workbench().event_generate("OutlineDoubleClick", item_text=self.tree.item(self.tree.focus(), option='text')) def load_plugin(): get_workbench().add_view(OutlineView, "Outline", "ne") thonny-2.1.16/thonny/plugins/paren_matcher.py0000666000000000000000000001355613201264465017451 0ustar 00000000000000from thonny.globals import get_workbench import tokenize import io from thonny.codeview import CodeViewText from thonny.shell import ShellText _OPENERS = {')': '(', ']': '[', '}': '{'} class ParenMatcher: def __init__(self, text, paren_highlight_font=None): self.text = text if paren_highlight_font: self._paren_highlight_font = paren_highlight_font else: self._paren_highlight_font = self.text["font"] self._configure_tags() self._update_scheduled = False def schedule_update(self): def perform_update(): try: self.update_highlighting() finally: self._update_scheduled = False if not self._update_scheduled: self._update_scheduled = True self.text.after_idle(perform_update) def update_highlighting(self): self.text.tag_remove("SURROUNDING_PARENS", "0.1", "end") self.text.tag_remove("UNCLOSED", "0.1", "end") if get_workbench().get_option("view.paren_highlighting"): self._update_highlighting_for_active_range() def _update_highlighting_for_active_range(self): start_index = "1.0" end_index = self.text.index("end") remaining = self._highlight_surrounding(start_index, end_index) self._highlight_unclosed(remaining, start_index, end_index) def _configure_tags(self): self.text.tag_configure("SURROUNDING_PARENS", foreground="Blue", font=self._paren_highlight_font) self.text.tag_configure("UNCLOSED", background="LightGray") self.text.tag_lower("UNCLOSED") self.text.tag_raise("sel") def _highlight_surrounding(self, start_index, end_index): open_index, close_index, remaining = self.find_surrounding(start_index, end_index) if None not in [open_index, close_index]: self.text.tag_add("SURROUNDING_PARENS", open_index) self.text.tag_add("SURROUNDING_PARENS", close_index) return remaining # highlights an unclosed bracket def _highlight_unclosed(self, remaining, start_index, end_index): # anything remaining in the stack is an unmatched opener # since the list is ordered, we can highlight everything starting from the first element if len(remaining) > 0: opener = remaining[0] open_index = "%d.%d" % (opener.start[0], opener.start[1]) self.text.tag_add("UNCLOSED", open_index, end_index) def _get_paren_tokens(self, source): result = [] try: tokens = tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline) for token in tokens: if token.string != "" and token.string in "()[]{}": result.append(token) except: # happens eg when parens are unbalanced or there is indentation error or ... pass return result def find_surrounding(self, start_index, end_index): stack = [] opener, closer = None, None open_index, close_index = None, None start_row, start_col = map(int, start_index.split(".")) source = self.text.get(start_index, end_index) # prepend source with empty lines and spaces to make # token rows and columns match with widget indices source = ("\n" * (start_row-1)) + (" "*start_col) + source for t in self._get_paren_tokens(source): if t.string == "" or t.string not in "()[]{}": continue if t.string in "([{": stack.append(t) elif len(stack) > 0: if stack[-1].string != _OPENERS[t.string]: continue if not closer: opener = stack.pop() open_index = "%d.%d" % (opener.start[0], opener.start[1]) token_index = "%d.%d" % (t.start[0], t.start[1]) if self._is_insert_between_indices(open_index, token_index): closer = t close_index = token_index else: stack.pop() return open_index, close_index, stack def _is_insert_between_indices(self, index1, index2): return self.text.compare("insert", ">=", index1) and \ self.text.compare("insert-1c", "<=", index2) class ShellParenMatcher(ParenMatcher): def _update_highlighting_for_active_range(self): # TODO: check that cursor is in this range index_parts = self.text.tag_prevrange("command", "end") if index_parts: start_index, end_index = index_parts remaining = self._highlight_surrounding(start_index, end_index) self._highlight_unclosed(remaining, start_index, "end") def update_highlighting(event=None): text = event.widget if not hasattr(text, "paren_matcher"): if isinstance(text, CodeViewText): text.paren_matcher = ParenMatcher(text, get_workbench().get_font("BoldEditorFont")) elif isinstance(text, ShellText): text.paren_matcher = ShellParenMatcher(text, get_workbench().get_font("BoldEditorFont")) else: return text.paren_matcher.schedule_update() def load_plugin(): wb = get_workbench() wb.set_default("view.paren_highlighting", True) wb.bind_class("CodeViewText", "<>", update_highlighting, True) wb.bind_class("CodeViewText", "<>", update_highlighting, True) wb.bind_class("ShellText", "<>", update_highlighting, True) wb.bind_class("ShellText", "<>", update_highlighting, True) wb.bind("<>", update_highlighting, True) thonny-2.1.16/thonny/plugins/pip_gui.py0000666000000000000000000010243413201264465016267 0ustar 00000000000000# -*- coding: utf-8 -*- import webbrowser import tkinter as tk from tkinter import ttk, messagebox from thonny import misc_utils, tktextext, ui_utils, THONNY_USER_BASE from thonny.globals import get_workbench, get_runner import subprocess from urllib.request import urlopen, urlretrieve import urllib.error import urllib.parse from concurrent.futures.thread import ThreadPoolExecutor import os import json from distutils.version import LooseVersion, StrictVersion import logging import re from tkinter.filedialog import askopenfilename from logging import exception from thonny.ui_utils import SubprocessDialog, AutoScrollbar, get_busy_cursor from thonny.misc_utils import running_on_windows import sys LINK_COLOR="#3A66DD" PIP_INSTALLER_URL="https://bootstrap.pypa.io/get-pip.py" class PipDialog(tk.Toplevel): def __init__(self, master, only_user=False): self._state = None # possible values: "listing", "fetching", "idle" self._process = None self._installed_versions = {} self._only_user = only_user self.current_package_data = None tk.Toplevel.__init__(self, master) main_frame = ttk.Frame(self) main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.title(self._get_title()) if misc_utils.running_on_mac_os(): self.configure(background="systemSheetBackground") self.transient(master) self.grab_set() # to make it active #self.grab_release() # to allow eg. copy something from the editor self._create_widgets(main_frame) self.search_box.focus_set() self.bind('', self._on_close, True) self.protocol("WM_DELETE_WINDOW", self._on_close) self._show_instructions() ui_utils.center_window(self, master) self._start_update_list() def _create_widgets(self, parent): header_frame = ttk.Frame(parent) header_frame.grid(row=1, column=0, sticky="nsew", padx=15, pady=(15,0)) header_frame.columnconfigure(0, weight=1) header_frame.rowconfigure(1, weight=1) name_font = tk.font.nametofont("TkDefaultFont").copy() name_font.configure(size=16) self.search_box = ttk.Entry(header_frame, background=ui_utils.CALM_WHITE) self.search_box.grid(row=1, column=0, sticky="nsew") self.search_box.bind("", self._on_search, False) self.search_button = ttk.Button(header_frame, text="Search", command=self._on_search) self.search_button.grid(row=1, column=1, sticky="nse", padx=(10,0)) main_pw = tk.PanedWindow(parent, orient=tk.HORIZONTAL, background=ui_utils.get_main_background(), sashwidth=10) main_pw.grid(row=2, column=0, sticky="nsew", padx=15, pady=15) parent.rowconfigure(2, weight=1) parent.columnconfigure(0, weight=1) listframe = ttk.Frame(main_pw, relief="groove", borderwidth=1) listframe.rowconfigure(0, weight=1) listframe.columnconfigure(0, weight=1) self.listbox = tk.Listbox(listframe, activestyle="dotbox", width=20, height=10, background=ui_utils.CALM_WHITE, selectborderwidth=0, relief="flat", highlightthickness=0, borderwidth=0) self.listbox.insert("end", " ") self.listbox.bind("<>", self._on_listbox_select, True) self.listbox.grid(row=0, column=0, sticky="nsew") list_scrollbar = AutoScrollbar(listframe, orient=tk.VERTICAL) list_scrollbar.grid(row=0, column=1, sticky="ns") list_scrollbar['command'] = self.listbox.yview self.listbox["yscrollcommand"] = list_scrollbar.set info_frame = ttk.Frame(main_pw) info_frame.columnconfigure(0, weight=1) info_frame.rowconfigure(1, weight=1) main_pw.add(listframe) main_pw.add(info_frame) self.name_label = ttk.Label(info_frame, text="", font=name_font) self.name_label.grid(row=0, column=0, sticky="w", padx=5) info_text_frame = tktextext.TextFrame(info_frame, read_only=True, horizontal_scrollbar=False, vertical_scrollbar_class=AutoScrollbar, width=60, height=10) info_text_frame.configure(borderwidth=1) info_text_frame.grid(row=1, column=0, columnspan=4, sticky="nsew", pady=(0,20)) self.info_text = info_text_frame.text self.info_text.tag_configure("url", foreground=LINK_COLOR, underline=True) self.info_text.tag_bind("url", "", self._handle_url_click) self.info_text.tag_bind("url", "", lambda e: self.info_text.config(cursor="hand2")) self.info_text.tag_bind("url", "", lambda e: self.info_text.config(cursor="")) self.info_text.tag_configure("install_file", foreground=LINK_COLOR, underline=True) self.info_text.tag_bind("install_file", "", self._handle_install_file_click) self.info_text.tag_bind("install_file", "", lambda e: self.info_text.config(cursor="hand2")) self.info_text.tag_bind("install_file", "", lambda e: self.info_text.config(cursor="")) default_font = tk.font.nametofont("TkDefaultFont") self.info_text.configure(background=ui_utils.get_main_background(), font=default_font, wrap="word") bold_font = default_font.copy() # need to explicitly copy size, because Tk 8.6 on certain Ubuntus use bigger font in copies bold_font.configure(weight="bold", size=default_font.cget("size")) self.info_text.tag_configure("caption", font=bold_font) self.info_text.tag_configure("bold", font=bold_font) self.command_frame = ttk.Frame(info_frame) self.command_frame.grid(row=2, column=0, sticky="w") self.install_button = ttk.Button(self.command_frame, text=" Upgrade ", command=self._on_click_install) if not self._read_only(): self.install_button.grid(row=0, column=0, sticky="w", padx=0) self.uninstall_button = ttk.Button(self.command_frame, text="Uninstall", command=lambda: self._perform_action("uninstall")) if not self._read_only(): self.uninstall_button.grid(row=0, column=1, sticky="w", padx=(5,0)) self.advanced_button = ttk.Button(self.command_frame, text="...", width=3, command=lambda: self._perform_action("advanced")) if not self._read_only(): self.advanced_button.grid(row=0, column=2, sticky="w", padx=(5,0)) self.close_button = ttk.Button(info_frame, text="Close", command=self._on_close) self.close_button.grid(row=2, column=3, sticky="e") def _on_click_install(self): name = self.current_package_data["info"]["name"] if self._confirm_install(name): self._perform_action("install") def _set_state(self, state, normal_cursor=False): self._state = state widgets = [self.listbox, # self.search_box, # looks funny when disabled self.search_button, self.install_button, self.advanced_button, self.uninstall_button] if state == "idle": self.config(cursor="") for widget in widgets: widget["state"] = tk.NORMAL else: self.config(cursor=get_busy_cursor()) for widget in widgets: widget["state"] = tk.DISABLED if normal_cursor: self.config(cursor="") def _get_state(self): return self._state def _handle_outdated_or_missing_pip(self): raise NotImplementedError() def _install_pip(self): self._clear() self.info_text.direct_insert("end", "Installing pip\n\n", ("caption", )) self.info_text.direct_insert("end", "pip, a required module for managing packages is missing or too old.\n\n" + "Downloading pip installer (about 1.5 MB), please wait ...\n") self.update() self.update_idletasks() installer_filename, _ = urlretrieve(PIP_INSTALLER_URL) self.info_text.direct_insert("end", "Installing pip, please wait ...\n") self.update() self.update_idletasks() proc, _ = self._create_backend_process([installer_filename], stderr=subprocess.PIPE) out, err = proc.communicate() os.remove(installer_filename) if err != "": raise RuntimeError("Error while installing pip:\n" + err) self.info_text.direct_insert("end", out + "\n") self.update() self.update_idletasks() # update list self._start_update_list() def _provide_pip_install_instructions(self): self._clear() self.info_text.direct_insert("end", "Outdated or missing pip\n\n", ("caption", )) self.info_text.direct_insert("end", "pip, a required module for managing packages is missing or too old for Thonny.\n\n" + "If your system package manager doesn't provide recent pip (9.0.0 or later), " + "then you can install newest version by downloading ") self.info_text.direct_insert("end", PIP_INSTALLER_URL, ("url",)) self.info_text.direct_insert("end", " and running it with " + self._get_interpreter() + " (probably needs admin privileges).\n\n") self.info_text.direct_insert("end", self._instructions_for_command_line_install()) self._set_state("disabled", True) def _instructions_for_command_line_install(self): return ("Alternatively, if you have an older pip installed, then you can install packages " + "on the command line (Tools → Open system shell...)") def _start_update_list(self, name_to_show=None): assert self._get_state() in [None, "idle"] self._set_state("listing") args = ["list"] if self._only_user: args.append("--user") args.extend(["--pre", "--format", "json"]) self._process, _ = self._create_pip_process(args) def poll_completion(): if self._process == None: return else: returncode = self._process.poll() if returncode is None: # not done yet self.after(200, poll_completion) else: self._set_state("idle") if returncode == 0: raw_data = self._process.stdout.read() self._update_list(json.loads(raw_data)) if name_to_show is None: self._show_instructions() else: self._start_show_package_info(name_to_show) else: error = self._process.stdout.read() if ("no module named pip" in error.lower() # pip not installed or "no such option" in error.lower()): # too old pip self._handle_outdated_or_missing_pip() return else: messagebox.showerror("pip list error", error) self._process = None poll_completion() def _update_list(self, data): self.listbox.delete(1, "end") self._installed_versions = {entry["name"] : entry["version"] for entry in data} for name in sorted(self._installed_versions.keys(), key=str.lower): self.listbox.insert("end", " " + name) def _on_listbox_select(self, event): selection = self.listbox.curselection() if len(selection) == 1: if selection[0] == 0: # special first item self._show_instructions() else: self._start_show_package_info(self.listbox.get(selection[0]).strip()) def _on_search(self, event=None): if not self._get_state() == "idle": # Search box is not made inactive for busy-states return if self.search_box.get().strip() == "": return self._start_show_package_info(self.search_box.get().strip()) def _clear(self): self.current_package_data = None self.name_label.grid_remove() self.command_frame.grid_remove() self.info_text.direct_delete("1.0", "end") def _show_instructions(self): self._clear() if self._read_only(): self.info_text.direct_insert("end", "With current interpreter you can only browse the packages here.\n" + "Use 'Tools → Open system shell...' for installing, upgrading or uninstalling.") else: self.info_text.direct_insert("end", "Install from PyPI\n", ("caption",)) self.info_text.direct_insert("end", "If you don't know where to get the package from, " + "then most likely you'll want to search the Python Package Index. " + "Start by entering the name of the package in the search box above and pressing ENTER.\n\n") self.info_text.direct_insert("end", "Install from local file\n", ("caption",)) self.info_text.direct_insert("end", "Click ") self.info_text.direct_insert("end", "here", ("install_file",)) self.info_text.direct_insert("end", " to locate and install the package file (usually with .whl, .tar.gz or .zip extension).\n\n") self.info_text.direct_insert("end", "Upgrade or uninstall\n", ("caption",)) self.info_text.direct_insert("end", 'Start by selecting the package from the left.') self._select_list_item(0) def _start_show_package_info(self, name): self.current_package_data = None self.info_text.direct_delete("1.0", "end") self.name_label["text"] = "" self.name_label.grid() self.command_frame.grid() installed_version = self._get_installed_version(name) if installed_version is not None: self.name_label["text"] = name self.info_text.direct_insert("end", "Installed version: ", ('caption',)) self.info_text.direct_insert("end", installed_version + "\n") # Fetch info from PyPI self._set_state("fetching") # Follwing url fetches info about latest version. # This is OK even when we're looking an installed older version # because new version may have more relevant and complete info. url = "https://pypi.python.org/pypi/{}/json".format(urllib.parse.quote(name)) url_future = _fetch_url_future(url) def poll_fetch_complete(): if url_future.done(): self._set_state("idle") try: _, bin_data = url_future.result() raw_data = bin_data.decode("UTF-8") self._show_package_info(name, json.loads(raw_data)) except urllib.error.HTTPError as e: self._show_package_info(name, self._generate_minimal_data(name), e.code) else: self.after(200, poll_fetch_complete) poll_fetch_complete() def _generate_minimal_data(self, name): return { "info" : {'name' : name}, "releases" : {} } def _show_package_info(self, name, data, error_code=None): self.current_package_data = data def write(s, tag=None): if tag is None: tags = () else: tags = (tag,) self.info_text.direct_insert("end", s, tags) def write_att(caption, value, value_tag=None): write(caption + ": ", "caption") write(value, value_tag) write("\n") if error_code is not None: if error_code == 404: write("Could not find the package from PyPI.") if not self._get_installed_version(name): # new package write("\nPlease check your spelling!" + "\nYou need to enter ") write("exact package name", "bold") write("!") else: write("Could not find the package info from PyPI. Error code: " + str(error_code)) return info = data["info"] self.name_label["text"] = info["name"] # search name could have been a bit different latest_stable_version = _get_latest_stable_version(data["releases"].keys()) if latest_stable_version is not None: write_att("Latest stable version", latest_stable_version) else: write_att("Latest version", data["info"]["version"]) write_att("Summary", info["summary"]) write_att("Author", info["author"]) write_att("Homepage", info["home_page"], "url") if info.get("bugtrack_url", None): write_att("Bugtracker", info["bugtrack_url"], "url") if info.get("docs_url", None): write_att("Documentation", info["docs_url"], "url") if info.get("package_url", None): write_att("PyPI page", info["package_url"], "url") if self._get_installed_version(info["name"]) is not None: self.install_button["text"] = "Upgrade" if not self._read_only(): self.uninstall_button.grid(row=0, column=1) self._select_list_item(info["name"]) if self._get_installed_version(info["name"]) == latest_stable_version: self.install_button["state"] = "disabled" else: self.install_button["state"] = "normal" else: self.install_button["text"] = "Install" self.uninstall_button.grid_forget() self._select_list_item(0) def _normalize_name(self, name): # looks like (in some cases?) pip list gives the name as it was used during install # ie. the list may contain lowercase entry, when actual metadata has uppercase name # Example: when you "pip install cx-freeze", then "pip list" # really returns "cx-freeze" although correct name is "cx_Freeze" # https://www.python.org/dev/peps/pep-0503/#id4 return re.sub(r"[-_.]+", "-", name).lower().strip() def _select_list_item(self, name_or_index): if isinstance(name_or_index, int): index = name_or_index else: normalized_items = list(map(self._normalize_name, self.listbox.get(0, "end"))) try: index = normalized_items.index(self._normalize_name(name_or_index)) except: exception("Can't find package name from the list: " + name_or_index) return self.listbox.select_clear(0, "end") self.listbox.select_set(index) self.listbox.see(index) def _perform_action(self, action): assert self._get_state() == "idle" assert self.current_package_data is not None data = self.current_package_data name = self.current_package_data["info"]["name"] install_args = ["install", "--no-cache-dir"] if self._only_user: install_args.append("--user") if action == "install": args = install_args if self._get_installed_version(name) is not None: args.append("--upgrade") args.append(name) elif action == "uninstall": if (name in ["pip", "setuptools"] and not messagebox.askyesno("Really uninstall?", "Package '{}' is required for installing and uninstalling other packages.\n\n".format(name) + "Are you sure you want to uninstall it?")): return args = ["uninstall", "-y", name] elif action == "advanced": details = _ask_installation_details(self, data, _get_latest_stable_version(list(data["releases"].keys()))) if details is None: # Cancel return version, upgrade_deps = details args = install_args if upgrade_deps: args.append("--upgrade") args.append(name + "==" + version) else: raise RuntimeError("Unknown action") proc, cmd = self._create_pip_process(args) title = subprocess.list2cmdline(cmd) # following call blocks _show_subprocess_dialog(self, proc, title) if action == "uninstall": self._show_instructions() # Make the old package go away as fast as possible self._start_update_list(None if action == "uninstall" else name) def _handle_install_file_click(self, event): if self._get_state() != "idle": return filename = askopenfilename ( filetypes = [('Package', '.whl .zip .tar.gz'), ('all files', '.*')], initialdir = get_workbench().get_option("run.working_directory") ) if filename: # Note that missing filename may be "" or () depending on tkinter version self._install_local_file(filename) def _install_local_file(self, filename): args = ["install"] if self._only_user: args.append("--user") args.append(filename) proc, cmd = self._create_pip_process(args) # following call blocks title = subprocess.list2cmdline(cmd) _, out, _ = _show_subprocess_dialog(self, proc, title) # Try to find out the name of the package we're installing name = None # output should include a line like this: # Installing collected packages: pytz, six, python-dateutil, numpy, pandas inst_lines = re.findall("^Installing collected packages:.*?$", out, re.MULTILINE | re.IGNORECASE) # @UndefinedVariable if len(inst_lines) == 1: # take last element elements = re.split(",|:", inst_lines[0]) name = elements[-1].strip() self._start_update_list(name) def _handle_url_click(self, event): url = _extract_click_text(self.info_text, event, "url") if url is not None: webbrowser.open(url) def _on_close(self, event=None): self.destroy() def _get_installed_version(self, name): for list_name in self._installed_versions: if self._normalize_name(name) == self._normalize_name(list_name): return self._installed_versions[list_name] return None def _prepare_env_for_pip_process(self, encoding): env = {} for name in os.environ: if ("python" not in name.lower() and name not in ["TK_LIBRARY", "TCL_LIBRARY"]): # skip python vars env[name] = os.environ[name] env["PYTHONIOENCODING"] = encoding env["PYTHONUNBUFFERED"] = "1" return env def _create_backend_process(self, args, stderr=subprocess.STDOUT): encoding = "UTF-8" cmd = [self._get_interpreter()] + args startupinfo = None creationflags = 0 if running_on_windows(): creationflags = subprocess.CREATE_NEW_PROCESS_GROUP startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW return (subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, env=self._prepare_env_for_pip_process(encoding), universal_newlines=True, creationflags=creationflags, startupinfo=startupinfo), cmd) def _create_pip_process(self, args): return self._create_backend_process(["-m", "pip"] + args) def _get_interpreter(self): pass def _get_title(self): return "Manage packages for " + self._get_interpreter() def _confirm_install(self, name): return True def _read_only(self): return False class BackendPipDialog(PipDialog): def _get_interpreter(self): return get_runner().get_interpreter_command() def _confirm_install(self, name): if name.lower().startswith("thonny"): return messagebox.askyesno("Confirmation", "Looks like you are installing a Thonny-related package.\n" + "If you meant to install a Thonny plugin, then you should\n" + "close this dialog and choose 'Tools → Manage plugins...'\n" + "\n" + "Are you sure you want to install '" + name + "' here?") else: return True def _handle_outdated_or_missing_pip(self): if get_runner().using_venv(): self._install_pip() else: self._provide_pip_install_instructions() def _read_only(self): return not get_runner().using_venv() class PluginsPipDialog(PipDialog): def __init__(self, master): PipDialog.__init__(self, master, only_user=True) def _get_interpreter(self): return sys.executable.replace("thonny.exe", "python.exe") def _prepare_env_for_pip_process(self, encoding): env = PipDialog._prepare_env_for_pip_process(self, encoding) env["PYTHONUSERBASE"] = THONNY_USER_BASE return env def _create_widgets(self, parent): bg = "#ffff99" banner = tk.Label(parent, background=bg) banner.grid(row=0, column=0, sticky="nsew") banner_text = tk.Label(banner, text="NB! This dialog is for managing Thonny plug-ins and their dependencies.\n" + "If you want to install packages for your own programs then close this and choose 'Tools → Manage packages...'\n" + "\n" + "This dialog installs packages into " + THONNY_USER_BASE + "\n" + "\n" + "NB! You need to restart Thonny after installing / upgrading / uninstalling a plug-in.", background=bg, justify="left") banner_text.grid(pady=10, padx=10) PipDialog._create_widgets(self, parent) def _get_title(self): return "Thonny plug-ins" def _handle_outdated_or_missing_pip(self): return self._provide_pip_install_instructions() def _instructions_for_command_line_install(self): # System shell is not suitable without correct PYTHONUSERBASE return "" class DetailsDialog(tk.Toplevel): def __init__(self, master, package_metadata, selected_version): tk.Toplevel.__init__(self, master) self.result = None self.title("Advanced install / upgrade / downgrade") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) main_frame = ttk.Frame(self) # To get styled background main_frame.grid(sticky="nsew") main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) version_label = ttk.Label(main_frame, text="Desired version") version_label.grid(row=0, column=0, columnspan=2, padx=20, pady=(15,0), sticky="w") def version_sort_key(s): # Trying to massage understandable versions into valid StrictVersions if s.replace(".", "").isnumeric(): # stable release s2 = s + "b999" # make it latest beta version elif "rc" in s: s2 = s.replace("rc", "b8") else: s2 = s try: return StrictVersion(s2) except: # use only numbers nums = re.findall(r"\d+", s) while len(nums) < 2: nums.append("0") return StrictVersion(".".join(nums[:3])) version_strings = list(package_metadata["releases"].keys()) version_strings.sort(key=version_sort_key, reverse=True) self.version_combo = ttk.Combobox(main_frame, values=version_strings, exportselection=False) try: self.version_combo.current(version_strings.index(selected_version)) except: pass self.version_combo.state(['!disabled', 'readonly']) self.version_combo.grid(row=1, column=0, columnspan=2, pady=(0,15), padx=20, sticky="ew") self.update_deps_var = tk.IntVar() self.update_deps_var.set(0) self.update_deps_cb = ttk.Checkbutton(main_frame, text="Upgrade dependencies", variable=self.update_deps_var) self.update_deps_cb.grid(row=2, column=0, columnspan=2, padx=20, sticky="w") self.ok_button = ttk.Button(main_frame, text="Install", command=self._ok) self.ok_button.grid(row=3, column=0, pady=15, padx=(20, 0), sticky="se") self.cancel_button = ttk.Button(main_frame, text="Cancel", command=self._cancel) self.cancel_button.grid(row=3, column=1, pady=15, padx=(5,20), sticky="se") if misc_utils.running_on_mac_os(): self.configure(background="systemSheetBackground") #self.resizable(height=tk.FALSE, width=tk.FALSE) self.transient(master) self.grab_set() # to make it active and modal self.version_combo.focus_set() self.bind('', self._cancel, True) self.protocol("WM_DELETE_WINDOW", self._cancel) ui_utils.center_window(self, master) def _ok(self, event=None): self.result = self.version_combo.get(), bool(self.update_deps_var.get()) self.destroy() def _cancel(self, event=None): self.result = None self.destroy() def _fetch_url_future(url, timeout=10): def load_url(): with urlopen(url, timeout=timeout) as conn: return (conn, conn.read()) executor = ThreadPoolExecutor(max_workers=1) return executor.submit(load_url) def _get_latest_stable_version(version_strings): versions = [] for s in version_strings: if s.replace(".", "").isnumeric(): # Assuming stable versions have only dots and numbers versions.append(LooseVersion(s)) # LooseVersion __str__ doesn't change the version string if len(versions) == 0: return None return str(sorted(versions)[-1]) def _show_subprocess_dialog(master, proc, title): dlg = SubprocessDialog(master, proc, title) dlg.wait_window() return dlg.returncode, dlg.stdout, dlg.stderr def _ask_installation_details(master, data, selected_version): dlg = DetailsDialog(master, data, selected_version) dlg.wait_window() return dlg.result def _extract_click_text(widget, event, tag): # http://stackoverflow.com/a/33957256/261181 try: index = widget.index("@%s,%s" % (event.x, event.y)) tag_indices = list(widget.tag_ranges(tag)) for start, end in zip(tag_indices[0::2], tag_indices[1::2]): # check if the tag matches the mouse click index if widget.compare(start, '<=', index) and widget.compare(index, '<', end): return widget.get(start, end) except: logging.exception("extracting click text") return None def load_plugin(): def open_backend_pip_gui(*args): pg = BackendPipDialog(get_workbench()) pg.wait_window() def open_backend_pip_gui_enabled(): return "pip_gui" in get_runner().supported_features() def open_frontend_pip_gui(*args): pg = PluginsPipDialog(get_workbench()) pg.wait_window() get_workbench().add_command("backendpipgui", "tools", "Manage packages...", open_backend_pip_gui, tester=open_backend_pip_gui_enabled, group=80) get_workbench().add_command("pluginspipgui", "tools", "Manage plug-ins...", open_frontend_pip_gui, group=180) thonny-2.1.16/thonny/plugins/refactor.py0000666000000000000000000003127113172664305016444 0ustar 00000000000000""" from rope.base.project import Project from rope.refactor.rename import Rename import rope.base.libutils import tkinter as tk from tkinter import ttk import os.path #arguments: 1) full path to the file, 2) new name to be applied to the identifier, 3) Rope offset (position) of the renamed identifier in the current file #returns a list of Rope change objects applying to this rename refactor #throws an exception if anything goes wrong, needs to be handled by callers! def get_list_of_rename_changes(full_path, new_variable_name, offset): filearr = os.path.split(full_path) project_path = filearr[0] module_name = filearr[1] project = Project(project_path, ropefolder=None) module = rope.base.libutils.path_to_resource(project, full_path) changes = Rename(project, module, offset).get_changes(new_variable_name) return (project, changes) #performs the changes #arguments: 1) Rope project, 2) Rope changes object def perform_changes(project, changes): project.do(changes) project.close() #cancels the changes, cleans up the project def cancel_changes(project): project.close() #utility method for convering a Text index to a Rope offset def calculate_offset(text): contents = text.get(1.0, 'end').split('\n') insert_index = text.index('insert') linearr = insert_index.split('.') line_no = int(linearr[0]) char_no = int(linearr[1]) totalchars = char_no for line in range(line_no - 1): totalchars += len(contents[line]) + 1 return totalchars #inspired by http://effbot.org/tkinterbook/tkinter-dialog-windows.htm #creates a window asking for a new identifier name, can later be accessed via the refactor_new_variable_name variable class RenameWindow(tk.Toplevel): def __init__(self, master, title = None): tk.Toplevel.__init__(self, master) self.refactor_new_variable_name = None self.transient(master) self.title('Rename') self.parent = master self.result = None ttk.Label(self, text="New name:").grid(row=0, columnspan=2) self.new_name_entry = ttk.Entry(self) self.new_name_entry.grid(row=1, columnspan=2) self.new_name_entry.focus_force(); self.ok_button = ttk.Button(self, text="OK", command=self.ok, default=tk.ACTIVE) self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel) self.ok_button.grid(row=2, column=0, sticky=tk.W + tk.E, padx=5) self.cancel_button.grid(row=2, column=1, sticky=tk.W + tk.E, padx=5) self.bind("", self.ok, True) self.bind("", self.cancel, True) self.grab_set() self.protocol("WM_DELETE_WINDOW", self.cancel) self.geometry("+%d+%d" % (master.winfo_rootx()+50, master.winfo_rooty()+50)) self.resizable(width=False, height=False) self.wait_window(self) #user clicked cancel - destroy the object, return the focus to master def cancel(self, event=None): self.parent.focus_set() self.destroy() #user clicked ok - set the variable name, destroy the object, return the focus to parent def ok(self, event=None): self.withdraw() self.update_idletasks() self.refactor_new_variable_name = self.new_name_entry.get() self.cancel() class RefactorRenameStartEvent(thonny.user_logging.UserEvent): #user initiated the refactoring process def __init__(self, editor): self.editor_id = id(editor) class RefactorRenameCancelEvent(thonny.user_logging.UserEvent): #user manually cancelled the refactoring process def __init__(self, editor): self.editor_id = id(editor) class RefactorRenameFailedEvent(thonny.user_logging.UserEvent): #refactoring process failed due to an error def __init__(self, editor): self.editor_id = id(editor) class RefactorRenameCompleteEvent(thonny.user_logging.UserEvent): #refactoring process was successfully completed def __init__(self, description, offset, affected_files): self.description = description self.offset = offset self.affected_files = affected_files def _cmd_refactor_rename(self): self.log_user_event(thonny.refactor.RefactorRenameStartEvent(self.editor_notebook.get_current_editor())) if not self.editor_notebook.get_current_editor(): self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) errorMessage = tkMessageBox.showerror( title="Rename failed", message="Rename operation failed (no active editor tabs?).", #TODO - more informative text needed master=self) return #create a list of active but unsaved/modified editors) unsaved_editors = [x for x in self.editor_notebook.winfo_children() if type(x) == Editor and x._cmd_save_file_enabled()] if len(unsaved_editors) != 0: #confirm with the user that all open editors need to be saved first confirm = tkMessageBox.askyesno( title="Save Files Before Rename", message="All modified files need to be saved before refactoring. Do you want to continue?", default=tkMessageBox.YES, master=self) if not confirm: self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor())) return #if user doesn't want it, return for editor in unsaved_editors: if not editor.get_filename(): self.editor_notebook.select(editor) #in the case of editors with no filename, show it, so user knows which one they're saving editor._cmd_save_file() if editor._cmd_save_file_enabled(): #just a sanity check - if after saving a file still needs saving, something is wrong self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) errorMessage = tkMessageBox.showerror( title="Rename failed", message="Rename operation failed (saving file failed).", #TODO - more informative text needed master=self) return filename = self.editor_notebook.get_current_editor().get_filename() if filename == None: #another sanity check - the current editor should have an associated filename by this point self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) errorMessage = tkMessageBox.showerror( title="Rename failed", message="Rename operation failed (no filename associated with current module).", #TODO - more informative text needed master=self) return identifier = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) #regex to compare valid python identifiers against while True: #ask for new variable name until a valid one is entered renameWindow = thonny.refactor.RenameWindow(self) newname = renameWindow.refactor_new_variable_name if newname == None: self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor())) return #user canceled, return if re.match(identifier, newname): break #valid identifier entered, continue errorMessage = tkMessageBox.showerror( title="Incorrect identifier", message="Incorrect Python identifier, please re-enter.", master=self) try: #calculate the offset for rope offset = thonny.refactor.calculate_offset(self.editor_notebook.get_current_editor()._code_view.text) #get the project handle and list of changes project, changes = thonny.refactor.get_list_of_rename_changes(filename, newname, offset) #if len(changes.changes == 0): raise Exception except Exception: try: #rope needs the cursor to be AFTER the first character of the variable being refactored #so the reason for failure might be that the user had the cursor before the variable name offset = offset + 1 project, changes = thonny.refactor.get_list_of_rename_changes(filename, newname, offset) #if len(changes.changes == 0): raise Exception except Exception: #couple of different reasons why this could happen, let's list them all in the error message self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) message = 'Rename operation failed. A few possible reasons: \n' message += '1) Not a valid Python identifier selected \n' message += '2) The current file or any other files in the same directory or in any of its subdirectores contain incorrect syntax. Make sure the current project is in its own separate folder.' errorMessage = tkMessageBox.showerror( title="Rename failed", message=message, #TODO - maybe also print stack trace for more info? master=self) return description = changes.description #needed for logging #sanity check if len(changes.changes) == 0: self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) errorMessage = tkMessageBox.showerror( title="Rename failed", message="Rename operation failed - no identifiers affected by change.", #TODO - more informative text needed master=self) return affected_files = [] #needed for logging #show the preview window to user messageText = 'Confirm the changes. The following files will be modified:\n' for change in changes.changes: affected_files.append(change.resource._path) messageText += '\n ' + change.resource._path messageText += '\n\n NB! This action cannot be undone.' confirm = tkMessageBox.askyesno( title="Confirm changes", message=messageText, default=tkMessageBox.YES, master=self) #confirm with user to finalize the changes if not confirm: self.log_user_event(thonny.refactor.RefactorRenameCancelEvent(self.editor_notebook.get_current_editor())) thonny.refactor.cancel_changes(project) return try: thonny.refactor.perform_changes(project, changes) except Exception: self.log_user_event(thonny.refactor.RefactorRenameFailedEvent(self.editor_notebook.get_current_editor())) errorMessage = tkMessageBox.showerror( title="Rename failed", message="Rename operation failed (Rope error).", #TODO - more informative text needed master=self) thonny.refactor.cancel_changes(project) return #everything went fine, let's load all the active tabs again and set their content for editor in self.editor_notebook.winfo_children(): try: filename = editor.get_filename() source, self.file_encoding = misc_utils.read_python_file(filename) editor._code_view.set_content(source) self.editor_notebook.tab(editor, text=self.editor_notebook._generate_editor_title(filename)) except Exception: try: #it is possible that a file (module) itself was renamed - Rope allows it. so let's see if a file exists with the new name. filename = filename.replace(os.path.split(filename)[1], newname + '.py') source, self.file_encoding = misc_utils.read_python_file(filename) editor._code_view.set_content(source) self.editor_notebook.tab(editor, text=self.editor_notebook._generate_editor_title(filename)) except Exception: #something went wrong with reloading the file, let's close this tab to avoid consistency problems self.editor_notebook.forget(editor) editor.destroy() self.log_user_event(thonny.refactor.RefactorRenameCompleteEvent(description, offset, affected_files)) current_browser_node_path = self.file_browser.get_selected_path() self.file_browser.refresh_tree() if current_browser_node_path is not None: self.file_browser.open_path_in_browser(current_browser_node_path) def _cmd_refactor_rename_enabled(self): return self.editor_notebook.get_current_editor() is not None def _load_plugin_(workbench): get_workbench().add_command("refactor_rename", "edit", "Rename identifier", ...) """thonny-2.1.16/thonny/plugins/replayer.py0000666000000000000000000003203513172664305016461 0ustar 00000000000000import tkinter as tk import tkinter.ttk as ttk from datetime import datetime from thonny import ui_utils from thonny.globals import get_workbench import json from thonny.base_file_browser import BaseFileBrowser import ast import os.path from thonny.plugins.coloring import SyntaxColorer class ReplayWindow(tk.Toplevel): def __init__(self): tk.Toplevel.__init__(self, get_workbench()) ui_utils.set_zoomed(self, True) self.main_pw = tk.PanedWindow(self, orient=tk.HORIZONTAL, sashwidth=10) self.center_pw = tk.PanedWindow(self.main_pw, orient=tk.VERTICAL, sashwidth=10) self.right_frame = ttk.Frame(self.main_pw) self.right_pw = tk.PanedWindow(self.right_frame, orient=tk.VERTICAL, sashwidth=10) self.editor_notebook = ReplayerEditorNotebook(self.center_pw) shell_book = ttk.Notebook(self.main_pw) self.shell = ShellFrame(shell_book) self.details_frame = EventDetailsFrame(self.right_pw) self.log_frame = LogFrame(self.right_pw, self.editor_notebook, self.shell, self.details_frame) self.browser = ReplayerFileBrowser(self.main_pw, self.log_frame) self.control_frame = ControlFrame(self.right_frame) self.main_pw.grid(padx=10, pady=10, sticky=tk.NSEW) self.main_pw.add(self.browser, width=200) self.main_pw.add(self.center_pw, width=1000) self.main_pw.add(self.right_frame, width=200) self.center_pw.add(self.editor_notebook, height=700) self.center_pw.add(shell_book, height=300) shell_book.add(self.shell, text="Shell") self.right_pw.grid(sticky=tk.NSEW) self.control_frame.grid(sticky=tk.NSEW) self.right_pw.add(self.log_frame, height=600) self.right_pw.add(self.details_frame, height=200) self.right_frame.columnconfigure(0, weight=1) self.right_frame.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) class ReplayerFileBrowser(BaseFileBrowser): def __init__(self, master, log_frame): BaseFileBrowser.__init__(self, master, True, "tools.replayer_last_browser_folder") self.log_frame = log_frame self.configure(border=1, relief=tk.GROOVE) def on_double_click(self, event): self.save_current_folder() path = self.get_selected_path() if path: self.log_frame.load_log(path) class ControlFrame(ttk.Frame): def __init__(self, master, **kw): ttk.Frame.__init__(self, master=master, **kw) self.toggle_button = ttk.Button(self, text="Play") self.speed_scale = ttk.Scale(self, from_=1, to=100, orient=tk.HORIZONTAL) self.toggle_button.grid(row=0, column=0, sticky=tk.NSEW, pady=(10,0), padx=(0,5)) self.speed_scale.grid(row=0, column=1, sticky=tk.NSEW, pady=(10,0), padx=(5,0)) self.columnconfigure(1, weight=1) class LogFrame(ui_utils.TreeFrame): def __init__(self, master, editor_book, shell, details_frame): ui_utils.TreeFrame.__init__(self, master, ("desc", "pause")) self.tree.heading('desc', text='Event', anchor=tk.W) self.tree.heading('pause', text='Pause (sec)', anchor=tk.W) self.configure(border=1, relief=tk.GROOVE) self.editor_notebook = editor_book self.shell = shell self.details_frame = details_frame self.all_events = [] self.last_event_index = -1 self.loading = False def load_log(self, filename): self._clear_tree() self.details_frame._clear_tree() self.all_events = [] self.last_event_index = -1 self.loading = True self.editor_notebook.reset() self.shell.reset() with open(filename, encoding="UTF-8") as f: events = json.load(f) last_event_time = None for event in events: node_id = self.tree.insert("", "end") self.tree.set(node_id, "desc", event["sequence"]) event_time = datetime.strptime(event["time"], "%Y-%m-%dT%H:%M:%S.%f") if last_event_time: delta = event_time - last_event_time pause = delta.seconds else: pause = 0 self.tree.set(node_id, "pause", str(pause if pause else "")) self.all_events.append(event) last_event_time = event_time self.loading = False def replay_event(self, event): "this should be called with events in correct order" #print("log replay", event) if "text_widget_id" in event: if event.get("text_widget_context", None) == "shell": self.shell.replay_event(event) else: self.editor_notebook.replay_event(event) def reset(self): self.shell.reset() self.editor_notebook.reset() self.last_event_index = -1 def on_select(self, event): # parameter "event" is here tkinter event if self.loading: return iid = self.tree.focus() if iid != '': self.select_event(self.tree.index(iid)) def select_event(self, event_index): event = self.all_events[event_index] self.details_frame.load_event(event) # here event means logged event if event_index > self.last_event_index: # replay all events between last replayed event up to and including this event while self.last_event_index < event_index: self.replay_event(self.all_events[self.last_event_index+1]) self.last_event_index += 1 elif event_index < self.last_event_index: # Undo by reseting and replaying again self.reset() self.select_event(event_index) class EventDetailsFrame(ui_utils.TreeFrame): def __init__(self, master): ui_utils.TreeFrame.__init__(self, master, columns=("attribute", "value")) self.tree.heading('attribute', text='Attribute', anchor=tk.W) self.tree.heading('value', text='Value', anchor=tk.W) self.configure(border=1, relief=tk.GROOVE) def load_event(self, event): self._clear_tree() for name in self.order_keys(event): node_id = self.tree.insert("", "end") self.tree.set(node_id, "attribute", name) self.tree.set(node_id, "value", event[name]) def order_keys(self, event): return event.keys() class ReplayerCodeView(ttk.Frame): def __init__(self, master): ttk.Frame.__init__(self, master) self.vbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.vbar.grid(row=0, column=2, sticky=tk.NSEW) self.hbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL) self.hbar.grid(row=1, column=0, sticky=tk.NSEW, columnspan=2) self.text = tk.Text(self, yscrollcommand=self.vbar.set, xscrollcommand=self.hbar.set, borderwidth=0, font=get_workbench().get_font("EditorFont"), wrap=tk.NONE, insertwidth=2, #selectborderwidth=2, inactiveselectbackground='gray', #highlightthickness=0, # TODO: try different in Mac and Linux #highlightcolor="gray", padx=5, pady=5, undo=True, autoseparators=False) self.text.grid(row=0, column=1, sticky=tk.NSEW) self.hbar['command'] = self.text.xview self.vbar['command'] = self.text.yview self.columnconfigure(1, weight=1) self.rowconfigure(0, weight=1) class ReplayerEditor(ttk.Frame): def __init__(self, master): ttk.Frame.__init__(self, master) self.code_view = ReplayerCodeView(self) self.code_view.grid(sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) def replay_event(self, event): if event["sequence"] in ["TextInsert", "TextDelete"]: if event["sequence"] == "TextInsert": self.code_view.text.insert(event["index"], event["text"], ast.literal_eval(event["tags"])) elif event["sequence"] == "TextDelete": if event["index2"] and event["index2"] != "None": self.code_view.text.delete(event["index1"], event["index2"]) else: self.code_view.text.delete(event["index1"]) self.see_event(event) def see_event(self, event): for key in ["index", "index1", "index2"]: if key in event and event[key] and event[key] != "None": self.code_view.text.see(event[key]) def reset(self): self.code_view.text.delete("1.0", "end") class ReplayerEditorProper(ReplayerEditor): def __init__(self, master): ReplayerEditor.__init__(self, master) self.set_colorer() def set_colorer(self): # TODO: some problem when doing fast rewind return self.colorer = SyntaxColorer(self.code_view.text, get_workbench().get_font("EditorFont"), get_workbench().get_font("BoldEditorFont")) def replay_event(self, event): ReplayerEditor.replay_event(self, event) # TODO: some problem when doing fast rewind #self.colorer.notify_range("1.0", "end") def reset(self): ReplayerEditor.reset(self) self.set_colorer() class ReplayerEditorNotebook(ttk.Notebook): def __init__(self, master): ttk.Notebook.__init__(self, master, padding=0) self._editors_by_text_widget_id = {} def clear(self): for child in self.winfo_children(): child.destroy() self._editors_by_text_widget_id = {} def get_editor_by_text_widget_id(self, text_widget_id): if text_widget_id not in self._editors_by_text_widget_id: editor = ReplayerEditorProper(self) self.add(editor, text="") self._editors_by_text_widget_id[text_widget_id] = editor return self._editors_by_text_widget_id[text_widget_id] def replay_event(self, event): if "text_widget_id" in event: editor = self.get_editor_by_text_widget_id(event["text_widget_id"]) #print(event.editor_id, id(editor), event) self.select(editor) editor.replay_event(event) if "filename" in event: self.tab(editor, text=os.path.basename(event["filename"])) def reset(self): for editor in self.winfo_children(): self.forget(editor) editor.destroy() self._editors_by_text_widget_id = {} class ShellFrame(ReplayerEditor): def __init__(self, master): ReplayerEditor.__init__(self, master) # TODO: use same source as shell vert_spacing = 10 io_indent = 16 self.code_view.text.tag_configure("toplevel", font=get_workbench().get_font("EditorFont")) self.code_view.text.tag_configure("prompt", foreground="purple", font=get_workbench().get_font("BoldEditorFont")) self.code_view.text.tag_configure("command", foreground="black") self.code_view.text.tag_configure("version", foreground="DarkGray") self.code_view.text.tag_configure("automagic", foreground="DarkGray") self.code_view.text.tag_configure("value", foreground="DarkBlue") # TODO: see also _text_key_press and _text_key_release self.code_view.text.tag_configure("error", foreground="Red") self.code_view.text.tag_configure("io", lmargin1=io_indent, lmargin2=io_indent, rmargin=io_indent, font=get_workbench().get_font("IOFont")) self.code_view.text.tag_configure("stdin", foreground="Blue") self.code_view.text.tag_configure("stdout", foreground="Black") self.code_view.text.tag_configure("stderr", foreground="Red") self.code_view.text.tag_configure("hyperlink", foreground="#3A66DD", underline=True) self.code_view.text.tag_configure("vertically_spaced", spacing1=vert_spacing) self.code_view.text.tag_configure("inactive", foreground="#aaaaaa") def load_plugin(): def open_replayer(): win = ReplayWindow() win.focus_set() win.grab_set() get_workbench().wait_window(win) get_workbench().set_default("tools.replayer_last_browser_folder", None) if (get_workbench().get_option("debug_mode") or get_workbench().get_option("expert_mode")): get_workbench().add_command("open_replayer", "tools", "Open replayer...", open_replayer, group=110) thonny-2.1.16/thonny/plugins/styler.py0000666000000000000000000000662413201264465016161 0ustar 00000000000000# -*- coding: utf-8 -*- from tkinter import ttk from thonny.misc_utils import running_on_linux from thonny.globals import get_workbench _images = set() # for keeping references to tkinter images to avoid garbace colleting them def tweak_notebooks(): style = ttk.Style() theme = style.theme_use() if theme in ["xpnative", "vista"]: get_workbench().get_image('gray_line.gif', "gray_line") style.element_create("gray_line", "image", "gray_line", ("!selected", "gray_line"), height=1, width=10, border=1) style.layout('Tab', [ ('Notebook.tab', {'sticky': 'nswe', 'children': [ ('Notebook.padding', {'sticky': 'nswe', 'side': 'top', 'children': [ ('Notebook.focus', {'sticky': 'nswe', 'side': 'top', 'children': [ ('Notebook.label', {'sticky': '', 'side': 'left'}), ]}) ]}), ('gray_line', {'sticky': 'we', 'side': 'bottom'}), ]}), ]) style.configure("Tab", padding=(4,1,0,0)) if theme == "clam": style.configure("ButtonNotebook.Tab", padding=(6,4,2,3)) else: style.configure("ButtonNotebook.Tab", padding=(4,1,1,3)) if theme == "aqua": style.map("TNotebook.Tab", foreground=[('selected', 'white'), ('!selected', 'black')]) def tweak_treeviews(): style = ttk.Style() # get rid of Treeview borders style.layout("Treeview", [ ('Treeview.treearea', {'sticky': 'nswe'}) ]) # necessary for Python 2.7 TODO: doesn't help for aqua style.configure("Treeview", background="white") #style.configure("Treeview", font='helvetica 14 bold') style.configure("Treeview", font=get_workbench().get_font("TreeviewFont")) #print(style.map("Treeview")) #print(style.layout("Treeview")) #style.configure("Treeview.treearea", font=TREE_FONT) # NB! Some Python or Tk versions (Eg. Py 3.2.3 + Tk 8.5.11 on Raspbian) # can't handle multi word color names in style.map light_blue = "#ADD8E6" light_grey = "#D3D3D3" if running_on_linux(): style.map("Treeview", background=[('selected', 'focus', light_blue), ('selected', '!focus', light_grey), ], foreground=[('selected', 'black'), ], ) else: style.map("Treeview", background=[('selected', 'focus', 'SystemHighlight'), ('selected', '!focus', light_grey), ], foreground=[('selected', 'SystemHighlightText')], ) def tweak_menubuttons(): style = ttk.Style() #print(style.layout("TMenubutton")) style.layout("TMenubutton", [ ('Menubutton.dropdown', {'side': 'right', 'sticky': 'ns'}), ('Menubutton.button', {'children': [ #('Menubutton.padding', {'children': [ ('Menubutton.label', {'sticky': ''}) #], 'expand': '1', 'sticky': 'we'}) ], 'expand': '1', 'sticky': 'nswe'}) ]) style.configure("TMenubutton", padding=14) def tweak_paned_windows(): style = ttk.Style() style.configure("Sash", sashthickness=10) def load_plugin(): tweak_notebooks() tweak_treeviews() tweak_paned_windows() thonny-2.1.16/thonny/plugins/system_shell/0000777000000000000000000000000013201324660016762 5ustar 00000000000000thonny-2.1.16/thonny/plugins/system_shell/explain_environment.py0000666000000000000000000001331413172664305023434 0ustar 00000000000000"""Prints information about how should one run python or pip so that the commands affect same Python installation that is used for running this script""" import os.path import sys import platform import subprocess from shutil import which def _find_commands(logical_command, reference_output, query_arguments, only_best=True): """Returns the commands that can be used for running given conceptual command (python or pip)""" def is_correct_command(command): # Don't try to run the command itself, but first expand it to full path. # The location of parent executable seems to affect command search. full_path = which(command) if full_path is None: return False try: output = subprocess.check_output([full_path] + query_arguments, universal_newlines=True, shell=False) expected = reference_output.strip() actual = output.strip() if platform.system() == "Windows": expected = expected.lower() actual = actual.lower() return expected == actual except: return False correct_commands = set() # first look for short commands for suffix in _get_version_suffixes(): command = logical_command + suffix if is_correct_command(command): if " " in command: command = '"' + command + '"' correct_commands.add(command) if only_best: return list(correct_commands) # if no Python found, then use executable if (len(correct_commands) == 0 and logical_command == "python" and platform.system() != "Windows"): # Unixes tend to use symlinks, not Windows correct_commands.add(sys.executable) if only_best: return list(correct_commands) # if still nothing found, then add full paths if len(correct_commands) == 0: if platform.system() == "Windows": exe_suffix = ".exe" else: exe_suffix = "" folders = [sys.exec_prefix, os.path.join(sys.exec_prefix, "bin"), os.path.join(sys.exec_prefix, "Scripts")] for suffix in _get_version_suffixes(): command = logical_command + suffix for folder in folders: full_command = os.path.join(folder, command) if os.path.exists(full_command + exe_suffix): if " " in full_command: full_command = '"' + full_command + '"' correct_commands.add(full_command) if only_best: return list(correct_commands) return sorted(correct_commands, key=lambda x: len(x)) def _find_python_commands(only_best=True): return _find_commands("python", sys.exec_prefix + "\n" + sys.version, ["-c", "import sys; print(sys.exec_prefix); print(sys.version)"], only_best) def _find_pip_commands(only_best=True): # Asking pip version is quite slow. # Trying a shortcut for common case: # if $(which ) lives in the same dir as current interpreter # and we're using Thonny-private venv, # we can trust the command is the right one. pref_cmd = "pip" + _get_version_suffixes()[0] pref_cmd_path = which(pref_cmd) if pref_cmd_path: pref_cmd_dir = os.path.dirname(pref_cmd_path) current_exe_dir = os.path.dirname(sys.executable) if (pref_cmd_dir == current_exe_dir and os.path.isfile(os.path.join(current_exe_dir, "is_private"))): return [pref_cmd]; # Fallback current_ver_string = _get_pip_version_string() if current_ver_string is not None: commands = _find_commands("pip", current_ver_string, ["--version"], only_best) if len(commands) > 0: return commands else: python_commands = _find_python_commands(True) return [python_commands[0] + " -m pip"] else: return [] def _get_version_suffixes(): major = str(sys.version_info.major) minor = "%d.%d" % (sys.version_info.major, sys.version_info.minor) if platform.system() == "Windows": return ["", major, minor] else: return [major, minor, ""] def _get_pip_version_string(): import io try: import pip except ImportError: return None # capture output original_stdout = sys.stdout try: sys.stdout = io.StringIO() try: pip.main(["--version"]) except SystemExit: pass return sys.stdout.getvalue().strip() finally: sys.stdout = original_stdout def _clear_screen(): if platform.system() == "Windows": os.system("cls") else: os.system("clear") if __name__ == "__main__": _clear_screen() print("*" * 80) print("This session is prepared for using Python %s installation in" % platform.python_version()) print(" ", os.path.realpath(sys.exec_prefix)) print("") print("Command for running the interpreter:") for command in _find_python_commands(True): print(" ", command) print("") print("Command for running pip:") #print(_get_pip_version_string()) pip_commands = _find_pip_commands(True) if len(pip_commands) == 0: print(" ", "") else: for command in pip_commands: print(" ", command) print("") print("*" * 80) thonny-2.1.16/thonny/plugins/system_shell/__init__.py0000666000000000000000000002112613172664305021107 0ustar 00000000000000# -*- coding: utf-8 -*- from subprocess import Popen, check_output import os.path import shlex import platform from tkinter.messagebox import showerror import shutil from thonny.globals import get_runner from thonny import THONNY_USER_DIR import subprocess from time import sleep def _create_pythonless_environment(): # If I want to call another python version, then # I need to remove from environment the items installed by current interpreter env = {} for key in os.environ: if ("python" not in key.lower() and key not in ["TK_LIBRARY", "TCL_LIBRARY"]): env[key] = os.environ[key] return env def _get_exec_prefix(python_interpreter): return check_output([python_interpreter, "-c", "import sys; print(sys.exec_prefix)"], universal_newlines=True, env=_create_pythonless_environment() ).strip() def _add_to_path(directory, path): # Always prepending to path may seem better, but this could mess up other things. # If the directory contains only one Python distribution executables, then # it probably won't be in path yet and therefore will be prepended. if (directory in path.split(os.pathsep) or platform.system() == "Windows" and directory.lower() in path.lower().split(os.pathsep)): return path else: return directory + os.pathsep + path def open_system_shell(): """Main task is to modify path and open terminal window. Bonus (and most difficult) part is executing a script in this window for recommending commands for running given python and related pip""" python_interpreter = get_runner().get_interpreter_command() if python_interpreter is None: return exec_prefix=_get_exec_prefix(python_interpreter) if ".." in exec_prefix: exec_prefix = os.path.realpath(exec_prefix) env = _create_pythonless_environment() # TODO: take care of SSL_CERT_FILE (unset when running external python and set for builtin) # Unset when we're in builtin python and target python is external # TODO: what if executable or explainer needs escaping? # Maybe try creating a script in temp folder and execute this, # passing required paths via environment variables. interpreter=python_interpreter.replace("pythonw","python") explainer=os.path.join(os.path.dirname(__file__), "explain_environment.py") cwd=get_runner().get_cwd() if platform.system() == "Windows": return _open_shell_in_windows(cwd, env, interpreter, explainer, exec_prefix) elif platform.system() == "Linux": return _open_shell_in_linux(cwd, env, interpreter, explainer, exec_prefix) elif platform.system() == "Darwin": _open_shell_in_macos(cwd, env, interpreter, explainer, exec_prefix) else: showerror("Problem", "Don't know how to open system shell on this platform (%s)" % platform.system()) return def _open_shell_in_windows(cwd, env, interpreter, explainer, exec_prefix): env["PATH"] = _add_to_path(exec_prefix + os.pathsep, env.get("PATH", "")) env["PATH"] = _add_to_path(os.path.join(exec_prefix, "Scripts"), env.get("PATH", "")) # Yes, the /K argument has weird quoting. Can't explain this, but it works cmd_line = """start "Shell for {interpreter}" /D "{cwd}" /W cmd /K ""{interpreter}" "{explainer}"" """.format( interpreter=interpreter, cwd=cwd, explainer=explainer) Popen(cmd_line, env=env, shell=True) def _open_shell_in_linux(cwd, env, interpreter, explainer, exec_prefix): def _shellquote(s): return subprocess.list2cmdline([s]) # No escaping in PATH possible: http://stackoverflow.com/a/29213487/261181 # (neither necessary except for colon) env["PATH"] = _add_to_path(os.path.join(exec_prefix, "bin"), env["PATH"]) if shutil.which("x-terminal-emulator"): term_cmd = "x-terminal-emulator" # Can't use konsole, because it doesn't pass on the environment # elif shutil.which("konsole"): # if (shutil.which("gnome-terminal") # and "gnome" in os.environ.get("DESKTOP_SESSION", "").lower()): # term_cmd = "gnome-terminal" # else: # term_cmd = "konsole" elif shutil.which("gnome-terminal"): term_cmd = "gnome-terminal" elif shutil.which("terminal"): # XFCE? term_cmd = "terminal" elif shutil.which("xterm"): term_cmd = "xterm" else: raise RuntimeError("Don't know how to open terminal emulator") # Need to prevent shell from closing after executing the command: # http://stackoverflow.com/a/4466566/261181 core_cmd = "{interpreter} {explainer}; exec bash -i".format(interpreter=_shellquote(interpreter), explainer=_shellquote(explainer)) in_term_cmd = "bash -c {core_cmd}".format(core_cmd=_shellquote(core_cmd)) whole_cmd = "{term_cmd} -e {in_term_cmd}".format(term_cmd=term_cmd, in_term_cmd=_shellquote(in_term_cmd)) Popen(whole_cmd, env=env, shell=True) def _open_shell_in_macos(cwd, env, interpreter, explainer, exec_prefix): _shellquote = shlex.quote # No quoting inside Linux PATH variable is possible: http://stackoverflow.com/a/29213487/261181 # (neither necessary except for colon) # Assuming this applies for OS X as well env["PATH"] = _add_to_path(os.path.join(exec_prefix, "bin"), env["PATH"]) # osascript "tell application" won't change Terminal's env # (at least when Terminal is already active) # At the moment I just explicitly set some important variables # TODO: Did I miss something? cmd = "PATH={}; unset TK_LIBRARY; unset TCL_LIBRARY".format(_shellquote(env["PATH"])) if "SSL_CERT_FILE" in env: cmd += ";export SSL_CERT_FILE=" + _shellquote(env["SSL_CERT_FILE"]) cmd += "; {interpreter} {explainer}".format( interpreter=_shellquote(interpreter), explainer=_shellquote(explainer)) # The script will be sent to Terminal with 'do script' command, which takes a string. # We'll prepare an AppleScript string literal for this # (http://stackoverflow.com/questions/10667800/using-quotes-in-a-applescript-string): cmd_as_apple_script_string_literal = ('"' + cmd .replace("\\", "\\\\") .replace('"', '\\"') + '"') # When Terminal is not open, then do script opens two windows. # do script ... in window 1 would solve this, but if Terminal is already # open, this could run the script in existing terminal (in undesirable env on situation) # That's why I need to prepare two variations of the 'do script' command doScriptCmd1 = """ do script %s """ % cmd_as_apple_script_string_literal doScriptCmd2 = """ do script %s in window 1 """ % cmd_as_apple_script_string_literal # The whole AppleScript will be executed with osascript by giving script # lines as arguments. The lines containing our script need to be shell-quoted: quotedCmd1 = subprocess.list2cmdline([doScriptCmd1]) quotedCmd2 = subprocess.list2cmdline([doScriptCmd2]) # Now we can finally assemble the osascript command line cmd_line = ("osascript" + """ -e 'if application "Terminal" is running then ' """ + """ -e ' tell application "Terminal" ' """ + """ -e """ + quotedCmd1 + """ -e ' activate ' """ + """ -e ' end tell ' """ + """ -e 'else ' """ + """ -e ' tell application "Terminal" ' """ + """ -e """ + quotedCmd2 + """ -e ' activate ' """ + """ -e ' end tell ' """ + """ -e 'end if ' """ ) Popen(cmd_line, env=env, shell=True) def load_plugin(): from thonny.globals import get_workbench def open_system_shell_for_selected_interpreter(): open_system_shell() get_workbench().add_command("OpenSystemShell", "tools", "Open system shell...", open_system_shell_for_selected_interpreter, tester=lambda: "system_shell" in get_runner().supported_features(), group=80) thonny-2.1.16/thonny/plugins/thonny_folders.py0000666000000000000000000000227213172664305017673 0ustar 00000000000000# -*- coding: utf-8 -*- """Adds commands for opening certain Thonny folders""" import os.path from thonny.misc_utils import running_on_mac_os, running_on_linux,\ running_on_windows import subprocess from thonny.globals import get_workbench from thonny import THONNY_USER_DIR def open_path_in_system_file_manager(path): if running_on_mac_os(): # http://stackoverflow.com/a/3520693/261181 # -R doesn't allow showing hidden folders subprocess.Popen(["open", path]) elif running_on_linux(): subprocess.Popen(["xdg-open", path]) else: assert running_on_windows() subprocess.Popen(["explorer", path]) def load_plugin(): def cmd_open_data_dir(): open_path_in_system_file_manager(THONNY_USER_DIR) def cmd_open_program_dir(): open_path_in_system_file_manager(get_workbench().get_package_dir()) get_workbench().add_command("open_program_dir", "tools", "Open Thonny program folder...", cmd_open_program_dir, group=110) get_workbench().add_command("open_data_dir", "tools", "Open Thonny data folder...", cmd_open_data_dir, group=110) thonny-2.1.16/thonny/plugins/variables.py0000666000000000000000000000222313201264465016576 0ustar 00000000000000# -*- coding: utf-8 -*- from thonny.memory import VariablesFrame from thonny.globals import get_workbench, get_runner from thonny.common import InlineCommand class GlobalsView(VariablesFrame): def __init__(self, master): VariablesFrame.__init__(self, master) get_workbench().bind("Globals", self._handle_globals_event, True) get_workbench().bind("BackendRestart", lambda e=None: self._clear_tree(), True) get_workbench().bind("DebuggerProgress", self._request_globals, True) get_workbench().bind("ToplevelResult", self._request_globals, True) def before_show(self): self._request_globals(even_when_hidden=True) def _handle_globals_event(self, event): # TODO: handle other modules as well self.update_variables(event.globals) def _request_globals(self, event=None, even_when_hidden=False): if not getattr(self, "hidden", False) or even_when_hidden: # TODO: module_name get_runner().send_command(InlineCommand("get_globals", module_name="__main__")) def load_plugin(): get_workbench().add_view(GlobalsView, "Variables", "ne")thonny-2.1.16/thonny/plugins/__init__.py0000666000000000000000000000002013172664305016362 0ustar 00000000000000# Package markerthonny-2.1.16/thonny/res/0000777000000000000000000000000013201324660013357 5ustar 00000000000000thonny-2.1.16/thonny/res/16x16_blank.gif0000666000000000000000000000150013172664305016010 0ustar 00000000000000GIF89aiACw %!5!mUku]-!57ߏ% 53%#yoW{a#!;=w!症))#]Wgcyc-)%!瓁sYg?COK#ua#99}{e}#+#kek o w_77'?7yo[=?!雋+'1-#}ekA?{ 3+w}u_-#߯''qY##;=陉)#ic--%!s[?COI#mgm 79'?7{!#}g!,H*\ȰÇ#JHŁ;thonny-2.1.16/thonny/res/1x1_white.gif0000666000000000000000000000144713172664305015677 0ustar 00000000000000GIF89a3f++3+f+++UU3UfUUU3f3f3fՙ3f3333f3333+3+33+f3+3+3+3U3U33Uf3U3U3U3333f3333333f3333333f3ՙ333333f333ff3ffffff+f+3f+ff+f+f+fUfU3fUffUfUfUff3fffffff3fffffff3fffՙffff3fffff3f̙++3+f++̙+UU3UfUU̙U3f̙3f̙3fՙ̙3f̙3f++3+f+++UU3UfUUÙ̀3̀f̪̪̀̀̀3̪f̪̪̪3fՙ3f3f++3+f+++UU3UfUUU3f3f3fՙ3f!,;thonny-2.1.16/thonny/res/arrow_down2.gif0000666000000000000000000000150513172664305016324 0ustar 00000000000000GIF89aljl!,"X8p 2LC/bTH0 ;thonny-2.1.16/thonny/res/class.gif0000666000000000000000000000024413172664305015165 0ustar 00000000000000GIF89a??_?_???_?߿_ߟ! ,QPIX@Gc0XG J)!h\%%Bz?%FX @ q;thonny-2.1.16/thonny/res/closed_folder.gif0000666000000000000000000000212713172664305016666 0ustar 00000000000000GIF89acf]afkm lql prlp sw yx|~y}|"$  (.('), -Ï*/Ǝ-44˒'ӏ.̓9ϔ,ė2˖0ř0̗1Ж2ƚ/ə7Θ4͘<͙6Ә2Ý2ԙ51Ȟ2ؚ5՛8ʟ7ߨNݴKWgmWs`cuqwflvrmww~ru߃s{}yv|!Created with GIMP! ,]*fl.anµ2>jdץ\Ei-W``"R2X"V,Z5BT"OhkYMÇ *Xj ;qЙsXe4MjY6grq4lH@a )3D*HbyYu` $. 1[`Y 1h%1h`"$CM7n($`Y!"@r*O3`Y" 592$%FT`  < $ p[h =ŊT'Q$aRkYS]5RS.( ;thonny-2.1.16/thonny/res/file.new_file.gif0000666000000000000000000000163013172664305016566 0ustar 00000000000000GIF89aԤԤ䔪4Jd䴺䤪!,uH@ .x0@HAPA l ɐ#I, + &Yé FlpS`v - !hN|YK uk%M ,;thonny-2.1.16/thonny/res/file.open_file.gif0000666000000000000000000000175113172664305016742 0ustar 00000000000000GIF89adddJ”4zdt4dj$4*Բt$ĒTJDT„ڄ42$TTjrTdZDԊDԪTz$4"$IJԒ҄$dztbTĊttdJ$ҤD4tr$ԒTʄڄD:4$tbD4*$ĺtz!,HA$P A0QE[`0C"ARFA(1X +VL0c`AZXHbpI -,9…W.`"  ,u+81AU &(`` BA !(8 >$H]@a-# PpX;thonny-2.1.16/thonny/res/file.save_file.gif0000666000000000000000000000176013172664305016737 0ustar 00000000000000GIF89atD:dj$JTbtrDJ4BDTR:dbTRTDRĤtz䴺4:䄊DJTZ$2Ddz4RdjtzTR4:tTZdjTZt4BDBԔ4JdbTJ$Bz䔒42dtzzD:tdZtjTbtĺDB!,H@08p@ (`G2j<ƒ [pؘQńA< H`beMBnjIbÆj*F@@- PAST-B /(0X%j]A$ PA Vh#A""!aF@4vhDL,P`'NM0 ;thonny-2.1.16/thonny/res/folder.gif0000666000000000000000000000060213172664305015331 0ustar 00000000000000GIF89a5GWƮtˮgȯu̳xζz׾sÄ{|Ȉ|}~~̀΁΁ρς҃҄ԅӃՅև׆ׇوڈ܉܊ۈߋߋ!Created with GIMP! ?,_A,Hd P#;hLv;Mh+˥d0k4I]RJ"` T'&# z|~x $5I!rz{? |r ZHzTTHA;thonny-2.1.16/thonny/res/generic_file.gif0000666000000000000000000000035613172664305016477 0ustar 00000000000000GIF89a!Created with GIMP! ,Vৌ$|hHl+) *PFE=3' &%$   ;thonny-2.1.16/thonny/res/hard_drive2.gif0000666000000000000000000000177313172664305016261 0ustar 00000000000000GIF89a  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~!Created with GIMP! , HVMd~(۸xRCMpR~M"y&cȖA3tP[nܶkV'Y7dڕ -Z`fX7a"IDiHZzWAQER)#Sիbb*SȾJ7MZ4hΚ5[LY2dX4!@zQS&-ZV;thonny-2.1.16/thonny/res/method.gif0000666000000000000000000000110213172664305015332 0ustar 00000000000000GIF89ahtɂЉӋE}MKNMQNRSS]`_badffnnv{~}͂ψґ֓זחךڨ۩۫ݱNORUUVW[]b_a۰߲!A,A??$$$4 &>& 4 -- +@@+"5*@@==;6 '@,@,@' 0(@//,.ɂ"3)@@:2%9=9<7# pah bX@я`H ŋ;thonny-2.1.16/thonny/res/open_folder.gif0000666000000000000000000000212413172664305016353 0ustar 00000000000000GIF89amonq ts ty uuvt yw#~x*–˔/ƕȕɔ ĖȖŘĚ Řř˘ɘ!ǚɛƜɜ Ǜ˚#͜"&Ş  ќʟО!Р ӡΥ%ܱABڹF̽V;޻iRW^`jhamluԊjn{z~rvvyu|~z~!Created with GIMP,QFnYcdR;AcVL\Z@bUN6pґ6`Tܬdblf0Ҕ)R'R+`„_NZ4(?֮J0X2 8 EHQ8QrESVOO6%H(y´ !NP qQ"EB芔AF؈R ?t챃g(`2#)5w䴙ƇR &1\tѲB%x @p`A4ό%%a#D(A¤ULVٴ9\5i;thonny-2.1.16/thonny/res/python_file.gif0000666000000000000000000000116213172664305016400 0ustar 00000000000000GIF89ao2l3l6p6q7r9u:v;w;x=y>{?}@}E}@~DDHMbUZ^fcÒltw͌ˠźϼMNPژRSY\TUWaX`Z\_]o`abdr}r!Created with GIMP! ,VJKORVWWXYZ`KGHD\PIK  ^``dTKMNV[bddgX& fCSiY e>Bƃ]QfgcF79l_ UE?<83.@`# f=:51+/Lnaa72jڬAC [Z$$'ܤ Q[6lpqFC3g>tDC0a;thonny-2.1.16/thonny/res/python_icon.gif0000666000000000000000000000203113172664305016405 0ustar 00000000000000GIF89a'Np'Oq(Oq(Or(Os(Pt)Ps*Pq*Ps)Qt)Qu-Ql*Rv+Ru,Ru+Sw-St-Su0Tk.Tv.Uy.V{0Vy1W{:Yd7\|8]}F_Z>a:dAe=nKoJqKwKyP}X~aZZ`_cf`;lmnoītrysuȻ{}zĠĠšššġšŢƢƣƣ ǣǤǤǤĤǤ Ȧɦ ɧǧɧɧɨɨȨċ˪˪̋ԏͭϰ%זҕؙ՗ۜոڝԹBݡ֢ۿ#ڿ,ڬ9==\QNL1TZU([/DN75Y]`Bc7a]cdfZ]Na`fa_\dXad`bex!Created with GIMP! ,  *:qX8p 8p3)2iH"VTh'K Jɿ %RD)ԤG-zD'@0ԉ0[P1PIK(U(sNFDH@E€;thonny-2.1.16/thonny/res/run.debug_current_script.gif0000666000000000000000000000166613172664305021110 0ustar 00000000000000GIF89a 92#>2#A2(A9(E9(H=-H=-N=-NE1NE1NJ1VJ8VN8VSbycxcydxey ey.j""fz!gy$hz'j{'kz*m{*m|+n|,o{-p|:|+S4tdJ$DTZdttrtTTZTTbĴTRDtd4„djddrDdR4d䴲􄊔D244tڤTB4dbdtzdbTTjTRTdҔdjtdz!Created with GIMP! ?,spH,G#DXJh$N"rD,BH]l!g4$G\.U9g]$ x `C/ .BX L (Cc . =O?,DA;thonny-2.1.16/thonny/res/run.step_into.gif0000666000000000000000000000030513172664305016665 0ustar 00000000000000GIF89a:C_g|VOY_}Һwŏϳxm!,B$dihZ>l{"O<$ZV h6!bF U mW2Y>;thonny-2.1.16/thonny/res/run.step_out.gif0000666000000000000000000000161213172664305016525 0ustar 00000000000000GIF89a:C_g|VOY_}Һwŏ~|srzvxnp!,g5H0` j  dHA ( ,8!,XH`% .h@ T@@A *B@XZPAիXG;thonny-2.1.16/thonny/res/run.step_over.gif0000666000000000000000000000032313172664305016667 0ustar 00000000000000GIF89a:C_g|VOY_ҺwĊŏاϳ.ŗ:xm!,P`%dihjұFbNDG$/WB1 ?H 2T 2:4&KW%&MQ WU9K@@0zB;thonny-2.1.16/thonny/res/run.stop.gif0000666000000000000000000000200413172664305015644 0ustar 00000000000000GIF89aiACw %!5!mUku]-!57ߏ% 53%#yoW{a#!;=w!症))#]Wgcyc-)%!瓁sYg?COK#ua#99}{e}#+#kek o w_77'?7yo[=?!雋+'1-#}ekA?{ 3+w}u_-#߯''qY##;=陉)#ic--%!s[?COI#mgm 79'?7{!#}g!,@$ aH1a/3,Xb 𠃉:LPMԩCeL^@ h!SZ@3H5#,N> HT'-&Hp%U]P"Fa^iiPPԪW~ܐb  |QHG S:,0AhCg:pc Yc G ȸaˇI1;#52>!GÀ;thonny-2.1.16/thonny/res/tab_close.gif0000666000000000000000000000150613172664305016015 0ustar 00000000000000GIF89a p!, 3f++3+f+++UU3UfUUU3f3f3fՙ3f3333f3333+3+33+f3+3+3+3U3U33Uf3U3U3U3333f3333333f3333333f3ՙ333333f333ff3ffffff+f+3f+ff+f+f+fUfU3fUffUfUfUff3fffffff3fffffff3fffՙffff3fffff3f̙++3+f++̙+UU3UfUU̙U3f̙3f̙3fՙ̙3f̙3f++3+f+++UU3UfUUÙ̀3̀f̪̪̀̀̀3̪f̪̪̪3fՙ3f3f++3+f+++UU3UfUUU3f3f3fՙ3f# H&$HL`CTC1>c.k.k/k?e=f6lDj9rCoHoOqNtMvRvD}L{SwQxTxUxSyTyUyTyVyWyTzR{VzWzXzOZzZzX{LZ{N[{Z{U}Z|\|Z|[|[~[~]URW_Zhccorsty€ʆϊьЕŘəȘ͝ʝ͞ˡ̞ΠϣͣΦϧΩѭּ޹پ!Created with GIMP! ,S_^WUQLB`vyrmgbM<.]wztplfVOaZw \|XYw|[keDTw +3@IR{qd Kw%&/761Hc Gзun Ew'#$,=>Ni yoFlE2,@Asѐ}i((ȝ8tAF%j(q%K ;thonny-2.1.16/thonny/res/thonny.ico0000666000000000000000000004041413172664305015407 0ustar 0000000000000000F  h) ,(0`VVVUUUTTT???~~~ttt222EEEdddyyyUUUjjj޵888aaa^^^oooDDDmmm444ooozzz]]]WWWuuu555CCCaaa>>>dddwww@@@;;;MMM:::dddgggSSSmmmCCC999ccckkkTTTAAAAAAQQQ```TTTYYYggg999nnnQQQfff111qqqBBBrrrHHH444UUUVVVEEEmmmoooBBBTTT222MMMQQQ𶶶www333JJJttt뙙VVVIII...aaa~~~hhhIIIrrr===UUU}}}IIIJJJlll~~~pppPPPUUU{{{YYYOOO@@@|||YYY⹹999NNN|||{{{QQQ<<<]]]```uuurrrrrrjjjQQQ<<<333333888AAA;;;~~~---===FFF[[[eee~~~bbbǵӼhhhfffaaaEEEHHHQQQvvvbbb񈈈eeepppYYYQQQOOOQQQbbbkkk[[[TTTXXXhhhkkkPPP___...KKK????????????????( @ ;;;;;;;;;;;;;;;;;;mmmmmmiiimmmiiizzziiizzzhhhzzzdddzzzdddzzzAAAzzzdddzzzdddzzz___zzzdddzzzdddzzzdddzzzdddzzzzzzdddzzzdddzzzfffdddyyydddzzzssssssssssssssssssfffdddzzzzzzdddzzzmmmjjjjjjjjjjjjjjjmmmdddzzziiizzzfffdddzzzdddzzz:::dddzzzdddzzzdddzzzhhhzzzdddyyyfffdddzzz::::::dddzzzdddzzzhhhzzz888zzz;;;mmmssssssssssssssssssssssssssssssssssssssssssfff888;;;mmmjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjmmm?    ( @???PNG  IHDR\rfIDATx_ř cYb;S`<]dp:v<89 y#$:.ҝtIĂxPS#~u޾FǕ:O:.q|vhS|$6b@^CT}2ʕ+Zl?~\MNNnslIJAD#:=TpyՖ-[ڵkՒ%K*ӧ޽{:v{"$ta:iYJ&&&pG_l.ul.٨W^mVzYu!ϫSN޿rJbs }gSw}J ccUۧި; _|QY! Br2P|rsNqFh"WFpEMPM[~ft`kBI@XוAN hko]]qEHޙMٯf*Dw$TɎaÆ9A۷`94h']u<{ M!T"x饗J=fhO6o9/@_ӱ[ RB;N `Yuv}K,58"@2AB@i !'C@`u@ҹ_=8dIqPPF`:+g:}Yʟs?gXT3i@m {ZzO/ge"+V|`P+6@j#On/,H oAyɤҷ࣏>R\rI'jؓo[nԓ'{zgJ=Bؓc-ARG@ {D'@dRx=N((M2z[vFYG#cb:r {RrRxDn"M:' l/GѦ;:=8 .wz? طh"ɥC9 KߓjjjLwd { PS~c {r)P 3,k-Ɖ 5<*W80 2 :K :k0 `GGu С  2Nt :8\ Wu A `A3#*@D#W@xg;Y9^H"ؑ@' *@D)4"3X~@= 2M)YA  `GS.Y {@d ;R@ct)@d ;@1 2ŐD"A P )@d ;@1 S{ᓭ>3j4RbDdֱIǭ:nбL4Nz̢*y[g[|_ǫ:8I `Ŋر,?' #K)pݼ|$V@Ez"ԲeJ=^J'''THktkƥ4h'W[lQk׮UKٳСCjǎw)/&8?`q AVZo߮e] +j֭C+Ѹ@n.l";:Ez+gΜQ<:p@:Qͬ@vdlL5\V^~auM7նϧ|/`Gf]J=>i!>|aLNju+ur@;wN v@<u+u TJH@~0'.urn*ϟ[ou{.wo9u+c]@!̅ O. }'.[hϞ=;0ĉj͚5R) {@dv .[T:!_~j޽jtt4 -cҥFO0`@ijXi`2vak}cӪF'OT^{%V@ybQzk tUf``@>|Ǹ{_^D7@ybQzؚzUV@y2םM6Jf7@y2gr)g}}}]Ȥnzd|GYgpU&! 0#3|||\t}B fMQpM(B`F\ޓ4 <111F9L8&#^lj'Ԛ5kok!dgzS6lP:uJ۷OvڄXlr,!! 0:~M8&'dKBrm ;^Աt ##& 6Rm򬎻 {TNbޑ&@nl񨎫{<5pœ܃Xw& ;F?q0Z:>=ľ#5M "bޑ$@'ؑ$ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 agKYIENDB`thonny-2.1.16/thonny/res/thonny.png0000666000000000000000000001201613172664305015416 0ustar 00000000000000PNG  IHDR\rfIDATx_ř cYb;S`<]dp:v<89 y#$:.ҝtIĂxPS#~u޾FǕ:O:.q|vhS|$6b@^CT}2ʕ+Zl?~\MNNnslIJAD#:=TpyՖ-[ڵkՒ%K*ӧ޽{:v{"$ta:iYJ&&&pG_l.ul.٨W^mVzYu!ϫSN޿rJbs }gSw}J ccUۧި; _|QY! Br2P|rsNqFh"WFpEMPM[~ft`kBI@XוAN hko]]qEHޙMٯf*Dw$TɎaÆ9A۷`94h']u<{ M!T"x饗J=fhO6o9/@_ӱ[ RB;N `Yuv}K,58"@2AB@i !'C@`u@ҹ_=8dIqPPF`:+g:}Yʟs?gXT3i@m {ZzO/ge"+V|`P+6@j#On/,H oAyɤҷ࣏>R\rI'jؓo[nԓ'{zgJ=Bؓc-ARG@ {D'@dRx=N((M2z[vFYG#cb:r {RrRxDn"M:' l/GѦ;:=8 .wz? طh"ɥC9 KߓjjjLwd { PS~c {r)P 3,k-Ɖ 5<*W80 2 :K :k0 `GGu С  2Nt :8\ Wu A `A3#*@D#W@xg;Y9^H"ؑ@' *@D)4"3X~@= 2M)YA  `GS.Y {@d ;R@ct)@d ;@1 2ŐD"A P )@d ;@1 S{ᓭ>3j4RbDdֱIǭ:nбL4Nz̢*y[g[|_ǫ:8I `Ŋر,?' #K)pݼ|$V@Ez"ԲeJ=^J'''THktkƥ4h'W[lQk׮UKٳСCjǎw)/&8?`q AVZo߮e] +j֭C+Ѹ@n.l";:Ez+gΜQ<:p@:Qͬ@vdlL5\V^~auM7նϧ|/`Gf]J=>i!>|aLNju+ur@;wN v@<u+u TJH@~0'.urn*ϟ[ou{.wo9u+c]@!̅ O. }'.[hϞ=;0ĉj͚5R) {@dv .[T:!_~j޽jtt4 -cҥFO0`@ijXi`2vak}cӪF'OT^{%V@ybQzk tUf``@>|Ǹ{_^D7@ybQzؚzUV@y2םM6Jf7@y2gr)g}}}]Ȥnzd|GYgpU&! 0#3|||\t}B fMQpM(B`F\ޓ4 <111F9L8&#^lj'Ԛ5kok!dgzS6lP:uJ۷OvڄXlr,!! 0:~M8&'dKBrm ;^Աt ##& 6Rm򬎻 {TNbޑ&@nl񨎫{<5pœ܃Xw& ;F?q0Z:>=ľ#5M "bޑ$@'ؑ$ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 a@ A $ H0 agKYIENDB`thonny-2.1.16/thonny/res/thonny_small.ico0000666000000000000000000000157613172664305016605 0ustar 00000000000000h( ``???thonny-2.1.16/thonny/roughparse.py0000666000000000000000000010134413172664305015334 0ustar 00000000000000"""Facilities for learning the structure of incomplete Python code Mostly copied/adapted from idlelib.HyperParser and idlelib.PyParse """ import re import sys from collections import Mapping import string from keyword import iskeyword NUM_CONTEXT_LINES = (50, 500, 5000000) # Reason last stmt is continued (or C_NONE if it's not). (C_NONE, C_BACKSLASH, C_STRING_FIRST_LINE, C_STRING_NEXT_LINES, C_BRACKET) = range(5) if 0: # for throwaway debugging output def dump(*stuff): sys.__stdout__.write(" ".join(map(str, stuff)) + "\n") # Find what looks like the start of a popular stmt. _synchre = re.compile(r""" ^ [ \t]* (?: while | else | def | return | assert | break | class | continue | elif | try | except | raise | import | yield ) \b """, re.VERBOSE | re.MULTILINE).search # Match blank line or non-indenting comment line. _junkre = re.compile(r""" [ \t]* (?: \# \S .* )? \n """, re.VERBOSE).match # Match any flavor of string; the terminating quote is optional # so that we're robust in the face of incomplete program text. _match_stringre = re.compile(r""" \""" [^"\\]* (?: (?: \\. | "(?!"") ) [^"\\]* )* (?: \""" )? | " [^"\\\n]* (?: \\. [^"\\\n]* )* "? | ''' [^'\\]* (?: (?: \\. | '(?!'') ) [^'\\]* )* (?: ''' )? | ' [^'\\\n]* (?: \\. [^'\\\n]* )* '? """, re.VERBOSE | re.DOTALL).match # Match a line that starts with something interesting; # used to find the first item of a bracket structure. _itemre = re.compile(r""" [ \t]* [^\s#\\] # if we match, m.end()-1 is the interesting char """, re.VERBOSE).match # Match start of stmts that should be followed by a dedent. _closere = re.compile(r""" \s* (?: return | break | continue | raise | pass ) \b """, re.VERBOSE).match # Chew up non-special chars as quickly as possible. If match is # successful, m.end() less 1 is the index of the last boring char # matched. If match is unsuccessful, the string starts with an # interesting char. _chew_ordinaryre = re.compile(r""" [^[\](){}#'"\\]+ """, re.VERBOSE).match class StringTranslatePseudoMapping(Mapping): r"""Utility class to be used with str.translate() This Mapping class wraps a given dict. When a value for a key is requested via __getitem__() or get(), the key is looked up in the given dict. If found there, the value from the dict is returned. Otherwise, the default value given upon initialization is returned. This allows using str.translate() to make some replacements, and to replace all characters for which no replacement was specified with a given character instead of leaving them as-is. For example, to replace everything except whitespace with 'x': >>> whitespace_chars = ' \t\n\r' >>> preserve_dict = {ord(c): ord(c) for c in whitespace_chars} >>> mapping = StringTranslatePseudoMapping(preserve_dict, ord('x')) >>> text = "a + b\tc\nd" >>> text.translate(mapping) 'x x x\tx\nx' """ def __init__(self, non_defaults, default_value): self._non_defaults = non_defaults self._default_value = default_value def _get(key, _get=non_defaults.get, _default=default_value): return _get(key, _default) self._get = _get def __getitem__(self, item): return self._get(item) def __len__(self): return len(self._non_defaults) def __iter__(self): return iter(self._non_defaults) def get(self, key, default=None): return self._get(key) class RoughParser: def __init__(self, indentwidth, tabwidth): self.indentwidth = indentwidth self.tabwidth = tabwidth def set_str(self, s): assert len(s) == 0 or s[-1] == '\n' self.str = s self.study_level = 0 # Return index of a good place to begin parsing, as close to the # end of the string as possible. This will be the start of some # popular stmt like "if" or "def". Return None if none found: # the caller should pass more prior context then, if possible, or # if not (the entire program text up until the point of interest # has already been tried) pass 0 to set_lo. # # This will be reliable iff given a reliable is_char_in_string # function, meaning that when it says "no", it's absolutely # guaranteed that the char is not in a string. def find_good_parse_start(self, is_char_in_string=None, _synchre=_synchre): str, pos = self.str, None # @ReservedAssignment if not is_char_in_string: # no clue -- make the caller pass everything return None # Peek back from the end for a good place to start, # but don't try too often; pos will be left None, or # bumped to a legitimate synch point. limit = len(str) for tries in range(5): # @UnusedVariable i = str.rfind(":\n", 0, limit) if i < 0: break i = str.rfind('\n', 0, i) + 1 # start of colon line m = _synchre(str, i, limit) if m and not is_char_in_string(m.start()): pos = m.start() break limit = i if pos is None: # Nothing looks like a block-opener, or stuff does # but is_char_in_string keeps returning true; most likely # we're in or near a giant string, the colorizer hasn't # caught up enough to be helpful, or there simply *aren't* # any interesting stmts. In any of these cases we're # going to have to parse the whole thing to be sure, so # give it one last try from the start, but stop wasting # time here regardless of the outcome. m = _synchre(str) if m and not is_char_in_string(m.start()): pos = m.start() return pos # Peeking back worked; look forward until _synchre no longer # matches. i = pos + 1 while 1: m = _synchre(str, i) if m: s, i = m.span() if not is_char_in_string(s): pos = s else: break return pos # Throw away the start of the string. Intended to be called with # find_good_parse_start's result. def set_lo(self, lo): assert lo == 0 or self.str[lo-1] == '\n' if lo > 0: self.str = self.str[lo:] # Build a translation table to map uninteresting chars to 'x', open # brackets to '(', close brackets to ')' while preserving quotes, # backslashes, newlines and hashes. This is to be passed to # str.translate() in _study1(). _tran = {} _tran.update((ord(c), ord('(')) for c in "({[") _tran.update((ord(c), ord(')')) for c in ")}]") _tran.update((ord(c), ord(c)) for c in "\"'\\\n#") _tran = StringTranslatePseudoMapping(_tran, default_value=ord('x')) # As quickly as humanly possible , find the line numbers (0- # based) of the non-continuation lines. # Creates self.{goodlines, continuation}. def _study1(self): if self.study_level >= 1: return self.study_level = 1 # Map all uninteresting characters to "x", all open brackets # to "(", all close brackets to ")", then collapse runs of # uninteresting characters. This can cut the number of chars # by a factor of 10-40, and so greatly speed the following loop. str = (self.str # @ReservedAssignment .translate(self._tran) .replace('xxxxxxxx', 'x') .replace('xxxx', 'x') .replace('xx', 'x') .replace('xx', 'x') .replace('\nx', '\n')) # note that replacing x\n with \n would be incorrect, because # x may be preceded by a backslash # March over the squashed version of the program, accumulating # the line numbers of non-continued stmts, and determining # whether & why the last stmt is a continuation. continuation = C_NONE level = lno = 0 # level is nesting level; lno is line number self.goodlines = goodlines = [0] push_good = goodlines.append i, n = 0, len(str) while i < n: ch = str[i] i = i+1 # cases are checked in decreasing order of frequency if ch == 'x': continue if ch == '\n': lno = lno + 1 if level == 0: push_good(lno) # else we're in an unclosed bracket structure continue if ch == '(': level = level + 1 continue if ch == ')': if level: level = level - 1 # else the program is invalid, but we can't complain continue if ch == '"' or ch == "'": # consume the string quote = ch if str[i-1:i+2] == quote * 3: quote = quote * 3 firstlno = lno w = len(quote) - 1 i = i+w while i < n: ch = str[i] i = i+1 if ch == 'x': continue if str[i-1:i+w] == quote: i = i+w break if ch == '\n': lno = lno + 1 if w == 0: # unterminated single-quoted string if level == 0: push_good(lno) break continue if ch == '\\': assert i < n if str[i] == '\n': lno = lno + 1 i = i+1 continue # else comment char or paren inside string else: # didn't break out of the loop, so we're still # inside a string if (lno - 1) == firstlno: # before the previous \n in str, we were in the first # line of the string continuation = C_STRING_FIRST_LINE else: continuation = C_STRING_NEXT_LINES continue # with outer loop if ch == '#': # consume the comment i = str.find('\n', i) assert i >= 0 continue assert ch == '\\' assert i < n if str[i] == '\n': lno = lno + 1 if i+1 == n: continuation = C_BACKSLASH i = i+1 # The last stmt may be continued for all 3 reasons. # String continuation takes precedence over bracket # continuation, which beats backslash continuation. if (continuation != C_STRING_FIRST_LINE and continuation != C_STRING_NEXT_LINES and level > 0): continuation = C_BRACKET self.continuation = continuation # Push the final line number as a sentinel value, regardless of # whether it's continued. assert (continuation == C_NONE) == (goodlines[-1] == lno) if goodlines[-1] != lno: push_good(lno) def get_continuation_type(self): self._study1() return self.continuation # study1 was sufficient to determine the continuation status, # but doing more requires looking at every character. study2 # does this for the last interesting statement in the block. # Creates: # self.stmt_start, stmt_end # slice indices of last interesting stmt # self.stmt_bracketing # the bracketing structure of the last interesting stmt; # for example, for the statement "say(boo) or die", stmt_bracketing # will be [(0, 0), (3, 1), (8, 0)]. Strings and comments are # treated as brackets, for the matter. # self.lastch # last non-whitespace character before optional trailing # comment # self.lastopenbracketpos # if continuation is C_BRACKET, index of last open bracket def _study2(self): if self.study_level >= 2: return self._study1() self.study_level = 2 # Set p and q to slice indices of last interesting stmt. str, goodlines = self.str, self.goodlines # @ReservedAssignment i = len(goodlines) - 1 p = len(str) # index of newest line while i: assert p # p is the index of the stmt at line number goodlines[i]. # Move p back to the stmt at line number goodlines[i-1]. q = p for nothing in range(goodlines[i-1], goodlines[i]): # @UnusedVariable # tricky: sets p to 0 if no preceding newline p = str.rfind('\n', 0, p-1) + 1 # The stmt str[p:q] isn't a continuation, but may be blank # or a non-indenting comment line. if _junkre(str, p): i = i-1 else: break if i == 0: # nothing but junk! assert p == 0 q = p self.stmt_start, self.stmt_end = p, q # Analyze this stmt, to find the last open bracket (if any) # and last interesting character (if any). lastch = "" stack = [] # stack of open bracket indices push_stack = stack.append bracketing = [(p, 0)] while p < q: # suck up all except ()[]{}'"#\\ m = _chew_ordinaryre(str, p, q) if m: # we skipped at least one boring char newp = m.end() # back up over totally boring whitespace i = newp - 1 # index of last boring char while i >= p and str[i] in " \t\n": i = i-1 if i >= p: lastch = str[i] p = newp if p >= q: break ch = str[p] if ch in "([{": push_stack(p) bracketing.append((p, len(stack))) lastch = ch p = p+1 continue if ch in ")]}": if stack: del stack[-1] lastch = ch p = p+1 bracketing.append((p, len(stack))) continue if ch == '"' or ch == "'": # consume string # Note that study1 did this with a Python loop, but # we use a regexp here; the reason is speed in both # cases; the string may be huge, but study1 pre-squashed # strings to a couple of characters per line. study1 # also needed to keep track of newlines, and we don't # have to. bracketing.append((p, len(stack)+1)) lastch = ch p = _match_stringre(str, p, q).end() bracketing.append((p, len(stack))) continue if ch == '#': # consume comment and trailing newline bracketing.append((p, len(stack)+1)) p = str.find('\n', p, q) + 1 assert p > 0 bracketing.append((p, len(stack))) continue assert ch == '\\' p = p+1 # beyond backslash assert p < q if str[p] != '\n': # the program is invalid, but can't complain lastch = ch + str[p] p = p+1 # beyond escaped char # end while p < q: self.lastch = lastch if stack: self.lastopenbracketpos = stack[-1] self.stmt_bracketing = tuple(bracketing) # Assuming continuation is C_BRACKET, return the number # of spaces the next line should be indented. def compute_bracket_indent(self): self._study2() assert self.continuation == C_BRACKET j = self.lastopenbracketpos str = self.str # @ReservedAssignment n = len(str) origi = i = str.rfind('\n', 0, j) + 1 j = j+1 # one beyond open bracket # find first list item; set i to start of its line while j < n: m = _itemre(str, j) if m: j = m.end() - 1 # index of first interesting char extra = 0 break else: # this line is junk; advance to next line i = j = str.find('\n', j) + 1 else: # nothing interesting follows the bracket; # reproduce the bracket line's indentation + a level j = i = origi while str[j] in " \t": j = j+1 extra = self.indentwidth return len(str[i:j].expandtabs(self.tabwidth)) + extra # Return number of physical lines in last stmt (whether or not # it's an interesting stmt! this is intended to be called when # continuation is C_BACKSLASH). def get_num_lines_in_stmt(self): self._study1() goodlines = self.goodlines return goodlines[-1] - goodlines[-2] # Assuming continuation is C_BACKSLASH, return the number of spaces # the next line should be indented. Also assuming the new line is # the first one following the initial line of the stmt. def compute_backslash_indent(self): self._study2() assert self.continuation == C_BACKSLASH str = self.str # @ReservedAssignment i = self.stmt_start while str[i] in " \t": i = i+1 startpos = i # See whether the initial line starts an assignment stmt; i.e., # look for an = operator endpos = str.find('\n', startpos) + 1 found = level = 0 while i < endpos: ch = str[i] if ch in "([{": level = level + 1 i = i+1 elif ch in ")]}": if level: level = level - 1 i = i+1 elif ch == '"' or ch == "'": i = _match_stringre(str, i, endpos).end() elif ch == '#': break elif level == 0 and ch == '=' and \ (i == 0 or str[i-1] not in "=<>!") and \ str[i+1] != '=': found = 1 break else: i = i+1 if found: # found a legit =, but it may be the last interesting # thing on the line i = i+1 # move beyond the = found = re.match(r"\s*\\", str[i:endpos]) is None if not found: # oh well ... settle for moving beyond the first chunk # of non-whitespace chars i = startpos while str[i] not in " \t\n": i = i+1 return len(str[self.stmt_start:i].expandtabs(\ self.tabwidth)) + 1 # Return the leading whitespace on the initial line of the last # interesting stmt. def get_base_indent_string(self): self._study2() i, n = self.stmt_start, self.stmt_end j = i str_ = self.str while j < n and str_[j] in " \t": j = j + 1 return str_[i:j] # Did the last interesting stmt open a block? def is_block_opener(self): self._study2() return self.lastch == ':' # Did the last interesting stmt close a block? def is_block_closer(self): self._study2() return _closere(self.str, self.stmt_start) is not None # index of last open bracket ({[, or None if none lastopenbracketpos = None def get_last_open_bracket_pos(self): self._study2() return self.lastopenbracketpos # the structure of the bracketing of the last interesting statement, # in the format defined in _study2, or None if the text didn't contain # anything stmt_bracketing = None def get_last_stmt_bracketing(self): self._study2() return self.stmt_bracketing # all ASCII chars that may be in an identifier _ASCII_ID_CHARS = frozenset(string.ascii_letters + string.digits + "_") # all ASCII chars that may be the first char of an identifier _ASCII_ID_FIRST_CHARS = frozenset(string.ascii_letters + "_") # lookup table for whether 7-bit ASCII chars are valid in a Python identifier _IS_ASCII_ID_CHAR = [(chr(x) in _ASCII_ID_CHARS) for x in range(128)] # lookup table for whether 7-bit ASCII chars are valid as the first # char in a Python identifier _IS_ASCII_ID_FIRST_CHAR = \ [(chr(x) in _ASCII_ID_FIRST_CHARS) for x in range(128)] class HyperParser: """Provide advanced parsing abilities for ParenMatch and other extensions. HyperParser uses PyParser. PyParser mostly gives information on the proper indentation of code. HyperParser gives additional information on the structure of code. """ def __init__(self, text, index): "To initialize, analyze the surroundings of the given index." self.text = text parser = RoughParser(text.indentwidth, text.tabwidth) def index2line(index): return int(float(index)) lno = index2line(text.index(index)) for context in NUM_CONTEXT_LINES: startat = max(lno - context, 1) startatindex = repr(startat) + ".0" stopatindex = "%d.end" % lno # We add the newline because PyParse requires a newline # at end. We add a space so that index won't be at end # of line, so that its status will be the same as the # char before it, if should. parser.set_str(text.get(startatindex, stopatindex)+' \n') bod = parser.find_good_parse_start( _build_char_in_string_func(startatindex)) if bod is not None or startat == 1: break parser.set_lo(bod or 0) # We want what the parser has, minus the last newline and space. self.rawtext = parser.str[:-2] # Parser.str apparently preserves the statement we are in, so # that stopatindex can be used to synchronize the string with # the text box indices. self.stopatindex = stopatindex self.bracketing = parser.get_last_stmt_bracketing() # find which pairs of bracketing are openers. These always # correspond to a character of rawtext. self.isopener = [i>0 and self.bracketing[i][1] > self.bracketing[i-1][1] for i in range(len(self.bracketing))] self.set_index(index) def set_index(self, index): """Set the index to which the functions relate. The index must be in the same statement. """ indexinrawtext = (len(self.rawtext) - len(self.text.get(index, self.stopatindex))) if indexinrawtext < 0: raise ValueError("Index %s precedes the analyzed statement" % index) self.indexinrawtext = indexinrawtext # find the rightmost bracket to which index belongs self.indexbracket = 0 while (self.indexbracket < len(self.bracketing)-1 and self.bracketing[self.indexbracket+1][0] < self.indexinrawtext): self.indexbracket += 1 if (self.indexbracket < len(self.bracketing)-1 and self.bracketing[self.indexbracket+1][0] == self.indexinrawtext and not self.isopener[self.indexbracket+1]): self.indexbracket += 1 def is_in_string(self): """Is the index given to the HyperParser in a string?""" # The bracket to which we belong should be an opener. # If it's an opener, it has to have a character. return (self.isopener[self.indexbracket] and self.rawtext[self.bracketing[self.indexbracket][0]] in ('"', "'")) def is_in_code(self): """Is the index given to the HyperParser in normal code?""" return (not self.isopener[self.indexbracket] or self.rawtext[self.bracketing[self.indexbracket][0]] not in ('#', '"', "'")) def get_surrounding_brackets(self, openers='([{', mustclose=False): """Return bracket indexes or None. If the index given to the HyperParser is surrounded by a bracket defined in openers (or at least has one before it), return the indices of the opening bracket and the closing bracket (or the end of line, whichever comes first). If it is not surrounded by brackets, or the end of line comes before the closing bracket and mustclose is True, returns None. """ bracketinglevel = self.bracketing[self.indexbracket][1] before = self.indexbracket while (not self.isopener[before] or self.rawtext[self.bracketing[before][0]] not in openers or self.bracketing[before][1] > bracketinglevel): before -= 1 if before < 0: return None bracketinglevel = min(bracketinglevel, self.bracketing[before][1]) after = self.indexbracket + 1 while (after < len(self.bracketing) and self.bracketing[after][1] >= bracketinglevel): after += 1 beforeindex = self.text.index("%s-%dc" % (self.stopatindex, len(self.rawtext)-self.bracketing[before][0])) if (after >= len(self.bracketing) or self.bracketing[after][0] > len(self.rawtext)): if mustclose: return None afterindex = self.stopatindex else: # We are after a real char, so it is a ')' and we give the # index before it. afterindex = self.text.index( "%s-%dc" % (self.stopatindex, len(self.rawtext)-(self.bracketing[after][0]-1))) return beforeindex, afterindex # the set of built-in identifiers which are also keywords, # i.e. keyword.iskeyword() returns True for them _ID_KEYWORDS = frozenset({"True", "False", "None"}) @classmethod def _eat_identifier(cls, str, limit, pos): """Given a string and pos, return the number of chars in the identifier which ends at pos, or 0 if there is no such one. This ignores non-identifier eywords are not identifiers. """ is_ascii_id_char = _IS_ASCII_ID_CHAR # Start at the end (pos) and work backwards. i = pos # Go backwards as long as the characters are valid ASCII # identifier characters. This is an optimization, since it # is faster in the common case where most of the characters # are ASCII. while i > limit and ( ord(str[i - 1]) < 128 and is_ascii_id_char[ord(str[i - 1])] ): i -= 1 # If the above loop ended due to reaching a non-ASCII # character, continue going backwards using the most generic # test for whether a string contains only valid identifier # characters. if i > limit and ord(str[i - 1]) >= 128: while i - 4 >= limit and ('a' + str[i - 4:pos]).isidentifier(): i -= 4 if i - 2 >= limit and ('a' + str[i - 2:pos]).isidentifier(): i -= 2 if i - 1 >= limit and ('a' + str[i - 1:pos]).isidentifier(): i -= 1 # The identifier candidate starts here. If it isn't a valid # identifier, don't eat anything. At this point that is only # possible if the first character isn't a valid first # character for an identifier. if not str[i:pos].isidentifier(): return 0 elif i < pos: # All characters in str[i:pos] are valid ASCII identifier # characters, so it is enough to check that the first is # valid as the first character of an identifier. if not _IS_ASCII_ID_FIRST_CHAR[ord(str[i])]: return 0 # All keywords are valid identifiers, but should not be # considered identifiers here, except for True, False and None. if i < pos and ( iskeyword(str[i:pos]) and str[i:pos] not in cls._ID_KEYWORDS ): return 0 return pos - i # This string includes all chars that may be in a white space _whitespace_chars = " \t\n\\" def get_expression(self): """Return a string with the Python expression which ends at the given index, which is empty if there is no real one. """ if not self.is_in_code(): raise ValueError("get_expression should only be called" "if index is inside a code.") rawtext = self.rawtext bracketing = self.bracketing brck_index = self.indexbracket brck_limit = bracketing[brck_index][0] pos = self.indexinrawtext last_identifier_pos = pos postdot_phase = True while 1: # Eat whitespaces, comments, and if postdot_phase is False - a dot while 1: if pos>brck_limit and rawtext[pos-1] in self._whitespace_chars: # Eat a whitespace pos -= 1 elif (not postdot_phase and pos > brck_limit and rawtext[pos-1] == '.'): # Eat a dot pos -= 1 postdot_phase = True # The next line will fail if we are *inside* a comment, # but we shouldn't be. elif (pos == brck_limit and brck_index > 0 and rawtext[bracketing[brck_index-1][0]] == '#'): # Eat a comment brck_index -= 2 brck_limit = bracketing[brck_index][0] pos = bracketing[brck_index+1][0] else: # If we didn't eat anything, quit. break if not postdot_phase: # We didn't find a dot, so the expression end at the # last identifier pos. break ret = self._eat_identifier(rawtext, brck_limit, pos) if ret: # There is an identifier to eat pos = pos - ret last_identifier_pos = pos # Now, to continue the search, we must find a dot. postdot_phase = False # (the loop continues now) elif pos == brck_limit: # We are at a bracketing limit. If it is a closing # bracket, eat the bracket, otherwise, stop the search. level = bracketing[brck_index][1] while brck_index > 0 and bracketing[brck_index-1][1] > level: brck_index -= 1 if bracketing[brck_index][0] == brck_limit: # We were not at the end of a closing bracket break pos = bracketing[brck_index][0] brck_index -= 1 brck_limit = bracketing[brck_index][0] last_identifier_pos = pos if rawtext[pos] in "([": # [] and () may be used after an identifier, so we # continue. postdot_phase is True, so we don't allow a dot. pass else: # We can't continue after other types of brackets if rawtext[pos] in "'\"": # Scan a string prefix while pos > 0 and rawtext[pos - 1] in "rRbBuU": pos -= 1 last_identifier_pos = pos break else: # We've found an operator or something. break return rawtext[last_identifier_pos:self.indexinrawtext] def _is_char_in_string(text_index): # in idlelib.EditorWindow this used info from colorer # to speed up things, but I dont want to rely on this return 1 def _build_char_in_string_func(startindex): # copied from idlelib.EditorWindow (Python 3.4.2) # Our editwin provides a _is_char_in_string function that works # with a Tk text index, but PyParse only knows about offsets into # a string. This builds a function for PyParse that accepts an # offset. def inner(offset, _startindex=startindex, _icis=_is_char_in_string): return _icis(_startindex + "+%dc" % offset) return inner thonny-2.1.16/thonny/running.py0000666000000000000000000012316313201264465014634 0ustar 00000000000000# -*- coding: utf-8 -*- """Code for maintaining the background process and for running user programs Commands get executed via shell, this way the command line in the shell becomes kind of title for the execution. """ from _thread import start_new_thread from logging import debug import os.path import subprocess import sys from thonny.common import serialize_message, ToplevelCommand, \ InlineCommand, parse_shell_command, \ CommandSyntaxError, parse_message, DebuggerCommand, InputSubmission,\ UserError from thonny.globals import get_workbench, get_runner import shlex from thonny import THONNY_USER_DIR from thonny.misc_utils import running_on_windows, running_on_mac_os, eqfn from shutil import which import shutil import tokenize import collections import signal import logging from time import sleep DEFAULT_CPYTHON_INTERPRETER = "default" SAME_AS_FRONTEND_INTERPRETER = "same as front-end" WINDOWS_EXE = "python.exe" class Runner: def __init__(self): get_workbench().set_default("run.working_directory", os.path.expanduser("~")) get_workbench().set_default("run.auto_cd", True) get_workbench().set_default("run.backend_configuration", "Python (%s)" % DEFAULT_CPYTHON_INTERPRETER) get_workbench().set_default("run.used_interpreters", []) get_workbench().add_backend("Python", CPythonProxy) from thonny.shell import ShellView get_workbench().add_view(ShellView, "Shell", "s", visible_by_default=True, default_position_key='A') self._init_commands() self._state = None self._proxy = None self._postponed_commands = [] self._current_toplevel_command = None self._current_command = None self._check_alloc_console() def start(self): try: self.reset_backend() finally: self._poll_vm_messages() def _init_commands(self): shell = get_workbench().get_view("ShellView") shell.add_command("Run", self.handle_execute_from_shell) shell.add_command("Reset", self._handle_reset_from_shell) shell.add_command("cd", self._handle_cd_from_shell) get_workbench().add_command('run_current_script', "run", 'Run current script', handler=self._cmd_run_current_script, default_sequence="", tester=self._cmd_run_current_script_enabled, group=10, image_filename="run.run_current_script.gif", include_in_toolbar=True) get_workbench().add_command('reset', "run", 'Interrupt/Reset', handler=self.cmd_interrupt_reset, default_sequence="", tester=self._cmd_interrupt_reset_enabled, group=70, image_filename="run.stop.gif", include_in_toolbar=True) get_workbench().add_command('interrupt', "run", "Interrupt execution", handler=self._cmd_interrupt, tester=self._cmd_interrupt_enabled, default_sequence="", bell_when_denied=False) def get_cwd(self): # TODO: make it nicer if hasattr(self._proxy, "cwd"): return self._proxy.cwd else: return "" def get_state(self): """State is one of "running", "waiting_input", "waiting_debugger_command", "waiting_toplevel_command" """ return self._state def _set_state(self, state): if self._state != state: logging.debug("Runner state changed: %s ==> %s" % (self._state, state)) self._state = state if self._state == "waiting_toplevel_command": self._current_toplevel_command = None if self._state != "running": self._current_command = None def get_current_toplevel_command(self): return self._current_toplevel_command def get_current_command(self): return self._current_command def get_sys_path(self): return self._proxy.get_sys_path() def send_command(self, cmd): if self._proxy is None: return if not self._state_is_suitable(cmd): if isinstance(cmd, DebuggerCommand) and self.get_state() == "running": # probably waiting behind some InlineCommand self._postpone_command(cmd) return elif isinstance(cmd, InlineCommand): self._postpone_command(cmd) return else: raise AssertionError("Trying to send " + str(cmd) + " in state " + self.get_state()) if cmd.command in ("Run", "Debug", "Reset"): get_workbench().event_generate("BackendRestart") accepted = self._proxy.send_command(cmd) if (accepted and isinstance(cmd, (ToplevelCommand, DebuggerCommand, InlineCommand))): self._set_state("running") self._current_command = cmd if isinstance(cmd, ToplevelCommand): self._current_toplevel_command = cmd def send_program_input(self, data): assert self.get_state() == "waiting_input" self._proxy.send_program_input(data) self._set_state("running") def execute_script(self, script_path, args, working_directory=None, command_name="Run"): if (working_directory is not None and self._proxy.cwd != working_directory): # create compound command # start with %cd cmd_line = "%cd " + shlex.quote(working_directory) + "\n" next_cwd = working_directory else: # create simple command cmd_line = "" next_cwd = self._proxy.cwd # append main command (Run, run, Debug or debug) rel_filename = os.path.relpath(script_path, next_cwd) cmd_line += "%" + command_name + " " + shlex.quote(rel_filename) # append args for arg in args: cmd_line += " " + shlex.quote(arg) cmd_line += "\n" # submit to shell (shell will execute it) get_workbench().get_view("ShellView").submit_command(cmd_line) def execute_current(self, command_name, always_change_to_script_dir=False): """ This method's job is to create a command for running/debugging current file/script and submit it to shell """ editor = get_workbench().get_current_editor() if not editor: return filename = editor.get_filename(True) if not filename: return if editor.is_modified(): filename = editor.save_file() if not filename: return # changing dir may be required script_dir = os.path.realpath(os.path.dirname(filename)) if (get_workbench().get_option("run.auto_cd") and command_name[0].isupper() or always_change_to_script_dir): working_directory = script_dir else: working_directory = None self.execute_script(filename, [], working_directory, command_name) def handle_execute_from_shell(self, cmd_line): """ Handles all commands that take a filename and 0 or more extra arguments. Passes the command to backend. (Debugger plugin may also use this method) """ command, args = parse_shell_command(cmd_line) if len(args) >= 1: get_workbench().get_editor_notebook().save_all_named_editors() cmd = ToplevelCommand(command=command, filename=args[0], args=args[1:]) if os.path.isabs(cmd.filename): cmd.full_filename = cmd.filename else: cmd.full_filename = os.path.join(self.get_cwd(), cmd.filename) if command in ["Run", "run", "Debug", "debug"]: with tokenize.open(cmd.full_filename) as fp: cmd.source = fp.read() self.send_command(cmd) else: raise CommandSyntaxError("Command '%s' takes at least one argument", command) def _handle_reset_from_shell(self, cmd_line): command, args = parse_shell_command(cmd_line) assert command == "Reset" if len(args) == 0: self.send_command(ToplevelCommand(command="Reset")) else: raise CommandSyntaxError("Command 'Reset' doesn't take arguments") def _handle_cd_from_shell(self, cmd_line): command, args = parse_shell_command(cmd_line) assert command == "cd" if len(args) == 1: self.send_command(ToplevelCommand(command="cd", path=args[0])) else: raise CommandSyntaxError("Command 'cd' takes one argument") def _cmd_run_current_script_enabled(self): return (get_workbench().get_editor_notebook().get_current_editor() is not None and get_runner().get_state() == "waiting_toplevel_command" and "run" in get_runner().supported_features()) def _cmd_run_current_script(self): self.execute_current("Run") def _cmd_interrupt(self): self.interrupt_backend() def _cmd_interrupt_enabled(self): widget = get_workbench().focus_get() if not running_on_mac_os(): # on Mac Ctrl+C is not used for Copy if hasattr(widget, "selection_get"): try: if widget.selection_get() != "": # assuming user meant to copy, not interrupt # (IDLE seems to follow same logic) return False except: # selection_get() gives error when calling without selection on Ubuntu pass return get_runner().get_state() != "waiting_toplevel_command" def cmd_interrupt_reset(self): if self.get_state() == "waiting_toplevel_command": get_workbench().get_view("ShellView").submit_command("%Reset\n") else: get_runner().interrupt_backend() def _cmd_interrupt_reset_enabled(self): return True def _postpone_command(self, cmd): # in case of InlineCommands, discard older same type command if isinstance(cmd, InlineCommand): for older_cmd in self._postponed_commands: if older_cmd.command == cmd.command: self._postponed_commands.remove(older_cmd) if len(self._postponed_commands) > 10: "Can't pile up too many commands. This command will be just ignored" else: self._postponed_commands.append(cmd) def _state_is_suitable(self, cmd): if isinstance(cmd, ToplevelCommand): return (self.get_state() == "waiting_toplevel_command" or cmd.command in ["Reset", "Run", "Debug"]) elif isinstance(cmd, DebuggerCommand): return self.get_state() == "waiting_debugger_command" elif isinstance(cmd, InlineCommand): # UI may send inline commands in any state, # but some backends don't accept them in some states return self.get_state() in self._proxy.allowed_states_for_inline_commands() else: raise RuntimeError("Unknown command class: " + str(type(cmd))) def _send_postponed_commands(self): remaining = [] for cmd in self._postponed_commands: if self._state_is_suitable(cmd): logging.debug("Sending postponed command", cmd) self.send_command(cmd) else: remaining.append(cmd) self._postponed_commands = remaining def _poll_vm_messages(self): """I chose polling instead of event_generate in listener thread, because event_generate across threads is not reliable http://www.thecodingforums.com/threads/more-on-tk-event_generate-and-threads.359615/ """ try: initial_state = self.get_state() while self._proxy is not None: try: msg = self._proxy.fetch_next_message() if not msg: break except BackendTerminatedError as exc: self._report_backend_crash(exc) self.reset_backend() return if msg.get("SystemExit", False): self.reset_backend() return # change state if "command_context" in msg: # message_context shows the state where corresponding command was handled in the backend # Now we got the response and we're return to that state self._set_state(msg["command_context"]) elif msg["message_type"] == "ToplevelResult": # some ToplevelResult-s don't have command_context self._set_state("waiting_toplevel_command") elif msg["message_type"] == "InputRequest": self._set_state("waiting_input") else: "other messages don't affect the state" if msg["message_type"] == "ToplevelResult": self._current_toplevel_command = None #logging.debug("Runner: State: %s, Fetched msg: %s" % (self.get_state(), msg)) get_workbench().event_generate(msg["message_type"], **msg) # TODO: maybe distinguish between workbench cwd and backend cwd ?? get_workbench().set_option("run.working_directory", self.get_cwd()) # TODO: is it necessary??? # https://stackoverflow.com/a/13520271/261181 #get_workbench().update() if self.get_state() != initial_state: self._send_postponed_commands() finally: get_workbench().after(50, self._poll_vm_messages) def _report_backend_crash(self, exc): err = "Backend terminated (returncode: %s)\n" % exc.returncode try: faults_file = os.path.join(THONNY_USER_DIR, "backend_faults.log") if os.path.exists(faults_file): with open(faults_file, encoding="ASCII") as fp: err += fp.read() except: logging.exception("Failed retrieving backend faults") err = err.strip() + "\nResetting ...\n" get_workbench().event_generate("ProgramOutput", stream_name="stderr", data=err) get_workbench().become_topmost_window() def reset_backend(self): self.kill_backend() configuration = get_workbench().get_option("run.backend_configuration") backend_name, configuration_option = parse_configuration(configuration) if backend_name not in get_workbench().get_backends(): raise UserError("Can't find backend '{}'. Please select another backend from options" .format(backend_name)) backend_class = get_workbench().get_backends()[backend_name] self._set_state("running") self._proxy = None self._proxy = backend_class(configuration_option) def interrupt_backend(self): if self._proxy is not None: self._proxy.interrupt() else: logging.warning("Interrupting without proxy") def kill_backend(self): self._current_toplevel_command = None self._current_command = None self._postponed_commands = [] if self._proxy: self._proxy.kill_current_process() self._proxy = None def get_interpreter_command(self): return self._proxy.get_interpreter_command() def get_backend_description(self): return self._proxy.get_description() def _check_alloc_console(self): if (sys.executable.endswith("thonny.exe") or sys.executable.endswith("pythonw.exe")): # These don't have console allocated. # Console is required for sending interrupts. # AllocConsole would be easier but flashes console window import ctypes kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) exe = (sys.executable .replace("thonny.exe", "python.exe") .replace("pythonw.exe", "python.exe")) cmd = [exe, "-c", "print('Hi!'); input()"] child = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) child.stdout.readline() result = kernel32.AttachConsole(child.pid) if not result: err = ctypes.get_last_error() logging.info("Could not allocate console. Error code: " +str(err)) child.stdin.write(b"\n") child.stdin.flush() def supported_features(self): if self._proxy is None: return [] else: return self._proxy.supported_features() def get_frontend_python(self): return sys.executable.replace("thonny.exe", "python.exe") def using_venv(self): return isinstance(self._proxy, CPythonProxy) and self._proxy.in_venv class BackendProxy: """Communicates with backend process. All communication methods must be non-blocking, ie. suitable for calling from GUI thread.""" def __init__(self, configuration_option): """Initializes (or starts the initialization of) the backend process. Backend is considered ready when the runner gets a ToplevelResult with attribute "welcome_text" from fetch_next_message. param configuration_option: If configuration is "Foo (bar)", then "Foo" is backend descriptor and "bar" is the configuration option""" @classmethod def get_configuration_options(cls): """Returns a list strings for populating interpreter selection dialog. The strings are without backend descriptor""" raise NotImplementedError() def get_description(self): """Returns a string that describes the backend""" raise NotImplementedError() def send_command(self, cmd): """Send the command to backend""" raise NotImplementedError() def allowed_states_for_inline_commands(self): return ["waiting_toplevel_command"] def send_program_input(self, data): """Send input data to backend""" raise NotImplementedError() def fetch_next_message(self): """Read next message from the queue or None if queue is empty""" raise NotImplementedError() def get_sys_path(self): "backend's sys.path" return [] def interrupt(self): """Tries to interrupt current command without reseting the backend""" self.kill_current_process() def kill_current_process(self): """Kill the backend. Is called when Thonny no longer needs this backend (Thonny gets closed or new backend gets selected) """ pass def get_interpreter_command(self): """Return system command for invoking current interpreter""" raise NotImplementedError() def supported_features(self): return ["run"] class CPythonProxy(BackendProxy): @classmethod def get_configuration_options(cls): return ([DEFAULT_CPYTHON_INTERPRETER, SAME_AS_FRONTEND_INTERPRETER] + CPythonProxy._get_interpreters()) def __init__(self, configuration_option): if configuration_option == DEFAULT_CPYTHON_INTERPRETER: self._prepare_private_venv() self._executable = get_private_venv_executable() elif configuration_option == SAME_AS_FRONTEND_INTERPRETER: self._executable = get_runner().get_frontend_python() elif configuration_option.startswith("."): # Relative paths are relative to front-end interpretator directory # (This must be written directly to conf file, as it can't be selected from Options dialog) self._executable = os.path.normpath(os.path.join(os.path.dirname(sys.executable), configuration_option)) else: self._executable = configuration_option # Rembember the usage of this non-default interpreter used_interpreters = get_workbench().get_option("run.used_interpreters") if self._executable not in used_interpreters: used_interpreters.append(self._executable) get_workbench().set_option("run.used_interpreters", used_interpreters) cwd = get_workbench().get_option("run.working_directory") if os.path.exists(cwd): self.cwd = cwd else: self.cwd = os.path.expanduser("~") self._proc = None self._message_queue = None self._sys_path = [] self._tkupdate_loop_id = None self.in_venv = None self._start_new_process() def fetch_next_message(self): if not self._message_queue or len(self._message_queue) == 0: if self._proc is not None: retcode = self._proc.poll() if retcode is not None: raise BackendTerminatedError(retcode) return None msg = self._message_queue.popleft() if "tkinter_is_active" in msg: self._update_tkupdating(msg) if "in_venv" in msg: self.in_venv = msg["in_venv"] if msg["message_type"] == "ProgramOutput": # combine available output messages to one single message, # in order to put less pressure on UI code while True: if len(self._message_queue) == 0: return msg else: next_msg = self._message_queue.popleft() if (next_msg["message_type"] == "ProgramOutput" and next_msg["stream_name"] == msg["stream_name"]): msg["data"] += next_msg["data"] else: # not same type of message, put it back self._message_queue.appendleft(next_msg) return msg else: return msg def get_description(self): # TODO: show backend version and interpreter path return "Python (current dir: {})".format(self.cwd) def send_command(self, cmd): if isinstance(cmd, ToplevelCommand) and cmd.command in ("Run", "Debug", "Reset"): self.kill_current_process() self._start_new_process(cmd) self._proc.stdin.write(serialize_message(cmd) + "\n") self._proc.stdin.flush() return True def send_program_input(self, data): self.send_command(InputSubmission(data=data)) def allowed_states_for_inline_commands(self): return ["waiting_toplevel_command", "waiting_debugger_command", "waiting_input"] def get_sys_path(self): return self._sys_path def interrupt(self): def do_kill(): self._proc.kill() get_workbench().event_generate("ProgramOutput", stream_name="stderr", data="KeyboardInterrupt: Forced reset") get_runner().reset_backend() if self._proc is not None: if self._proc.poll() is None: command_to_interrupt = get_runner().get_current_toplevel_command() if running_on_windows(): try: os.kill(self._proc.pid, signal.CTRL_BREAK_EVENT) # @UndefinedVariable except: logging.exception("Could not interrupt backend process") else: self._proc.send_signal(signal.SIGINT) # Tkinter programs can't be interrupted so easily: # http://stackoverflow.com/questions/13784232/keyboardinterrupt-taking-a-while # so let's chedule a hard kill in case the program refuses to be interrupted def go_hard(): if (get_runner().get_state() != "waiting_toplevel_command" and get_runner().get_current_toplevel_command() == command_to_interrupt): # still running same command do_kill() # 100 ms was too little for Mac # 250 ms was too little for one of the Windows machines get_workbench().after(500, go_hard) else: do_kill() def kill_current_process(self): if self._proc is not None and self._proc.poll() is None: self._proc.kill() self._proc = None self._message_queue = None def _prepare_jedi(self): """Make jedi available for the backend""" # Copy jedi import jedi dirname = os.path.join(THONNY_USER_DIR, "jedi_" + str(jedi.__version__)) if not os.path.exists(dirname): shutil.copytree(jedi.__path__[0], os.path.join(dirname, "jedi")) return dirname # TODO: clean up old versions def _start_new_process(self, cmd=None): this_python = get_runner().get_frontend_python() # deque, because in one occasion I need to put messages back self._message_queue = collections.deque() # prepare the environment my_env = os.environ.copy() # Delete some environment variables if the backend is (based on) a different Python instance if self._executable not in [ this_python, this_python.replace("python.exe", "pythonw.exe"), this_python.replace("pythonw.exe", "python.exe"), get_private_venv_executable()]: # Keep only the variables, that are not related to Python my_env = {name : my_env[name] for name in my_env if "python" not in name.lower() and name not in ["TK_LIBRARY", "TCL_LIBRARY"]} # Remove variables used to tweak bundled Thonny-private Python if using_bundled_python(): my_env = {name : my_env[name] for name in my_env if name not in ["SSL_CERT_FILE", "SSL_CERT_DIR", "LD_LIBRARY_PATH"]} # variables controlling communication with the back-end process my_env["PYTHONIOENCODING"] = "ASCII" my_env["PYTHONUNBUFFERED"] = "1" my_env["THONNY_USER_DIR"] = THONNY_USER_DIR # venv may not find (correct) Tk without assistance (eg. in Ubuntu) if self._executable == get_private_venv_executable(): try: my_env["TCL_LIBRARY"] = get_workbench().tk.exprstring('$tcl_library') my_env["TK_LIBRARY"] = get_workbench().tk.exprstring('$tk_library') except: logging.exception("Can't find Tcl/Tk library") # If the back-end interpreter is something else than front-end's one, # then it may not have jedi installed. # In this case fffer front-end's jedi for the back-end if self._executable != get_runner().get_frontend_python(): # I don't want to use PYTHONPATH for making jedi available # because that would add it to the front of sys.path my_env["JEDI_LOCATION"] = self._prepare_jedi() if not os.path.exists(self._executable): raise UserError("Interpreter (%s) not found. Please recheck corresponding option!" % self._executable) import thonny.shared.backend_launcher cmd_line = [ self._executable, '-u', # unbuffered IO (neccessary in Python 3.1) '-B', # don't write pyo/pyc files # (to avoid problems when using different Python versions without write permissions) thonny.shared.backend_launcher.__file__ ] if hasattr(cmd, "filename"): cmd_line.append(cmd.filename) if hasattr(cmd, "args"): cmd_line.extend(cmd.args) if hasattr(cmd, "environment"): my_env.update(cmd.environment) creationflags = 0 if running_on_windows(): creationflags = subprocess.CREATE_NEW_PROCESS_GROUP debug("Starting the backend: %s %s", cmd_line, self.cwd) self._proc = subprocess.Popen ( cmd_line, #bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.cwd, env=my_env, universal_newlines=True, creationflags=creationflags ) if cmd: # Consume the ready message, cmd will get its own result message ready_line = self._proc.stdout.readline() if ready_line == "": # There was some problem error_msg = self._proc.stderr.read() raise Exception("Error starting backend process: " + error_msg) #ready_msg = parse_message(ready_line) #self._sys_path = ready_msg["path"] #debug("Backend ready: %s", ready_msg) # setup asynchronous output listeners start_new_thread(self._listen_stdout, ()) start_new_thread(self._listen_stderr, ()) def _listen_stdout(self): #debug("... started listening to stdout") # will be called from separate thread while True: data = self._proc.stdout.readline() #debug("... read some stdout data", repr(data)) if data == '': break else: msg = parse_message(data) if "cwd" in msg: self.cwd = msg["cwd"] # TODO: it was "with self._state_lock:". Is it necessary? self._message_queue.append(msg) if len(self._message_queue) > 100: # Probably backend runs an infinite/long print loop. # Throttle message thougput in order to keep GUI thread responsive. sleep(0.1) def _listen_stderr(self): # stderr is used only for debugger debugging while True: data = self._proc.stderr.readline() if data == '': break else: debug("### BACKEND ###: %s", data.strip()) @classmethod def _get_interpreters(cls): result = set() if running_on_windows(): # registry result.update(CPythonProxy._get_interpreters_from_windows_registry()) # Common locations for dir_ in ["C:\\Python34", "C:\\Python35", "C:\\Program Files\\Python 3.5", "C:\\Program Files (x86)\\Python 3.5", "C:\\Python36", "C:\\Program Files\\Python 3.6", "C:\\Program Files (x86)\\Python 3.6", ]: path = os.path.join(dir_, WINDOWS_EXE) if os.path.exists(path): result.add(os.path.realpath(path)) else: # Common unix locations for dir_ in ["/bin", "/usr/bin", "/usr/local/bin", os.path.expanduser("~/.local/bin")]: for name in ["python3", "python3.4", "python3.5", "python3.6"]: path = os.path.join(dir_, name) if os.path.exists(path): result.add(path) if running_on_mac_os(): for version in ["3.4", "3.5", "3.6"]: dir_ = os.path.join("/Library/Frameworks/Python.framework/Versions", version, "bin") path = os.path.join(dir_, "python3") if os.path.exists(path): result.add(path) for command in ["pythonw", "python3", "python3.4", "python3.5", "python3.6"]: path = which(command) if path is not None and os.path.isabs(path): result.add(path) current_configuration = get_workbench().get_option("run.backend_configuration") backend, configuration_option = parse_configuration(current_configuration) if backend == "Python" and configuration_option and os.path.exists(configuration_option): result.add(os.path.realpath(configuration_option)) for path in get_workbench().get_option("run.used_interpreters"): if os.path.exists(path): result.add(os.path.realpath(path)) return sorted(result) @classmethod def _get_interpreters_from_windows_registry(cls): import winreg result = set() for key in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]: for version in ["3.4", "3.5", "3.5-32", "3.5-64", "3.6", "3.6-32", "3.6-64"]: try: for subkey in [ 'SOFTWARE\\Python\\PythonCore\\' + version + '\\InstallPath', 'SOFTWARE\\Python\\PythonCore\\Wow6432Node\\' + version + '\\InstallPath' ]: dir_ = winreg.QueryValue(key, subkey) if dir_: path = os.path.join(dir_, WINDOWS_EXE) if os.path.exists(path): result.add(path) except: pass return result def get_interpreter_command(self): return self._executable def _update_tkupdating(self, msg): """Enables running Tkinter programs which doesn't call mainloop. When mainloop is omitted, then program can be interacted with from the shell after it runs to the end. Each ToplevelResponse is supposed to tell, whether tkinter window is open and needs updating. """ if not "tkinter_is_active" in msg: return if msg["tkinter_is_active"] and self._tkupdate_loop_id is None: # Start updating self._tkupdate_loop_id = self._loop_tkupdate(True) elif not msg["tkinter_is_active"] and self._tkupdate_loop_id is not None: # Cancel updating try: get_workbench().after_cancel(self._tkupdate_loop_id) finally: self._tkupdate_loop_id = None def _loop_tkupdate(self, force=False): if force or get_runner().get_state() == "waiting_toplevel_command": self.send_command(InlineCommand("tkupdate")) self._tkupdate_loop_id = get_workbench().after(50, self._loop_tkupdate) else: self._tkupdate_loop_id = None def _prepare_private_venv(self): path = _get_private_venv_path() if os.path.isdir(path) and os.path.isfile(os.path.join(path, "pyvenv.cfg")): self._check_upgrade_private_venv(path) else: self._create_private_venv(path, "Please wait!\nThonny prepares its virtual environment.") def _check_upgrade_private_venv(self, path): # If home is wrong then regenerate # If only micro version is different, then upgrade info = _get_venv_info(path) if not eqfn(info["home"], os.path.dirname(sys.executable)): self._create_private_venv(path, "Thonny's virtual environment was created for another interpreter.\n" + "Regenerating the virtual environment for current interpreter.\n" + "(You may need to reinstall your 3rd party packages)\n" + "Please wait!.", clear=True) else: venv_version = tuple(map(int, info["version"].split("."))) sys_version = sys.version_info[:3] assert venv_version[0] == sys_version[0] assert venv_version[1] == sys_version[1] if venv_version[2] != sys_version[2]: self._create_private_venv(path, "Please wait!\nUpgrading Thonny's virtual environment.", upgrade=True) def _create_private_venv(self, path, description, clear=False, upgrade=False): base_exe = sys.executable if sys.executable.endswith("thonny.exe"): # assuming that thonny.exe is in the same dir as "python.exe" base_exe = sys.executable.replace("thonny.exe", "python.exe") # Don't include system site packages # This way all students will have similar configuration # independently of system Python (if Thonny is used with system Python) # NB! Cant run venv.create directly, because in Windows # it tries to link venv to thonny.exe. # Need to run it via proper python cmd = [base_exe, "-m", "venv"] if clear: cmd.append("--clear") if upgrade: cmd.append("--upgrade") try: import ensurepip # @UnusedImport except ImportError: cmd.append("--without-pip") cmd.append(path) startupinfo = None if running_on_windows(): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW proc = subprocess.Popen(cmd, startupinfo=startupinfo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) from thonny.ui_utils import SubprocessDialog dlg = SubprocessDialog(get_workbench(), proc, "Preparing the backend", long_description=description) try: get_workbench().wait_window(dlg) except: # if using --without-pip the dialog may close very quickly # and for some reason wait_window would give error then logging.exception("Problem with waiting for venv creation dialog") get_workbench().become_topmost_window() # Otherwise focus may get stuck somewhere bindir = os.path.dirname(get_private_venv_executable()) # create private env marker marker_path = os.path.join(bindir, "is_private") with open(marker_path, mode="w") as fp: fp.write("# This file marks Thonny-private venv") # Create recommended pip conf to get rid of list deprecation warning # https://github.com/pypa/pip/issues/4058 pip_conf = "pip.ini" if running_on_windows() else "pip.conf" with open(os.path.join(path, pip_conf), mode="w") as fp: fp.write("[list]\nformat = columns") assert os.path.isdir(path) def supported_features(self): return ["run", "debug", "pip_gui", "system_shell"] def parse_configuration(configuration): """ "Python (C:\Python34\python.exe)" becomes ("Python", "C:\Python34\python.exe") "BBC micro:bit" becomes ("BBC micro:bit", "") """ parts = configuration.split("(", maxsplit=1) if len(parts) == 1: return configuration, "" else: return parts[0].strip(), parts[1].strip(" )") def _get_private_venv_path(): if "thonny" in sys.executable.lower(): prefix = "BundledPython" else: prefix = "Python" return os.path.join(THONNY_USER_DIR, prefix + "%d%d" % (sys.version_info[0], sys.version_info[1])) def get_private_venv_executable(): venv_path = _get_private_venv_path() if running_on_windows(): exe = os.path.join(venv_path, "Scripts", WINDOWS_EXE) else: exe = os.path.join(venv_path, "bin", "python3") return exe def _get_venv_info(venv_path): cfg_path = os.path.join(venv_path, "pyvenv.cfg") result = {} with open(cfg_path, encoding="UTF-8") as fp: for line in fp: if "=" in line: key, val = line.split("=", maxsplit=1) result[key.strip()] = val.strip() return result; def using_bundled_python(): return os.path.exists(os.path.join( os.path.dirname(sys.executable), "thonny_python.ini" )) class BackendTerminatedError(Exception): def __init__(self, returncode): Exception.__init__(self) self.returncode = returncode thonny-2.1.16/thonny/shared/0000777000000000000000000000000013201324660014034 5ustar 00000000000000thonny-2.1.16/thonny/shared/backend_launcher.py0000666000000000000000000000305513201264465017667 0ustar 00000000000000# -*- coding: utf-8 -*- """ This file is run by VMProxy (Why separate file for launching? I want to have clean global scope in toplevel __main__ module (because that's where user scripts run), but backend's global scope is far from clean. I could also do python -c "from backend import VM: VM().mainloop()", but looks like this gives relative __file__-s on imported modules.) """ if __name__ == "__main__": # imports required by backend itself import sys import logging import os.path THONNY_USER_DIR = os.environ["THONNY_USER_DIR"] # set up logging logger = logging.getLogger() logFormatter = logging.Formatter('%(levelname)s: %(message)s') file_handler = logging.FileHandler(os.path.join(THONNY_USER_DIR,"backend.log"), encoding="UTF-8", mode="w"); file_handler.setFormatter(logFormatter) file_handler.setLevel(logging.INFO); logger.addHandler(file_handler) # TODO: sending log records to original stdout could be better (reading from stderr may introduce sync problems) stream_handler = logging.StreamHandler(stream=sys.stderr) stream_handler.setLevel(logging.INFO); stream_handler.setFormatter(logFormatter) logger.addHandler(stream_handler) logger.setLevel(logging.INFO) import faulthandler fault_out = open(os.path.join(THONNY_USER_DIR, "backend_faults.log"), mode="w") faulthandler.enable(fault_out) from thonny.backend import VM # @UnresolvedImport VM().mainloop() thonny-2.1.16/thonny/shared/thonny/0000777000000000000000000000000013201324660015353 5ustar 00000000000000thonny-2.1.16/thonny/shared/thonny/ast_utils.py0000666000000000000000000004554513201264465017757 0ustar 00000000000000# -*- coding: utf-8 -*- import ast import _ast import io import sys import token import tokenize import traceback def extract_text_range(source, text_range): lines = source.splitlines(True) # get relevant lines lines = lines[text_range.lineno-1:text_range.end_lineno] # trim last and first lines lines[-1] = lines[-1][:text_range.end_col_offset] lines[0] = lines[0][text_range.col_offset:] return "".join(lines) def find_expression(node, text_range): if (hasattr(node, "lineno") and node.lineno == text_range.lineno and node.col_offset == text_range.col_offset and node.end_lineno == text_range.end_lineno and node.end_col_offset == text_range.end_col_offset # expression and Expr statement can have same range and isinstance(node, _ast.expr)): return node else: for child in ast.iter_child_nodes(node): result = find_expression(child, text_range) if result is not None: return result return None def contains_node(parent_node, child_node): for child in ast.iter_child_nodes(parent_node): if child == child_node or contains_node(child, child_node): return True return False def has_parent_with_class(target_node, parent_class, tree): for node in ast.walk(tree): if isinstance(node, parent_class) and contains_node(node, target_node): return True return False def parse_source(source, filename='', mode="exec"): root = ast.parse(source, filename, mode) mark_text_ranges(root, source) return root def get_last_child(node): if isinstance(node, ast.Call): # TODO: take care of Python 3.5 updates (Starred etc.) if hasattr(node, "kwargs") and node.kwargs is not None: return node.kwargs elif hasattr(node, "starargs") and node.starargs is not None: return node.starargs elif len(node.keywords) > 0: return node.keywords[-1] elif len(node.args) > 0: # TODO: ast.Starred doesn't exist in Python 3.4 ?? if isinstance(node.args[-1], ast.Starred): # return the thing under Starred return node.args[-1].value else: return node.args[-1] else: return node.func elif isinstance(node, ast.BoolOp): return node.values[-1] elif isinstance(node, ast.BinOp): return node.right elif isinstance(node, ast.Compare): return node.comparators[-1] elif isinstance(node, ast.UnaryOp): return node.operand elif (isinstance(node, (ast.Tuple, ast.List, ast.Set)) and len(node.elts)) > 0: return node.elts[-1] elif (isinstance(node, ast.Dict) and len(node.values)) > 0: return node.values[-1] elif (isinstance(node, (ast.Return, ast.Assign, ast.AugAssign, ast.Yield, ast.YieldFrom)) and node.value is not None): return node.value elif isinstance(node, ast.Delete): return node.targets[-1] elif isinstance(node, ast.Expr): return node.value elif isinstance(node, ast.Assert): if node.msg is not None: return node.msg else: return node.test elif isinstance(node, ast.Subscript): if hasattr(node.slice, "value"): return node.slice.value else: assert (hasattr(node.slice, "lower") and hasattr(node.slice, "upper") and hasattr(node.slice, "step")) if node.slice.step is not None: return node.slice.step elif node.slice.upper is not None: return node.slice.upper else: return node.slice.lower elif isinstance(node, (ast.For, ast.While, ast.If, ast.With)): return True # There is last child, but I don't know which it will be else: return None # TODO: pick more cases from here: """ (isinstance(node, (ast.IfExp, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)) or isinstance(node, ast.Raise) and (node.exc is not None or node.cause is not None) # or isinstance(node, ast.FunctionDef, ast.Lambda) and len(node.args.defaults) > 0 and (node.dest is not None or len(node.values) > 0)) #"TODO: Import ja ImportFrom" # TODO: what about ClassDef ??? """ def mark_text_ranges(node, source): """ Node is an AST, source is corresponding source as string. Function adds recursively attributes end_lineno and end_col_offset to each node which has attributes lineno and col_offset. """ def _extract_tokens(tokens, lineno, col_offset, end_lineno, end_col_offset): return list(filter((lambda tok: tok.start[0] >= lineno and (tok.start[1] >= col_offset or tok.start[0] > lineno) and tok.end[0] <= end_lineno and (tok.end[1] <= end_col_offset or tok.end[0] < end_lineno) and tok.string != ''), tokens)) def _mark_text_ranges_rec(node, tokens, prelim_end_lineno, prelim_end_col_offset): """ Returns the earliest starting position found in given tree, this is convenient for internal handling of the siblings """ # set end markers to this node if "lineno" in node._attributes and "col_offset" in node._attributes: tokens = _extract_tokens(tokens, node.lineno, node.col_offset, prelim_end_lineno, prelim_end_col_offset) try: tokens = _mark_end_and_return_child_tokens(node, tokens, prelim_end_lineno, prelim_end_col_offset) except: traceback.print_exc() # TODO: log it somewhere # fallback to incorrect marking instead of exception node.end_lineno = node.lineno node.end_col_offset = node.col_offset + 1 # mark its children, starting from last one # NB! need to sort children because eg. in dict literal all keys come first and then all values children = list(_get_ordered_child_nodes(node)) for child in reversed(children): (prelim_end_lineno, prelim_end_col_offset) = \ _mark_text_ranges_rec(child, tokens, prelim_end_lineno, prelim_end_col_offset) if "lineno" in node._attributes and "col_offset" in node._attributes: # new "front" is beginning of this node prelim_end_lineno = node.lineno prelim_end_col_offset = node.col_offset return (prelim_end_lineno, prelim_end_col_offset) def _strip_trailing_junk_from_expressions(tokens): while (tokens[-1].type not in (token.RBRACE, token.RPAR, token.RSQB, token.NAME, token.NUMBER, token.STRING) and not (hasattr(token, "ELLIPSIS") and tokens[-1].type == token.ELLIPSIS) and tokens[-1].string not in ")}]" or tokens[-1].string in ['and', 'as', 'assert', 'class', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'try', 'while', 'with', 'yield']): del tokens[-1] def _strip_trailing_extra_closers(tokens, remove_naked_comma): level = 0 for i in range(len(tokens)): if tokens[i].string in "({[": level += 1 elif tokens[i].string in ")}]": level -= 1 if level == 0 and tokens[i].string == "," and remove_naked_comma: tokens[:] = tokens[0:i] return if level < 0: tokens[:] = tokens[0:i] return def _strip_unclosed_brackets(tokens): level = 0 for i in range(len(tokens)-1, -1, -1): if tokens[i].string in "({[": level -= 1 elif tokens[i].string in ")}]": level += 1 if level < 0: tokens[:] = tokens[0:i] level = 0 # keep going, there may be more unclosed brackets def _mark_end_and_return_child_tokens(node, tokens, prelim_end_lineno, prelim_end_col_offset): """ # shortcut node.end_lineno = prelim_end_lineno node.end_col_offset = prelim_end_col_offset return tokens """ # prelim_end_lineno and prelim_end_col_offset are the start of # next positioned node or end of source, ie. the suffix of given # range may contain keywords, commas and other stuff not belonging to current node # Function returns the list of tokens which cover all its children if isinstance(node, _ast.stmt): # remove empty trailing lines while (tokens[-1].type in (tokenize.NL, tokenize.COMMENT, token.NEWLINE, token.INDENT) or tokens[-1].string in (":", "else", "elif", "finally", "except")): del tokens[-1] else: _strip_trailing_extra_closers(tokens, not (isinstance(node, ast.Tuple) or isinstance(node, ast.Lambda))) _strip_trailing_junk_from_expressions(tokens) _strip_unclosed_brackets(tokens) # set the end markers of this node node.end_lineno = tokens[-1].end[0] node.end_col_offset = tokens[-1].end[1] # Peel off some trailing tokens which can't be part any # positioned child node. # TODO: maybe cleaning from parent side is better than # _strip_trailing_junk_from_expressions # Remove trailing empty parens from no-arg call if (isinstance(node, ast.Call) and _tokens_text(tokens[-2:]) == "()"): del tokens[-2:] # Remove trailing full slice elif isinstance(node, ast.Subscript): if _tokens_text(tokens[-3:]) == "[:]": del tokens[-3:] elif _tokens_text(tokens[-4:]) == "[::]": del tokens[-4:] # Attribute name would confuse the "value" of Attribute elif isinstance(node, ast.Attribute): assert tokens[-1].type == token.NAME del tokens[-1] _strip_trailing_junk_from_expressions(tokens) return tokens all_tokens = list(tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline)) source_lines = source.splitlines(True) fix_ast_problems(node, source_lines, all_tokens) prelim_end_lineno = len(source_lines) prelim_end_col_offset = len(source_lines[len(source_lines)-1]) _mark_text_ranges_rec(node, all_tokens, prelim_end_lineno, prelim_end_col_offset) def value_to_literal(value): if value is None: return ast.Name(id="None", ctx=ast.Load()) elif isinstance(value, bool): if value: return ast.Name(id="True", ctx=ast.Load()) else: return ast.Name(id="False", ctx=ast.Load()) elif isinstance(value, str): return ast.Str(s=value) else: raise NotImplementedError("only None, bool and str supported at the moment, not " + str(type(value))) def fix_ast_problems(tree, source_lines, tokens): # Problem 1: # Python parser gives col_offset as offset to its internal UTF-8 byte array # I need offsets to chars utf8_byte_lines = list(map(lambda line: line.encode("UTF-8"), source_lines)) # Problem 2: # triple-quoted strings have just plain wrong positions: http://bugs.python.org/issue18370 # Fortunately lexer gives them correct positions string_tokens = list(filter(lambda tok: tok.type == token.STRING, tokens)) # Problem 3: # Binary operations have wrong positions: http://bugs.python.org/issue18374 # Problem 4: # Function calls have wrong positions in Python 3.4: http://bugs.python.org/issue21295 # similar problem is with Attributes and Subscripts def fix_node(node): for child in _get_ordered_child_nodes(node): #for child in ast.iter_child_nodes(node): fix_node(child) if isinstance(node, ast.Str): # fix triple-quote problem # get position from tokens token = string_tokens.pop(0) node.lineno, node.col_offset = token.start elif ((isinstance(node, ast.Expr) or isinstance(node, ast.Attribute)) and isinstance(node.value, ast.Str)): # they share the wrong offset of their triple-quoted child # get position from already fixed child # TODO: try whether this works when child is in parentheses node.lineno = node.value.lineno node.col_offset = node.value.col_offset elif (isinstance(node, ast.BinOp) and compare_node_positions(node, node.left) > 0): # fix binop problem # get position from an already fixed child node.lineno = node.left.lineno node.col_offset = node.left.col_offset elif (isinstance(node, ast.Call) and compare_node_positions(node, node.func) > 0): # Python 3.4 call problem # get position from an already fixed child node.lineno = node.func.lineno node.col_offset = node.func.col_offset elif (isinstance(node, ast.Attribute) and compare_node_positions(node, node.value) > 0): # Python 3.4 attribute problem ... node.lineno = node.value.lineno node.col_offset = node.value.col_offset elif (isinstance(node, ast.Subscript) and compare_node_positions(node, node.value) > 0): # Python 3.4 Subscript problem ... node.lineno = node.value.lineno node.col_offset = node.value.col_offset else: # Let's hope this node has correct lineno, and byte-based col_offset # Now compute char-based col_offset if hasattr(node, "lineno"): byte_line = utf8_byte_lines[node.lineno-1] char_col_offset = len(byte_line[:node.col_offset].decode("UTF-8")) node.col_offset = char_col_offset fix_node(tree) def compare_node_positions(n1, n2): if n1.lineno > n2.lineno: return 1 elif n1.lineno < n2.lineno: return -1 elif n1.col_offset > n2.col_offset: return 1 elif n2.col_offset < n2.col_offset: return -1 else: return 0 def pretty(node, key="/", level=0): """Used for testing and new test generation via AstView. Don't change the format without updating tests""" if isinstance(node, ast.AST): fields = [(key, val) for key, val in ast.iter_fields(node)] value_label = node.__class__.__name__ if isinstance(node, ast.Call): # Try to make 3.4 AST-s more similar to 3.5 if sys.version_info[:2] == (3,4): if ("kwargs", None) in fields: fields.remove(("kwargs", None)) if ("starargs", None) in fields: fields.remove(("starargs", None)) # TODO: translate also non-None kwargs and starargs elif isinstance(node, list): fields = list(enumerate(node)) if len(node) == 0: value_label = "[]" else: value_label = "[...]" else: fields = [] value_label = repr(node) item_text = level * ' ' + str(key) + "=" + value_label if hasattr(node, "lineno"): item_text += " @ " + str(getattr(node, "lineno")) if hasattr(node, "col_offset"): item_text += "." + str(getattr(node, "col_offset")) if hasattr(node, "end_lineno"): item_text += " - " + str(getattr(node, "end_lineno")) if hasattr(node, "end_col_offset"): item_text += "." + str(getattr(node, "end_col_offset")) lines = [item_text] + [pretty(field_value, field_key, level+1) for field_key, field_value in fields] return "\n".join(lines) def _get_ordered_child_nodes(node): if isinstance(node, ast.Dict): children = [] for i in range(len(node.keys)): children.append(node.keys[i]) children.append(node.values[i]) return children elif isinstance(node, ast.Call): children = [node.func] + node.args for kw in node.keywords: children.append(kw.value) # TODO: take care of Python 3.5 updates (eg. args=[Starred] and keywords) if hasattr(node, "starargs") and node.starargs is not None: children.append(node.starargs) if hasattr(node, "kwargs") and node.kwargs is not None: children.append(node.kwargs) children.sort(key=lambda x: (x.lineno, x.col_offset)) return children # arguments and their defaults are detached in the AST elif isinstance(node, ast.arguments): children = node.args + node.kwonlyargs + node.kw_defaults + node.defaults if node.vararg is not None: children.append(node.vararg) if node.kwarg is not None: children.append(node.kwarg) children.sort(key=lambda x: (x.lineno, x.col_offset)) return children else: return ast.iter_child_nodes(node) def _tokens_text(tokens): return "".join([t.string for t in tokens]) def _range_contains_smaller(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset): this_start = (self_lineno, self_col_offset) this_end = (self_end_lineno, self_end_col_offset) other_start = (other_lineno, other_col_offset) other_end = (other_end_lineno, other_end_col_offset) return (this_start < other_start and this_end > other_end or this_start == other_start and this_end > other_end or this_start < other_start and this_end == other_end) def _range_contains_smaller_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset): return (_range_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset) or _range_contains_smaller(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset)) def _range_eq(self_lineno, self_col_offset, self_end_lineno, self_end_col_offset, other_lineno, other_col_offset, other_end_lineno, other_end_col_offset): return (self_lineno == other_lineno and self_col_offset == other_col_offset and self_end_lineno == other_end_lineno and self_end_col_offset == other_end_col_offset) thonny-2.1.16/thonny/shared/thonny/backend.py0000666000000000000000000014502613201264465017332 0ustar 00000000000000# -*- coding: utf-8 -*- import sys import os.path import inspect import ast import _ast import _io import traceback import types import logging import pydoc import builtins import site import __main__ # @UnresolvedImport from thonny import ast_utils from thonny.common import TextRange,\ parse_message, serialize_message, DebuggerCommand,\ ToplevelCommand, FrameInfo, InlineCommand, InputSubmission import signal import warnings BEFORE_STATEMENT_MARKER = "_thonny_hidden_before_stmt" BEFORE_EXPRESSION_MARKER = "_thonny_hidden_before_expr" AFTER_STATEMENT_MARKER = "_thonny_hidden_after_stmt" AFTER_EXPRESSION_MARKER = "_thonny_hidden_after_expr" EXCEPTION_TRACEBACK_LIMIT = 100 DEBUG = True logger = logging.getLogger() info = logger.info class VM: def __init__(self): self._main_dir = os.path.dirname(sys.modules["thonny"].__file__) self._heap = {} # WeakValueDictionary would be better, but can't store reference to None site.sethelper() # otherwise help function is not available pydoc.pager = pydoc.plainpager # otherwise help command plays tricks self._install_fake_streams() self._current_executor = None self._io_level = 0 original_argv = sys.argv.copy() original_path = sys.path.copy() # clean up path sys.path = [d for d in sys.path if d != ""] # script mode if len(sys.argv) > 1: special_names_to_remove = set() sys.argv[:] = sys.argv[1:] # shift argv[1] to position of script name sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0]))) # add program's dir __main__.__dict__["__file__"] = sys.argv[0] # TODO: inspect.getdoc # shell mode else: special_names_to_remove = {"__file__", "__cached__"} sys.argv[:] = [""] # empty "script name" sys.path.insert(0, "") # current dir # add jedi if "JEDI_LOCATION" in os.environ: sys.path.append(os.environ["JEDI_LOCATION"]) # clean __main__ global scope for key in list(__main__.__dict__.keys()): if not key.startswith("__") or key in special_names_to_remove: del __main__.__dict__[key] # unset __doc__, then exec dares to write doc of the script there __main__.__doc__ = None self.send_message(self.create_message("ToplevelResult", main_dir=self._main_dir, original_argv=original_argv, original_path=original_path, argv=sys.argv, path=sys.path, welcome_text="Python " + _get_python_version_string(), executable=sys.executable, in_venv=hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix, python_version=_get_python_version_string(), cwd=os.getcwd())) self._install_signal_handler() def mainloop(self): try: while True: try: cmd = self._fetch_command() self.handle_command(cmd, "waiting_toplevel_command") except KeyboardInterrupt: logger.exception("Interrupt in mainloop") # Interrupt must always result in waiting_toplevel_command state # Don't show error messages, as the interrupted command may have been InlineCommand # (handlers of ToplevelCommands in normal cases catch the interrupt and provide # relevant message) self.send_message(self.create_message("ToplevelResult")) except: logger.exception("Crash in mainloop") def handle_command(self, cmd, command_context): assert isinstance(cmd, ToplevelCommand) or isinstance(cmd, InlineCommand) error_response_type = "ToplevelResult" if isinstance(cmd, ToplevelCommand) else "InlineError" try: handler = getattr(self, "_cmd_" + cmd.command) except AttributeError: response = self.create_message(error_response_type, error="Unknown command: " + cmd.command) else: try: response = handler(cmd) except: response = self.create_message(error_response_type, error="Thonny internal error: {0}".format(traceback.format_exc(EXCEPTION_TRACEBACK_LIMIT))) if response is not None: response["command_context"] = command_context response["command"] = cmd.command if response["message_type"] == "ToplevelResult": self._add_tkinter_info(response) self.send_message(response) def _install_signal_handler(self): def signal_handler(signal, frame): raise KeyboardInterrupt("Execution interrupted") if os.name == 'nt': signal.signal(signal.SIGBREAK, signal_handler) else: signal.signal(signal.SIGINT, signal_handler) def _cmd_cd(self, cmd): try: os.chdir(cmd.path) return self.create_message("ToplevelResult") except Exception as e: # TODO: should output user error return self.create_message("ToplevelResult", error=str(e)) def _cmd_Reset(self, cmd): # nothing to do, because Reset always happens in fresh process return self.create_message("ToplevelResult", welcome_text="Python " + _get_python_version_string(), executable=sys.executable) def _cmd_Run(self, cmd): return self._execute_file(cmd, False) def _cmd_run(self, cmd): return self._execute_file(cmd, False) def _cmd_Debug(self, cmd): return self._execute_file(cmd, True) def _cmd_debug(self, cmd): return self._execute_file(cmd, True) def _cmd_execute_source(self, cmd): return self._execute_source(cmd, "ToplevelResult") def _cmd_execute_source_inline(self, cmd): return self._execute_source(cmd, "InlineResult") def _cmd_tkupdate(self, cmd): # advance the event loop # http://bugs.python.org/issue989712 # http://bugs.python.org/file6090/run.py.diff try: root = self._get_tkinter_default_root() if root is None: return import tkinter while root.dooneevent(tkinter._tkinter.DONT_WAIT): pass except: pass return None def _cmd_get_globals(self, cmd): if not cmd.module_name in sys.modules: raise ThonnyClientError("Module '{0}' is not loaded".format(cmd.module_name)) return self.create_message("Globals", module_name=cmd.module_name, globals=self.export_variables(sys.modules[cmd.module_name].__dict__)) def _cmd_get_locals(self, cmd): for frame in inspect.stack(): if id(frame) == cmd.frame_id: return self.create_message("Locals", locals=self.export_variables(frame.f_locals)) else: raise ThonnyClientError("Frame '{0}' not found".format(cmd.frame_id)) def _cmd_get_heap(self, cmd): result = {} for key in self._heap: result[key] = self.export_value(self._heap[key]) return self.create_message("Heap", heap=result) def _cmd_shell_autocomplete(self, cmd): error = None try: import jedi except ImportError: completions = [] error = "Could not import jedi" else: try: #with warnings.catch_warnings(): interpreter = jedi.Interpreter(cmd.source, [__main__.__dict__]) completions = self._export_completions(interpreter.completions()) except Exception as e: completions = [] error = "Autocomplete error: " + str(e) except: completions = [] error = "Autocomplete error" return self.create_message("ShellCompletions", source=cmd.source, completions=completions, error=error ) def _cmd_editor_autocomplete(self, cmd): error = None try: import jedi with warnings.catch_warnings(): script = jedi.Script(cmd.source, cmd.row, cmd.column, cmd.filename) completions = self._export_completions(script.completions()) except ImportError: completions = [] error = "Could not import jedi" except Exception as e: completions = [] error = "Autocomplete error: " + str(e) except: completions = [] error = "Autocomplete error" return self.create_message("EditorCompletions", source=cmd.source, row=cmd.row, column=cmd.column, filename=cmd.filename, completions=completions, error=error) def _export_completions(self, jedi_completions): result = [] for c in jedi_completions: if not c.name.startswith("__"): record = {"name":c.name, "complete":c.complete, "type":c.type, "description":c.description} try: """ TODO: if c.type in ["class", "module", "function"]: if c.type == "function": record["docstring"] = c.docstring() else: record["docstring"] = c.description + "\n" + c.docstring() """ except: pass result.append(record) return result def _cmd_get_object_info(self, cmd): if cmd.object_id in self._heap: value = self._heap[cmd.object_id] attributes = {} for name in dir(value): if not name.startswith("__") or cmd.all_attributes: #attributes[name] = inspect.getattr_static(value, name) try: attributes[name] = getattr(value, name) except: pass self._heap[id(type(value))] = type(value) info = {'id' : cmd.object_id, 'repr' : repr(value), 'type' : str(type(value)), 'type_id' : id(type(value)), 'attributes': self.export_variables(attributes)} if isinstance(value, _io.TextIOWrapper): self._add_file_handler_info(value, info) elif (type(value) in (types.BuiltinFunctionType, types.BuiltinMethodType, types.FunctionType, types.LambdaType, types.MethodType)): self._add_function_info(value, info) elif (isinstance(value, list) or isinstance(value, tuple) or isinstance(value, set)): self._add_elements_info(value, info) elif (isinstance(value, dict)): self._add_entries_info(value, info) else: info = {'id' : cmd.object_id, "repr": "", "type" : "object", "type_id" : id(object), "attributes" : {}} return self.create_message("ObjectInfo", id=cmd.object_id, info=info) def _get_tkinter_default_root(self): tkinter = sys.modules.get("tkinter") if tkinter is not None: return getattr(tkinter, "_default_root", None) else: return None def _add_file_handler_info(self, value, info): try: assert isinstance(value.name, str) assert value.mode in ("r", "rt", "tr", "br", "rb") assert value.errors in ("strict", None) assert value.newlines is None or value.tell() > 0 # TODO: cache the content # TODO: don't read too big files with open(value.name, encoding=value.encoding) as f: info["file_encoding"] = f.encoding info["file_content"] = f.read() info["file_tell"] = value.tell() except Exception as e: info["file_error"] = "Could not get file content, error:" + str(e) pass def _add_tkinter_info(self, msg): # tkinter._default_root is not None, # when window has been created and mainloop isn't called or hasn't ended yet msg["tkinter_is_active"] = self._get_tkinter_default_root() is not None def _add_function_info(self, value, info): try: info["source"] = inspect.getsource(value) except: pass def _add_elements_info(self, value, info): info["elements"] = [] for element in value: info["elements"].append(self.export_value(element)) def _add_entries_info(self, value, info): info["entries"] = [] for key in value: info["entries"].append((self.export_value(key), self.export_value(value[key]))) def _execute_file(self, cmd, debug_mode): # args are accepted only in Run and Debug, # and were stored in sys.argv already in VM.__init__ result_attributes = self._execute_source_ex(cmd.source, cmd.full_filename, "exec", debug_mode) return self.create_message("ToplevelResult", **result_attributes) def _execute_source(self, cmd, result_type): filename = "" if hasattr(cmd, "global_vars"): global_vars = cmd.global_vars elif hasattr(cmd, "extra_vars"): global_vars = __main__.__dict__.copy() # Don't want to mess with main namespace global_vars.update(cmd.extra_vars) else: global_vars = __main__.__dict__ # let's see if it's single expression or something more complex try: root = ast.parse(cmd.source, filename=filename, mode="exec") except SyntaxError as e: return self.create_message(result_type, error="".join(traceback.format_exception_only(SyntaxError, e))) assert isinstance(root, ast.Module) if len(root.body) == 1 and isinstance(root.body[0], ast.Expr): mode = "eval" else: mode = "exec" result_attributes = self._execute_source_ex(cmd.source, filename, mode, hasattr(cmd, "debug_mode") and cmd.debug_mode, global_vars) if "__result__" in global_vars: result_attributes["__result__"] = global_vars["__result__"] if hasattr(cmd, "request_id"): result_attributes["request_id"] = cmd.request_id else: result_attributes["request_id"] = None return self.create_message(result_type, **result_attributes) def _execute_source_ex(self, source, filename, execution_mode, debug_mode, global_vars=None): if debug_mode: self._current_executor = FancyTracer(self) else: self._current_executor = Executor(self) try: return self._current_executor.execute_source(source, filename, execution_mode, global_vars) finally: self._current_executor = None def _install_fake_streams(self): self._original_stdin = sys.stdin self._original_stdout = sys.stdout self._original_stderr = sys.stderr # yes, both out and err will be directed to out (but with different tags) # this allows client to see the order of interleaving writes to stdout/stderr sys.stdin = VM.FakeInputStream(self, sys.stdin) sys.stdout = VM.FakeOutputStream(self, sys.stdout, "stdout") sys.stderr = VM.FakeOutputStream(self, sys.stdout, "stderr") # fake it properly: replace also "backup" streams sys.__stdin__ = sys.stdin sys.__stdout__ = sys.stdout sys.__stderr__ = sys.stderr def _fetch_command(self): line = self._original_stdin.readline() if line == "": logger.info("Read stdin EOF") sys.exit() cmd = parse_message(line) return cmd def create_message(self, message_type, **kwargs): kwargs["message_type"] = message_type if "cwd" not in kwargs: kwargs["cwd"] = os.getcwd() return kwargs def send_message(self, msg): self._original_stdout.write(serialize_message(msg) + "\n") self._original_stdout.flush() def export_value(self, value, skip_None=False): if value is None and skip_None: return None self._heap[id(value)] = value try: type_name = value.__class__.__name__ except: type_name = type(value).__name__ result = {'id' : id(value), 'repr' : repr(value), 'type_name' : type_name} return result def export_variables(self, variables): result = {} for name in variables: if not name.startswith("_thonny_hidden_"): result[name] = self.export_value(variables[name]) return result def _debug(self, *args): print("VM:", *args, file=self._original_stderr) def _enter_io_function(self): self._io_level += 1 def _exit_io_function(self): self._io_level -= 1 def is_doing_io(self): return self._io_level > 0 class FakeStream: def __init__(self, vm, target_stream): self._vm = vm self._target_stream = target_stream def isatty(self): return True def __getattr__(self, name): # TODO: is it safe to perform those other functions without notifying vm # via _enter_io_function? return getattr(self._target_stream, name) class FakeOutputStream(FakeStream): def __init__(self, vm, target_stream, stream_name): VM.FakeStream.__init__(self, vm, target_stream) self._stream_name = stream_name def write(self, data): try: self._vm._enter_io_function() if data != "": self._vm.send_message(self._vm.create_message("ProgramOutput", stream_name=self._stream_name, data=data)) finally: self._vm._exit_io_function() def writelines(self, lines): try: self._vm._enter_io_function() self.write(''.join(lines)) finally: self._vm._exit_io_function() class FakeInputStream(FakeStream): def _generic_read(self, method, limit=-1): try: self._vm._enter_io_function() self._vm.send_message(self._vm.create_message("InputRequest", method=method, limit=limit)) while True: cmd = self._vm._fetch_command() if isinstance(cmd, InputSubmission): return cmd.data elif isinstance(cmd, InlineCommand): self._vm.handle_command(cmd, "waiting_input") else: raise ThonnyClientError("Wrong type of command when waiting for input") finally: self._vm._exit_io_function() def read(self, limit=-1): return self._generic_read("read", limit) def readline(self, limit=-1): return self._generic_read("readline", limit) def readlines(self, limit=-1): return self._generic_read("readlines", limit) class Executor: def __init__(self, vm): self._vm = vm def execute_source(self, source, filename, mode, global_vars=None): if global_vars is None: global_vars = __main__.__dict__ try: bytecode = self._compile_source(source, filename, mode) if hasattr(self, "_trace"): sys.settrace(self._trace) if mode == "eval": value = eval(bytecode, global_vars) if value is not None: builtins._ = value return {"value_info" : self._vm.export_value(value)} else: assert mode == "exec" exec(bytecode, global_vars) # return {"context_info" : "after normal execution", "source" : source, "filename" : filename, "mode" : mode} except SyntaxError as e: return {"error" : "".join(traceback.format_exception_only(SyntaxError, e))} except ThonnyClientError as e: return {"error" : str(e)} except SystemExit: e_type, e_value, e_traceback = sys.exc_info() self._print_user_exception(e_type, e_value, e_traceback) return {"SystemExit" : True} except: # other unhandled exceptions (supposedly client program errors) are printed to stderr, as usual # for VM mainloop they are not exceptions e_type, e_value, e_traceback = sys.exc_info() self._print_user_exception(e_type, e_value, e_traceback) return {"context_info" : "other unhandled exception"} finally: sys.settrace(None) def _print_user_exception(self, e_type, e_value, e_traceback): lines = traceback.format_exception(e_type, e_value, e_traceback) for line in lines: # skip lines denoting thonny execution frame if ("thonny/backend" in line or "thonny\\backend" in line or "remove this line from stacktrace" in line): continue else: sys.stderr.write(line) def _compile_source(self, source, filename, mode): return compile(source, filename, mode) class FancyTracer(Executor): def __init__(self, vm): self._vm = vm self._normcase_thonny_src_dir = os.path.normcase(os.path.dirname(sys.modules["thonny"].__file__)) self._instrumented_files = _PathSet() self._interesting_files = _PathSet() # only events happening in these files are reported self._current_command = None self._unhandled_exception = None self._install_marker_functions() self._custom_stack = [] def execute_source(self, source, filename, mode, global_vars=None): self._current_command = DebuggerCommand(command="step", state=None, focus=None, frame_id=None, exception=None) return Executor.execute_source(self, source, filename, mode, global_vars) #assert len(self._custom_stack) == 0 def _install_marker_functions(self): # Make dummy marker functions universally available by putting them # into builtin scope self.marker_function_names = { BEFORE_STATEMENT_MARKER, AFTER_STATEMENT_MARKER, BEFORE_EXPRESSION_MARKER, AFTER_EXPRESSION_MARKER, } for name in self.marker_function_names: if not hasattr(builtins, name): setattr(builtins, name, getattr(self, name)) def _is_interesting_exception(self, frame): # interested only in exceptions in command frame or it's parent frames cmd = self._current_command return (id(frame) == cmd.frame_id or not self._frame_is_alive(cmd.frame_id)) def _compile_source(self, source, filename, mode): root = ast.parse(source, filename, mode) ast_utils.mark_text_ranges(root, source) self._tag_nodes(root) self._insert_expression_markers(root) self._insert_statement_markers(root) self._instrumented_files.add(filename) return compile(root, filename, mode) def _may_step_in(self, code): return not ( code is None or code.co_filename is None or code.co_flags & inspect.CO_GENERATOR # @UndefinedVariable or sys.version_info >= (3,5) and code.co_flags & inspect.CO_COROUTINE # @UndefinedVariable or sys.version_info >= (3,5) and code.co_flags & inspect.CO_ITERABLE_COROUTINE # @UndefinedVariable or sys.version_info >= (3,6) and code.co_flags & inspect.CO_ASYNC_GENERATOR # @UndefinedVariable or "importlib._bootstrap" in code.co_filename or os.path.normcase(code.co_filename) not in self._instrumented_files and code.co_name not in self.marker_function_names or os.path.normcase(code.co_filename).startswith(self._normcase_thonny_src_dir) and code.co_name not in self.marker_function_names or self._vm.is_doing_io() ) def _trace(self, frame, event, arg): """ 1) Detects marker calls and responds to client queries in these spots 2) Maintains a customized view of stack """ if not self._may_step_in(frame.f_code): return code_name = frame.f_code.co_name if event == "call": self._unhandled_exception = None # some code is running, therefore exception is not propagating anymore if code_name in self.marker_function_names: # the main thing if code_name == BEFORE_STATEMENT_MARKER: event = "before_statement" elif code_name == AFTER_STATEMENT_MARKER: event = "after_statement" elif code_name == BEFORE_EXPRESSION_MARKER: event = "before_expression" elif code_name == AFTER_EXPRESSION_MARKER: event = "after_expression" else: raise AssertionError("Unknown marker function") marker_function_args = frame.f_locals.copy() del marker_function_args["self"] self._handle_progress_event(frame.f_back, event, marker_function_args) self._try_interpret_as_again_event(frame.f_back, event, marker_function_args) else: # Calls to proper functions. # Client doesn't care about these events, # it cares about "before_statement" events in the first statement of the body self._custom_stack.append(CustomStackFrame(frame, "call")) elif event == "return": if code_name not in self.marker_function_names: self._custom_stack.pop() if len(self._custom_stack) == 0: # We popped last frame, this means our program has ended. # There may be more events coming from upper (system) frames # but we're not interested in those sys.settrace(None) else: pass elif event == "exception": exc = arg[1] if self._unhandled_exception is None: # this means it's the first time we see this exception exc.causing_frame = frame else: # this means the exception is propagating to older frames # get the causing_frame from previous occurrence exc.causing_frame = self._unhandled_exception.causing_frame self._unhandled_exception = exc if self._is_interesting_exception(frame): self._report_state_and_fetch_next_message(frame) # TODO: support line event in non-instrumented files elif event == "line": self._unhandled_exception = None return self._trace def _handle_progress_event(self, frame, event, args): """ Tries to respond to current command in this state. If it can't, then it returns, program resumes and _trace will call it again in another state. Otherwise sends response and fetches next command. """ self._debug("Progress event:", event, self._current_command) focus = TextRange(*args["text_range"]) self._custom_stack[-1].last_event = event self._custom_stack[-1].last_event_focus = focus self._custom_stack[-1].last_event_args = args # Select the correct method according to the command tester = getattr(self, "_cmd_" + self._current_command.command + "_completed") # If method decides we're in the right place to respond to the command ... if tester(frame, event, args, focus, self._current_command): if event == "after_expression": value = self._vm.export_value(args["value"]) else: value = None self._report_state_and_fetch_next_message(frame, value) def _report_state_and_fetch_next_message(self, frame, value=None): #self._debug("Completed command: ", self._current_command) if self._unhandled_exception is not None: frame_infos = traceback.format_stack(self._unhandled_exception.causing_frame) # I want to show frames from current frame to causing_frame if frame == self._unhandled_exception.causing_frame: interesting_frame_infos = [] else: # c how far is current frame from causing_frame? _distance = 0 _f = self._unhandled_exception.causing_frame while _f != frame: _distance += 1 _f = _f.f_back if _f == None: break interesting_frame_infos = frame_infos[-_distance:] exception_lower_stack_description = "".join(interesting_frame_infos) exception_msg = str(self._unhandled_exception) else: exception_lower_stack_description = None exception_msg = None self._vm.send_message(self._vm.create_message("DebuggerProgress", command=self._current_command.command, stack=self._export_stack(), exception=self._vm.export_value(self._unhandled_exception, True), exception_msg=exception_msg, exception_lower_stack_description=exception_lower_stack_description, value=value, command_context="waiting_debugger_command" )) # Fetch next debugger command self._current_command = self._vm._fetch_command() self._debug("got command:", self._current_command) # get non-progress commands out our way self._respond_to_inline_commands() assert isinstance(self._current_command, DebuggerCommand) # Return and let Python run to next progress event def _try_interpret_as_again_event(self, frame, original_event, original_args): """ Some after_* events can be interpreted also as "before_*_again" events (eg. when last argument of a call was evaluated, then we are just before executing the final stage of the call) """ if original_event == "after_expression": node_tags = original_args.get("node_tags") value = original_args.get("value") if (node_tags is not None and ("last_child" in node_tags or "or_arg" in node_tags and value or "and_arg" in node_tags and not value)): # next step will be finalizing evaluation of parent of current expr # so let's say we're before that parent expression again_args = {"text_range" : original_args.get("parent_range"), "node_tags" : ""} again_event = ("before_expression_again" if "child_of_expression" in node_tags else "before_statement_again") self._handle_progress_event(frame, again_event, again_args) def _respond_to_inline_commands(self): while isinstance(self._current_command, InlineCommand): self._vm.handle_command(self._current_command, "waiting_debugger_command") self._current_command = self._vm._fetch_command() def _get_frame_source_info(self, frame): if frame.f_code.co_name == "": obj = inspect.getmodule(frame) lineno = 1 else: obj = frame.f_code lineno = obj.co_firstlineno # lineno returned by getsourcelines is not consistent between modules vs functions lines, _ = inspect.getsourcelines(obj) return "".join(lines), lineno def _cmd_exec_completed(self, frame, event, args, focus, cmd): """ Identifies the moment when piece of code indicated by cmd.frame_id and cmd.focus has completed execution (either successfully or not). """ # it's meant to be executed in before* state, but if we're not there # we'll step there if cmd.state not in ("before_expression", "before_expression_again", "before_statement", "before_statement_again"): return self._cmd_step_completed(frame, event, args, focus, cmd) if id(frame) == cmd.frame_id: if focus.is_smaller_in(cmd.focus): # we're executing a child of command focus, # keep running return False elif focus == cmd.focus: if event.startswith("before_"): # we're just starting return False elif (event == "after_expression" and cmd.state in ("before_expression", "before_expression_again") or event == "after_statement" and cmd.state in ("before_statement", "before_statement_again")): # Normal completion # Maybe there was an exception, but this is forgotten now cmd._unhandled_exception = False self._debug("Exec normal") return True elif (cmd.state in ("before_statement", "before_statement_again") and event == "after_expression"): # Same code range can contain expression statement and expression. # Here we need to run just a bit more return False else: # shouldn't be here raise AssertionError("Unexpected state in responding to " + str(cmd)) else: # We're outside of starting focus, assumedly because of an exception self._debug("Exec outside", cmd.focus, focus) return True else: # We're in another frame if self._frame_is_alive(cmd.frame_id): # We're in a successor frame, keep running return False else: # Original frame has completed, assumedly because of an exception # We're done self._debug("Exec wrong frame") return True def _cmd_step_completed(self, frame, event, args, focus, cmd): return True def _cmd_run_to_before_completed(self, frame, event, args, focus, cmd): return event.startswith("before") def _cmd_out_completed(self, frame, event, args, focus, cmd): """Complete current frame""" return ( # the frame has completed not self._frame_is_alive(cmd.frame_id) # we're in the same frame but on higher level or id(frame) == cmd.frame_id and focus.contains_smaller(cmd.focus) ) def _cmd_line_completed(self, frame, event, args, focus, cmd): return (event == "before_statement" and os.path.normcase(frame.f_code.co_filename) == os.path.normcase(cmd.target_filename) and focus.lineno == cmd.target_lineno and (focus != cmd.focus or id(frame) != cmd.frame_id)) def _frame_is_alive(self, frame_id): for frame in self._custom_stack: if frame.id == frame_id: return True else: return False def _export_stack(self): result = [] for custom_frame in self._custom_stack: last_event_args = custom_frame.last_event_args.copy() if "value" in last_event_args: last_event_args["value"] = self._vm.export_value(last_event_args["value"]) system_frame = custom_frame.system_frame source, firstlineno = self._get_frame_source_info(system_frame) result.append(FrameInfo( id=id(system_frame), filename=system_frame.f_code.co_filename, module_name=system_frame.f_globals["__name__"], code_name=system_frame.f_code.co_name, locals=self._vm.export_variables(system_frame.f_locals), source=source, firstlineno=firstlineno, last_event=custom_frame.last_event, last_event_args=last_event_args, last_event_focus=custom_frame.last_event_focus, )) return result def _thonny_hidden_before_stmt(self, text_range, node_tags): """ The code to be debugged will be instrumented with this function inserted before each statement. Entry into this function indicates that statement as given by the code range is about to be evaluated next. """ return None def _thonny_hidden_after_stmt(self, text_range, node_tags): """ The code to be debugged will be instrumented with this function inserted after each statement. Entry into this function indicates that statement as given by the code range was just executed successfully. """ return None def _thonny_hidden_before_expr(self, text_range, node_tags): """ Entry into this function indicates that expression as given by the code range is about to be evaluated next """ return text_range def _thonny_hidden_after_expr(self, text_range, node_tags, value, parent_range): """ The code to be debugged will be instrumented with this function wrapped around each expression (given as 2nd argument). Entry into this function indicates that expression as given by the code range was just evaluated to given value """ return value def _tag_nodes(self, root): """Marks interesting properties of AST nodes""" def add_tag(node, tag): if not hasattr(node, "tags"): node.tags = set() node.tags.add("class=" + node.__class__.__name__) node.tags.add(tag) for node in ast.walk(root): # tag last children last_child = ast_utils.get_last_child(node) if last_child is not None and last_child: add_tag(node, "has_children") if isinstance(last_child, ast.AST): last_child.parent_node = node add_tag(last_child, "last_child") if isinstance(node, _ast.expr): add_tag(last_child, "child_of_expression") else: add_tag(last_child, "child_of_statement") if isinstance(node, ast.Call): add_tag(last_child, "last_call_arg") # other cases if isinstance(node, ast.Call): add_tag(node.func, "call_function") node.func.parent_node = node if isinstance(node, ast.BoolOp) and node.op == ast.Or(): for child in node.values: add_tag(child, "or_arg") child.parent_node = node if isinstance(node, ast.BoolOp) and node.op == ast.And(): for child in node.values: add_tag(child, "and_arg") child.parent_node = node # TODO: assert (it doesn't evaluate msg when test == True) if isinstance(node, ast.Str): add_tag(node, "StringLiteral") if isinstance(node, ast.Num): add_tag(node, "NumberLiteral") if isinstance(node, ast.ListComp): add_tag(node.elt, "ListComp.elt") if isinstance(node, ast.SetComp): add_tag(node.elt, "SetComp.elt") if isinstance(node, ast.DictComp): add_tag(node.key, "DictComp.key") add_tag(node.value, "DictComp.value") if isinstance(node, ast.comprehension): for expr in node.ifs: add_tag(expr, "comprehension.if") # make sure every node has this field if not hasattr(node, "tags"): node.tags = set() def _should_instrument_as_expression(self, node): return (isinstance(node, _ast.expr) and (not hasattr(node, "ctx") or isinstance(node.ctx, ast.Load)) # TODO: repeatedly evaluated subexpressions of comprehensions # can be supported (but it requires some redesign both in backend and GUI) and "ListComp.elt" not in node.tags and "SetComp.elt" not in node.tags and "DictComp.key" not in node.tags and "DictComp.value" not in node.tags and "comprehension.if" not in node.tags ) return def _should_instrument_as_statement(self, node): return (isinstance(node, _ast.stmt) # Shouldn't insert anything before from __future__ import # as this is not a normal statement # https://bitbucket.org/plas/thonny/issues/183/thonny-throws-false-positive-syntaxerror and (not isinstance(node, _ast.ImportFrom) or node.module != "__future__")) def _insert_statement_markers(self, root): # find lists of statements and insert before/after markers for each statement for name, value in ast.iter_fields(root): if isinstance(value, ast.AST): self._insert_statement_markers(value) elif isinstance(value, list): if len(value) > 0: new_list = [] for node in value: if self._should_instrument_as_statement(node): # self._debug("EBFOMA", node) # add before marker new_list.append(self._create_statement_marker(node, BEFORE_STATEMENT_MARKER)) # original statement if self._should_instrument_as_statement(node): self._insert_statement_markers(node) new_list.append(node) if isinstance(node, _ast.stmt): # add after marker new_list.append(self._create_statement_marker(node, AFTER_STATEMENT_MARKER)) setattr(root, name, new_list) def _create_statement_marker(self, node, function_name): call = self._create_simple_marker_call(node, function_name) stmt = ast.Expr(value=call) ast.copy_location(stmt, node) ast.fix_missing_locations(stmt) return stmt def _insert_expression_markers(self, node): """ each expression e gets wrapped like this: _after(_before(_loc, _node_is_zoomable), e, _node_role, _parent_range) where _after is function that gives the resulting value _before is function that signals the beginning of evaluation of e _loc gives the code range of e _node_is_zoomable indicates whether this node has subexpressions _node_role is either 'last_call_arg', 'last_op_arg', 'first_or_arg', 'first_and_arg', 'function' or None """ tracer = self class ExpressionVisitor(ast.NodeTransformer): def generic_visit(self, node): if isinstance(node, _ast.expr): if isinstance(node, ast.Starred): # keep this node as is, but instrument its children return ast.NodeTransformer.generic_visit(self, node) elif tracer._should_instrument_as_expression(node): # before marker before_marker = tracer._create_simple_marker_call(node, BEFORE_EXPRESSION_MARKER) ast.copy_location(before_marker, node) # after marker after_marker = ast.Call ( func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()), args=[ before_marker, tracer._create_tags_literal(node), ast.NodeTransformer.generic_visit(self, node), tracer._create_location_literal(node.parent_node if hasattr(node, "parent_node") else None) ], keywords=[] ) ast.copy_location(after_marker, node) ast.fix_missing_locations(after_marker) return after_marker else: # This expression (and its children) should be ignored return node else: # Descend into statements return ast.NodeTransformer.generic_visit(self, node) return ExpressionVisitor().visit(node) def _create_location_literal(self, node): if node is None: return ast_utils.value_to_literal(None) assert hasattr(node, "end_lineno") assert hasattr(node, "end_col_offset") nums = [] for value in node.lineno, node.col_offset, node.end_lineno, node.end_col_offset: nums.append(ast.Num(n=value)) return ast.Tuple(elts=nums, ctx=ast.Load()) def _create_tags_literal(self, node): if hasattr(node, "tags"): # maybe set would perform as well, but I think string is faster return ast_utils.value_to_literal(",".join(node.tags)) #self._debug("YESTAGS") else: #self._debug("NOTAGS " + str(node)) return ast_utils.value_to_literal("") def _create_simple_marker_call(self, node, fun_name): assert hasattr(node, "end_lineno") assert hasattr(node, "end_col_offset") args = [ self._create_location_literal(node), self._create_tags_literal(node), ] return ast.Call ( func=ast.Name(id=fun_name, ctx=ast.Load()), args=args, keywords=[] ) def _debug(self, *args): print("TRACER:", *args, file=self._vm._original_stderr) class CustomStackFrame: def __init__(self, frame, last_event, focus=None): self.id = id(frame) self.system_frame = frame self.last_event = last_event self.focus = None class ThonnyClientError(Exception): pass def fdebug(frame, msg, *args): if logger.isEnabledFor(logging.DEBUG): logger.debug(_get_frame_prefix(frame) + msg, *args) def _get_frame_prefix(frame): return str(id(frame)) + " " + ">" * len(inspect.getouterframes(frame, 0)) + " " def _get_python_version_string(add_word_size=False): result = ".".join(map(str, sys.version_info[:3])) if sys.version_info[3] != "final": result += "-" + sys.version_info[3] if add_word_size: result += " (" + ("64" if sys.maxsize > 2**32 else "32")+ " bit)" return result class _PathSet: "implementation of set whose in operator works well for filenames" def __init__(self): self._normcase_set = set() def add(self, name): self._normcase_set.add(os.path.normcase(name)) def remove(self, name): self._normcase_set.remove(os.path.normcase(name)) def clear(self): self._normcase_set.clear() def __contains__(self, name): return os.path.normcase(name) in self._normcase_set def __iter__(self): for item in self._normcase_set: yield item thonny-2.1.16/thonny/shared/thonny/common.py0000666000000000000000000001220113201264465017217 0ustar 00000000000000# -*- coding: utf-8 -*- """ Classes used both by front-end and back-end """ import shlex class Record: def __init__(self, **kw): self.__dict__.update(kw) def update(self, **kw): self.__dict__.update(kw) def setdefault(self, **kw): "updates those fields that are not yet present (similar to dict.setdefault)" for key in kw: if not hasattr(self, key): setattr(self, key, kw[key]) def __repr__(self): keys = self.__dict__.keys() items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(self.__class__.__name__, ", ".join(items)) def __str__(self): keys = sorted(self.__dict__.keys()) items = ("{}={!r}".format(k, str(self.__dict__[k])) for k in keys) return "{}({})".format(self.__class__.__name__, ", ".join(items)) def __eq__(self, other): if type(self) != type(other): return False if len(self.__dict__) != len(other.__dict__): return False for key in self.__dict__: if not hasattr(other, key): return False self_value = getattr(self, key) other_value = getattr(other, key) if type(self_value) != type(other_value) or self_value != other_value: return False return True def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(repr(self)) class TextRange(Record): def __init__(self, lineno, col_offset, end_lineno, end_col_offset): self.lineno = lineno self.col_offset = col_offset self.end_lineno = end_lineno self.end_col_offset = end_col_offset def contains_smaller(self, other): this_start = (self.lineno, self.col_offset) this_end = (self.end_lineno, self.end_col_offset) other_start = (other.lineno, other.col_offset) other_end = (other.end_lineno, other.end_col_offset) return (this_start < other_start and this_end > other_end or this_start == other_start and this_end > other_end or this_start < other_start and this_end == other_end) def contains_smaller_eq(self, other): return self.contains_smaller(other) or self == other def not_smaller_in(self, other): return not other.contains_smaller(self) def is_smaller_in(self, other): return other.contains_smaller(self) def not_smaller_eq_in(self, other): return not other.contains_smaller_eq(self) def is_smaller_eq_in(self, other): return other.contains_smaller_eq(self) def get_start_index(self): return str(self.lineno) + "." + str(self.col_offset) def get_end_index(self): return str(self.end_lineno) + "." + str(self.end_col_offset) def __str__(self): return "TR(" + str(self.lineno) + "." + str(self.col_offset) + ", " \ + str(self.end_lineno) + "." + str(self.end_col_offset) + ")" class FrameInfo(Record): def get_description(self): return ( "[" + str(self.id) + "] " + self.code_name + " in " + self.filename + ", focus=" + str(self.focus) ) class ToplevelCommand(Record): pass class DebuggerCommand(Record): def __init__(self, command, **kw): Record.__init__(self, **kw) self.command = command class InputSubmission(Record): pass class InlineCommand(Record): """ Can be used both during debugging and between debugging. Initially meant for sending variable and heap info requests """ def __init__(self, command, **kw): Record.__init__(self, **kw) self.command = command class CommandSyntaxError(Exception): pass def parse_shell_command(cmd_line, split_arguments=True): assert cmd_line.startswith("%") parts = cmd_line.strip().split(maxsplit=1) command = parts[0][1:] # remove % if len(parts) == 1: arg_str = "" else: arg_str = parts[1] if split_arguments: args = shlex.split(arg_str.strip(), posix=True) else: args = [arg_str] return (command, args) def serialize_message(msg): # I want to transfer only ASCII chars because # encodings are not reliable # (eg. can't find a way to specify PYTHONIOENCODING for cx_freeze'd program) return repr(msg).encode("UTF-7").decode("ASCII") def parse_message(msg_string): return eval(msg_string.encode("ASCII").decode("UTF-7")) def quote_path_for_shell(path): for c in path: if (not c.isalpha() and not c.isnumeric() and c not in "-_./\\"): return '"' + path.replace('"', '\\"') + '"' else: return path def print_structure(o): print(o.__class__.__name__) for attr in dir(o): print(attr, "=", getattr(o, attr)) class UserError(RuntimeError): pass if __name__ == "__main__": tr1 = TextRange(1,0,1,10) tr2 = TextRange(1,0,1,10) print(tr2.contains_smaller(tr1)) thonny-2.1.16/thonny/shared/thonny/__init__.py0000666000000000000000000000021113172664305017470 0ustar 00000000000000# Frontend will interpret this folder as thonny.shared.thonny # For backend parent folder will be in path, so this will be package thonnythonny-2.1.16/thonny/shared/__init__.py0000666000000000000000000000014013172664305016152 0ustar 00000000000000# Frontend will interpret this folder as thonny.shared # For backend this folder will be in paththonny-2.1.16/thonny/shell.py0000666000000000000000000006412413201264465014264 0ustar 00000000000000# -*- coding: utf-8 -*- import os.path import re from tkinter import ttk import traceback import thonny from thonny import memory, roughparse from thonny.common import ToplevelCommand, parse_shell_command from thonny.misc_utils import running_on_mac_os, shorten_repr from thonny.ui_utils import EnhancedTextWithLogging import tkinter as tk from thonny.globals import get_workbench, get_runner from thonny.codeview import EDIT_BACKGROUND, PythonText from thonny.tktextext import index2line class ShellView (ttk.Frame): def __init__(self, master, **kw): ttk.Frame.__init__(self, master, **kw) self.vert_scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.vert_scrollbar.grid(row=0, column=2, sticky=tk.NSEW) self.text = ShellText(self, font=get_workbench().get_font("EditorFont"), #foreground="white", #background="#666666", highlightthickness=0, #highlightcolor="LightBlue", borderwidth=0, yscrollcommand=self.vert_scrollbar.set, padx=4, insertwidth=2, height=10, undo=True) get_workbench().event_generate("ShellTextCreated", text_widget=self.text) get_workbench().add_command("clear_shell", "edit", "Clear shell", self.clear_shell, group=200) self.text.grid(row=0, column=1, sticky=tk.NSEW) self.vert_scrollbar['command'] = self.text.yview self.columnconfigure(1, weight=1) self.rowconfigure(0, weight=1) def focus_set(self): self.text.focus_set() def add_command(self, command, handler): self.text.add_command(command, handler) def submit_command(self, cmd_line): self.text.submit_command(cmd_line) def clear_shell(self): self.text._clear_shell() def report_exception(self, prelude=None, conclusion=None): if prelude is not None: self.text.direct_insert("end", prelude + "\n", ("stderr",)) self.text.direct_insert("end", traceback.format_exc() + "\n", ("stderr",)) if conclusion is not None: self.text.direct_insert("end", conclusion + "\n", ("stderr",)) class ShellText(EnhancedTextWithLogging, PythonText): def __init__(self, master, cnf={}, **kw): if not "background" in kw: kw["background"] = EDIT_BACKGROUND EnhancedTextWithLogging.__init__(self, master, cnf, **kw) self.bindtags(self.bindtags() + ('ShellText',)) self._before_io = True self._command_history = [] # actually not really history, because each command occurs only once self._command_history_current_index = None """ self.margin = tk.Text(self, width = 4, padx = 4, highlightthickness = 0, takefocus = 0, bd = 0, #font = self.font, cursor = "dotbox", background = '#e0e0e0', foreground = '#999999', #state='disabled' ) self.margin.grid(row=0, column=0) """ self.bind("", self._arrow_up, True) self.bind("", self._arrow_down, True) self.bind("", self._text_key_press, True) self.bind("", self._text_key_release, True) prompt_font = get_workbench().get_font("BoldEditorFont") vert_spacing = 10 io_indent = 16 code_indent = prompt_font.measure(">>> ") self.tag_configure("toplevel", font=get_workbench().get_font("EditorFont")) self.tag_configure("prompt", foreground="purple", font=prompt_font) self.tag_configure("command", foreground="black", lmargin1=code_indent, lmargin2=code_indent) self.tag_configure("welcome", foreground="DarkGray", font=get_workbench().get_font("EditorFont")) self.tag_configure("automagic", foreground="DarkGray", font=get_workbench().get_font("EditorFont")) self.tag_configure("value", foreground="DarkBlue") self.tag_configure("error", foreground="Red") self.tag_configure("io", lmargin1=io_indent, lmargin2=io_indent, rmargin=io_indent, font=get_workbench().get_font("IOFont")) self.tag_configure("stdin", foreground="Blue") self.tag_configure("stdout", foreground="Black") self.tag_configure("stderr", foreground="Red") self.tag_configure("hyperlink", foreground="#3A66DD", underline=True) self.tag_bind("hyperlink", "", self._handle_hyperlink) self.tag_bind("hyperlink", "", self._hyperlink_enter) self.tag_bind("hyperlink", "", self._hyperlink_leave) self.tag_configure("vertically_spaced", spacing1=vert_spacing) self.tag_configure("inactive", foreground="#aaaaaa") # create 3 marks: input_start shows the place where user entered but not-yet-submitted # input starts, output_end shows the end of last output, # output_insert shows where next incoming program output should be inserted self.mark_set("input_start", "end-1c") self.mark_gravity("input_start", tk.LEFT) self.mark_set("output_end", "end-1c") self.mark_gravity("output_end", tk.LEFT) self.mark_set("output_insert", "end-1c") self.mark_gravity("output_insert", tk.RIGHT) self.active_object_tags = set() self._command_handlers = {} self._last_configuration = None get_workbench().bind("InputRequest", self._handle_input_request, True) get_workbench().bind("ProgramOutput", self._handle_program_output, True) get_workbench().bind("ToplevelResult", self._handle_toplevel_result, True) self._init_menu() def _init_menu(self): self._menu = tk.Menu(self, tearoff=False) self._menu.add_command(label="Clear shell", command=self._clear_shell) def add_command(self, command, handler): self._command_handlers[command] = handler def submit_command(self, cmd_line): assert get_runner().get_state() == "waiting_toplevel_command" self.delete("input_start", "end") self.insert("input_start", cmd_line, ("automagic",)) self.see("end") self.mark_set("insert", "end") self._try_submit_input() def _handle_input_request(self, msg): self["font"] = get_workbench().get_font("IOFont") # otherwise the cursor is of toplevel size self.focus_set() self.mark_set("insert", "end") self.tag_remove("sel", "1.0", tk.END) self._current_input_request = msg self._try_submit_input() # try to use leftovers from previous request self.see("end") def _handle_program_output(self, msg): self["font"] = get_workbench().get_font("IOFont") # mark first line of io if self._before_io: self._insert_text_directly(msg.data[0], ("io", msg.stream_name, "vertically_spaced")) self._before_io = False self._insert_text_directly(msg.data[1:], ("io", msg.stream_name)) else: self._insert_text_directly(msg.data, ("io", msg.stream_name)) self.mark_set("output_end", self.index("end-1c")) self.see("end") def _handle_toplevel_result(self, msg): self["font"] = get_workbench().get_font("EditorFont") self._before_io = True if hasattr(msg, "error"): self._insert_text_directly(msg.error + "\n", ("toplevel", "error")) if hasattr(msg, "welcome_text"): configuration = get_workbench().get_option("run.backend_configuration") welcome_text = msg.welcome_text if hasattr(msg, "executable") and msg.executable != thonny.running.get_private_venv_executable(): welcome_text += " (" + msg.executable + ")" if (configuration != self._last_configuration and not (self._last_configuration is None and not configuration)): self._insert_text_directly(welcome_text, ("welcome",)) self._last_configuration = get_workbench().get_option("run.backend_configuration") if hasattr(msg, "value_info"): value_repr = shorten_repr(msg.value_info["repr"], 10000) if value_repr != "None": if get_workbench().in_heap_mode(): value_repr = memory.format_object_id(msg.value_info["id"]) object_tag = "object_" + str(msg.value_info["id"]) self._insert_text_directly(value_repr + "\n", ("toplevel", "value", object_tag)) if running_on_mac_os(): sequence = "" else: sequence = "" self.tag_bind(object_tag, sequence, lambda _: get_workbench().event_generate( "ObjectSelect", object_id=msg.value_info["id"])) self.active_object_tags.add(object_tag) self.mark_set("output_end", self.index("end-1c")) self._insert_prompt() self._try_submit_input() # Trying to submit leftover code (eg. second magic command) self.see("end") def _insert_prompt(self): # if previous output didn't put a newline, then do it now if not self.index("output_insert").endswith(".0"): self._insert_text_directly("\n", ("io",)) prompt_tags = ("toplevel", "prompt") # if previous line has value or io then add little space prev_line = self.index("output_insert - 1 lines") prev_line_tags = self.tag_names(prev_line) if "io" in prev_line_tags or "value" in prev_line_tags: prompt_tags += ("vertically_spaced",) #self.tag_add("last_result_line", prev_line) self._insert_text_directly(">>> ", prompt_tags) self.edit_reset(); def intercept_insert(self, index, txt, tags=()): if (self._editing_allowed() and self._in_current_input_range(index)): #self._print_marks("before insert") # I want all marks to stay in place self.mark_gravity("input_start", tk.LEFT) self.mark_gravity("output_insert", tk.LEFT) if get_runner().get_state() == "waiting_input": tags = tags + ("io", "stdin") else: tags = tags + ("toplevel", "command") EnhancedTextWithLogging.intercept_insert(self, index, txt, tags) if get_runner().get_state() == "waiting_input": if self._before_io: # tag first char of io differently self.tag_add("vertically_spaced", index) self._before_io = False self._try_submit_input() self.see("insert") else: self.bell() def intercept_delete(self, index1, index2=None, **kw): if index1 == "sel.first" and index2 == "sel.last" and not self.has_selection(): return if (self._editing_allowed() and self._in_current_input_range(index1) and (index2 is None or self._in_current_input_range(index2))): self.direct_delete(index1, index2, **kw) else: self.bell() def perform_return(self, event): if get_runner().get_state() == "waiting_input": # if we are fixing the middle of the input string and pressing ENTER # then we expect the whole line to be submitted not linebreak to be inserted # (at least that's how IDLE works) self.mark_set("insert", "end") # move cursor to the end # Do the return without auto indent EnhancedTextWithLogging.perform_return(self, event) self._try_submit_input() elif get_runner().get_state() == "waiting_toplevel_command": # Same with editin middle of command, but only if it's a single line command whole_input = self.get("input_start", "end-1c") # asking the whole input if ("\n" not in whole_input and self._code_is_ready_for_submission(whole_input)): self.mark_set("insert", "end") # move cursor to the end # Do the return without auto indent EnhancedTextWithLogging.perform_return(self, event) else: # Don't want auto indent when code is ready for submission source = self.get("input_start", "insert") tail = self.get("insert", "end") if self._code_is_ready_for_submission(source + "\n", tail): # No auto-indent EnhancedTextWithLogging.perform_return(self, event) else: # Allow auto-indent PythonText.perform_return(self, event) self._try_submit_input() return "break" def on_secondary_click(self, event): super().on_secondary_click(event) self._menu.tk_popup(event.x_root, event.y_root) def _in_current_input_range(self, index): try: return self.compare(index, ">=", "input_start") except: return False def _insert_text_directly(self, txt, tags=()): def _insert(txt, tags): if txt != "": self.direct_insert("output_insert", txt, tags) # I want the insertion to go before marks #self._print_marks("before output") self.mark_gravity("input_start", tk.RIGHT) self.mark_gravity("output_insert", tk.RIGHT) tags = tuple(tags) if "stderr" in tags or "error" in tags: # show lines pointing to source lines as hyperlinks for line in txt.splitlines(True): parts = re.split(r'(File .* line \d+.*)$', line, maxsplit=1) if len(parts) == 3 and " 0: assert tail.strip() == "" self.delete("insert", "end-1c") # leftover text will be kept in widget, waiting for next request. start_index = self.index("input_start") end_index = self.index("input_start+{0}c".format(len(submittable_text))) # apply correct tags (if it's leftover then it doesn't have them yet) if get_runner().get_state() == "waiting_input": self.tag_add("io", start_index, end_index) self.tag_add("stdin", start_index, end_index) else: self.tag_add("toplevel", start_index, end_index) self.tag_add("command", start_index, end_index) # update start mark for next input range self.mark_set("input_start", end_index) # Move output_insert mark after the requested_text # Leftover input, if any, will stay after output_insert, # so that any output that will come in before # next input request will go before leftover text self.mark_set("output_insert", end_index) # remove tags from leftover text for tag in ("io", "stdin", "toplevel", "command"): # don't remove automagic, because otherwise I can't know it's auto self.tag_remove(tag, end_index, "end") self._submit_input(submittable_text) def _editing_allowed(self): return get_runner().get_state() in ('waiting_toplevel_command', 'waiting_input') def _extract_submittable_input(self, input_text, tail): if get_runner().get_state() == "waiting_toplevel_command": if input_text.endswith("\n"): if input_text.strip().startswith("%"): # if several magic command are submitted, then take only first return input_text[:input_text.index("\n")+1] elif self._code_is_ready_for_submission(input_text, tail): return input_text else: return None else: return None elif get_runner().get_state() == "waiting_input": input_request = self._current_input_request method = input_request.method limit = input_request.limit # TODO: what about EOF? if isinstance(limit, int) and limit < 0: limit = None if method == "readline": # TODO: is it correct semantics? i = 0 if limit == 0: return "" while True: if i >= len(input_text): return None elif limit is not None and i+1 == limit: return input_text[:i+1] elif input_text[i] == "\n": return input_text[:i+1] else: i += 1 else: raise AssertionError("only readline is supported at the moment") def _code_is_ready_for_submission(self, source, tail=""): # Ready to submit if ends with empty line # or is complete single-line code if tail.strip() != "": return False # First check if it has unclosed parens, unclosed string or ending with : or \ parser = roughparse.RoughParser(self.indentwidth, self.tabwidth) parser.set_str(source.rstrip() + "\n") if (parser.get_continuation_type() != roughparse.C_NONE or parser.is_block_opener()): return False # Multiline compound statements need to end with empty line to be considered # complete. lines = source.splitlines() # strip starting empty and comment lines while (len(lines) > 0 and (lines[0].strip().startswith("#") or lines[0].strip() == "")): lines.pop(0) compound_keywords = ["if", "while", "for", "with", "try", "def", "class", "async", "await"] if len(lines) > 0: first_word = lines[0].strip().split()[0] if (first_word in compound_keywords and not source.replace(" ", "").replace("\t", "").endswith("\n\n")): # last line is not empty return False return True def _submit_input(self, text_to_be_submitted): if get_runner().get_state() == "waiting_toplevel_command": # register in history and count if text_to_be_submitted in self._command_history: self._command_history.remove(text_to_be_submitted) self._command_history.append(text_to_be_submitted) self._command_history_current_index = None # meaning command selection is not in process try: if text_to_be_submitted.startswith("%"): command, _ = parse_shell_command(text_to_be_submitted) get_workbench().event_generate("MagicCommand", cmd_line=text_to_be_submitted) if command in self._command_handlers: self._command_handlers[command](text_to_be_submitted) get_workbench().event_generate("AfterKnownMagicCommand", cmd_line=text_to_be_submitted) else: self._insert_text_directly("Unknown magic command: " + command) self._insert_prompt() else: get_runner().send_command( ToplevelCommand(command="execute_source", source=text_to_be_submitted)) except: get_workbench().report_exception() self._insert_prompt() get_workbench().event_generate("ShellCommand", command_text=text_to_be_submitted) else: assert get_runner().get_state() == "waiting_input" get_runner().send_program_input(text_to_be_submitted) get_workbench().event_generate("ShellInput", input_text=text_to_be_submitted) def _arrow_up(self, event): if not self._in_current_input_range("insert"): return insert_line = index2line(self.index("insert")) input_start_line = index2line(self.index("input_start")) if insert_line != input_start_line: # we're in the middle of a multiline command return if len(self._command_history) == 0 or self._command_history_current_index == 0: # can't take previous command return "break" if self._command_history_current_index is None: self._command_history_current_index = len(self._command_history)-1 else: self._command_history_current_index -= 1 cmd = self._command_history[self._command_history_current_index] if cmd[-1] == "\n": cmd = cmd[:-1] # remove the submission linebreak self._propose_command(cmd) return "break" def _arrow_down(self, event): if not self._in_current_input_range("insert"): return insert_line = index2line(self.index("insert")) last_line = index2line(self.index("end-1c")) if insert_line != last_line: # we're in the middle of a multiline command return if (len(self._command_history) == 0 or self._command_history_current_index == len(self._command_history)-1): # can't take next command return "break" if self._command_history_current_index is None: self._command_history_current_index = len(self._command_history)-1 else: self._command_history_current_index += 1 self._propose_command(self._command_history[self._command_history_current_index].strip("\n")) return "break" def _propose_command(self, cmd_line): self.delete("input_start", "end") self.intercept_insert("input_start", cmd_line) self.see("insert") def _text_key_press(self, event): # TODO: this underline may confuse, when user is just copying on pasting # try to add this underline only when mouse is over the value """ if event.keysym in ("Control_L", "Control_R", "Command"): # TODO: check in Mac self.tag_configure("value", foreground="DarkBlue", underline=1) """ def _text_key_release(self, event): if event.keysym in ("Control_L", "Control_R", "Command"): # TODO: check in Mac self.tag_configure("value", foreground="DarkBlue", underline=0) def _clear_shell(self): end_index = self.index("output_end") self.direct_delete("1.0", end_index) def compute_smart_home_destination_index(self): """Is used by EnhancedText""" if self._in_current_input_range("insert"): # on input line, go to just after prompt return "input_start" else: return super().compute_smart_home_destination_index() def _hyperlink_enter(self, event): self.config(cursor="hand2") def _hyperlink_leave(self, event): self.config(cursor="") def _handle_hyperlink(self, event): try: line = self.get("insert linestart", "insert lineend") matches = re.findall(r'File "([^"]+)", line (\d+)', line) if len(matches) == 1 and len(matches[0]) == 2: filename, lineno = matches[0] lineno = int(lineno) if os.path.exists(filename) and os.path.isfile(filename): # TODO: better use events instead direct referencing get_workbench().get_editor_notebook().show_file(filename, lineno) except: traceback.print_exc() def _invalidate_current_data(self): """ Grayes out input & output displayed so far """ end_index = self.index("output_end") self.tag_add("inactive", "1.0", end_index) self.tag_remove("value", "1.0", end_index) while len(self.active_object_tags) > 0: self.tag_remove(self.active_object_tags.pop(), "1.0", "end") thonny-2.1.16/thonny/test/0000777000000000000000000000000013201324660013545 5ustar 00000000000000thonny-2.1.16/thonny/test/plugins/0000777000000000000000000000000013201324660015226 5ustar 00000000000000thonny-2.1.16/thonny/test/plugins/test_coloring.py0000666000000000000000000000235113201277734020465 0ustar 00000000000000import tkinter from thonny.plugins.coloring import SyntaxColorer import tkinter.font as tk_font TEST_STR1 = """def my_function(): str1 = "aslas'" str2 = 'asdasd"asda str3 = '''asdasdasd asdas sdsds''' """ def test_open_closed_strings(): text_widget = tkinter.Text() text_widget.insert("insert", TEST_STR1) font = tk_font.nametofont("TkDefaultFont") colorer = SyntaxColorer(text_widget, font, font) colorer._update_coloring() open_ranges = text_widget.tag_ranges("STRING_OPEN") closed_ranges = text_widget.tag_ranges("STRING_CLOSED") + text_widget.tag_ranges("STRING_CLOSED3") expected_open_ranges = {('3.11', '4.0'), } expected_closed_ranges = {('2.11', '2.19'), ('4.11', '6.12'), } open_ranges_set = set([(str(open_ranges[i]), str(open_ranges[i+1])) for i in range(0, len(open_ranges), 2)]) closed_ranges_set = set([(str(closed_ranges[i]), str(closed_ranges[i+1])) for i in range(0, len(closed_ranges), 2)]) assert open_ranges_set == expected_open_ranges assert closed_ranges_set == expected_closed_ranges print("test passed") def run_tests(): test_open_closed_strings() if __name__ == "__main__": print("Test input: ") print(TEST_STR1) run_tests() thonny-2.1.16/thonny/test/plugins/test_locals_marker.py0000666000000000000000000000144213172664305021470 0ustar 00000000000000import tkinter from thonny.plugins.locals_marker import LocalsHighlighter TEST_STR1 = """num_cars = 3 def foo(): print(num_cars + num_cars) def too(): num_cars = 4 print(num_cars + num_cars) def joo(): global num_cars num_cars = 2 """ def test_regular_closed(): expected_local = {('5.4', '5.12'), ('6.10', '6.18'), ('6.21', '6.29'), } text_widget = tkinter.Text() text_widget.insert("end", TEST_STR1) highlighter = LocalsHighlighter(text_widget) actual_local = highlighter.get_positions() assert actual_local == expected_local print("Passed.") def run_tests(): test_regular_closed() if __name__ == "__main__": print("Test input: ") print(TEST_STR1) run_tests()thonny-2.1.16/thonny/test/plugins/test_name_highlighter.py0000666000000000000000000000630113201275264022142 0ustar 00000000000000import tkinter from thonny.plugins.highlight_names import VariablesHighlighter TEST_STR1 = """def foo(): foo() pass def boo(narg): foo = 2 # line 5 boo = foo + 4 print(narg + 2) for i in range(5): boo() narg = 2 # line 10 def bar(): x + x def blarg(): x = 2 """ # tuple of tuples, where an inner tuple corresponds to a group of insert positions # that should produce the same output (corresponding expected output is in the # expected_indices tuple at the same index) # # consider TEST_STR1: # # The first group is four indices, where we would expect the two locations of the name "foo" # to be returned. Those expected two locations are specified at index 0 of tuple expected_indices. # # Second tuple is a group of one index, where we would expect output with the locations for "boo" # And if the insert location is at "pass", we would expect an empty set for output CURSOR_POSITIONS1 = (("1.4", "1.5", "1.7", "2.5"), ("4.6",), ("3.4",), ("5.7", "6.12"), ("4.10", "7.11"), ("10.2",), ("12.5", "12.9",), ("14.5",), ) EXPECTED_INDICES1 = ({("1.4", "1.7"), ("2.4", "2.7")}, {("4.4", "4.7"), ("9.4", "9.7")}, set(), {("5.4", "5.7"), ("6.10", "6.13")}, {("4.8", "4.12"), ("7.10", "7.14")}, {("10.0", "10.4")}, {("12.4", "12.5"), ("12.8", "12.9")}, {("14.4", "14.5")}, ) TEST_STR2 = """import too def foo(): foo = too + 4 x = foo + bow # 5 class TestClass: def foo(self): pass def add3(self): self.foo() # 10 foo() """ CURSOR_POSITIONS2 = (("1.8", "3.10"), ("2.4", "2.5", "11.10"), ("3.5", "4.9"), ) EXPECTED_INDICES2 = ({("1.7", "1.10"), ("3.10", "3.13"), }, {("2.4", "2.7"), ("11.8", "11.11"), }, {("3.4", "3.7"), ("4.8", "4.11"), }, ) TEST_GROUPS = ( (CURSOR_POSITIONS1, EXPECTED_INDICES1, TEST_STR1), (CURSOR_POSITIONS2, EXPECTED_INDICES2, TEST_STR2), ) def run_tests(): for i, test in enumerate(TEST_GROUPS): print("Running test group %d: " % (i + 1)) _assert_returns_correct_indices(test[0], test[1], test[2]) def _assert_returns_correct_indices(insert_pos_groups, expected_indices, input_str): text_widget = tkinter.Text() text_widget.insert("end", input_str) nh = VariablesHighlighter() nh.text = text_widget for i, group in enumerate(insert_pos_groups): for insert_pos in group: text_widget.mark_set("insert", insert_pos) actual = nh.get_positions() expected = expected_indices[i] assert actual == expected, "\nInsert position: %s" \ "\nExpected: %s" \ "\nGot: %s" % (insert_pos, expected, actual) print("\rPassed %d of %d" % (i+1, len(insert_pos_groups)), end="") print() if __name__ == "__main__": run_tests() thonny-2.1.16/thonny/test/plugins/test_paren_matcher.py0000666000000000000000000000220413201300456021442 0ustar 00000000000000import tkinter from thonny.plugins.paren_matcher import ParenMatcher TEST_STR1 = """age = int(input("Enter age: ")) if age > 18: l = ["H", "I"] print(l) else: print("Hello!", end='') print("What's your name?") """ def test_regular_closed(): insert_pos_groups = (("1.9", "1.10", "1.13", "1.31"), ("1.30", "1.29", "1.25", "1.15")) expected_indices = (("1.9", "1.30", []), ("1.15", "1.29", [])) text_widget = tkinter.Text() text_widget.insert("end", TEST_STR1) matcher = ParenMatcher(text_widget) matcher.text = text_widget for i, group in enumerate(insert_pos_groups): for insert_pos in group: text_widget.mark_set("insert", insert_pos) actual = matcher.find_surrounding("1.0", "end") expected = expected_indices[i] assert actual == expected, "\nExpected: %s\nGot: %s" % (expected, actual) print("\rPassed %d of %d" % (i+1, len(insert_pos_groups)), end="") def run_tests(): test_regular_closed() if __name__ == "__main__": print("Test input: ") print(TEST_STR1) run_tests() thonny-2.1.16/thonny/test/plugins/__init__.py0000666000000000000000000000000013201267236017332 0ustar 00000000000000thonny-2.1.16/thonny/test/test_ast_utils.py0000666000000000000000000000473013172664305017203 0ustar 00000000000000import ast from thonny.ast_utils import pretty from textwrap import dedent def test_pretty_without_end_markers(): p = pretty(ast.parse(dedent(""" age = int(input("Enter age: ")) if age > 18: print("Hi") else: print("Hello!", end='') print("What's your name?") """).strip())) assert p == """/=Module body=[...] 0=Assign @ 1.0 targets=[...] 0=Name @ 1.0 id='age' ctx=Store value=Call @ 1.6 func=Name @ 1.6 id='int' ctx=Load args=[...] 0=Call @ 1.10 func=Name @ 1.10 id='input' ctx=Load args=[...] 0=Str @ 1.16 s='Enter age: ' keywords=[] keywords=[] 1=If @ 2.0 test=Compare @ 2.3 left=Name @ 2.3 id='age' ctx=Load ops=[...] 0=Gt comparators=[...] 0=Num @ 2.9 n=18 body=[...] 0=Expr @ 3.4 value=Call @ 3.4 func=Name @ 3.4 id='print' ctx=Load args=[...] 0=Str @ 3.10 s='Hi' keywords=[] orelse=[...] 0=Expr @ 5.4 value=Call @ 5.4 func=Name @ 5.4 id='print' ctx=Load args=[...] 0=Str @ 5.10 s='Hello!' keywords=[...] 0=keyword arg='end' value=Str @ 5.24 s='' 1=Expr @ 6.4 value=Call @ 6.4 func=Name @ 6.4 id='print' ctx=Load args=[...] 0=Str @ 6.10 s="What's your name?" keywords=[]""" thonny-2.1.16/thonny/test/test_ast_utils_mark_text_ranges.py0000666000000000000000000003334413172664305022623 0ustar 00000000000000import ast from thonny.ast_utils import pretty from textwrap import dedent from thonny import ast_utils def test_single_assignment(): check_marked_ast("x=1", """/=Module body=[...] 0=Assign @ 1.0 - 1.3 targets=[...] 0=Name @ 1.0 - 1.1 id='x' ctx=Store value=Num @ 1.2 - 1.3 n=1""") def test_simple_io_program(): check_marked_ast("""age = int(input("Enter age: ")) if age > 18: print("Hi") else: print("Hello!", end='') print("What's your name?") """, """/=Module body=[...] 0=Assign @ 1.0 - 1.31 targets=[...] 0=Name @ 1.0 - 1.3 id='age' ctx=Store value=Call @ 1.6 - 1.31 func=Name @ 1.6 - 1.9 id='int' ctx=Load args=[...] 0=Call @ 1.10 - 1.30 func=Name @ 1.10 - 1.15 id='input' ctx=Load args=[...] 0=Str @ 1.16 - 1.29 s='Enter age: ' keywords=[] keywords=[] 1=If @ 2.0 - 6.30 test=Compare @ 2.3 - 2.11 left=Name @ 2.3 - 2.6 id='age' ctx=Load ops=[...] 0=Gt comparators=[...] 0=Num @ 2.9 - 2.11 n=18 body=[...] 0=Expr @ 3.4 - 3.15 value=Call @ 3.4 - 3.15 func=Name @ 3.4 - 3.9 id='print' ctx=Load args=[...] 0=Str @ 3.10 - 3.14 s='Hi' keywords=[] orelse=[...] 0=Expr @ 5.4 - 5.27 value=Call @ 5.4 - 5.27 func=Name @ 5.4 - 5.9 id='print' ctx=Load args=[...] 0=Str @ 5.10 - 5.18 s='Hello!' keywords=[...] 0=keyword arg='end' value=Str @ 5.24 - 5.26 s='' 1=Expr @ 6.4 - 6.30 value=Call @ 6.4 - 6.30 func=Name @ 6.4 - 6.9 id='print' ctx=Load args=[...] 0=Str @ 6.10 - 6.29 s="What's your name?" keywords=[]""") def test_two_trivial_defs(): check_marked_ast("""def f(): pass def f(): pass""", """/=Module body=[...] 0=FunctionDef @ 1.0 - 2.8 name='f' args=arguments args=[] vararg=None kwonlyargs=[] kw_defaults=[] kwarg=None defaults=[] body=[...] 0=Pass @ 2.4 - 2.8 decorator_list=[] returns=None 1=FunctionDef @ 3.0 - 4.8 name='f' args=arguments args=[] vararg=None kwonlyargs=[] kw_defaults=[] kwarg=None defaults=[] body=[...] 0=Pass @ 4.4 - 4.8 decorator_list=[] returns=None""") def test_id_def(): check_marked_ast("""def f(x): return x """, """/=Module body=[...] 0=FunctionDef @ 1.0 - 2.12 name='f' args=arguments args=[...] 0=arg @ 1.6 - 1.7 arg='x' annotation=None vararg=None kwonlyargs=[] kw_defaults=[] kwarg=None defaults=[] body=[...] 0=Return @ 2.4 - 2.12 value=Name @ 2.11 - 2.12 id='x' ctx=Load decorator_list=[] returns=None""") def test_simple_while_program(): check_marked_ast("""x = int(input("Enter number: ")) while x > 0: print(x) x -= 1 """, """/=Module body=[...] 0=Assign @ 1.0 - 1.32 targets=[...] 0=Name @ 1.0 - 1.1 id='x' ctx=Store value=Call @ 1.4 - 1.32 func=Name @ 1.4 - 1.7 id='int' ctx=Load args=[...] 0=Call @ 1.8 - 1.31 func=Name @ 1.8 - 1.13 id='input' ctx=Load args=[...] 0=Str @ 1.14 - 1.30 s='Enter number: ' keywords=[] keywords=[] 1=While @ 3.0 - 5.10 test=Compare @ 3.6 - 3.11 left=Name @ 3.6 - 3.7 id='x' ctx=Load ops=[...] 0=Gt comparators=[...] 0=Num @ 3.10 - 3.11 n=0 body=[...] 0=Expr @ 4.4 - 4.12 value=Call @ 4.4 - 4.12 func=Name @ 4.4 - 4.9 id='print' ctx=Load args=[...] 0=Name @ 4.10 - 4.11 id='x' ctx=Load keywords=[] 1=AugAssign @ 5.4 - 5.10 target=Name @ 5.4 - 5.5 id='x' ctx=Store op=Sub value=Num @ 5.9 - 5.10 n=1 orelse=[]""") def test_call_with_pos_and_kw_arg(): check_marked_ast("""f(3, t=45) """, """/=Module body=[...] 0=Expr @ 1.0 - 1.10 value=Call @ 1.0 - 1.10 func=Name @ 1.0 - 1.1 id='f' ctx=Load args=[...] 0=Num @ 1.2 - 1.3 n=3 keywords=[...] 0=keyword arg='t' value=Num @ 1.7 - 1.9 n=45""") def test_call_with_pos_star_kw(): check_marked_ast("""f(3, *kala, t=45) """, """/=Module body=[...] 0=Expr @ 1.0 - 1.17 value=Call @ 1.0 - 1.17 func=Name @ 1.0 - 1.1 id='f' ctx=Load args=[...] 0=Num @ 1.2 - 1.3 n=3 1=Starred @ 1.5 - 1.10 value=Name @ 1.6 - 1.10 id='kala' ctx=Load ctx=Load keywords=[...] 0=keyword arg='t' value=Num @ 1.14 - 1.16 n=45""") def test_call_with_single_keyword(): check_marked_ast("""fff(t=45) """, """/=Module body=[...] 0=Expr @ 1.0 - 1.9 value=Call @ 1.0 - 1.9 func=Name @ 1.0 - 1.3 id='fff' ctx=Load args=[] keywords=[...] 0=keyword arg='t' value=Num @ 1.6 - 1.8 n=45""") def test_call_without_arguments(): check_marked_ast("""fff() """, """/=Module body=[...] 0=Expr @ 1.0 - 1.5 value=Call @ 1.0 - 1.5 func=Name @ 1.0 - 1.3 id='fff' ctx=Load args=[] keywords=[]""") def test_call_with_attribute_function_and_keyword_arg(): check_marked_ast("""rida.strip().split(maxsplit=1) """, """/=Module body=[...] 0=Expr @ 1.0 - 1.30 value=Call @ 1.0 - 1.30 func=Attribute @ 1.0 - 1.18 value=Call @ 1.0 - 1.12 func=Attribute @ 1.0 - 1.10 value=Name @ 1.0 - 1.4 id='rida' ctx=Load attr='strip' ctx=Load args=[] keywords=[] attr='split' ctx=Load args=[] keywords=[...] 0=keyword arg='maxsplit' value=Num @ 1.28 - 1.29 n=1""") def test_del_from_list_with_integer(): check_marked_ast("""del x[0]""", """/=Module body=[...] 0=Delete @ 1.0 - 1.8 targets=[...] 0=Subscript @ 1.4 - 1.8 value=Name @ 1.4 - 1.5 id='x' ctx=Load slice=Index value=Num @ 1.6 - 1.7 n=0 ctx=Del""") def test_del_from_list_with_string(): check_marked_ast("""del x["blah"]""", """/=Module body=[...] 0=Delete @ 1.0 - 1.13 targets=[...] 0=Subscript @ 1.4 - 1.13 value=Name @ 1.4 - 1.5 id='x' ctx=Load slice=Index value=Str @ 1.6 - 1.12 s='blah' ctx=Del""") def test_full_slice1(): check_marked_ast("""blah[:] """, """/=Module body=[...] 0=Expr @ 1.0 - 1.7 value=Subscript @ 1.0 - 1.7 value=Name @ 1.0 - 1.4 id='blah' ctx=Load slice=Slice lower=None upper=None step=None ctx=Load""") def test_full_slice2(): check_marked_ast("""blah[::] """, """/=Module body=[...] 0=Expr @ 1.0 - 1.8 value=Subscript @ 1.0 - 1.8 value=Name @ 1.0 - 1.4 id='blah' ctx=Load slice=Slice lower=None upper=None step=None ctx=Load""") def test_non_ascii_letters_with_calls_etc(): check_marked_ast("""täpitähed = "täpitähed" print(täpitähed["tšahh"]) pöhh(pöhh=3) """, """/=Module body=[...] 0=Assign @ 1.0 - 1.23 targets=[...] 0=Name @ 1.0 - 1.9 id='täpitähed' ctx=Store value=Str @ 1.12 - 1.23 s='täpitähed' 1=Expr @ 2.0 - 2.25 value=Call @ 2.0 - 2.25 func=Name @ 2.0 - 2.5 id='print' ctx=Load args=[...] 0=Subscript @ 2.6 - 2.24 value=Name @ 2.6 - 2.15 id='täpitähed' ctx=Load slice=Index value=Str @ 2.16 - 2.23 s='tšahh' ctx=Load keywords=[] 2=Expr @ 3.0 - 3.12 value=Call @ 3.0 - 3.12 func=Name @ 3.0 - 3.4 id='pöhh' ctx=Load args=[] keywords=[...] 0=keyword arg='pöhh' value=Num @ 3.10 - 3.11 n=3""") def test_nested_binops(): """http://bugs.python.org/issue18374""" check_marked_ast("1+2-3", """/=Module body=[...] 0=Expr @ 1.0 - 1.5 value=BinOp @ 1.0 - 1.5 left=BinOp @ 1.0 - 1.3 left=Num @ 1.0 - 1.1 n=1 op=Add right=Num @ 1.2 - 1.3 n=2 op=Sub right=Num @ 1.4 - 1.5 n=3""") def test_multiline_string(): """http://bugs.python.org/issue18370""" check_marked_ast("""pass blah = \"\"\"first second third\"\"\" pass""", """/=Module body=[...] 0=Pass @ 1.0 - 1.4 1=Assign @ 2.0 - 4.8 targets=[...] 0=Name @ 2.0 - 2.4 id='blah' ctx=Store value=Str @ 2.7 - 4.8 s='first\\nsecond\\nthird' 2=Pass @ 5.0 - 5.4""") def check_marked_ast(source, expected_pretty_ast #,expected_for_py_34=None ): #if (sys.version_info[:2] == (3,4) # and expected_for_py_34 is not None): # expected_pretty_ast = expected_for_py_34 source = dedent(source) root = ast.parse(source) ast_utils.mark_text_ranges(root, source) actual_pretty_ast = pretty(root) #print("ACTUAL", actual_pretty_ast) #print("EXPECTED", expected_pretty_ast) assert actual_pretty_ast.strip() == expected_pretty_ast.strip() thonny-2.1.16/thonny/test/__init__.py0000666000000000000000000000000013201267236015651 0ustar 00000000000000thonny-2.1.16/thonny/tktextext.py0000666000000000000000000007463613201264465015232 0ustar 00000000000000# coding=utf-8 """Extensions for tk.Text""" import time import traceback from logging import exception try: import tkinter as tk from tkinter import ttk from tkinter import font as tkfont from tkinter import TclError except ImportError: import Tkinter as tk import ttk import tkFont as tkfont from Tkinter import TclError class TweakableText(tk.Text): """Allows intercepting Text commands at Tcl-level""" def __init__(self, master=None, cnf={}, read_only=False, **kw): tk.Text.__init__(self, master=master, cnf=cnf, **kw) self._read_only = read_only self._original_widget_name = self._w + "_orig" self.tk.call("rename", self._w, self._original_widget_name) self.tk.createcommand(self._w, self._dispatch_tk_operation) self._tk_proxies = {} self._original_insert = self._register_tk_proxy_function("insert", self.intercept_insert) self._original_delete = self._register_tk_proxy_function("delete", self.intercept_delete) self._original_mark = self._register_tk_proxy_function("mark", self.intercept_mark) def _register_tk_proxy_function(self, operation, function): self._tk_proxies[operation] = function setattr(self, operation, function) def original_function(*args): self.tk.call((self._original_widget_name, operation) + args) return original_function def _dispatch_tk_operation(self, operation, *args): f = self._tk_proxies.get(operation) try: if f: return f(*args) else: return self.tk.call((self._original_widget_name, operation) + args) except TclError as e: # Some Tk internal actions (eg. paste and cut) can cause this error if (str(e).lower() == '''text doesn't contain any characters tagged with "sel"''' and operation in ["delete", "index", "get"] and args in [("sel.first", "sel.last"), ("sel.first",)]): pass else: traceback.print_exc() return "" # Taken from idlelib.WidgetRedirector def set_read_only(self, value): self._read_only = value def is_read_only(self): return self._read_only def set_content(self, chars): self.direct_delete("1.0", tk.END) self.direct_insert("1.0", chars) def intercept_mark(self, *args): self.direct_mark(*args) def intercept_insert(self, index, chars, tags=None): assert isinstance(chars, str) if chars >= "\uf704" and chars <= "\uf70d": # Function keys F1..F10 in Mac cause these pass elif self.is_read_only(): self.bell() else: self.direct_insert(index, chars, tags) def intercept_delete(self, index1, index2=None): if index1 == "sel.first" and index2 == "sel.last" and not self.has_selection(): return if self.is_read_only(): self.bell() elif self._is_erroneous_delete(index1, index2): pass else: self.direct_delete(index1, index2) def _is_erroneous_delete(self, index1, index2): """Paste can cause deletes where index1 is sel.start but text has no selection. This would cause errors""" return index1.startswith("sel.") and not self.has_selection() def direct_mark(self, *args): self._original_mark(*args) if args[:2] == ('set', 'insert'): self.event_generate("<>") def index_sel_first(self): # Tk will give error without this check if self.tag_ranges("sel"): return self.index("sel.first") else: return None def index_sel_last(self): if self.tag_ranges("sel"): return self.index("sel.last") else: return None def has_selection(self): return len(self.tag_ranges("sel")) > 0 def get_selection_indices(self): # If a selection is defined in the text widget, return (start, # end) as Tkinter text indices, otherwise return (None, None) if self.has_selection(): return self.index("sel.first"), self.index("sel.last") else: return None, None def direct_insert(self, index, chars, tags=None): self._original_insert(index, chars, tags) self.event_generate("<>") def direct_delete(self, index1, index2=None): self._original_delete(index1, index2) self.event_generate("<>") class EnhancedText(TweakableText): """Text widget with extra navigation and editing aids. Provides more comfortable deletion, indentation and deindentation, and undo handling. Not specific to Python code. Most of the code is adapted from idlelib.EditorWindow. """ def __init__(self, master=None, cnf={}, **kw): # Parent class shouldn't autoseparate # TODO: take client provided autoseparators value into account kw["autoseparators"] = False TweakableText.__init__(self, master=master, cnf=cnf, **kw) self.tabwidth = 8 # See comments in idlelib.EditorWindow self.indentwidth = 4 self.usetabs = False self._last_event_kind = None self._last_key_time = None self._bind_editing_aids() self._bind_movement_aids() self._bind_selection_aids() self._bind_undo_aids() self._bind_mouse_aids() def _bind_mouse_aids(self): if _running_on_mac(): self.bind("", self.on_secondary_click) self.bind("", self.on_secondary_click) else: self.bind("", self.on_secondary_click) def _bind_editing_aids(self): def if_not_readonly(fun): def dispatch(event): if not self.is_read_only(): return fun(event) else: return "break" return dispatch self.bind("", if_not_readonly(self.delete_word_left), True) self.bind("", if_not_readonly(self.delete_word_right), True) self.bind("", if_not_readonly(self.perform_smart_backspace), True) self.bind("", if_not_readonly(self.perform_return), True) self.bind("", if_not_readonly(self.perform_return), True) self.bind("", if_not_readonly(self.perform_tab), True) try: # Is needed on eg. Ubuntu with Estonian keyboard self.bind("", if_not_readonly(self.perform_tab), True) except: pass def _bind_movement_aids(self): self.bind("", self.perform_smart_home, True) self.bind("", self.move_to_edge_if_selection(0), True) self.bind("", self.move_to_edge_if_selection(1), True) self.bind("", self.perform_page_down, True) self.bind("", self.perform_page_up, True) def _bind_selection_aids(self): self.bind("" if _running_on_mac() else "", self.select_all, True) def _bind_undo_aids(self): self.bind("<>", self._on_undo, True) self.bind("<>", self._on_redo, True) self.bind("<>", self._on_cut, True) self.bind("<>", self._on_copy, True) self.bind("<>", self._on_paste, True) self.bind("", self._on_get_focus, True) self.bind("", self._on_lose_focus, True) self.bind("", self._on_key_press, True) self.bind("<1>", self._on_mouse_click, True) self.bind("<2>", self._on_mouse_click, True) self.bind("<3>", self._on_mouse_click, True) def delete_word_left(self, event): self.event_generate('') self.edit_separator() return "break" def delete_word_right(self, event): self.event_generate('') self.edit_separator() return "break" def perform_smart_backspace(self, event): self._log_keypress_for_undo(event) text = self first, last = self.get_selection_indices() if first and last: text.delete(first, last) text.mark_set("insert", first) return "break" # Delete whitespace left, until hitting a real char or closest # preceding virtual tab stop. chars = text.get("insert linestart", "insert") if chars == '': if text.compare("insert", ">", "1.0"): # easy: delete preceding newline text.delete("insert-1c") else: text.bell() # at start of buffer return "break" if chars.strip() != "": # there are non-whitespace chars somewhere to the left of the cursor # easy: delete preceding real char text.delete("insert-1c") self._log_keypress_for_undo(event) return "break" # Ick. It may require *inserting* spaces if we back up over a # tab character! This is written to be clear, not fast. tabwidth = self.tabwidth have = len(chars.expandtabs(tabwidth)) assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... #if self.context_use_ps1: # last_line_of_prompt = sys.ps1.split('\n')[-1] #else: last_line_of_prompt = '' ncharsdeleted = 0 while 1: if chars == last_line_of_prompt: break chars = chars[:-1] ncharsdeleted = ncharsdeleted + 1 have = len(chars.expandtabs(tabwidth)) if have <= want or chars[-1] not in " \t": break text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: text.insert("insert", ' ' * (want - have)) return "break" def perform_midline_tab(self, event=None): "autocompleter can put its magic here" # by default return self.perform_smart_tab(event) def perform_smart_tab(self, event=None): self._log_keypress_for_undo(event) # if intraline selection: # delete it # elif multiline selection: # do indent-region # else: # indent one level first, last = self.get_selection_indices() if first and last: if index2line(first) != index2line(last): return self.indent_region(event) self.delete(first, last) self.mark_set("insert", first) prefix = self.get("insert linestart", "insert") raw, effective = classifyws(prefix, self.tabwidth) if raw == len(prefix): # only whitespace to the left self._reindent_to(effective + self.indentwidth) else: # tab to the next 'stop' within or to right of line's text: if self.usetabs: pad = '\t' else: effective = len(prefix.expandtabs(self.tabwidth)) n = self.indentwidth pad = ' ' * (n - effective % n) self.insert("insert", pad) self.see("insert") return "break" def get_cursor_position(self): return map(int, self.index("insert").split(".")) def get_line_count(self): return list(map(int, self.index("end-1c").split(".")))[0] def perform_return(self, event): self.insert("insert", "\n") return "break" def perform_page_down(self, event): # if last line is visible then go to last line # (by default it doesn't move then) try: last_visible_idx = self.index("@0,%d" % self.winfo_height()) row, _ = map(int, last_visible_idx.split(".")) line_count = self.get_line_count() if (row == line_count or row == line_count-1): # otherwise tk doesn't show last line self.mark_set("insert", "end") except: traceback.print_exc() def perform_page_up(self, event): # if first line is visible then go there # (by default it doesn't move then) try: first_visible_idx = self.index("@0,0") row, _ = map(int, first_visible_idx.split(".")) if row == 1: self.mark_set("insert", "1.0") except: traceback.print_exc() def compute_smart_home_destination_index(self): """Is overridden in shell""" line = self.get("insert linestart", "insert lineend") for insertpt in range(len(line)): if line[insertpt] not in (' ','\t'): break else: insertpt=len(line) lineat = int(self.index("insert").split('.')[1]) if insertpt == lineat: insertpt = 0 return "insert linestart+"+str(insertpt)+"c" def perform_smart_home(self, event): if (event.state & 4) != 0 and event.keysym == "Home": # state&4==Control. If , use the Tk binding. return dest = self.compute_smart_home_destination_index() if (event.state&1) == 0: # shift was not pressed self.tag_remove("sel", "1.0", "end") else: if not self.index_sel_first(): # there was no previous selection self.mark_set("my_anchor", "insert") else: if self.compare(self.index_sel_first(), "<", self.index("insert")): self.mark_set("my_anchor", "sel.first") # extend back else: self.mark_set("my_anchor", "sel.last") # extend forward first = self.index(dest) last = self.index("my_anchor") if self.compare(first,">",last): first,last = last,first self.tag_remove("sel", "1.0", "end") self.tag_add("sel", first, last) self.mark_set("insert", dest) self.see("insert") return "break" def move_to_edge_if_selection(self, edge_index): """Cursor move begins at start or end of selection When a left/right cursor key is pressed create and return to Tkinter a function which causes a cursor move from the associated edge of the selection. """ def move_at_edge(event): if (self.has_selection() and (event.state & 5) == 0): # no shift(==1) or control(==4) pressed try: self.mark_set("insert", ("sel.first+1c", "sel.last-1c")[edge_index]) except tk.TclError: pass return move_at_edge def perform_tab(self, event=None): self._log_keypress_for_undo(event) if event.state & 0x0001: # shift is pressed (http://stackoverflow.com/q/32426250/261181) return self.dedent_region(event) else: # check whether there are letters before cursor on this line index = self.index("insert") left_text = self.get(index + " linestart", index) if left_text.strip() == "" or self.has_selection(): return self.perform_smart_tab(event) else: return self.perform_midline_tab(event) def indent_region(self, event=None): return self._change_indentation(True) def dedent_region(self, event=None): return self._change_indentation(False) def _change_indentation(self, increase=True): head, tail, chars, lines = self._get_region() # Text widget plays tricks if selection ends on last line # and content doesn't end with empty line, text_last_line = index2line(self.index("end-1c")) sel_last_line = index2line(tail) if sel_last_line >= text_last_line: while not self.get(head, "end").endswith("\n\n"): self.insert("end", "\n") for pos in range(len(lines)): line = lines[pos] if line: raw, effective = classifyws(line, self.tabwidth) if increase: effective = effective + self.indentwidth else: effective = max(effective - self.indentwidth, 0) lines[pos] = self._make_blanks(effective) + line[raw:] self._set_region(head, tail, chars, lines) return "break" def select_all(self, event): self.tag_remove("sel", "1.0", tk.END) self.tag_add('sel', '1.0', tk.END) def _reindent_to(self, column): # Delete from beginning of line to insert point, then reinsert # column logical (meaning use tabs if appropriate) spaces. if self.compare("insert linestart", "!=", "insert"): self.delete("insert linestart", "insert") if column: self.insert("insert", self._make_blanks(column)) def _get_region(self): first, last = self.get_selection_indices() if first and last: head = self.index(first + " linestart") tail = self.index(last + "-1c lineend +1c") else: head = self.index("insert linestart") tail = self.index("insert lineend +1c") chars = self.get(head, tail) lines = chars.split("\n") return head, tail, chars, lines def _set_region(self, head, tail, chars, lines): newchars = "\n".join(lines) if newchars == chars: self.bell() return self.tag_remove("sel", "1.0", "end") self.mark_set("insert", head) self.delete(head, tail) self.insert(head, newchars) self.tag_add("sel", head, "insert") def _log_keypress_for_undo(self, e): if e is None: return # NB! this may not execute if the event is cancelled in another handler event_kind = self._get_event_kind(e) if (event_kind != self._last_event_kind or e.char in ("\r", "\n", " ", "\t") or e.keysym in ["Return", "KP_Enter"] or time.time() - self.last_key_time > 2 ): self.edit_separator() self._last_event_kind = event_kind self.last_key_time = time.time() def _get_event_kind(self, event): if event.keysym in ("BackSpace", "Delete"): return "delete" elif event.char: return "insert" else: # eg. e.keysym in ("Left", "Up", "Right", "Down", "Home", "End", "Prior", "Next"): return "other_key" def _make_blanks(self, n): # Make string that displays as n leading blanks. if self.usetabs: ntabs, nspaces = divmod(n, self.tabwidth) return '\t' * ntabs + ' ' * nspaces else: return ' ' * n def _on_undo(self, e): self._last_event_kind = "undo" def _on_redo(self, e): self._last_event_kind = "redo" def _on_cut(self, e): self._last_event_kind = "cut" self.edit_separator() def _on_copy(self, e): self._last_event_kind = "copy" self.edit_separator() def _on_paste(self, e): self._last_event_kind = "paste" self.edit_separator() self.see("insert") self.after_idle(lambda : self.see("insert")) def _on_get_focus(self, e): self._last_event_kind = "get_focus" self.edit_separator() def _on_lose_focus(self, e): self._last_event_kind = "lose_focus" self.edit_separator() def _on_key_press(self, e): return self._log_keypress_for_undo(e) def _on_mouse_click(self, event): self.edit_separator() def on_secondary_click(self, event=None): "Use this for invoking context menu" self.focus_set() class TextFrame(ttk.Frame): "Decorates text with scrollbars, line numbers and print margin" def __init__(self, master, line_numbers=False, line_length_margin=0, first_line_number=1, text_class=EnhancedText, horizontal_scrollbar=True, vertical_scrollbar=True, vertical_scrollbar_class=ttk.Scrollbar, horizontal_scrollbar_class=ttk.Scrollbar, **text_options): ttk.Frame.__init__(self, master=master) final_text_options = {'borderwidth' : 0, 'insertwidth' : 2, 'spacing1' : 0, 'spacing3' : 0, 'highlightthickness' : 0, 'inactiveselectbackground' : 'gray', 'padx' : 5, 'pady' : 5 } final_text_options.update(text_options) self.text = text_class(self, **final_text_options) self.text.grid(row=0, column=1, sticky=tk.NSEW) self._margin = tk.Text(self, width=4, padx=5, pady=5, highlightthickness=0, bd=0, takefocus=False, font=self.text['font'], background='#e0e0e0', foreground='#999999', selectbackground='#e0e0e0', selectforeground='#999999', cursor='arrow', state='disabled', undo=False ) self._margin.bind("", self.on_margin_click) self._margin.bind("", self.on_margin_click) self._margin.bind("", self.on_margin_motion) self._margin['yscrollcommand'] = self._margin_scroll # margin will be gridded later self._first_line_number = first_line_number self.set_line_numbers(line_numbers) if vertical_scrollbar: self._vbar = vertical_scrollbar_class(self, orient=tk.VERTICAL) self._vbar.grid(row=0, column=2, sticky=tk.NSEW) self._vbar['command'] = self._vertical_scroll self.text['yscrollcommand'] = self._vertical_scrollbar_update if horizontal_scrollbar: self._hbar = horizontal_scrollbar_class(self, orient=tk.HORIZONTAL) self._hbar.grid(row=1, column=0, sticky=tk.NSEW, columnspan=2) self._hbar['command'] = self._horizontal_scroll self.text['xscrollcommand'] = self._horizontal_scrollbar_update self.columnconfigure(1, weight=1) self.rowconfigure(0, weight=1) self._recommended_line_length=line_length_margin self._margin_line = tk.Canvas(self.text, borderwidth=0, width=1, height=1200, highlightthickness=0, background="lightgray") self.update_margin_line() self.text.bind("<>", self._text_changed, True) # TODO: add context menu? def focus_set(self): self.text.focus_set() def set_line_numbers(self, value): if value and not self._margin.winfo_ismapped(): self._margin.grid(row=0, column=0, sticky=tk.NSEW) self.update_line_numbers() elif not value and self._margin.winfo_ismapped(): self._margin.grid_forget() # insert first line number (NB! Without trailing linebreak. See update_line_numbers) self._margin.config(state='normal') self._margin.delete("1.0", "end") self._margin.insert("1.0", str(self._first_line_number)) self._margin.config(state='disabled') self.update_line_numbers() def set_line_length_margin(self, value): self._recommended_line_length = value self.update_margin_line() def _text_changed(self, event): self.update_line_numbers() self.update_margin_line() def _vertical_scrollbar_update(self, *args): self._vbar.set(*args) self._margin.yview(tk.MOVETO, args[0]) def _margin_scroll(self, *args): # FIXME: this doesn't work properly # Can't scroll to bottom when line numbers are not visible # and can't type normally at the bottom, when line numbers are visible return #self._vbar.set(*args) #self.text.yview(tk.MOVETO, args[0]) def _horizontal_scrollbar_update(self,*args): self._hbar.set(*args) self.update_margin_line() def _vertical_scroll(self,*args): self.text.yview(*args) self._margin.yview(*args) def _horizontal_scroll(self,*args): self.text.xview(*args) self.update_margin_line() def update_line_numbers(self): text_line_count = int(self.text.index("end").split(".")[0]) margin_line_count = int(self._margin.index("end").split(".")[0]) if text_line_count != margin_line_count: self._margin.config(state='normal') # NB! Text acts weird with last symbol # (don't really understand whether it automatically keeps a newline there or not) # Following seems to ensure both Text-s have same height if text_line_count > margin_line_count: delta = text_line_count - margin_line_count start = margin_line_count + self._first_line_number - 1 for i in range(start, start + delta): self._margin.insert("end-1c", "\n" + str(i)) else: self._margin.delete(line2index(text_line_count)+"-1c", "end-1c") self._margin.config(state='disabled') # synchronize margin scroll position with text # https://mail.python.org/pipermail/tkinter-discuss/2010-March/002197.html first, _ = self.text.yview() self._margin.yview_moveto(first) def update_margin_line(self): if self._recommended_line_length == 0: self._margin_line.place_forget() else: try: self.text.update_idletasks() # How far left has text been scrolled first_visible_idx = self.text.index("@0,0") first_visible_col = int(first_visible_idx.split(".")[1]) bbox = self.text.bbox(first_visible_idx) first_visible_col_x = bbox[0] margin_line_visible_col = self._recommended_line_length - first_visible_col delta = first_visible_col_x except: # fall back to ignoring scroll position margin_line_visible_col = self._recommended_line_length delta = 0 if margin_line_visible_col > -1: x = (get_text_font(self.text).measure((margin_line_visible_col-1) * "M") + delta + self.text["padx"]) else: x = -10 #print(first_visible_col, first_visible_col_x) self._margin_line.place(y=-10, x=x) def on_margin_click(self, event=None): try: linepos = self._margin.index("@%s,%s" % (event.x, event.y)).split(".")[0] self.text.mark_set("insert", "%s.0" % linepos) self._margin.mark_set("margin_selection_start", "%s.0" % linepos) if event.type == "4": # In Python 3.6 you can use tk.EventType.ButtonPress instead of "4" self.text.tag_remove("sel", "1.0", "end") except tk.TclError: exception() def on_margin_motion(self, event=None): try: linepos = int(self._margin.index("@%s,%s" % (event.x, event.y)).split(".")[0]) margin_selection_start = int(self._margin.index("margin_selection_start").split(".")[0]) self.select_lines(min(margin_selection_start, linepos), max(margin_selection_start - 1, linepos - 1)) self.text.mark_set("insert", "%s.0" % linepos) except tk.TclError: exception() def get_text_font(text): font = text["font"] if isinstance(font, str): return tkfont.nametofont(font) else: return font def classifyws(s, tabwidth): raw = effective = 0 for ch in s: if ch == ' ': raw = raw + 1 effective = effective + 1 elif ch == '\t': raw = raw + 1 effective = (effective // tabwidth + 1) * tabwidth else: break return raw, effective def index2line(index): return int(float(index)) def line2index(line): return str(float(line)) def fixwordbreaks(root): # Adapted from idlelib.EditorWindow (Python 3.4.2) # Modified to include non-ascii chars # Make sure that Tk's double-click and next/previous word # operations use our definition of a word (i.e. an identifier) tk = root.tk tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded tk.call('set', 'tcl_wordchars', u'[a-zA-Z0-9_À-ÖØ-öø-ÿĀ-ſƀ-ɏА-я]') tk.call('set', 'tcl_nonwordchars', u'[^a-zA-Z0-9_À-ÖØ-öø-ÿĀ-ſƀ-ɏА-я]') def rebind_control_a(root): # Tk 8.6 has <> event but 8.5 doesn't # http://stackoverflow.com/questions/22907200/remap-default-keybinding-in-tkinter def control_a(event): widget = event.widget if isinstance(widget, tk.Text): widget.tag_remove("sel","1.0","end") widget.tag_add("sel","1.0","end") root.bind_class("Text", "", control_a) def _running_on_mac(): return tk._default_root.call('tk', 'windowingsystem') == "aqua" if __name__ == "__main__": # demo root = tk.Tk() frame = TextFrame(root, read_only=False, wrap=tk.NONE, line_numbers=True, line_length_margin=13, text_class=TweakableText) frame.grid() text = frame.text text.direct_insert("1.0", "Essa\n 'tessa\nkossa\nx=34+(45*89*(a+45)") text.tag_configure('string', background='yellow') text.tag_add("string", "2.0", "3.0") text.tag_configure('paren', underline=True) text.tag_add("paren", "4.6", "5.0") root.mainloop()thonny-2.1.16/thonny/token_utils.py0000666000000000000000000000275313172664305015521 0ustar 00000000000000import keyword import builtins def matches_any(name, alternates): "Return a named group pattern matching list of alternates." return "(?P<%s>" % name + "|".join(alternates) + ")" KW = r"\b" + matches_any("KEYWORD", keyword.kwlist) + r"\b" _builtinlist = [str(name) for name in dir(builtins) if not name.startswith('_') and \ name not in keyword.kwlist] # TODO: move builtin handling to global-local BUILTIN = r"([^.'\"\\#]\b|^)" + matches_any("BUILTIN", _builtinlist) + r"\b" COMMENT = matches_any("COMMENT", [r"#[^\n]*"]) MAGIC_COMMAND = matches_any("MAGIC_COMMAND", [r"^%[^\n]*"]) # used only in shell STRINGPREFIX = r"(\br|u|ur|R|U|UR|Ur|uR|b|B|br|Br|bR|BR|rb|rB|Rb|RB)?" SQSTRING_OPEN = STRINGPREFIX + r"'[^'\\\n]*(\\.[^'\\\n]*)*\n?" SQSTRING_CLOSED = STRINGPREFIX + r"'[^'\\\n]*(\\.[^'\\\n]*)*'" DQSTRING_OPEN = STRINGPREFIX + r'"[^"\\\n]*(\\.[^"\\\n]*)*\n?' DQSTRING_CLOSED = STRINGPREFIX + r'"[^"\\\n]*(\\.[^"\\\n]*)*"' SQ3STRING = STRINGPREFIX + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?" DQ3STRING = STRINGPREFIX + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?' SQ3DELIMITER = STRINGPREFIX + "'''" DQ3DELIMITER = STRINGPREFIX + '"""' STRING_OPEN = matches_any("STRING_OPEN", [SQSTRING_OPEN, DQSTRING_OPEN]) STRING_CLOSED = matches_any("STRING_CLOSED", [SQSTRING_CLOSED, DQSTRING_CLOSED]) STRING3_DELIMITER = matches_any("DELIMITER3", [SQ3DELIMITER, DQ3DELIMITER]) STRING3 = matches_any("STRING3", [DQ3STRING, SQ3STRING]) thonny-2.1.16/thonny/ui_utils.py0000666000000000000000000010177113201264465015012 0ustar 00000000000000# -*- coding: utf-8 -*- from tkinter import ttk, messagebox from tkinter.dialog import Dialog from thonny import tktextext, misc_utils from thonny.globals import get_workbench from thonny.misc_utils import running_on_mac_os, running_on_windows, running_on_linux import tkinter as tk import tkinter.messagebox as tkMessageBox import traceback import textwrap import re import collections import threading import signal import subprocess import os CALM_WHITE = '#fdfdfd' _images = set() # for keeping references to tkinter images to avoid garbace colleting them class AutomaticPanedWindow(tk.PanedWindow): """ Enables inserting panes according to their position_key-s. Automatically adds/removes itself to/from its master AutomaticPanedWindow. Fixes some style glitches. """ def __init__(self, master, position_key=None, first_pane_size=1/3, last_pane_size=1/3, **kwargs): if not "sashwidth" in kwargs: kwargs["sashwidth"]=10 if not "background" in kwargs: kwargs["background"] = get_main_background() tk.PanedWindow.__init__(self, master, **kwargs) self.position_key = position_key self.visible_panes = set() self.first_pane_size = first_pane_size self.last_pane_size = last_pane_size self._restoring_pane_sizes = False self._last_window_size = (0,0) self._full_size_not_final = True self._configure_binding = self.winfo_toplevel().bind("", self._on_window_resize, True) self.bind("", self._on_mouse_dragged, True) def insert(self, pos, child, **kw): if pos == "auto": # According to documentation I should use self.panes() # but this doesn't return expected widgets for sibling in sorted(self.visible_panes, key=lambda p:p.position_key if hasattr(p, "position_key") else 0): if (not hasattr(sibling, "position_key") or sibling.position_key == None or sibling.position_key > child.position_key): pos = sibling break else: pos = "end" if isinstance(pos, tk.Widget): kw["before"] = pos self.add(child, **kw) def add(self, child, **kw): if not "minsize" in kw: kw["minsize"]=60 tk.PanedWindow.add(self, child, **kw) self.visible_panes.add(child) self._update_visibility() self._check_restore_pane_sizes() def remove(self, child): tk.PanedWindow.remove(self, child) self.visible_panes.remove(child) self._update_visibility() self._check_restore_pane_sizes() def forget(self, child): tk.PanedWindow.forget(self, child) self.visible_panes.remove(child) self._update_visibility() self._check_restore_pane_sizes() def destroy(self): self.winfo_toplevel().unbind("", self._configure_binding) tk.PanedWindow.destroy(self) def is_visible(self): if not isinstance(self.master, AutomaticPanedWindow): return self.winfo_ismapped() else: return self in self.master.visible_panes def _on_window_resize(self, event): window = self.winfo_toplevel() window_size = (window.winfo_width(), window.winfo_height()) initializing = hasattr(window, "initializing") and window.initializing if (not initializing and not self._restoring_pane_sizes and (window_size != self._last_window_size or self._full_size_not_final)): self._check_restore_pane_sizes() self._last_window_size = window_size def _on_mouse_dragged(self, event): if event.widget == self and not self._restoring_pane_sizes: self._store_pane_sizes() def _store_pane_sizes(self): if len(self.panes()) > 1: self.last_pane_size = self._get_pane_size("last") if len(self.panes()) > 2: self.first_pane_size = self._get_pane_size("first") def _check_restore_pane_sizes(self): """last (and maybe first) pane sizes are stored, first (or middle) pane changes its size when window is resized""" window = self.winfo_toplevel() if hasattr(window, "initializing") and window.initializing: return try: self._restoring_pane_sizes = True if len(self.panes()) > 1: self._set_pane_size("last", self.last_pane_size) if len(self.panes()) > 2: self._set_pane_size("first", self.first_pane_size) finally: self._restoring_pane_sizes = False def _get_pane_size(self, which): self.update_idletasks() if which == "first": coord = self.sash_coord(0) else: coord = self.sash_coord(len(self.panes())-2) if self.cget("orient") == tk.HORIZONTAL: full_size = self.winfo_width() sash_distance = coord[0] else: full_size = self.winfo_height() sash_distance = coord[1] if which == "first": return sash_distance else: return full_size - sash_distance def _set_pane_size(self, which, size): #print("setsize", which, size) self.update_idletasks() if self.cget("orient") == tk.HORIZONTAL: full_size = self.winfo_width() else: full_size = self.winfo_height() self._full_size_not_final = full_size == 1 if self._full_size_not_final: return if isinstance(size, float): size = int(full_size * size) #print("full vs size", full_size, size) if which == "first": sash_index = 0 sash_distance = size else: sash_index = len(self.panes())-2 sash_distance = full_size - size if self.cget("orient") == tk.HORIZONTAL: self.sash_place(sash_index, sash_distance, 0) #print("PLACE", sash_index, sash_distance, 0) else: self.sash_place(sash_index, 0, sash_distance) #print("PLACE", sash_index, 0, sash_distance) def _update_visibility(self): if not isinstance(self.master, AutomaticPanedWindow): return if len(self.visible_panes) == 0 and self.is_visible(): self.master.forget(self) if len(self.panes()) > 0 and not self.is_visible(): self.master.insert("auto", self) class AutomaticNotebook(ttk.Notebook): """ Enables inserting views according to their position keys. Remember its own position key. Automatically updates its visibility. """ def __init__(self, master, position_key): ttk.Notebook.__init__(self, master) self.position_key = position_key def add(self, child, **kw): ttk.Notebook.add(self, child, **kw) self._update_visibility() def insert(self, pos, child, **kw): if pos == "auto": for sibling in map(self.nametowidget, self.tabs()): if (not hasattr(sibling, "position_key") or sibling.position_key == None or sibling.position_key > child.position_key): pos = sibling break else: pos = "end" ttk.Notebook.insert(self, pos, child, **kw) self._update_visibility() def hide(self, tab_id): ttk.Notebook.hide(self, tab_id) self._update_visibility() def forget(self, tab_id): ttk.Notebook.forget(self, tab_id) self._update_visibility() def is_visible(self): return self in self.master.visible_panes def get_visible_child(self): for child in self.winfo_children(): if str(child) == str(self.select()): return child return None def _update_visibility(self): if not isinstance(self.master, AutomaticPanedWindow): return if len(self.tabs()) == 0 and self.is_visible(): self.master.remove(self) if len(self.tabs()) > 0 and not self.is_visible(): self.master.insert("auto", self) class TreeFrame(ttk.Frame): def __init__(self, master, columns, displaycolumns='#all', show_scrollbar=True): ttk.Frame.__init__(self, master) self.vert_scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) if show_scrollbar: self.vert_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) self.tree = ttk.Treeview(self, columns=columns, displaycolumns=displaycolumns, yscrollcommand=self.vert_scrollbar.set) self.tree['show'] = 'headings' self.tree.grid(row=0, column=0, sticky=tk.NSEW) self.vert_scrollbar['command'] = self.tree.yview self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.tree.bind("<>", self.on_select, "+") self.tree.bind("", self.on_double_click, "+") def _clear_tree(self): for child_id in self.tree.get_children(): self.tree.delete(child_id) def on_select(self, event): pass def on_double_click(self, event): pass def sequence_to_accelerator(sequence): """Translates Tk event sequence to customary shortcut string for showing in the menu""" if not sequence: return "" if not sequence.startswith("<"): return sequence accelerator = (sequence .strip("<>") .replace("Key-", "") .replace("KeyPress-", "") .replace("Control", "Ctrl") ) # Tweaking individual parts parts = accelerator.split("-") # tkinter shows shift with capital letter, but in shortcuts it's customary to include it explicitly if len(parts[-1]) == 1 and parts[-1].isupper() and not "Shift" in parts: parts.insert(-1, "Shift") # even when shift is not required, it's customary to show shortcut with capital letter if len(parts[-1]) == 1: parts[-1] = parts[-1].upper() accelerator = "+".join(parts) # Post processing accelerator = (accelerator .replace("Minus", "-").replace("minus", "-") .replace("Plus", "+").replace("plus", "+")) return accelerator def get_zoomed(toplevel): if "-zoomed" in toplevel.wm_attributes(): # Linux return bool(toplevel.wm_attributes("-zoomed")) else: # Win/Mac return toplevel.wm_state() == "zoomed" def set_zoomed(toplevel, value): if "-zoomed" in toplevel.wm_attributes(): # Linux toplevel.wm_attributes("-zoomed", str(int(value))) else: # Win/Mac if value: toplevel.wm_state("zoomed") else: toplevel.wm_state("normal") class EnhancedTextWithLogging(tktextext.EnhancedText): def direct_insert(self, index, chars, tags=()): try: # try removing line numbers # TODO: shouldn't it take place only on paste? # TODO: does it occur when opening a file with line numbers in it? #if self._propose_remove_line_numbers and isinstance(chars, str): # chars = try_remove_linenumbers(chars, self) concrete_index = self.index(index) return tktextext.EnhancedText.direct_insert(self, index, chars, tags=tags) finally: get_workbench().event_generate("TextInsert", index=concrete_index, text=chars, tags=tags, text_widget=self) def direct_delete(self, index1, index2=None): try: # index1 may be eg "sel.first" and it doesn't make sense *after* deletion concrete_index1 = self.index(index1) if index2 is not None: concrete_index2 = self.index(index2) else: concrete_index2 = None return tktextext.EnhancedText.direct_delete(self, index1, index2=index2) finally: get_workbench().event_generate("TextDelete", index1=concrete_index1, index2=concrete_index2, text_widget=self) class SafeScrollbar(ttk.Scrollbar): def set(self, first, last): try: ttk.Scrollbar.set(self, first, last) except: traceback.print_exc() class AutoScrollbar(SafeScrollbar): # http://effbot.org/zone/tkinter-autoscrollbar.htm # a vert_scrollbar that hides itself if it's not needed. only # works if you use the grid geometry manager. def set(self, lo, hi): # TODO: this can make GUI hang or max out CPU when scrollbar wobbles back and forth if float(lo) <= 0.0 and float(hi) >= 1.0: self.grid_remove() else: self.grid() ttk.Scrollbar.set(self, lo, hi) def pack(self, **kw): raise tk.TclError("cannot use pack with this widget") def place(self, **kw): raise tk.TclError("cannot use place with this widget") def update_entry_text(entry, text): original_state = entry.cget("state") entry.config(state="normal") entry.delete(0, "end") entry.insert(0, text) entry.config(state=original_state) class ScrollableFrame(tk.Frame): # http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame def __init__(self, master): tk.Frame.__init__(self, master, bg=CALM_WHITE) # set up scrolling with canvas vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.canvas = tk.Canvas(self, bg=CALM_WHITE, bd=0, highlightthickness=0, yscrollcommand=vscrollbar.set) vscrollbar.config(command=self.canvas.yview) self.canvas.xview_moveto(0) self.canvas.yview_moveto(0) self.canvas.grid(row=0, column=0, sticky=tk.NSEW) vscrollbar.grid(row=0, column=1, sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.interior = tk.Frame(self.canvas, bg=CALM_WHITE) self.interior.columnconfigure(0, weight=1) self.interior.rowconfigure(0, weight=1) self.interior_id = self.canvas.create_window(0,0, window=self.interior, anchor=tk.NW) self.bind('', self._configure_interior, "+") self.bind('', self._expose, "+") def _expose(self, event): self.update_idletasks() self._configure_interior(event) def _configure_interior(self, event): # update the scrollbars to match the size of the inner frame size = (self.canvas.winfo_width() , self.interior.winfo_reqheight()) self.canvas.config(scrollregion="0 0 %s %s" % size) if (self.interior.winfo_reqwidth() != self.canvas.winfo_width() and self.canvas.winfo_width() > 10): # update the interior's width to fit canvas #print("CAWI", self.canvas.winfo_width()) self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width()) class TtkDialog(Dialog): def buttonbox(self): '''add standard button box. override if you do not want the standard buttons ''' box = ttk.Frame(self) w = ttk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE) w.pack(side=tk.LEFT, padx=5, pady=5) w = ttk.Button(box, text="Cancel", width=10, command=self.cancel) w.pack(side=tk.LEFT, padx=5, pady=5) self.bind("", self.ok, True) self.bind("", self.cancel, True) box.pack() class _QueryDialog(TtkDialog): def __init__(self, title, prompt, initialvalue=None, minvalue = None, maxvalue = None, master = None, selection_range=None): if not master: master = tk._default_root self.prompt = prompt self.minvalue = minvalue self.maxvalue = maxvalue self.initialvalue = initialvalue self.selection_range = selection_range Dialog.__init__(self, master, title) def destroy(self): self.entry = None Dialog.destroy(self) def body(self, master): w = ttk.Label(master, text=self.prompt, justify=tk.LEFT) w.grid(row=0, padx=5, sticky=tk.W) self.entry = ttk.Entry(master, name="entry") self.entry.grid(row=1, padx=5, sticky="we") if self.initialvalue is not None: self.entry.insert(0, self.initialvalue) if self.selection_range: self.entry.icursor(self.selection_range[0]) self.entry.select_range(self.selection_range[0], self.selection_range[1]) else: self.entry.select_range(0, tk.END) return self.entry def validate(self): try: result = self.getresult() except ValueError: messagebox.showwarning( "Illegal value", self.errormessage + "\nPlease try again", parent = self ) return 0 if self.minvalue is not None and result < self.minvalue: messagebox.showwarning( "Too small", "The allowed minimum value is %s. " "Please try again." % self.minvalue, parent = self ) return 0 if self.maxvalue is not None and result > self.maxvalue: messagebox.showwarning( "Too large", "The allowed maximum value is %s. " "Please try again." % self.maxvalue, parent = self ) return 0 self.result = result return 1 class _QueryString(_QueryDialog): def __init__(self, *args, **kw): if "show" in kw: self.__show = kw["show"] del kw["show"] else: self.__show = None _QueryDialog.__init__(self, *args, **kw) def body(self, master): entry = _QueryDialog.body(self, master) if self.__show is not None: entry.configure(show=self.__show) return entry def getresult(self): return self.entry.get() class ToolTip(object): """Taken from http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml""" def __init__(self, widget, options): self.widget = widget self.tipwindow = None self.id = None self.x = self.y = 0 self.options = options def showtip(self, text): "Display text in tooltip window" self.text = text if self.tipwindow or not self.text: return x, y, _, cy = self.widget.bbox("insert") x = x + self.widget.winfo_rootx() + 27 y = y + cy + self.widget.winfo_rooty() +27 self.tipwindow = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(1) if running_on_mac_os(): # TODO: maybe it's because of Tk 8.5, not because of Mac tw.wm_transient(self.widget) tw.wm_geometry("+%d+%d" % (x, y)) try: # For Mac OS tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "noActivates") except tk.TclError: pass label = tk.Label(tw, text=self.text, **self.options) label.pack() def hidetip(self): tw = self.tipwindow self.tipwindow = None if tw: tw.destroy() def create_tooltip(widget, text, background="#ffffe0", relief=tk.SOLID, borderwidth=1, padx=1, pady=0, **kw): options = kw.copy() options["background"] = background options["relief"] = relief options["borderwidth"] = borderwidth options["padx"] = padx options["pady"] = pady toolTip = ToolTip(widget, options) def enter(event): toolTip.showtip(text) def leave(event): toolTip.hidetip() widget.bind('', enter) widget.bind('', leave) def askstring(title, prompt, **kw): '''get a string from the user Arguments: title -- the dialog title prompt -- the label text **kw -- see SimpleDialog class Return value is a string ''' d = _QueryString(title, prompt, **kw) return d.result def get_current_notebook_tab_widget(notebook): for child in notebook.winfo_children(): if str(child) == str(notebook.select()): return child return None def create_string_var(value, modification_listener=None): """Creates a tk.StringVar with "modified" attribute showing whether the variable has been modified after creation""" return _create_var(tk.StringVar, value, modification_listener) def create_int_var(value, modification_listener=None): """See create_string_var""" return _create_var(tk.IntVar, value, modification_listener) def create_double_var(value, modification_listener=None): """See create_string_var""" return _create_var(tk.DoubleVar, value, modification_listener) def create_boolean_var(value, modification_listener=None): """See create_string_var""" return _create_var(tk.BooleanVar, value, modification_listener) def _create_var(class_, value, modification_listener): var = class_(value=value) var.modified = False def on_write(*args): var.modified = True if modification_listener: try: modification_listener() except: # Otherwise whole process will be brought down # because for some reason Tk tries to call non-existing method # on variable get_workbench().report_exception() # TODO: https://bugs.python.org/issue22115 (deprecation warning) var.trace("w", on_write) return var def shift_is_pressed(event_state): # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html # http://stackoverflow.com/q/32426250/261181 return event_state & 0x0001 def control_is_pressed(event_state): # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-handlers.html # http://stackoverflow.com/q/32426250/261181 return event_state & 0x0004 def select_sequence(win_version, mac_version, linux_version=None): if running_on_windows(): return win_version elif running_on_mac_os(): return mac_version elif running_on_linux() and linux_version: return linux_version else: return win_version def try_remove_linenumbers(text, master): try: if has_line_numbers(text) and tkMessageBox.askyesno ( title="Remove linenumbers", message="Do you want to remove linenumbers from pasted text?", default=tkMessageBox.YES, master=master): return remove_line_numbers(text) else: return text except: traceback.print_exc() return text def has_line_numbers(text): lines = text.splitlines() return (len(lines) > 2 and all([len(split_after_line_number(line)) == 2 for line in lines])) def split_after_line_number(s): parts = re.split("(^\s*\d+\.?)", s) if len(parts) == 1: return parts else: assert len(parts) == 3 and parts[0] == '' return parts[1:] def remove_line_numbers(s): cleaned_lines = [] for line in s.splitlines(): parts = split_after_line_number(line) if len(parts) != 2: return s else: cleaned_lines.append(parts[1]) return textwrap.dedent(("\n".join(cleaned_lines)) + "\n") def get_main_background(): main_background_option = get_workbench().get_option("theme.main_background") if main_background_option is not None: return main_background_option else: theme = ttk.Style().theme_use() if theme == "clam": return "#dcdad5" elif theme == "aqua": return "systemSheetBackground" else: return "SystemButtonFace" def get_dialog_background_color(): theme = ttk.Style().theme_use() if theme == "aqua": return "systemSheetBackground" else: return "SystemButtonFace" def center_window(win, master=None): # looks like it doesn't take window border into account win.update_idletasks() if getattr(master, "initializing", False): # can't get reliable positions when main window is not in mainloop yet left = (win.winfo_screenwidth() - 600) // 2 top = (win.winfo_screenheight() - 400) // 2 else: if master is None: left = win.winfo_screenwidth() - win.winfo_width() // 2 top = win.winfo_screenheight() - win.winfo_height() // 2 else: left = master.winfo_rootx() + master.winfo_width() // 2 - win.winfo_width() // 2 top = master.winfo_rooty() + master.winfo_height() // 2 - win.winfo_height() // 2 win.geometry("+%d+%d" % (left, top)) class BusyTk(tk.Tk): def __init__(self, async_result, description, title="Please wait!"): self._async_result = async_result tk.Tk.__init__(self) self.update_idletasks() screen_width = self.winfo_screenwidth() screen_height = self.winfo_screenheight() win_width = screen_width // 3 win_height = screen_height // 3 x = screen_width//2 - win_width//2 y = screen_height//2 - win_height//2 self.geometry("%dx%d+%d+%d" % (win_width, win_height, x, y)) main_frame = ttk.Frame(self) main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) self.title(title) self.resizable(height=tk.FALSE, width=tk.FALSE) self.protocol("WM_DELETE_WINDOW", self._ok) self.desc_label = ttk.Label(main_frame, text=description) self.desc_label.grid(padx=20, pady=20, sticky="nsew") self.update_idletasks() self.after(500, self._poll) def _poll(self): if self._async_result.ready(): self._ok() else: self.after(500, self._poll) self.desc_label["text"] = self.desc_label["text"] + "." def _ok(self): self.destroy() def run_with_busy_window(action, args=(), description=""): # http://stackoverflow.com/a/14299004/261181 from multiprocessing.pool import ThreadPool pool = ThreadPool(processes=1) async_result = pool.apply_async(action, args) dlg = BusyTk(async_result, description=description) dlg.mainloop() return async_result.get() class SubprocessDialog(tk.Toplevel): """Shows incrementally the output of given subprocess. Allows cancelling""" def __init__(self, master, proc, title, long_description=None, autoclose=True): self._proc = proc self.stdout = "" self.stderr = "" self._stdout_thread = None self._stderr_thread = None self.returncode = None self.cancelled = False self._autoclose = autoclose self._event_queue = collections.deque() tk.Toplevel.__init__(self, master) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) main_frame = ttk.Frame(self) # To get styled background main_frame.grid(sticky="nsew") text_font=tk.font.nametofont("TkFixedFont").copy() text_font["size"] = int(text_font["size"] * 0.9) text_font["family"] = "Courier" if running_on_mac_os() else "Courier New" text_frame = tktextext.TextFrame(main_frame, read_only=True, horizontal_scrollbar=False, background=get_main_background(), font=text_font, wrap="word") text_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=15, pady=15) self.text = text_frame.text self.text["width"] = 60 self.text["height"] = 7 if long_description is not None: self.text.direct_insert("1.0", long_description + "\n\n") self.button = ttk.Button(main_frame, text="Cancel", command=self._close) self.button.grid(row=1, column=0, pady=(0,15)) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) self.title(title) if misc_utils.running_on_mac_os(): self.configure(background="systemSheetBackground") #self.resizable(height=tk.FALSE, width=tk.FALSE) self.transient(master) self.grab_set() # to make it active and modal self.text.focus_set() self.bind('', self._close_if_done, True) # escape-close only if process has completed self.protocol("WM_DELETE_WINDOW", self._close) center_window(self, master) self._start_listening() def _start_listening(self): def listen_stream(stream_name): stream = getattr(self._proc, stream_name) while True: data = stream.readline() self._event_queue.append((stream_name, data)) setattr(self, stream_name, getattr(self, stream_name) + data) if data == '': break self.returncode = self._proc.wait() self._stdout_thread = threading.Thread(target=listen_stream, args=["stdout"]) self._stdout_thread.start() if self._proc.stderr is not None: self._stderr_thread = threading.Thread(target=listen_stream, args=["stderr"]) self._stderr_thread.start() def poll_output_events(): while len(self._event_queue) > 0: stream_name, data = self._event_queue.popleft() self.text.direct_insert("end", data, tags=(stream_name, )) self.text.see("end") self.returncode = self._proc.poll() if self.returncode == None: self.after(200, poll_output_events) else: self.button["text"] = "OK" self.button.focus_set() if self.returncode != 0: self.text.direct_insert("end", "\n\nReturn code: ", ("stderr", )) elif self._autoclose: self._close() poll_output_events() def _close_if_done(self, event): if self._proc.poll() is not None: self._close(event) def _close(self, event=None): if self._proc.poll() is None: if messagebox.askyesno("Cancel the process?", "The process is still running.\nAre you sure you want to cancel?"): # try gently first try: if running_on_windows(): os.kill(self._proc.pid, signal.CTRL_BREAK_EVENT) # @UndefinedVariable else: os.kill(self._proc.pid, signal.SIGINT) self._proc.wait(2) except subprocess.TimeoutExpired: if self._proc.poll() is None: # now let's be more concrete self._proc.kill() self.cancelled = True # Wait for threads to finish self._stdout_thread.join(2) if self._stderr_thread is not None: self._stderr_thread.join(2) # fetch output about cancelling while len(self._event_queue) > 0: stream_name, data = self._event_queue.popleft() self.text.direct_insert("end", data, tags=(stream_name, )) self.text.direct_insert("end", "\n\nPROCESS CANCELLED") self.text.see("end") else: return else: self.destroy() def get_busy_cursor(): if running_on_windows(): return "wait" elif running_on_mac_os(): return "spinning" else: return "watch" def get_tk_version_str(): return tk._default_root.tk.call('info', 'patchlevel') def get_tk_version_info(): result = [] for part in get_tk_version_str().split("."): try: result.append(int(part)) except: result.append(0) return tuple(result) thonny-2.1.16/thonny/VERSION0000666000000000000000000000000613201322770013632 0ustar 000000000000002.1.16thonny-2.1.16/thonny/workbench.py0000666000000000000000000013626713201264465015147 0ustar 00000000000000# -*- coding: utf-8 -*- import importlib import os.path import sys from tkinter import ttk import traceback from thonny import ui_utils from thonny.code import EditorNotebook from thonny.common import Record, UserError from thonny.config import try_load_configuration from thonny.misc_utils import running_on_mac_os, running_on_linux from thonny.ui_utils import sequence_to_accelerator, AutomaticPanedWindow, AutomaticNotebook,\ create_tooltip, get_current_notebook_tab_widget, select_sequence import tkinter as tk import tkinter.font as tk_font import tkinter.messagebox as tk_messagebox from thonny.running import Runner import thonny.globals import logging from thonny.globals import register_runner, get_runner from thonny.config_ui import ConfigurationDialog import pkgutil import socket import queue from _thread import start_new_thread import ast from thonny import THONNY_USER_DIR THONNY_PORT = 4957 SERVER_SUCCESS = "OK" CONFIGURATION_FILE_NAME = os.path.join(THONNY_USER_DIR, "configuration.ini") SINGLE_INSTANCE_DEFAULT = True class Workbench(tk.Tk): """ Thonny's main window and communication hub. Is responsible for: * creating the main window * maintaining layout (_init_containers) * loading plugins (_init_plugins, add_view, add_command) * providing references to main components (editor_notebook and runner) * communication between other components (see event_generate and bind) * configuration services (get_option, set_option, add_defaults) * loading translations * maintaining fonts (get_font, increasing and decreasing font size) After workbench and plugins get loaded, 3 kinds of events start happening: * User events (keypresses, mouse clicks, menu selections, ...) * Virtual events (mostly via get_workbench().event_generate). These include: events reported via and dispatched by Tk event system; WorkbenchEvent-s, reported via and dispatched by enhanced get_workbench().event_generate. * Events from the background process (program output notifications, input requests, notifications about debugger's progress) """ def __init__(self, server_socket=None): self._destroying = False self.initializing = True tk.Tk.__init__(self, className="Thonny") # self.tk.call("tk", "scaling", 2.0) tk.Tk.report_callback_exception = self._on_tk_exception self._event_handlers = {} self._images = set() # to avoid Python garbage collecting them self._image_mapping = {} # to allow specify different images in a theme self._backends = {} self._theme_tweaker = None thonny.globals.register_workbench(self) self._init_configuration() self._init_diagnostic_logging() logging.info("Loading early plugins from " + str(sys.path)) self._load_early_plugins() self._editor_notebook = None self._select_theme() self._init_fonts() self._init_window() self._init_menu() self.title("Thonny") self._init_containers() self._init_runner() self._init_commands() self._load_plugins() self._update_toolbar() try: self._editor_notebook.load_startup_files() except: self.report_exception() self._editor_notebook.focus_set() self._try_action(self._open_views) if server_socket is not None: self._init_server_loop(server_socket) self.bind_class("CodeViewText", "<>", self.update_title, True) self.bind_class("CodeViewText", "<>", self.update_title, True) self.bind_class("CodeViewText", "<>", self.update_title, True) self.get_editor_notebook().bind("<>", self.update_title ,True) self.initializing = False def _try_action(self, action): try: action() except: self.report_exception() def _init_configuration(self): self._configuration_manager = try_load_configuration(CONFIGURATION_FILE_NAME) self._configuration_pages = {} self.set_default("general.single_instance", SINGLE_INSTANCE_DEFAULT) self.set_default("general.expert_mode", False) self.set_default("debug_mode", False) def _init_diagnostic_logging(self): logFormatter = logging.Formatter('%(levelname)s: %(message)s') root_logger = logging.getLogger() log_file = os.path.join(THONNY_USER_DIR, "frontend.log") file_handler = logging.FileHandler(log_file, encoding="UTF-8", mode="w") file_handler.setFormatter(logFormatter) file_handler.setLevel(logging.INFO); root_logger.addHandler(file_handler) console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(logFormatter) console_handler.setLevel(logging.INFO); root_logger.addHandler(console_handler) root_logger.setLevel(logging.INFO) import faulthandler fault_out = open(os.path.join(THONNY_USER_DIR, "frontend_faults.log"), mode="w") faulthandler.enable(fault_out) def _init_window(self): self.set_default("layout.zoomed", False) self.set_default("layout.top", 15) self.set_default("layout.left", 150) self.set_default("layout.width", 700) self.set_default("layout.height", 650) self.set_default("layout.w_width", 200) self.set_default("layout.e_width", 200) self.set_default("layout.s_height", 200) # I don't actually need saved options for Full screen/maximize view, # but it's easier to create menu items, if I use configuration manager's variables self.set_default("view.full_screen", False) self.set_default("view.maximize_view", False) # In order to avoid confusion set these settings to False # even if they were True when Thonny was last run self.set_option("view.full_screen", False) self.set_option("view.maximize_view", False) self.geometry("{0}x{1}+{2}+{3}".format(self.get_option("layout.width"), self.get_option("layout.height"), self.get_option("layout.left"), self.get_option("layout.top"))) if self.get_option("layout.zoomed"): ui_utils.set_zoomed(self, True) self.protocol("WM_DELETE_WINDOW", self._on_close) # Window icons window_icons = self.get_option("theme.window_icons") if window_icons: imgs = [self.get_image(filename) for filename in window_icons] self.iconphoto(True, *imgs) elif running_on_linux() and ui_utils.get_tk_version_info() >= (8,6): self.iconphoto(True, self.get_image("thonny.png")) else: icon_file = os.path.join(self.get_package_dir(), "res", "thonny.ico") try: self.iconbitmap(icon_file, default=icon_file) except: try: # seems to work in mac self.iconbitmap(icon_file) except: pass # TODO: try to get working in Ubuntu self.bind("", self._on_configure, True) def _init_menu(self): self.option_add('*tearOff', tk.FALSE) self._menubar = tk.Menu(self, **self.get_option("theme.menubar_options", { #"relief" : "flat", "activeborderwidth" : 0 })) self["menu"] = self._menubar self._menus = {} self._menu_item_groups = {} # key is pair (menu_name, command_label) self._menu_item_testers = {} # key is pair (menu_name, command_label) # create standard menus in correct order self.get_menu("file", "File") self.get_menu("edit", "Edit") self.get_menu("view", "View") self.get_menu("run", "Run") self.get_menu("tools", "Tools") self.get_menu("help", "Help") def _load_early_plugins(self): """load_early_plugin can't use nor GUI neither Runner""" self._load_plugins("load_early_plugin") def _load_plugins(self, load_function_name="load_plugin"): # built-in plugins import thonny.plugins self._load_plugins_from_path(thonny.plugins.__path__, "thonny.plugins.", load_function_name=load_function_name) # 3rd party plugins from namespace package try: import thonnycontrib # @UnresolvedImport except ImportError: # No 3rd party plugins installed pass else: self._load_plugins_from_path(thonnycontrib.__path__, "thonnycontrib.", load_function_name=load_function_name) def _load_plugins_from_path(self, path, prefix="", load_function_name="load_plugin"): for _, module_name, _ in pkgutil.iter_modules(path, prefix): try: m = importlib.import_module(module_name) if hasattr(m, load_function_name): getattr(m, load_function_name)() except: logging.exception("Failed loading plugin '" + module_name + "'") def _init_fonts(self): self.set_default("view.io_font_family", "Courier" if running_on_mac_os() else "Courier New") default_editor_family = "Courier New" families = tk_font.families() for family in ["Consolas", "Ubuntu Mono", "Menlo", "DejaVu Sans Mono"]: if family in families: default_editor_family = family break self.set_default("view.editor_font_family", default_editor_family) self.set_default("view.editor_font_size", 14 if running_on_mac_os() else 11) default_font = tk_font.nametofont("TkDefaultFont") self._fonts = { 'IOFont' : tk_font.Font(family=self.get_option("view.io_font_family")), 'EditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family")), 'BoldEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"), weight="bold"), 'ItalicEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"), slant="italic"), 'BoldItalicEditorFont' : tk_font.Font(family=self.get_option("view.editor_font_family"), weight="bold", slant="italic"), 'TreeviewFont' : tk_font.Font(family=default_font.cget("family"), size=default_font.cget("size")) } self.update_fonts() def _init_runner(self): try: runner = Runner() register_runner(runner) runner.start() except: self.report_exception("Error when initializing backend") def _init_server_loop(self, server_socket): """Socket will listen requests from newer Thonny instances, which try to delegate opening files to older instance""" self._requests_from_socket = queue.Queue() def server_loop(): while True: logging.debug("Waiting for next client") (client_socket, _) = server_socket.accept() try: self._handle_socket_request(client_socket) except: traceback.print_exc() start_new_thread(server_loop, ()) self._poll_socket_requests() def _init_commands(self): self.add_command("exit", "file", "Exit", self._on_close, default_sequence=select_sequence("", "")) self.add_command("show_options", "tools", "Options...", self._cmd_show_options, group=180) self.createcommand("::tk::mac::ShowPreferences", self._cmd_show_options) self.add_command("increase_font_size", "view", "Increase font size", lambda: self._change_font_size(1), default_sequence=select_sequence("", ""), group=60) self.add_command("decrease_font_size", "view", "Decrease font size", lambda: self._change_font_size(-1), default_sequence=select_sequence("", ""), group=60) self.bind("", self._cmd_zoom_with_mouse, True) self.add_command("focus_editor", "view", "Focus editor", self._cmd_focus_editor, default_sequence="", group=70) self.add_command("focus_shell", "view", "Focus shell", self._cmd_focus_shell, default_sequence="", group=70) if self.get_option("general.expert_mode"): self.add_command("toggle_maximize_view", "view", "Maximize view", self._cmd_toggle_maximize_view, flag_name="view.maximize_view", default_sequence=None, group=80) self.bind_class("TNotebook", "", self._maximize_view, True) self.bind("", self._unmaximize_view, True) if not running_on_mac_os(): # TODO: approach working in Win/Linux doesn't work in mac as it should and only confuses self.add_command("toggle_maximize_view", "view", "Full screen", self._cmd_toggle_full_screen, flag_name="view.full_screen", default_sequence=select_sequence("", ""), group=80) def _init_containers(self): # Main frame functions as # - a backgroud behind padding of main_pw, without this OS X leaves white border # - a container to be hidden, when a view is maximized and restored when view is back home main_frame= ttk.Frame(self) # self._main_frame = main_frame main_frame.grid(row=0, column=0, sticky=tk.NSEW) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self._maximized_view = None self._toolbar = ttk.Frame(main_frame, padding=0) # TODO: height=30 ? self._toolbar.grid(column=0, row=0, sticky=tk.NSEW, padx=10, pady=(5,0)) self.set_default("layout.main_pw_first_pane_size", 1/3) self.set_default("layout.main_pw_last_pane_size", 1/3) self._main_pw = AutomaticPanedWindow(main_frame, orient=tk.HORIZONTAL, first_pane_size=self.get_option("layout.main_pw_first_pane_size"), last_pane_size=self.get_option("layout.main_pw_last_pane_size") ) self._main_pw.grid(column=0, row=1, sticky=tk.NSEW, padx=10, pady=10) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(1, weight=1) self.set_default("layout.west_pw_first_pane_size", 1/3) self.set_default("layout.west_pw_last_pane_size", 1/3) self.set_default("layout.center_pw_first_pane_size", 1/3) self.set_default("layout.center_pw_last_pane_size", 1/3) self.set_default("layout.east_pw_first_pane_size", 1/3) self.set_default("layout.east_pw_last_pane_size", 1/3) self._west_pw = AutomaticPanedWindow(self._main_pw, 1, orient=tk.VERTICAL, first_pane_size=self.get_option("layout.west_pw_first_pane_size"), last_pane_size=self.get_option("layout.west_pw_last_pane_size") ) self._center_pw = AutomaticPanedWindow(self._main_pw, 2, orient=tk.VERTICAL, first_pane_size=self.get_option("layout.center_pw_first_pane_size"), last_pane_size=self.get_option("layout.center_pw_last_pane_size") ) self._east_pw = AutomaticPanedWindow(self._main_pw, 3, orient=tk.VERTICAL, first_pane_size=self.get_option("layout.east_pw_first_pane_size"), last_pane_size=self.get_option("layout.east_pw_last_pane_size") ) self._view_records = {} self._view_notebooks = { 'nw' : AutomaticNotebook(self._west_pw, 1), 'w' : AutomaticNotebook(self._west_pw, 2), 'sw' : AutomaticNotebook(self._west_pw, 3), 's' : AutomaticNotebook(self._center_pw, 3), 'ne' : AutomaticNotebook(self._east_pw, 1), 'e' : AutomaticNotebook(self._east_pw, 2), 'se' : AutomaticNotebook(self._east_pw, 3), } for nb_name in self._view_notebooks: self.set_default("layout.notebook_" + nb_name + "_visible_view", None) self._editor_notebook = EditorNotebook(self._center_pw) self._editor_notebook.position_key = 1 self._center_pw.insert("auto", self._editor_notebook) def _select_theme(self): style = ttk.Style() preferred_theme = self.get_option("theme.preferred_theme") if preferred_theme in style.theme_names(): style.theme_use(preferred_theme) elif 'xpnative' in style.theme_names(): # in Win7 'xpnative' gives better scrollbars than 'vista' style.theme_use('xpnative') elif 'vista' in style.theme_names(): style.theme_use('vista') elif 'clam' in style.theme_names(): style.theme_use('clam') if self._theme_tweaker is not None: self._theme_tweaker() def add_command(self, command_id, menu_name, command_label, handler, tester=None, default_sequence=None, flag_name=None, skip_sequence_binding=False, accelerator=None, group=99, position_in_group="end", image_filename=None, include_in_toolbar=False, bell_when_denied=True): """Adds an item to specified menu. Args: menu_name: Name of the menu the command should appear in. Standard menu names are "file", "edit", "run", "view", "help". If a menu with given name doesn't exist, then new menu is created (with label=name). command_label: Label for this command handler: Function to be called when the command is invoked. Should be callable with one argument (the event or None). tester: Function to be called for determining if command is available or not. Should be callable with one argument (the event or None). Should return True or False. If None then command is assumed to be always available. default_sequence: Default shortcut (Tk style) flag_name: Used for toggle commands. Indicates the name of the boolean option. group: Used for grouping related commands together. Value should be int. Groups with smaller numbers appear before. Returns: None """ def dispatch(event=None): if not tester or tester(): denied = False handler() else: denied = True logging.debug("Command '" + command_id + "' execution denied") if bell_when_denied: self.bell() self.event_generate("Command", command_id=command_id, denied=denied) sequence_option_name = "shortcuts." + command_id self.set_default(sequence_option_name, default_sequence) sequence = self.get_option(sequence_option_name) if sequence and not skip_sequence_binding: self.bind_all(sequence, dispatch, True) def dispatch_from_menu(): # I don't like that Tk menu toggles checbutton variable # automatically before calling the handler. # So I revert the toggle before calling the actual handler. # This way the handler doesn't have to worry whether it # needs to toggle the variable or not, and it can choose to # decline the toggle. if flag_name is not None: var = self.get_variable(flag_name) var.set(not var.get()) dispatch(None) if image_filename: image = self.get_image(image_filename) else: image = None if image and self.get_option("theme.icons_in_menus", True): menu_image = image elif flag_name: # no image or black next to a checkbox menu_image = None else: menu_image = self.get_image ("16x16_blank.gif") if not accelerator and sequence: accelerator = sequence_to_accelerator(sequence) menu = self.get_menu(menu_name) menu.insert( self._find_location_for_menu_item(menu_name, command_label, group, position_in_group), "checkbutton" if flag_name else "command", label=command_label, accelerator=accelerator, image=menu_image, compound=tk.LEFT, variable=self.get_variable(flag_name) if flag_name else None, command=dispatch_from_menu) # remember the details that can't be stored in Tkinter objects self._menu_item_groups[(menu_name, command_label)] = group self._menu_item_testers[(menu_name, command_label)] = tester if include_in_toolbar: toolbar_group = self._get_menu_index(menu) * 100 + group self._add_toolbar_button(image, command_label, accelerator, handler, tester, toolbar_group) def add_view(self, class_, label, default_location, visible_by_default=False, default_position_key=None): """Adds item to "View" menu for showing/hiding given view. Args: view_class: Class or constructor for view. Should be callable with single argument (the master of the view) label: Label of the view tab location: Location descriptor. Can be "nw", "sw", "s", "se", "ne" Returns: None """ view_id = class_.__name__ if default_position_key == None: default_position_key = label self.set_default("view." + view_id + ".visible" , visible_by_default) self.set_default("view." + view_id + ".location", default_location) self.set_default("view." + view_id + ".position_key", default_position_key) self._view_records[view_id] = { "class" : class_, "label" : label, "location" : self.get_option("view." + view_id + ".location"), "position_key" : self.get_option("view." + view_id + ".position_key") } visibility_flag = self.get_variable("view." + view_id + ".visible") # handler def toggle_view_visibility(): if visibility_flag.get(): self.hide_view(view_id) else: self.show_view(view_id, True) self.add_command("toggle_" + view_id, menu_name="view", command_label=label, handler=toggle_view_visibility, flag_name="view." + view_id + ".visible", group=10, position_in_group="alphabetic") if visibility_flag.get(): self.show_view(view_id, False) def add_configuration_page(self, title, page_class): self._configuration_pages[title] = page_class def map_image(self, original_image, new_image): self._image_mapping[original_image] = new_image def add_backend(self, descriptor, proxy_class): self._backends[descriptor] = proxy_class def get_backends(self): return self._backends def get_option(self, name, default=None): return self._configuration_manager.get_option(name, default) def set_option(self, name, value): self._configuration_manager.set_option(name, value) def set_theme_tweaker(self, fun): self._theme_tweaker = fun def set_default(self, name, default_value): """Registers a new option. If the name contains a period, then the part left to the (first) period will become the section of the option and rest will become name under that section. If the name doesn't contain a period, then it will be added under section "general". """ self._configuration_manager.set_default(name, default_value) def get_variable(self, name): return self._configuration_manager.get_variable(name) def get_font(self, name): """ Supported names are EditorFont and BoldEditorFont """ return self._fonts[name] def get_menu(self, name, label=None): """Gives the menu with given name. Creates if not created yet. Args: name: meant to be used as not translatable menu name label: translated label, used only when menu with given name doesn't exist yet """ if name not in self._menus: menu = tk.Menu(self._menubar, self.get_option("theme.menu_options", { #"relief" : "flat", #"activeborderwidth" : 0 })) menu["postcommand"] = lambda: self._update_menu(menu, name) self._menubar.add_cascade(label=label if label else name, menu=menu) self._menus[name] = menu if label: self._menus[label] = menu return self._menus[name] def get_view(self, view_id, create=True): if "instance" not in self._view_records[view_id]: if not create: return None class_ = self._view_records[view_id]["class"] location = self._view_records[view_id]["location"] master = self._view_notebooks[location] # create the view view = class_(self) # View's master is workbench to allow making it maximized view.position_key = self._view_records[view_id]["position_key"] self._view_records[view_id]["instance"] = view # create the view home_widget to be added into notebook view.home_widget = ttk.Frame(master) view.home_widget.columnconfigure(0, weight=1) view.home_widget.rowconfigure(0, weight=1) view.home_widget.maximizable_widget = view if hasattr(view, "position_key"): view.home_widget.position_key = view.position_key # initially the view will be in it's home_widget view.grid(row=0, column=0, sticky=tk.NSEW, in_=view.home_widget) view.hidden = True return self._view_records[view_id]["instance"] def get_current_editor(self): return self._editor_notebook.get_current_editor() def get_editor_notebook(self): return self._editor_notebook def get_package_dir(self): """Returns thonny package directory""" return os.path.dirname(sys.modules["thonny"].__file__) def get_image(self, filename, tk_name=None): if filename in self._image_mapping: filename = self._image_mapping[filename] # if path is relative then interpret it as living in res folder if not os.path.isabs(filename): filename = os.path.join(self.get_package_dir(), "res", filename) img = tk.PhotoImage(tk_name, file=filename) self._images.add(img) return img def show_view(self, view_id, set_focus=True): """View must be already registered. Args: view_id: View class name without package name (eg. 'ShellView') """ # NB! Don't forget that view.home_widget is added to notebook, not view directly # get or create view = self.get_view(view_id) notebook = view.home_widget.master if hasattr(view, "before_show") and view.before_show() == False: return False if view.hidden: notebook.insert("auto", view.home_widget, text=self._view_records[view_id]["label"]) view.hidden = False # switch to the tab notebook.select(view.home_widget) # add focus if set_focus: view.focus_set() self.set_option("view." + view_id + ".visible", True) self.event_generate("ShowView", view=view, view_id=view_id) return view def hide_view(self, view_id): # NB! Don't forget that view.home_widget is added to notebook, not view directly if "instance" in self._view_records[view_id]: # TODO: handle the case, when view is maximized view = self._view_records[view_id]["instance"] if hasattr(view, "before_hide") and view.before_hide() == False: return False view.home_widget.master.forget(view.home_widget) self.set_option("view." + view_id + ".visible", False) self.event_generate("HideView", view=view, view_id=view_id) view.hidden = True def event_generate(self, sequence, **kwargs): """Uses custom event handling when sequence doesn't start with <. In this case arbitrary attributes can be added to the event. Otherwise forwards the call to Tk's event_generate""" if sequence.startswith("<"): tk.Tk.event_generate(self, sequence, **kwargs) else: if sequence in self._event_handlers: for handler in self._event_handlers[sequence]: try: # Yes, I'm creating separate event object for each handler # so that they can't misuse the mutabilty event = WorkbenchEvent(sequence, **kwargs) handler(event) except: self.report_exception("Problem when handling '" + sequence + "'") def bind(self, sequence, func, add=None): """Uses custom event handling when sequence doesn't start with <. Otherwise forwards the call to Tk's bind""" if not add: logging.warning("Workbench.bind({}, ..., add={}) -- did you really want to replace existing bindings?".format(sequence, add)) if sequence.startswith("<"): tk.Tk.bind(self, sequence, func, add) else: if sequence not in self._event_handlers or not add: self._event_handlers[sequence] = set() self._event_handlers[sequence].add(func) def unbind(self, sequence, funcid=None): if sequence.startswith("<"): tk.Tk.unbind(self, sequence, funcid=funcid) else: if (sequence in self._event_handlers and funcid in self._event_handlers[sequence]): self._event_handlers[sequence].remove(funcid) def in_heap_mode(self): # TODO: add a separate command for enabling the heap mode # untie the mode from HeapView return (self._configuration_manager.has_option("view.HeapView.visible") and self.get_option("view.HeapView.visible")) def update_fonts(self): editor_font_size = self._guard_font_size(self.get_option("view.editor_font_size")) editor_font_family = self.get_option("view.editor_font_family") io_font_family = self.get_option("view.io_font_family") self.get_font("IOFont").configure(family=io_font_family, size=min(editor_font_size - 2, int(editor_font_size * 0.8 + 3))) self.get_font("EditorFont").configure(family=editor_font_family, size=editor_font_size) self.get_font("BoldEditorFont").configure(family=editor_font_family, size=editor_font_size) self.get_font("ItalicEditorFont").configure(family=editor_font_family, size=editor_font_size) self.get_font("BoldItalicEditorFont").configure(family=editor_font_family, size=editor_font_size) style = ttk.Style() if running_on_mac_os(): treeview_font_size = int(editor_font_size * 0.7 + 4) rowheight = int(treeview_font_size*1.2 + 4 ) else: treeview_font_size = int(editor_font_size * 0.7 + 2) rowheight = int(treeview_font_size * 2.0 + 6) self.get_font("TreeviewFont").configure(size=treeview_font_size) style.configure("Treeview", rowheight=rowheight) if self._editor_notebook is not None: self._editor_notebook.update_appearance() def _get_menu_index(self, menu): for i in range(len(self._menubar.winfo_children())): if menu == self._menubar.winfo_children()[i]: return i else: return None def _add_toolbar_button(self, image, command_label, accelerator, handler, tester, toolbar_group): slaves = self._toolbar.grid_slaves(0, toolbar_group) if len(slaves) == 0: group_frame = ttk.Frame(self._toolbar) group_frame.grid(row=0, column=toolbar_group, padx=(0, 10)) else: group_frame = slaves[0] button = ttk.Button(group_frame, command=handler, image=image, style="Toolbutton", # TODO: does this cause problems in some Macs? state=tk.NORMAL ) button.pack(side=tk.LEFT) button.tester = tester tooltip_text = command_label if accelerator and self.get_option("theme.shortcuts_in_tooltips", True): tooltip_text += " (" + accelerator + ")" create_tooltip(button, tooltip_text, **self.get_option("theme.tooltip_options", {'padx':3, 'pady':1}) ) def _update_toolbar(self): for group_frame in self._toolbar.grid_slaves(0): for button in group_frame.pack_slaves(): if button.tester and not button.tester(): button["state"] = tk.DISABLED else: button["state"] = tk.NORMAL self.after(300, self._update_toolbar) def _cmd_zoom_with_mouse(self, event): if event.delta > 0: self._change_font_size(1) else: self._change_font_size(-1) def _change_font_size(self, delta): if delta != 0: editor_font_size = self.get_option("view.editor_font_size") editor_font_size += delta self.set_option("view.editor_font_size", self._guard_font_size(editor_font_size)) self.update_fonts() def _guard_font_size(self, size): # https://bitbucket.org/plas/thonny/issues/164/negative-font-size-crashes-thonny MIN_SIZE = 4 MAX_SIZE = 200 if size < MIN_SIZE: return MIN_SIZE elif size > MAX_SIZE: return MAX_SIZE else: return size def _check_update_window_width(self, delta): if not ui_utils.get_zoomed(self): self.update_idletasks() # TODO: shift to left if right edge goes away from screen # TODO: check with screen width new_geometry = "{0}x{1}+{2}+{3}".format(self.winfo_width() + delta, self.winfo_height(), self.winfo_x(), self.winfo_y()) self.geometry(new_geometry) def _maximize_view(self, event=None): if self._maximized_view is not None: return # find the widget that can be relocated widget = self.focus_get() if isinstance(widget, EditorNotebook) or isinstance(widget, AutomaticNotebook): current_tab = get_current_notebook_tab_widget(widget) if current_tab is None: return if not hasattr(current_tab, "maximizable_widget"): return widget = current_tab.maximizable_widget while widget is not None: if hasattr(widget, "home_widget"): # if widget is view, then widget.master is workbench widget.grid(row=0, column=0, sticky=tk.NSEW, in_=widget.master) # hide main_frame self._main_frame.grid_forget() self._maximized_view = widget self.get_variable("view.maximize_view").set(True) break else: widget = widget.master def _unmaximize_view(self, event=None): if self._maximized_view is None: return # restore main_frame self._main_frame.grid(row=0, column=0, sticky=tk.NSEW, in_=self) # put the maximized view back to its home_widget self._maximized_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self._maximized_view.home_widget) self._maximized_view = None self.get_variable("view.maximize_view").set(False) def _cmd_show_options(self): dlg = ConfigurationDialog(self, self._configuration_pages) dlg.focus_set() dlg.transient(self) dlg.grab_set() self.wait_window(dlg) def _cmd_focus_editor(self): self._editor_notebook.focus_set() def _cmd_focus_shell(self): self.show_view("ShellView", True) def _cmd_toggle_full_screen(self): var = self.get_variable("view.full_screen") var.set(not var.get()) self.attributes("-fullscreen", var.get()) def _cmd_toggle_maximize_view(self): if self._maximized_view is not None: self._unmaximize_view() else: self._maximize_view() def _update_menu(self, menu, menu_name): if menu.index("end") == None: return for i in range(menu.index("end")+1): item_data = menu.entryconfigure(i) if "label" in item_data: command_label = menu.entrycget(i, "label") tester = self._menu_item_testers[(menu_name, command_label)] if tester and not tester(): menu.entryconfigure(i, state=tk.DISABLED) else: menu.entryconfigure(i, state=tk.ACTIVE) def _find_location_for_menu_item(self, menu_name, command_label, group, position_in_group="end"): menu = self.get_menu(menu_name) if menu.index("end") == None: # menu is empty return "end" this_group_exists = False for i in range(0, menu.index("end")+1): data = menu.entryconfigure(i) if "label" in data: # it's a command, not separator sibling_label = menu.entrycget(i, "label") sibling_group = self._menu_item_groups[(menu_name, sibling_label)] if sibling_group == group: this_group_exists = True if position_in_group == "alphabetic" and sibling_label > command_label: return i if sibling_group > group: assert not this_group_exists # otherwise we would have found the ending separator menu.insert_separator(i) return i else: # We found a separator if this_group_exists: # it must be the ending separator for this group return i else: # no group was bigger, ie. this should go to the end if not this_group_exists: menu.add_separator() return "end" def _handle_socket_request(self, client_socket): """runs in separate thread""" # read the request data = bytes() while True: new_data = client_socket.recv(1024) if len(new_data) > 0: data += new_data else: break self._requests_from_socket.put(data) # respond OK client_socket.sendall(SERVER_SUCCESS.encode(encoding='utf-8')) client_socket.shutdown(socket.SHUT_WR) print("AFTER NEW REQUEST", client_socket) def _poll_socket_requests(self): """runs in gui thread""" try: while not self._requests_from_socket.empty(): data = self._requests_from_socket.get() args = ast.literal_eval(data.decode("UTF-8")) assert isinstance(args, list) for filename in args: if os.path.exists(filename): self._editor_notebook.show_file(filename) self.become_topmost_window() finally: self.after(50, self._poll_socket_requests) def _on_close(self): if not self._editor_notebook.check_allow_closing(): return try: self._save_layout() #ui_utils.delete_images() self.event_generate("WorkbenchClose") except: self.report_exception() self.destroy() def focus_get(self): try: return tk.Tk.focus_get(self) except: # This may give error in Ubuntu return None def destroy(self): try: self._destroying = True tk.Tk.destroy(self) except tk.TclError: logging.exception("Error while destroying workbench") finally: runner = get_runner() if runner != None: runner.kill_backend() def _on_configure(self, event): # called when window is moved or resized if (hasattr(self, "_maximized_view") # configure may happen before the attribute is defined and self._maximized_view): # grid again, otherwise it acts weird self._maximized_view.grid(row=0, column=0, sticky=tk.NSEW, in_=self._maximized_view.master) def _on_tk_exception(self, exc, val, tb): # copied from tkinter.Tk.report_callback_exception with modifications # see http://bugs.python.org/issue22384 sys.last_type = exc sys.last_value = val sys.last_traceback = tb self.report_exception() def report_exception(self, title="Internal error"): logging.exception(title) if tk._default_root and not self._destroying: (typ, value, _) = sys.exc_info() if issubclass(typ, UserError): msg = str(value) else: msg = traceback.format_exc() tk_messagebox.showerror(title, msg) def _open_views(self): for nb_name in self._view_notebooks: view_name = self.get_option("layout.notebook_" + nb_name + "_visible_view") if view_name != None: self.show_view(view_name) def _save_layout(self): self.update_idletasks() self.set_option("layout.zoomed", ui_utils.get_zoomed(self)) # each AutomaticPanedWindow remember it's splits for both 2 and 3 panes self.set_option("layout.main_pw_first_pane_size", self._main_pw.first_pane_size) self.set_option("layout.main_pw_last_pane_size", self._main_pw.last_pane_size) self.set_option("layout.east_pw_first_pane_size", self._east_pw.first_pane_size) self.set_option("layout.east_pw_last_pane_size", self._east_pw.last_pane_size) self.set_option("layout.center_pw_last_pane_size", self._center_pw.last_pane_size) self.set_option("layout.west_pw_first_pane_size", self._west_pw.first_pane_size) self.set_option("layout.west_pw_last_pane_size", self._west_pw.last_pane_size) for nb_name in self._view_notebooks: widget = self._view_notebooks[nb_name].get_visible_child() if hasattr(widget, "maximizable_widget"): view = widget.maximizable_widget view_name = type(view).__name__ self.set_option("layout.notebook_" + nb_name + "_visible_view", view_name) else: self.set_option("layout.notebook_" + nb_name + "_visible_view", None) if not ui_utils.get_zoomed(self): self.set_option("layout.top", self.winfo_y()) self.set_option("layout.left", self.winfo_x()) self.set_option("layout.width", self.winfo_width()) self.set_option("layout.height", self.winfo_height()) self._configuration_manager.save() #def focus_set(self): # tk.Tk.focus_set(self) # self._editor_notebook.focus_set() def update_title(self, event=None): editor = self.get_editor_notebook().get_current_editor() title_text = "Thonny" if editor != None: title_text += " - " + editor.get_long_description() self.title(title_text) def become_topmost_window(self): # Looks like at least on Windows all following is required for the window to get focus # (deiconify, ..., iconify, deiconify) self.deiconify() self.attributes('-topmost', True) self.after_idle(self.attributes, '-topmost', False) self.lift() if not running_on_linux(): # http://stackoverflow.com/a/13867710/261181 self.iconify() self.deiconify() editor = self.get_current_editor() if editor is not None: # This method is meant to be called when new file is opened, so it's safe to # send the focus to the editor editor.focus_set() else: self.focus_set() class WorkbenchEvent(Record): def __init__(self, sequence, **kwargs): Record.__init__(self, **kwargs) self.sequence = sequence thonny-2.1.16/thonny/__init__.py0000666000000000000000000001360513201264465014712 0ustar 00000000000000import os.path import sys import runpy try: runpy.run_module("thonny.customize", run_name="__main__") except ImportError: pass THONNY_USER_DIR = os.environ.get("THONNY_USER_DIR", os.path.expanduser(os.path.join("~", ".thonny"))) THONNY_USER_BASE = os.path.join(THONNY_USER_DIR, "plugins") def launch(): _prepare_thonny_user_dir() try: _update_sys_path() from thonny import workbench if _should_delegate(): # First check if there is existing Thonny instance to handle the request delegation_result = _try_delegate_to_existing_instance(sys.argv[1:]) if delegation_result == True: # we're done print("Delegated to an existing Thonny instance. Exiting now.") return if hasattr(delegation_result, "accept"): # we have server socket to put in use server_socket = delegation_result else: server_socket = None bench = workbench.Workbench(server_socket) else: bench = workbench.Workbench() try: bench.mainloop() except SystemExit: bench.destroy() return 0 except SystemExit as e: from tkinter import messagebox messagebox.showerror("System exit", str(e)) except: from logging import exception exception("Internal error") import tkinter.messagebox import traceback tkinter.messagebox.showerror("Internal error", traceback.format_exc()) return -1 finally: from thonny.globals import get_runner runner = get_runner() if runner != None: runner.kill_backend() def _prepare_thonny_user_dir(): if not os.path.exists(THONNY_USER_DIR): os.makedirs(THONNY_USER_DIR, mode=0o700, exist_ok=True) # user_dir_template is a post-installation means for providing # alternative default user environment in multi-user setups template_dir = os.path.join(os.path.dirname(__file__), "user_dir_template") if os.path.isdir(template_dir): import shutil def copy_contents(src_dir, dest_dir): # I want the copy to have current user permissions for name in os.listdir(src_dir): src_item = os.path.join(src_dir, name) dest_item = os.path.join(dest_dir, name) if os.path.isdir(src_item): os.makedirs(dest_item, mode=0o700) copy_contents(src_item, dest_item) else: shutil.copyfile(src_item, dest_item) os.chmod(dest_item, 0o600) copy_contents(template_dir, THONNY_USER_DIR) def _update_sys_path(): import site # remove old dir from path if site.getusersitepackages() in sys.path: sys.path.remove(site.getusersitepackages()) # compute usersitepackages that plugins installation subprocess would see import subprocess env = os.environ.copy() env["PYTHONUSERBASE"] = THONNY_USER_BASE proc = subprocess.Popen( [sys.executable.replace("thonny.exe", "pythonw.exe"), "-c", "import site; print(site.getusersitepackages())"], universal_newlines=True, env=env, stdout=subprocess.PIPE) plugins_sitepackages = proc.stdout.readline().strip() sys.path.append(plugins_sitepackages) def _should_delegate(): from thonny import workbench from thonny.config import try_load_configuration configuration_manager = try_load_configuration(workbench.CONFIGURATION_FILE_NAME) # Setting the default configuration_manager.set_default("general.single_instance", workbench.SINGLE_INSTANCE_DEFAULT) # getting the value (may use the default or return saved value) return configuration_manager.get_option("general.single_instance") def _try_delegate_to_existing_instance(args): import socket from thonny import workbench try: # Try to create server socket. # This is fastest way to find out if Thonny is already running serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(("localhost", workbench.THONNY_PORT)) serversocket.listen(10) # we were able to create server socket (ie. Thonny was not running) # Let's use the socket in Thonny so that requests coming while # UI gets constructed don't get lost. # (Opening several files with Thonny in Windows results in many # Thonny processes opened quickly) return serversocket except OSError: # port was already taken, most likely by previous Thonny instance. # Try to connect and send arguments try: return _delegate_to_existing_instance(args) except: import traceback traceback.print_exc() return False def _delegate_to_existing_instance(args): import socket from thonny import workbench data = repr(args).encode(encoding='utf_8') sock = socket.create_connection(("localhost", workbench.THONNY_PORT)) sock.sendall(data) sock.shutdown(socket.SHUT_WR) response = bytes([]) while len(response) < len(workbench.SERVER_SUCCESS): new_data = sock.recv(2) if len(new_data) == 0: break else: response += new_data return response.decode("UTF-8") == workbench.SERVER_SUCCESS def get_version(): try: package_dir = os.path.dirname(sys.modules["thonny"].__file__) with open(os.path.join(package_dir, "VERSION"), encoding="ASCII") as fp: return fp.read().strip() except: return "0.0.0" thonny-2.1.16/thonny/__main__.py0000666000000000000000000000004313172664305014667 0ustar 00000000000000from thonny import launch launch()thonny-2.1.16/thonny.egg-info/0000777000000000000000000000000013201324660014260 5ustar 00000000000000thonny-2.1.16/thonny.egg-info/dependency_links.txt0000666000000000000000000000000113201324657020334 0ustar 00000000000000 thonny-2.1.16/thonny.egg-info/entry_points.txt0000666000000000000000000000004613201324657017564 0ustar 00000000000000[gui_scripts] thonny = thonny:launch thonny-2.1.16/thonny.egg-info/PKG-INFO0000666000000000000000000000301213201324657015357 0ustar 00000000000000Metadata-Version: 1.2 Name: thonny Version: 2.1.16 Summary: Python IDE for beginners Home-page: http://thonny.org Author: Aivar Annamaa and others Author-email: thonny@googlegroups.com License: MIT Description: Thonny is a simple Python IDE with features useful for learning programming. See http://thonny.org for more info. Keywords: IDE education debugger Platform: Windows Platform: macOS Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: MacOS X Classifier: Environment :: Win32 (MS Windows) Classifier: Environment :: X11 Applications Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Education Classifier: Intended Audience :: End Users/Desktop Classifier: License :: Freeware Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Education Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Debuggers Classifier: Topic :: Text Editors Requires-Python: >=3.4 thonny-2.1.16/thonny.egg-info/requires.txt0000666000000000000000000000001213201324657016657 0ustar 00000000000000jedi>=0.9 thonny-2.1.16/thonny.egg-info/SOURCES.txt0000666000000000000000000000664413201324657016164 0ustar 00000000000000CHANGELOG.rst CREDITS.rst LICENSE.txt MANIFEST.in README.rst requirements.txt setup.py licenses/ECLIPSE-ICONS-LICENSE.txt packaging/icons/thonny-128x128.png packaging/icons/thonny-16x16.png packaging/icons/thonny-2.png packaging/icons/thonny-22x22.png packaging/icons/thonny-256x256.png packaging/icons/thonny-32x32.png packaging/icons/thonny-48x48.png packaging/icons/thonny-64x64.png packaging/linux/org.thonny.Thonny.appdata.xml packaging/linux/org.thonny.Thonny.desktop packaging/linux/thonny.1 thonny/VERSION thonny/__init__.py thonny/__main__.py thonny/ast_utils.py thonny/base_file_browser.py thonny/code.py thonny/codeview.py thonny/common.py thonny/config.py thonny/config_ui.py thonny/globals.py thonny/jedi_utils.py thonny/memory.py thonny/misc_utils.py thonny/roughparse.py thonny/running.py thonny/shell.py thonny/tktextext.py thonny/token_utils.py thonny/ui_utils.py thonny/workbench.py thonny.egg-info/PKG-INFO thonny.egg-info/SOURCES.txt thonny.egg-info/dependency_links.txt thonny.egg-info/entry_points.txt thonny.egg-info/requires.txt thonny.egg-info/top_level.txt thonny/plugins/__init__.py thonny/plugins/about.py thonny/plugins/ast_view.py thonny/plugins/autocomplete.py thonny/plugins/coloring.py thonny/plugins/commenting.py thonny/plugins/common_editing_commands.py thonny/plugins/debugger.py thonny/plugins/editor_config_page.py thonny/plugins/event_logging.py thonny/plugins/event_view.py thonny/plugins/find_replace.py thonny/plugins/font_config_page.py thonny/plugins/general_config_page.py thonny/plugins/goto_definition.py thonny/plugins/heap.py thonny/plugins/highlight_names.py thonny/plugins/interpreter_config_page.py thonny/plugins/locals_marker.py thonny/plugins/main_file_browser.py thonny/plugins/object_inspector.py thonny/plugins/outline.py thonny/plugins/paren_matcher.py thonny/plugins/pip_gui.py thonny/plugins/refactor.py thonny/plugins/replayer.py thonny/plugins/styler.py thonny/plugins/thonny_folders.py thonny/plugins/variables.py thonny/plugins/help/__init__.py thonny/plugins/help/help.rst thonny/plugins/system_shell/__init__.py thonny/plugins/system_shell/explain_environment.py thonny/res/16x16_blank.gif thonny/res/1x1_white.gif thonny/res/arrow_down2.gif thonny/res/class.gif thonny/res/closed_folder.gif thonny/res/file.new_file.gif thonny/res/file.open_file.gif thonny/res/file.save_file.gif thonny/res/folder.gif thonny/res/generic_file.gif thonny/res/gray_line.gif thonny/res/hard_drive.gif thonny/res/hard_drive2.gif thonny/res/method.gif thonny/res/open_folder.gif thonny/res/python_file.gif thonny/res/python_icon.gif thonny/res/run.debug_current_script.gif thonny/res/run.reset.gif thonny/res/run.run_current_script.gif thonny/res/run.run_to_cursor.gif thonny/res/run.step.gif thonny/res/run.step_into.gif thonny/res/run.step_out.gif thonny/res/run.step_over.gif thonny/res/run.stop.gif thonny/res/tab_close.gif thonny/res/tab_close_active.gif thonny/res/text_file.gif thonny/res/thonny.ico thonny/res/thonny.png thonny/res/thonny_small.ico thonny/shared/__init__.py thonny/shared/backend_launcher.py thonny/shared/thonny/__init__.py thonny/shared/thonny/ast_utils.py thonny/shared/thonny/backend.py thonny/shared/thonny/common.py thonny/test/__init__.py thonny/test/test_ast_utils.py thonny/test/test_ast_utils_mark_text_ranges.py thonny/test/plugins/__init__.py thonny/test/plugins/test_coloring.py thonny/test/plugins/test_locals_marker.py thonny/test/plugins/test_name_highlighter.py thonny/test/plugins/test_paren_matcher.pythonny-2.1.16/thonny.egg-info/top_level.txt0000666000000000000000000000000713201324657017015 0ustar 00000000000000thonny