diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cb8a72 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*$py.class diff --git a/MagicDrawJythonTerminal.py b/MagicDrawJythonTerminal.py index 81c14f5..80bdfdf 100644 --- a/MagicDrawJythonTerminal.py +++ b/MagicDrawJythonTerminal.py @@ -79,8 +79,8 @@ class MagicDrawJythonTerminal(JFrame): terminalOut = TerminalOutputStream(terminalArea) printStream = PrintStream(terminalOut, True) # use these if you want writer based output instead of the outputstream -# terminalOut = TerminalWriter(terminalArea) -# printWriter = PrintWriter(terminalOut, True) + terminalOut = TerminalWriter(terminalArea) + printWriter = PrintWriter(terminalOut, True) def __init__(self): JFrame.__init__(self, 'MagicDraw Jython Console') @@ -113,17 +113,9 @@ def CommandEntered(self, event): s = 'in : ' + event.source.text self.printStream.println(s) - # try the embedded interp (the getLocals was just to see if I could interact with it) - # it's not returning anything - a = self.interpreter.getLocals() - self.printWriter.println(a) - self.interpreter.exec(event.source.txt) - # try the main interp (the getLocals was just to see if I could interact with it) # it's not returning anything - a = getLocals() - self.printWriter.println(a) - exec(event.source.txt) + # eval(event.source.txt) # set the input text blank and give it focus back. we don't get here if we try to use # the interpreter, though we do when we don't try to interact with the interpreter @@ -156,4 +148,4 @@ def CreateComponents(self): # Main script #PythonInterpreter.initialize(System.getProperties(), None, None) #project = Application.getInstance().getProject() -t = MagicDrawJythonTerminal() \ No newline at end of file +t = MagicDrawJythonTerminal() diff --git a/jythonconsole/COPYING.txt b/jythonconsole/COPYING.txt new file mode 100644 index 0000000..b1e3f5a --- /dev/null +++ b/jythonconsole/COPYING.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/jythonconsole/README.txt b/jythonconsole/README.txt new file mode 100644 index 0000000..8ff26d1 --- /dev/null +++ b/jythonconsole/README.txt @@ -0,0 +1,21 @@ +Jython Console - Jython Interactive Interpreter with Code Completion + +Visit http://code.google.com/p/jythonconsole for more information. + +Run the code: + * Open a terminal or cmd prompt + * cd jythonconsole-0.0.7 + * jython console.py + +Hints: + * and choose method completion + * remember to use the keyboard not the mouse + * makes the popup go away + +Author: + Don Coleman + +License: + Read the COPYING.txt file. + + diff --git a/jythonconsole/__init__.py b/jythonconsole/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jythonconsole/bug_test_case.py b/jythonconsole/bug_test_case.py new file mode 100644 index 0000000..ba038e6 --- /dev/null +++ b/jythonconsole/bug_test_case.py @@ -0,0 +1,49 @@ +import unittest +import introspect +import jintrospect + +class BugTestCase(unittest.TestCase): + + def testPythonStackOverflow(self): + """ + jythonconsole-0.0.1 has a stack overflow when autocomplete with Jython-2.2b1 + + import sys + sys. <-- autocomplete starts and then the following stacktrace + + Traceback (most recent call last): + File "bug_test_case.py", line 8, in testJython22b1Bug + list = jintrospect.getAutoCompleteList("sys.", locals()) + File "/Users/don/jythonconsole/jintrospect.py", line 90, in getAutoCompleteList + attributes = getAttributeNames(object, includeMagic, includeSingle, includeDouble) + File "/Users/don/jythonconsole/introspect.py", line 61, in getAttributeNames + attrdict = getAllAttributeNames(object) + File "/Users/don/jythonconsole/introspect.py", line 125, in getAllAttributeNames + attrdict.update(getAllAttributeNames(klass)) + ... + File "/Users/don/jythonconsole/introspect.py", line 138, in getAllAttributeNames + attrdict.update(getAllAttributeNames(base)) + File "/Users/don/jythonconsole/introspect.py", line 125, in getAllAttributeNames + attrdict.update(getAllAttributeNames(klass)) + File "/Users/don/jythonconsole/introspect.py", line 138, in getAllAttributeNames + attrdict.update(getAllAttributeNames(base)) + File "/Users/don/jythonconsole/introspect.py", line 125, in getAllAttributeNames + attrdict.update(getAllAttributeNames(klass)) + File "/Users/don/jythonconsole/introspect.py", line 101, in getAllAttributeNames + wakeupcall = dir(object) + java.lang.StackOverflowError: java.lang.StackOverflowError + """ + dict = introspect.getAllAttributeNames("sys") + # if the bug is happening you'll never get here + # you'll get a stack overflow instead + self.assert_(len(dict) > 0) + + # method completion for python strings was failing in 0.0.2 with python2.b1 + def testAutoCompleteString(self): + f = "foo" + list = jintrospect.getAutoCompleteList("f", locals()) + self.assert_(len(list) > 0) + self.assert_(list.index("startswith") > 0) + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/console.py b/jythonconsole/console.py new file mode 100644 index 0000000..3c6e68c --- /dev/null +++ b/jythonconsole/console.py @@ -0,0 +1,439 @@ +""" +Jython Console with Code Completion + +This uses the basic Jython Interactive Interpreter. +The UI uses code from Carlos Quiroz's 'Jython Interpreter for JEdit' http://www.jedit.org +""" + +from javax.swing import JFrame, JScrollPane, JTextPane, Action, KeyStroke, WindowConstants +from javax.swing.text import JTextComponent, TextAction, SimpleAttributeSet, StyleConstants +from java.awt import Color, Font, Point +from java.awt.event import InputEvent, KeyEvent, WindowAdapter +from java.lang import System +from java.awt import Toolkit +from java.awt.datatransfer import DataFlavor + +import jintrospect +from popup import Popup +from tip import Tip +from history import History + +import sys +from code import InteractiveInterpreter +from org.python.util import InteractiveConsole + +__author__ = "Don Coleman " + +import re +# allows multiple imports like "from java.lang import String, Properties" +_re_from_import = re.compile("from\s+\S+\s+import(\s+\S+,\s?)?") + +try: + True, False +except NameError: + (True, False) = (1, 0) + +class Console: + PROMPT = sys.ps1 + PROCESS = sys.ps2 + BANNER = ["Jython Completion Shell", InteractiveConsole.getDefaultBanner()] + + include_single_underscore_methods = False + include_double_underscore_methods = False + + def __init__(self, namespace=None): + """ + Create a Jython Console. + namespace is an optional and should be a dictionary or Map + """ + self.history = History(self) + + if namespace != None: + self.locals = namespace + else: + self.locals = {} + + self.buffer = [] # buffer for multi-line commands + + self.interp = Interpreter(self, self.locals) + sys.stdout = StdOutRedirector(self) + + self.text_pane = JTextPane(keyTyped = self.keyTyped, keyPressed = self.keyPressed) + self.__initKeyMap() + + self.doc = self.text_pane.document + self.__propertiesChanged() + self.__inittext() + self.initialLocation = self.doc.createPosition(self.doc.length-1) + + # Don't pass frame to popups. JWindows with null owners are not focusable + # this fixes the focus problem on Win32, but make the mouse problem worse + self.popup = Popup(None, self.text_pane) + self.tip = Tip(None) + + # get fontmetrics info so we can position the popup + metrics = self.text_pane.getFontMetrics(self.text_pane.getFont()) + self.dotWidth = metrics.charWidth('.') + self.textHeight = metrics.getHeight() + + # add some handles to our objects + self.locals['console'] = self + + def insertText(self, text): + """insert text at the current caret position""" + # seems like there should be a better way to do this.... + # might be better as a method on the text component? + caretPosition = self.text_pane.getCaretPosition() + self.text_pane.select(caretPosition, caretPosition) + self.text_pane.replaceSelection(text) + self.text_pane.setCaretPosition(caretPosition + len(text)) + + def getText(self): + """get text from last line of console""" + offsets = self.__lastLine() + text = self.doc.getText(offsets[0], offsets[1]-offsets[0]) + return text.rstrip() + + def getDisplayPoint(self): + """Get the point where the popup window should be displayed""" + screenPoint = self.text_pane.getLocationOnScreen() + caretPoint = self.text_pane.caret.getMagicCaretPosition() + + # BUG: sometimes caretPoint is None + # To duplicate type "java.aw" and hit '.' to complete selection while popup is visible + + x = screenPoint.getX() + caretPoint.getX() + self.dotWidth + y = screenPoint.getY() + caretPoint.getY() + self.textHeight + return Point(int(x),int(y)) + + def hide(self, event=None): + """Hide the popup or tip window if visible""" + if self.popup.visible: + self.popup.hide() + if self.tip.visible: + self.tip.hide() + + def hideTip(self, event=None): + self.tip.hide() + self.insertText(')') + + def showTip(self, event=None): + # get the display point before writing text + # otherwise magicCaretPosition is None + displayPoint = self.getDisplayPoint() + + if self.popup.visible: + self.popup.hide() + + line = self.getText() + + self.insertText('(') + + (name, argspec, tip) = jintrospect.getCallTipJava(line, self.locals) + + if tip: + self.tip.showTip(tip, displayPoint) + + def showPopup(self, event=None): + """show code completion popup""" + + try: + line = self.getText() + list = jintrospect.getAutoCompleteList(line, self.locals, includeSingle=self.include_single_underscore_methods, includeDouble=self.include_double_underscore_methods) + if len(list) > 0: + self.popup.showMethodCompletionList(list, self.getDisplayPoint()) + + except Exception, e: + print >> sys.stderr, "Error getting completion list: ", e + #traceback.print_exc(file=sys.stderr) + + def inLastLine(self, include = 1): + """ Determines whether the cursor is in the last line """ + limits = self.__lastLine() + caret = self.text_pane.caretPosition + if self.text_pane.selectedText: + caret = self.text_pane.selectionStart + if include: + return (caret >= limits[0] and caret <= limits[1]) + else: + return (caret > limits[0] and caret <= limits[1]) + + def enter(self, event=None): + """ Triggered when enter is pressed """ + text = self.getText() + self.buffer.append(text) + source = "\n".join(self.buffer) + more = self.interp.runsource(source) + if more: + self.printOnProcess() + else: + self.resetbuffer() + self.printPrompt() + self.history.append(text) + + self.hide() + + def quit(self, event=None): + sys.exit() + + def resetbuffer(self): + self.buffer = [] + + def home(self, event): + """ Triggered when HOME is pressed """ + if self.inLastLine(): + # go to end of PROMPT + self.text_pane.caretPosition = self.__lastLine()[0] + else: + lines = self.doc.rootElements[0].elementCount + for i in xrange(0,lines-1): + offsets = (self.doc.rootElements[0].getElement(i).startOffset, \ + self.doc.rootElements[0].getElement(i).endOffset) + line = self.doc.getText(offsets[0], offsets[1]-offsets[0]) + if self.text_pane.caretPosition >= offsets[0] and \ + self.text_pane.caretPosition <= offsets[1]: + if line.startswith(Console.PROMPT) or line.startswith(Console.PROCESS): + self.text_pane.caretPosition = offsets[0] + len(Console.PROMPT) + else: + self.text_pane.caretPosition = offsets[0] + + def end(self, event): + if self.inLastLine(): + self.text_pane.caretPosition = self.__lastLine()[1] - 1 + + # TODO look using text_pane replace selection like self.insertText + def replaceRow(self, text): + """ Replaces the last line of the textarea with text """ + offset = self.__lastLine() + last = self.doc.getText(offset[0], offset[1]-offset[0]) + if last != "\n": + self.doc.remove(offset[0], offset[1]-offset[0]-1) + self.__addOutput(self.infoColor, text) + + def delete(self, event): + """ Intercepts delete events only allowing it to work in the last line """ + if self.inLastLine(): + if self.text_pane.selectedText: + self.doc.remove(self.text_pane.selectionStart, self.text_pane.selectionEnd - self.text_pane.selectionStart) + elif self.text_pane.caretPosition < self.doc.length: + self.doc.remove(self.text_pane.caretPosition, 1) + + def backSpaceListener(self, event=None): + """ Don't allow backspace or left arrow to go over prompt """ + onFirstPosition = self.text_pane.getCaretPosition() <= self.__lastLine()[0] + if onFirstPosition and not self.text_pane.selectedText: + event.consume() + + def spaceTyped(self, event=None): + """check we we should complete on the space key""" + matches = _re_from_import.match(self.getText()) + if matches: + self.showPopup() + + def killToEndLine(self, event=None): + if self.inLastLine(): + caretPosition = self.text_pane.getCaretPosition() + self.text_pane.setSelectionStart(caretPosition) + self.text_pane.setSelectionEnd(self.__lastLine()[1] - 1) + self.text_pane.cut() + + def paste(self, event=None): + # if getText was smarter, this method would be unnecessary + if self.inLastLine(): + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() + clipboard.getContents(self.text_pane) + contents = clipboard.getData(DataFlavor.stringFlavor) + + lines = contents.split("\n") + for line in lines: + self.insertText(line) + if len(lines) > 1: + self.enter() + + def keyTyped(self, event): + #print >> sys.stderr, "keyTyped", event.getKeyCode() + if not self.inLastLine(): + event.consume() + + def keyPressed(self, event): + if self.popup.visible: + self.popup.key(event) + #print >> sys.stderr, "keyPressed", event.getKeyCode() + if event.keyCode == KeyEvent.VK_BACK_SPACE or event.keyCode == KeyEvent.VK_LEFT: + self.backSpaceListener(event) + + def keyboardInterrupt(self, event=None): + """ Raises a KeyboardInterrupt""" + self.hide() + self.interp.runsource("raise KeyboardInterrupt\n") + self.resetbuffer() + self.printPrompt() + + # TODO refactor me + def write(self, text): + self.__addOutput(self.infoColor, text) + + def printResult(self, msg): + """ Prints the results of an operation """ + self.__addOutput(self.text_pane.foreground, "\n" + str(msg)) + + def printError(self, msg): + self.__addOutput(self.errorColor, "\n" + str(msg)) + + def printOnProcess(self): + """ Prints the process symbol """ + self.__addOutput(self.infoColor, "\n" + Console.PROCESS) + + def printPrompt(self): + """ Prints the prompt """ + self.__addOutput(self.infoColor, "\n" + Console.PROMPT) + + def __addOutput(self, color, msg): + """ Adds the output to the text area using a given color """ + from javax.swing.text import BadLocationException + style = SimpleAttributeSet() + + if color: + style.addAttribute(StyleConstants.Foreground, color) + + self.doc.insertString(self.doc.length, msg, style) + self.text_pane.caretPosition = self.doc.length + + def __propertiesChanged(self): + """ Detects when the properties have changed """ + self.text_pane.background = Color.white #jEdit.getColorProperty("jython.bgColor") + self.text_pane.foreground = Color.blue #jEdit.getColorProperty("jython.resultColor") + self.infoColor = Color.black #jEdit.getColorProperty("jython.textColor") + self.errorColor = Color.red # jEdit.getColorProperty("jython.errorColor") + + family = "Monospaced" # jEdit.getProperty("jython.font", "Monospaced") + size = 14 #jEdit.getIntegerProperty("jython.fontsize", 14) + style = Font.PLAIN #jEdit.getIntegerProperty("jython.fontstyle", Font.PLAIN) + self.text_pane.setFont(Font(family,style,size)) + + def __inittext(self): + """ Inserts the initial text with the jython banner """ + self.doc.remove(0, self.doc.length) + for line in "\n".join(Console.BANNER): + self.__addOutput(self.infoColor, line) + self.printPrompt() + self.text_pane.requestFocus() + + def __initKeyMap(self): + os_name = System.getProperty("os.name") + if os_name.startswith("Win"): + exit_key = KeyEvent.VK_Z + interrupt_key = KeyEvent.VK_PAUSE # BREAK + else: + exit_key = KeyEvent.VK_D + interrupt_key = KeyEvent.VK_C + + keyBindings = [ + (KeyEvent.VK_ENTER, 0, "jython.enter", self.enter), + (KeyEvent.VK_DELETE, 0, "jython.delete", self.delete), + (KeyEvent.VK_HOME, 0, "jython.home", self.home), + (KeyEvent.VK_LEFT, InputEvent.META_DOWN_MASK, "jython.home", self.home), + (KeyEvent.VK_UP, 0, "jython.up", self.history.historyUp), + (KeyEvent.VK_DOWN, 0, "jython.down", self.history.historyDown), + (KeyEvent.VK_PERIOD, 0, "jython.showPopup", self.showPopup), + (KeyEvent.VK_ESCAPE, 0, "jython.hide", self.hide), + + ('(', 0, "jython.showTip", self.showTip), + (')', 0, "jython.hideTip", self.hideTip), + (exit_key, InputEvent.CTRL_MASK, "jython.exit", self.quit), + (KeyEvent.VK_SPACE, InputEvent.CTRL_MASK, "jython.showPopup", self.showPopup), + (KeyEvent.VK_SPACE, 0, "jython.space", self.spaceTyped), + + # explicitly set paste since we're overriding functionality + (KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(), "jython.paste", self.paste), + + # Mac/Emacs keystrokes + (KeyEvent.VK_A, InputEvent.CTRL_MASK, "jython.home", self.home), + (KeyEvent.VK_E, InputEvent.CTRL_MASK, "jython.end", self.end), + (KeyEvent.VK_K, InputEvent.CTRL_MASK, "jython.killToEndLine", self.killToEndLine), + (KeyEvent.VK_Y, InputEvent.CTRL_MASK, "jython.paste", self.paste), + + (interrupt_key, InputEvent.CTRL_MASK, "jython.keyboardInterrupt", self.keyboardInterrupt), + ] + + keymap = JTextComponent.addKeymap("jython", self.text_pane.keymap) + for (key, modifier, name, function) in keyBindings: + keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(key, modifier), ActionDelegator(name, function)) + self.text_pane.keymap = keymap + + def __lastLine(self): + """ Returns the char offests of the last line """ + lines = self.doc.rootElements[0].elementCount + offsets = (self.doc.rootElements[0].getElement(lines-1).startOffset, \ + self.doc.rootElements[0].getElement(lines-1).endOffset) + line = self.doc.getText(offsets[0], offsets[1]-offsets[0]) + if len(line) >= 4 and (line[0:4]==Console.PROMPT or line[0:4]==Console.PROCESS): + return (offsets[0] + len(Console.PROMPT), offsets[1]) + return offsets + + +class ActionDelegator(TextAction): + """ + Class action delegator encapsulates a TextAction delegating the action + event to a simple function + """ + def __init__(self, name, delegate): + TextAction.__init__(self, name) + self.delegate = delegate + + def actionPerformed(self, event): + if isinstance(self.delegate, Action): + self.delegate.actionPerformed(event) + else: + self.delegate(event) + +class Interpreter(InteractiveInterpreter): + def __init__(self, console, locals): + InteractiveInterpreter.__init__(self, locals) + self.console = console + + def write(self, data): + # send all output to the textpane + # KLUDGE remove trailing linefeed + self.console.printError(data[:-1]) + +# redirect stdout to the textpane +class StdOutRedirector: + def __init__(self, console): + self.console = console + + def write(self, data): + #print >> sys.stderr, ">>%s<<" % data + if data != '\n': + # This is a sucky hack. Fix printResult + self.console.printResult(data) + +class JythonFrame(JFrame): + def __init__(self): + self.title = "Jython" + self.size = (600, 400) + try: + self.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) + except: + # assume jdk < 1.4 + self.addWindowListener(KillListener()) + self.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) + +class KillListener(WindowAdapter): + """ + Handle EXIT_ON_CLOSE for jdk < 1.4 + Thanks to James Richards for this method + """ + def windowClosed(self, evt): + import java.lang.System as System + System.exit(0) + +def main(namespace=None): + frame = JythonFrame() + console = Console(namespace) + frame.getContentPane().add(JScrollPane(console.text_pane)) + frame.visible = True + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/jythonconsole/history.py b/jythonconsole/history.py new file mode 100644 index 0000000..dcb7af0 --- /dev/null +++ b/jythonconsole/history.py @@ -0,0 +1,82 @@ +""" + history.py - Handles the History of the jython console + Copyright (C) 2001 Carlos Quiroz + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +""" + +from java.lang import System, Runtime +from java.lang import Runnable, Thread + +class History(Runnable): + """ + Command line history + """ + + default_history_file = System.getProperty("user.home") + '/.jythonconsole.history' + MAX_SIZE = 200 + + def __init__(self, console, history_file=default_history_file): + Runtime.getRuntime().addShutdownHook(Thread(self)) + + self.history_file = history_file + self.history = [] + self.loadHistory() + + self.console = console + self.index = len(self.history) - 1 + self.last = "" + + def append(self, line): + if line == None or line == '\n' or len(line) == 0: + return + + if line != self.last: # avoids duplicates + self.last = line + self.history.append(line) + + self.index = len(self.history) - 1 + + def historyUp(self, event=None): + if len(self.history) > 0 and self.console.inLastLine(): + self.console.replaceRow(self.history[self.index]) + self.index = max(self.index - 1, 0) + + def historyDown(self, event=None): + if len(self.history) > 0 and self.console.inLastLine(): + if self.index == len(self.history) - 1: + self.console.replaceRow("") + else: + self.index += 1 + self.console.replaceRow(self.history[self.index]) + + def loadHistory(self): + try: + f = open(self.history_file) + for line in f.readlines(): + self.history.append(line[:-1]) + f.close() + except: + pass + + def saveHistory(self): + f = open(self.history_file, 'w') + for item in self.history[-self.MAX_SIZE:]: + f.write("%s\n" % item) + f.flush() + f.close() + + def run(self): + self.saveHistory() diff --git a/jythonconsole/history_test_case.py b/jythonconsole/history_test_case.py new file mode 100644 index 0000000..9c3f9fa --- /dev/null +++ b/jythonconsole/history_test_case.py @@ -0,0 +1,133 @@ +import unittest +import tempfile +from history import History + +try: + True, False +except NameError: + (True, False) = (1, 0) + +class MockConsole: + def replaceRow(self, text): + self.text = text + + def inLastLine(self): + return True + +class HistoryTestCase(unittest.TestCase): + + def setUp(self): + self.console = MockConsole() + + def tearDown(self): + self.console = None + + def testHistoryUp(self): + h = History(self.console, tempfile.mktemp()) + h.append("one") + h.append("two") + h.append("three") + + h.historyUp() + self.assertEquals("three", self.console.text) + + h.historyUp() + self.assertEquals("two", self.console.text) + + h.historyUp() + self.assertEquals("one", self.console.text) + + # history doesn't roll, just stops at the last item + h.historyUp() + self.assertEquals("one", self.console.text) + + def testHistoryDown(self): + h = History(self.console,tempfile.mktemp()) + h.append("one") + h.append("two") + h.append("three") + + h.historyUp() + h.historyUp() + + h.historyUp() + self.assertEquals("one", self.console.text) + + h.historyDown() + self.assertEquals("two", self.console.text) + + h.historyDown() + self.assertEquals("three", self.console.text) + + h.historyDown() + self.assertEquals("", self.console.text) + + # History doesn't wrap + h.historyDown() + self.assertEquals("", self.console.text) + + def testSkipDuplicates(self): + h = History(self.console, tempfile.mktemp()) + h.append("one") + h.append("one") + h.append("two") + h.append("two") + h.append("three") + h.append("three") + + h.historyUp() + self.assertEquals("three", self.console.text) + + h.historyUp() + self.assertEquals("two", self.console.text) + + h.historyUp() + self.assertEquals("one", self.console.text) + + def testSkipEmpty(self): + h = History(self.console, tempfile.mktemp()) + size = len(h.history) + h.append("") + self.assert_(len(h.history) == size) + + h.append("\n") + self.assert_(len(h.history) == size) + + h.append(None) + self.assert_(len(h.history) == size) + + def testLoadHistory(self): + file = tempfile.mktemp() + f = open(file, "w") + f.write("red\n") + f.write("yellow\n") + f.write("blue\n") + f.flush() + f.close() + + h = History(self.console, file) + h.historyUp() + self.assertEquals("blue", self.console.text) + + h.historyUp() + self.assertEquals("yellow", self.console.text) + + h.historyUp() + self.assertEquals("red", self.console.text) + + def testSaveHistory(self): + file = tempfile.mktemp() + + h = History(self.console, file) + h.append("a") + h.append("b") + h.append("c") + h.saveHistory() + + f = open(file) + self.assertEquals("a", f.readline()[:-1]) + self.assertEquals("b", f.readline()[:-1]) + self.assertEquals("c", f.readline()[:-1]) + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/introspect.py b/jythonconsole/introspect.py new file mode 100644 index 0000000..297dd43 --- /dev/null +++ b/jythonconsole/introspect.py @@ -0,0 +1,373 @@ +"""Provides a variety of introspective-type support functions for +things like call tips and command auto completion. + +NOTE: this file is a modification of Patrick O'Brien's version 1.62 +""" + +from __future__ import nested_scopes + +__author__ = "Patrick K. O'Brien " +__cvsid__ = "$Id: introspect.py,v 1.5 2003/05/01 03:43:53 dcoleman Exp $" +__revision__ = "$Revision: 1.5 $"[11:-2] + + +import cStringIO +import inspect +import sys +import tokenize +import types + +try: + True +except NameError: + True = 1==1 + False = 1==0 + +def getAutoCompleteList(command='', locals=None, includeMagic=1, + includeSingle=1, includeDouble=1): + """Return list of auto-completion options for command. + + The list of options will be based on the locals namespace.""" + attributes = [] + # Get the proper chunk of code from the command. + root = getRoot(command, terminator='.') + try: + if locals is not None: + object = eval(root, locals) + else: + object = eval(root) + except: + pass + else: + attributes = getAttributeNames(object, includeMagic, + includeSingle, includeDouble) + return attributes + +def getAttributeNames(object, includeMagic=1, includeSingle=1, + includeDouble=1): + """Return list of unique attributes, including inherited, for object.""" + attributes = [] + dict = {} + if not hasattrAlwaysReturnsTrue(object): + # Add some attributes that don't always get picked up. If + # they don't apply, they'll get filtered out at the end. + attributes += ['__bases__', '__class__', '__dict__', '__name__', + 'func_closure', 'func_code', 'func_defaults', + 'func_dict', 'func_doc', 'func_globals', 'func_name'] + if includeMagic: + try: attributes += object._getAttributeNames() + except: pass + # Get all attribute names. + attrdict = getAllAttributeNames(object) + for attrlist in attrdict.values(): + attributes += attrlist + # Remove duplicates from the attribute list. + for item in attributes: + dict[item] = None + attributes = dict.keys() + attributes.sort(lambda x, y: cmp(x.upper(), y.upper())) + if not includeSingle: + attributes = filter(lambda item: item[0]!='_' \ + or item[1]=='_', attributes) + if not includeDouble: + attributes = filter(lambda item: item[:2]!='__', attributes) + # Make sure we haven't picked up any bogus attributes somehow. + attributes = [attribute for attribute in attributes \ + if hasattr(object, attribute)] + return attributes + +def hasattrAlwaysReturnsTrue(object): + return hasattr(object, 'bogu5_123_aTTri8ute') + +def getAllAttributeNames(object): + """Return dict of all attributes, including inherited, for an object. + + Recursively walk through a class and all base classes. + """ + attrdict = {} # (object, technique, count): [list of attributes] + # !!! + # Do Not use hasattr() as a test anywhere in this function, + # because it is unreliable with remote objects: xmlrpc, soap, etc. + # They always return true for hasattr(). + # !!! + try: + # Yes, this can fail if object is an instance of a class with + # __str__ (or __repr__) having a bug or raising an + # exception. :-( + key = str(object) + except: + key = 'anonymous' + # Wake up sleepy objects - a hack for ZODB objects in "ghost" state. + wakeupcall = dir(object) + del wakeupcall + # Get attributes available through the normal convention. + attributes = dir(object) + attrdict[(key, 'dir', len(attributes))] = attributes + # Get attributes from the object's dictionary, if it has one. + try: + attributes = object.__dict__.keys() + attributes.sort() + except: # Must catch all because object might have __getattr__. + pass + else: + attrdict[(key, '__dict__', len(attributes))] = attributes + # For a class instance, get the attributes for the class. + try: + klass = object.__class__ + except: # Must catch all because object might have __getattr__. + pass + else: + if klass is object: + # Break a circular reference. This happens with extension + # classes. + pass + else: + attrdict.update(getAllAttributeNames(klass)) + # Also get attributes from any and all parent classes. + try: + bases = object.__bases__ + except: # Must catch all because object might have __getattr__. + pass + else: + if isinstance(bases, types.TupleType): + for base in bases: + if type(base) is types.TypeType: + # Break a circular reference. Happens in Python 2.2. + pass + if type(base) is type(type): + # Break a circular reference. Happens in Jython 2.2b1. + pass + else: + attrdict.update(getAllAttributeNames(base)) + return attrdict + +def getCallTip(command='', locals=None): + """For a command, return a tuple of object name, argspec, tip text. + + The call tip information will be based on the locals namespace.""" + calltip = ('', '', '') # object name, argspec, tip text. + # Get the proper chunk of code from the command. + root = getRoot(command, terminator='(') + try: + if locals is not None: + object = eval(root, locals) + else: + object = eval(root) + except: + return calltip + name = '' + object, dropSelf = getBaseObject(object) + try: + name = object.__name__ + except AttributeError: + pass + tip1 = '' + argspec = '' + if inspect.isbuiltin(object): + # Builtin functions don't have an argspec that we can get. + pass + elif inspect.isfunction(object): + # tip1 is a string like: "getCallTip(command='', locals=None)" + argspec = apply(inspect.formatargspec, inspect.getargspec(object)) + if dropSelf: + # The first parameter to a method is a reference to an + # instance, usually coded as "self", and is usually passed + # automatically by Python; therefore we want to drop it. + temp = argspec.split(',') + if len(temp) == 1: # No other arguments. + argspec = '()' + else: # Drop the first argument. + argspec = '(' + ','.join(temp[1:]).lstrip() + tip1 = name + argspec + doc = '' + if callable(object): + doc = inspect.getdoc(object) + if doc: + # tip2 is the first separated line of the docstring, like: + # "Return call tip text for a command." + # tip3 is the rest of the docstring, like: + # "The call tip information will be based on ... + firstline = doc.split('\n')[0].lstrip() + if tip1 == firstline: + tip1 = '' + else: + tip1 += '\n\n' + docpieces = doc.split('\n\n') + tip2 = docpieces[0] + tip3 = '\n\n'.join(docpieces[1:]) + tip = '%s%s\n\n%s' % (tip1, tip2, tip3) + else: + tip = tip1 + calltip = (name, argspec[1:-1], tip.strip()) + return calltip + +def getRoot(command, terminator=None): + """Return the rightmost root portion of an arbitrary Python command. + + Return only the root portion that can be eval()'d without side + effects. The command would normally terminate with a '(' or + '.'. The terminator and anything after the terminator will be + dropped.""" + command = command.split('\n')[-1] + if command.startswith(sys.ps2): + command = command[len(sys.ps2):] + command = command.lstrip() + command = rtrimTerminus(command, terminator) + tokens = getTokens(command) + if not tokens: + return '' + if tokens[-1][0] is tokenize.ENDMARKER: + # Remove the end marker. + del tokens[-1] + if not tokens: + return '' + if terminator == '.' and \ + (tokens[-1][1] <> '.' or tokens[-1][0] is not tokenize.OP): + # Trap decimals in numbers, versus the dot operator. + return '' + else: + # Strip off the terminator. + if terminator and command.endswith(terminator): + size = 0 - len(terminator) + command = command[:size] + command = command.rstrip() + tokens = getTokens(command) + tokens.reverse() + line = '' + start = None + prefix = '' + laststring = '.' + emptyTypes = ('[]', '()', '{}') + for token in tokens: + tokentype = token[0] + tokenstring = token[1] + line = token[4] + if tokentype is tokenize.ENDMARKER: + continue + if tokentype in (tokenize.NAME, tokenize.STRING, tokenize.NUMBER) \ + and laststring != '.': + # We've reached something that's not part of the root. + if prefix and line[token[3][1]] != ' ': + # If it doesn't have a space after it, remove the prefix. + prefix = '' + break + if tokentype in (tokenize.NAME, tokenize.STRING, tokenize.NUMBER) \ + or (tokentype is tokenize.OP and tokenstring == '.'): + if prefix: + # The prefix isn't valid because it comes after a dot. + prefix = '' + break + else: + # start represents the last known good point in the line. + start = token[2][1] + elif len(tokenstring) == 1 and tokenstring in ('[({])}'): + # Remember, we're working backwords. + # So prefix += tokenstring would be wrong. + if prefix in emptyTypes and tokenstring in ('[({'): + # We've already got an empty type identified so now we + # are in a nested situation and we can break out with + # what we've got. + break + else: + prefix = tokenstring + prefix + else: + # We've reached something that's not part of the root. + break + laststring = tokenstring + if start is None: + start = len(line) + root = line[start:] + if prefix in emptyTypes: + # Empty types are safe to be eval()'d and introspected. + root = prefix + root + return root + +def getTokens(command): + """Return list of token tuples for command.""" + command = str(command) # In case the command is unicode, which fails. + f = cStringIO.StringIO(command) + # tokens is a list of token tuples, each looking like: + # (type, string, (srow, scol), (erow, ecol), line) + tokens = [] + # Can't use list comprehension: + # tokens = [token for token in tokenize.generate_tokens(f.readline)] + # because of need to append as much as possible before TokenError. + try: +## This code wasn't backward compatible with Python 2.1.3. +## +## for token in tokenize.generate_tokens(f.readline): +## tokens.append(token) + + # This works with Python 2.1.3 (with nested_scopes). + def eater(*args): + tokens.append(args) + tokenize.tokenize_loop(f.readline, eater) + except tokenize.TokenError: + # This is due to a premature EOF, which we expect since we are + # feeding in fragments of Python code. + pass + return tokens + +def rtrimTerminus(command, terminator=None): + """Return command minus anything that follows the final terminator.""" + if terminator: + pieces = command.split(terminator) + if len(pieces) > 1: + command = terminator.join(pieces[:-1]) + terminator + return command + +def getBaseObject(object): + """Return base object and dropSelf indicator for an object.""" + if inspect.isbuiltin(object): + # Builtin functions don't have an argspec that we can get. + dropSelf = 0 + elif inspect.ismethod(object): + # Get the function from the object otherwise + # inspect.getargspec() complains that the object isn't a + # Python function. + try: + if object.im_self is None: + # This is an unbound method so we do not drop self + # from the argspec, since an instance must be passed + # as the first arg. + dropSelf = 0 + else: + dropSelf = 1 + object = object.im_func + except AttributeError: + dropSelf = 0 + elif inspect.isclass(object): + # Get the __init__ method function for the class. + constructor = getConstructor(object) + if constructor is not None: + object = constructor + dropSelf = 1 + else: + dropSelf = 0 + elif callable(object): + # Get the __call__ method instead. + try: + call_method = object.__call__.im_func + if call_method.__name__ == '__call__': + # unbound jython method end up here, example: string.index( + dropSelf = 0 + else: + object = call_method + dropSelf = 1 + except AttributeError: + # unbound python methods end up here + dropSelf = 0 + else: + dropSelf = 0 + return object, dropSelf + +def getConstructor(object): + """Return constructor for class object, or None if there isn't one.""" + try: + return object.__init__.im_func + except AttributeError: + for base in object.__bases__: + constructor = getConstructor(base) + if constructor is not None: + return constructor + return None diff --git a/jythonconsole/introspect_test_case.py b/jythonconsole/introspect_test_case.py new file mode 100644 index 0000000..42a1af3 --- /dev/null +++ b/jythonconsole/introspect_test_case.py @@ -0,0 +1,19 @@ +import unittest +import introspect + +class IntrospectTestCase(unittest.TestCase): + + def setUp(self): + pass + + def testGetRoot(self): + "figure out how getRoot behaves" + import string + root = introspect.getRoot("string.", ".") + self.assertEquals("string", root) + + root = introspect.getRoot("string.join", "(") + self.assertEquals("string.join", root) + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/issue_17_test_case.py b/jythonconsole/issue_17_test_case.py new file mode 100644 index 0000000..c047113 --- /dev/null +++ b/jythonconsole/issue_17_test_case.py @@ -0,0 +1,36 @@ +import unittest +import jintrospect + +class Issue17TestCase(unittest.TestCase): + """http://code.google.com/p/jythonconsole/issues/detail?id=17""" + + def setUp(self): + pass + + def testStaticCompletion(self): + from java.util import Calendar + list = jintrospect.getAutoCompleteList("Calendar", locals()) + self.assert_(len(list) > 0) + self.assert_(list.index("getInstance") > -1) + + def testStaticCallTip(self): + # Call Tip was failing because Calendar.getInstance + # returns PyReflectedMethod which was not being handled properly + from java.util import Calendar + command = "Calendar.getInstance"; + tip = jintrospect.getCallTipJava(command, locals()) + # tip should be something like + # getInstance(java.util.TimeZone, java.util.Locale) -> java.util.Calendar + # getInstance(java.util.TimeZone) -> java.util.Calendar + # getInstance(java.util.Locale) -> java.util.Calendar + # getInstance() -> java.util.Calendar' + self.assert_(tip[2] != '') + self.assert_(tip[2].index("getInstance") > -1) + self.assert_(tip[2].index("TimeZone") > -1) + + def testIsPython(self): + from java.util import Calendar + self.failIf(jintrospect.ispython(Calendar.getInstance), "Calendar.getInstance is not Python") + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/jintrospect.py b/jythonconsole/jintrospect.py new file mode 100644 index 0000000..aa76c28 --- /dev/null +++ b/jythonconsole/jintrospect.py @@ -0,0 +1,316 @@ +"""Extend introspect.py for Java based Jython classes.""" + +from org.python.core import PyReflectedFunction +from java.lang import Class, Object +from java.lang.reflect import Modifier +from introspect import * +from sets import Set +import string +import re +import types + +__author__ = "Don Coleman " + +_re_import_package = re.compile('import\s+(.+)\.') # import package +# TODO need to check for a trailing '.' example: "from java import lang." don't autocomplete on trailing '.' +_re_from_package_import = re.compile('from\s+(\w+(?:\.\w+)*)\.?(?:\s*import\s*)?') # from package import class + +def completePackageName(target): + """ Get a package object given the full name.""" + targetComponents = target.split('.') + base = targetComponents[0] + baseModule = __import__(base, globals(), locals()) + module = baseModule + + for component in targetComponents[1:]: + module = getattr(module, component) + + list = dir(module) + list.remove('__name__') + list.append('*') + return list + +def getPackageName(command): + + match = _re_import_package.match(command) + if not match: + #try the other re + match = _re_from_package_import.match(command) + + return match.groups()[0] + +def getAutoCompleteList(command='', locals=None, includeMagic=1, includeSingle=1, includeDouble=1): + """ + Return list of auto-completion options for command. + The list of options will be based on the locals namespace. + """ + + # Temp KLUDGE here rather than in console.py + command += "." + + attributes = [] + # Get the proper chunk of code from the command. + root = getRoot(command, terminator='.') + + # check to see if the user is attempting to import a package + # this may need to adjust this so that it doesn't pollute the namespace + if command.startswith('import ') or command.startswith('from '): + target = getPackageName(command) + return completePackageName(target) + + try: + if locals is not None: + object = eval(root, locals) + else: + object = eval(root) + except: + return attributes + + if ispython(object): # use existing code + attributes = getAttributeNames(object, includeMagic, includeSingle, includeDouble) + else: + if inspect.isclass(object): + attributes = staticMethodNames(object) + attributes.extend(staticFieldNames(object)) + else: + attributes = list(instanceMethodNames(object.__class__)) + + return attributes + +def instanceMethodNames(clazz): + """return a Set of instance method name for a Class""" + + method_names = Set() + declared_methods = Class.getDeclaredMethods(clazz) + for method in declared_methods: + modifiers = method.getModifiers() + if not Modifier.isStatic(modifiers) and Modifier.isPublic(modifiers): + name = method.name + method_names.add(name) + if name.startswith("get") and len(name) > 3 and len(method.getParameterTypes()) == 0: + property_name = name[3].lower() + name[4:] + method_names.add(property_name) + + for eachBase in clazz.__bases__: + if not ispython(eachBase): + method_names = method_names | instanceMethodNames(eachBase) + + return method_names + +def staticMethodNames(clazz): + """return a list of static method name for a class""" + + static_methods = {} + declared_methods = Class.getDeclaredMethods(clazz) + for method in declared_methods: + if Modifier.isStatic(method.getModifiers()) and Modifier.isPublic(method.getModifiers()): + static_methods[method.name] = method + methods = static_methods.keys() + + for eachBase in clazz.__bases__: + # with Jython 2.5 type is a base of Object, which puts asName in the list + # will be a problem for real Java objects that extend Python objects + # see similar "fixes" in instanceMethodNames and staticFieldNames + if not ispython(eachBase): + methods.extend(staticMethodNames(eachBase)) + + return methods + +def staticFieldNames(clazz): + """return a list of static field names for class""" + + static_fields = {} + declared_fields = Class.getDeclaredFields(clazz) + for field in declared_fields: + if Modifier.isStatic(field.getModifiers()) and Modifier.isPublic(field.getModifiers()): + static_fields[field.name] = field + fields = static_fields.keys() + + for eachBase in clazz.__bases__: + if not ispython(eachBase): + fields.extend(staticFieldNames(eachBase)) + + return fields + +def getCallTipJava(command='', locals=None): + """For a command, return a tuple of object name, argspec, tip text. + + The call tip information will be based on the locals namespace.""" + + calltip = ('', '', '') # object name, argspec, tip text. + + # Get the proper chunk of code from the command. + root = getRoot(command, terminator='(') + + try: + if locals is not None: + object = eval(root, locals) + else: + object = eval(root) + except: + return calltip + + if ispython(object): + # Patrick's code handles python code + # TODO fix in future because getCallTip runs eval() again + return getCallTip(command, locals) + + name = '' + try: + name = object.__name__ + except AttributeError: + pass + + tipList = [] + argspec = '' # not using argspec for Java + + if inspect.isclass(object): + # get the constructor(s) + # TODO consider getting modifiers since jython can access private methods + constructors = object.getConstructors() + for constructor in constructors: + paramList = [] + paramTypes = constructor.getParameterTypes() + # paramTypes is an array of classes, we need Strings + # TODO consider list comprehension + for param in paramTypes: + paramList.append(param.__name__) + paramString = string.join(paramList,', ') + tip = "%s(%s)" % (constructor.name, paramString) + tipList.append(tip) + + elif inspect.ismethod(object) or isinstance(object, PyReflectedFunction): + method = object + try: + object = method.im_class + except: # PyReflectedFunction + object = method.argslist[0].declaringClass + + # java allows overloading so we may have more than one method + methodArray = object.getMethods() + + for eachMethod in methodArray: + if eachMethod.name == method.__name__: + paramList = [] + for eachParam in eachMethod.parameterTypes: + paramList.append(eachParam.__name__) + + paramString = string.join(paramList,', ') + + # create a python style string a la PyCrust + # we're showing the parameter type rather than the parameter name, since that's all I can get + # we need to show multiple methods for overloading + # do we want to show the method visibility? how about exceptions? + # note: name, return type and exceptions same for EVERY overload method + + tip = "%s(%s) -> %s" % (eachMethod.name, paramString, eachMethod.returnType.name) + tipList.append(tip) + + tip_text = beautify(string.join(tipList,"\n")) + calltip = (name, argspec, tip_text) + return calltip + +def beautify(tip_text): + "Make the call tip text prettier" + tip_text = tip_text.replace("java.lang.", "") + if "[" in tip_text: + tip_text = tip_text.replace("[B", "byte[]") + tip_text = tip_text.replace("[S", "short[]") + tip_text = tip_text.replace("[I", "int[]") + tip_text = tip_text.replace("[J", "long[]") + tip_text = tip_text.replace("[F", "float[]") + tip_text = tip_text.replace("[D", "double[]") + tip_text = tip_text.replace("[Z", "boolean[]") + tip_text = tip_text.replace("[C", "char[]") + return tip_text + +def ispython21(object): + """ + Figure out if this is Python code or Java Code + + """ + pyclass = 0 + pycode = 0 + pyinstance = 0 + + if inspect.isclass(object): + try: + object.__doc__ + pyclass = 1 + except AttributeError: + pyclass = 0 + + elif inspect.ismethod(object): + try: + object.__dict__ + pycode = 1 + except AttributeError: + pycode = 0 + else: # I guess an instance of an object falls here + try: + object.__dict__ + pyinstance = 1 + except AttributeError: + pyinstance = 0 + + # print "object", object, "pyclass", pyclass, "pycode", pycode, "returning", pyclass | pycode + + return pyclass | pycode | pyinstance + + +def ispython22(object): + """ + Return true if object is Python code. + """ + + object_type = type(object) + + if object_type.__name__.startswith("java") or isinstance(object, PyReflectedFunction): + python = False + + elif object_type is types.MethodType: + # both Java and Python methods return MethodType + try: + object.__dict__ + python = True + except AttributeError: + python = False + else: + # assume everything else is python + python = True + + return python + +def ispython25(object): + """ + Return true if object is Python code. + """ + + if isinstance(object, Class): + python = False + elif isinstance(object, Object): + python = False + elif isinstance(object, PyReflectedFunction): + python = False + elif type(object) == types.MethodType and not ispython(object.im_class): + python = False + else: + # assume everything else is python + python = True + + return python + +# Dynamically assign the version of ispython +# To deal with differences between Jython 2.1, 2.2 and 2.5 +if sys.version == '2.1': + ispython = ispython21 +elif sys.version.startswith('2.5'): + ispython = ispython25 +else: + ispython = ispython22 + +def debug(name, value=None): + if value == None: + print >> sys.stderr, name + else: + print >> sys.stderr, "%s = %s" % (name, value) diff --git a/jythonconsole/jintrospect_test_case.py b/jythonconsole/jintrospect_test_case.py new file mode 100644 index 0000000..fff41c5 --- /dev/null +++ b/jythonconsole/jintrospect_test_case.py @@ -0,0 +1,107 @@ +import unittest +import jintrospect +from java.lang import String + +class JIntrospectTestCase(unittest.TestCase): + + def testGetAutoCompleteList(self): + s = String("Unit Test") + list = jintrospect.getAutoCompleteList("s", locals()) + self.assertNotEmpty(list) + self.assertContains(list, "contains") + + def testGetCallTipJava(self): + s = String("Unit Test") + tip = jintrospect.getCallTipJava("s.contains", locals()) + self.assertEquals("contains(CharSequence) -> boolean", tip[2]) + + def testGetPackageName(self): + package_name = jintrospect.getPackageName("import java.") + self.assertEquals("java", package_name) + + package_name = jintrospect.getPackageName("from java.awt import") + self.assertEquals("java.awt", package_name) + + def testCompletePackageName(self): + try: + list = jintrospect.completePackageName("bogus") + fail("Expecting import error.") + except ImportError: + pass + + list = jintrospect.completePackageName("java") + self.assertNotEmpty(list) + self.assertContains(list, "awt") + + list = jintrospect.completePackageName("java.util") + self.assertNotEmpty(list) + self.assertContains(list, "ArrayList") + + def testIsPython(self): + s = String("Java String") + self.assert_(not jintrospect.ispython(s)) + + self.assert_(jintrospect.ispython(jintrospect)) + + def testIsPython22(self): + # NOTE: This will fail with AP 2.1. Would it fail for old version too? + ps = "python string" + self.assert_(jintrospect.ispython(ps)) + d = {} + self.assert_(jintrospect.ispython(d)) + + def testStaticJavaMethods(self): + """ Instances of Java classes should not show static methods as completion choices. """ + static_methods = jintrospect.getAutoCompleteList("String", {"String" : String}) + self.assert_("valueOf" in static_methods, "'valueOf' missing from static method list") + instance_methods = jintrospect.getAutoCompleteList("s", {"s" : String("Test")}) + self.assert_("valueOf" not in instance_methods, "'valueOf' should not be in the instance method list") + + def testJavaAccessorAsProperty(self): + instance_methods = jintrospect.getAutoCompleteList("s", {"s" : String("Test")}) + self.assert_("class" in instance_methods, "'class' property should be in the instance method list") + + def testJavaLangRemoved(self): + object_name, argspec, tip_text = jintrospect.getCallTip('String', {'String' : 's'}) + self.assertDoesNotContain(tip_text, "java.lang.") + + def testPrimitiveArrayConversion(self): + """[B, [C and [I should be replaced""" + object_name, argspec, tip_text = jintrospect.getCallTipJava('String', { 'String' : String }) + self.assertDoesNotContain(tip_text, "[B") + self.assertDoesNotContain(tip_text, "[C") + self.assertDoesNotContain(tip_text, "[I") + self.assertContains(tip_text, "byte[]") + self.assertContains(tip_text, "char[]") + self.assertContains(tip_text, "int[]") + + # http://code.google.com/p/jythonconsole/issues/detail?id=2 + # def testMultipleStatements(self): + # command = "import sys; sys" + # list = jintrospect.getAutoCompleteList(command, locals()) + # self.assert_(len(list) > 0) + + # note: static methods and fields are tested in static_test_case + + def assertNotEmpty(self, list): + if list == None: + self.fail("list is None") + if len(list) < 1: + self.fail("list is empty") + + def assertContains(self, list, value): + try: + list.index(value) + except ValueError: + self.fail("%s does not contain %s" % (type(list).__name__, value)) + + def assertDoesNotContain(self, list, value): + try: + list.index(value) + self.fail("%s should contain %s" % (type(list).__name__, value)) + except ValueError: + self.assert_(True) + + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/popup.py b/jythonconsole/popup.py new file mode 100644 index 0000000..4165a4d --- /dev/null +++ b/jythonconsole/popup.py @@ -0,0 +1,178 @@ +from java.lang import Character +from javax.swing import JWindow, JList, JScrollPane +from java.awt import Color, Dimension +from java.awt.event import KeyEvent +import sys + +__author__ = "Don Coleman " +__cvsid__ = "$Id: popup.py,v 1.9 2003/05/01 03:43:53 dcoleman Exp $" + +class Popup(JWindow): + """Popup window to display list of methods for completion""" + + MAX_HEIGHT = 300 + MIN_WIDTH = 200 + MAX_WIDTH = 400 + + def __init__(self, frame, textComponent): + JWindow.__init__(self, frame) + self.textComponent = textComponent + self.size = (200,200) + self.list = JList(keyPressed=self.key) + self.list.setBackground(Color(255,255,225)) # TODO move this color + self.getContentPane().add(JScrollPane(self.list)) + self.list.setSelectedIndex(0) + + self.originalData = "" + self.data = "" + self.typed = "" + + def key(self, e): + # print >> sys.stderr, "Other Listener" + if not self.visible: + return + + code = e.getKeyCode() + + if code == KeyEvent.VK_ESCAPE: + self.hide() + + elif code == KeyEvent.VK_ENTER or code == KeyEvent.VK_TAB: + self.chooseSelected() + e.consume() + + elif code == KeyEvent.VK_SPACE: + # TODO for functions: choose the selected option, add parenthesis + # and put the cursor between them. example: obj.function(^cursor_here) + self.chooseSelected() + + elif code == KeyEvent.VK_PERIOD: + self.chooseSelected() + #e.consume() + + # This fails because the key listener in console gets it first + elif code == KeyEvent.VK_LEFT_PARENTHESIS: + self.chooseSelected() + + elif code == 8: # BACKSPACE + if len(self.typed) == 0: + self.hide() + self.typed = self.typed[:-1] + print >> sys.stderr, self.typed + self.data = filter(self.originalData, self.typed) + self.list.setListData(self.data) + self.list.setSelectedIndex(0) + + elif code == KeyEvent.VK_UP: + self.up() + # consume event to avoid history previous + e.consume() + + elif code == KeyEvent.VK_DOWN: + self.down() + # consume event to avoid history next + e.consume() + + elif code == KeyEvent.VK_PAGE_UP: + self.pageUp() + e.consume() + + elif code == KeyEvent.VK_PAGE_DOWN: + self.pageDown() + e.consume() + + else: + char = e.getKeyChar() + if Character.isJavaLetterOrDigit(char): + self.typed += char + self.data = filter(self.data, self.typed) + self.list.setListData(self.data) + self.list.setSelectedIndex(0) + + def down(self): + index = self.list.getSelectedIndex() + max = self.getListSize() - 1 + + if index < max: + index += 1 + self.setSelected(index) + + def up(self): + index = self.list.getSelectedIndex() + + if index > 0: + index -= 1 + self.setSelected(index) + + def pageUp(self): + index = self.list.getSelectedIndex() + visibleRows = self.list.getVisibleRowCount() + index = max(index - visibleRows, 0) + self.setSelected(index) + + def pageDown(self): + index = self.list.getSelectedIndex() + visibleRows = self.list.getVisibleRowCount() + index = min(index + visibleRows, self.getListSize() - 1) + self.setSelected(index) + + def setSelected(self, index): + self.list.setSelectedIndex(index) + self.list.ensureIndexIsVisible(index) + + def getListSize(self): + return self.list.getModel().getSize() + + def chooseSelected(self): + """Choose the selected value in the list""" + value = self.list.getSelectedValue() + if value != None: + startPosition = self.dotPosition + 1 + caretPosition = self.textComponent.getCaretPosition() + self.textComponent.select(startPosition, caretPosition) + self.textComponent.replaceSelection(value) + self.textComponent.setCaretPosition(startPosition + len(value)) + self.hide() + + def setMethods(self, methodList): + methodList.sort() + self.data = methodList + self.originalData = methodList + self.typed = "" + self.list.setListData(methodList) + + def show(self): + # when the popup becomes visible, get the cursor + # so we know how to replace the selection + self.dotPosition = self.textComponent.getCaretPosition() + self.setSize(self.getPreferredSize()) + self.super__show() + + def showMethodCompletionList(self, list, displayPoint): + self.setLocation(displayPoint) + self.setMethods(list) + self.show() + self.list.setSelectedIndex(0) + + def getPreferredSize(self): + # need to add a magic amount to the size to avoid scrollbars + # I'm sure there's a better way to do this + MAGIC = 20 + size = self.list.getPreferredScrollableViewportSize() + height = size.height + MAGIC + width = size.width + MAGIC + if height > Popup.MAX_HEIGHT: + height = Popup.MAX_HEIGHT + if width > Popup.MAX_WIDTH: + width = Popup.MAX_WIDTH + if width < Popup.MIN_WIDTH: + width = Popup.MIN_WIDTH + return Dimension(width, height) + + +# this needs a list renderer that will hilight the prefix +def filter(list, prefix): + prefix = prefix.lower() + list = [eachItem for eachItem in list if str(eachItem).lower().startswith(prefix)] + return list + diff --git a/jythonconsole/problems.txt b/jythonconsole/problems.txt new file mode 100644 index 0000000..94ee6a3 --- /dev/null +++ b/jythonconsole/problems.txt @@ -0,0 +1,15 @@ +========================== +Known problems +========================== + +You must use the keyboard not the mouse to select methods. (Due to focus problems.) + +Builtin types don't work like they do in PyCrust. This is because they have a null __doc__ string in Jython. + +Sometimes introspect.getAllAttributeNames goes into an infine recursive loop + +python.security.respectJavaAccessibility must be set to true in Jython's registry (this is the default setting) + +-- + +Please submit any new issues http://code.google.com/p/jythonconsole/issues/list diff --git a/jythonconsole/run_tests.py b/jythonconsole/run_tests.py new file mode 100644 index 0000000..0753e95 --- /dev/null +++ b/jythonconsole/run_tests.py @@ -0,0 +1,8 @@ +import glob + +for filename in glob.glob('*_test_case.py'): + exec 'from %s import *'%filename[:-3] + +if __name__ == '__main__': + import unittest + unittest.main() \ No newline at end of file diff --git a/jythonconsole/static_test_case.py b/jythonconsole/static_test_case.py new file mode 100644 index 0000000..8ae8cc5 --- /dev/null +++ b/jythonconsole/static_test_case.py @@ -0,0 +1,36 @@ +import unittest +import jintrospect + +class StaticTestCase(unittest.TestCase): + + def setUp(self): + pass + + def testStaticAutoComplete(self): + from java.util.logging import Level + list = jintrospect.getAutoCompleteList("Level", locals()) + self.assertEquals(10, len(list)) + self.assert_(list.index("INFO") > -1) + + # This works but sometimes prints a bunch of junk on OS 10.4.9 if a Swing GUI is running + def testStaticPropertyFromAncestor(self): + from javax.swing import JButton + list = jintrospect.getAutoCompleteList("JButton", locals()) + self.assert_(len(list) > 0) + self.assert_(list.index("TEXT_CHANGED_PROPERTY") > -1) + + def testStaticPropertyFromAncestorInterface(self): + from javax.xml.transform.stream import StreamResult + list = jintrospect.getAutoCompleteList("StreamResult", locals()) + self.assert_(len(list) > 0) + self.assert_(list.index("PI_ENABLE_OUTPUT_ESCAPING") > -1) + + def testStaticMethodFromAncestor(self): + from javax.swing.border import EtchedBorder + list = jintrospect.getAutoCompleteList("EtchedBorder", locals()) + #print list + self.assert_(len(list) > 0) + self.assert_(list.index("getInteriorRectangle") > -1) + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/testcase.py b/jythonconsole/testcase.py new file mode 100644 index 0000000..3eefa97 --- /dev/null +++ b/jythonconsole/testcase.py @@ -0,0 +1,38 @@ +import sys +import unittest +import introspect + +class IntrospectTestCase(unittest.TestCase): + + def setUp(self): + try: + sys.ps2 + except AttributeError: + sys.ps2 = '... ' + + def testUnboundMethod(self): + import string + method = 'string.index(' + (name, argspec, tip) = introspect.getCallTip(method, locals()) + + self.assertEquals('index', name) + self.assertEquals('s, *args', argspec) + self.assertEquals('index(s, *args)\n\nindex(s, sub [,start [,end]]) -> int\n\nLike find but raises ValueError when the substring is not found.', tip) + + def testBuiltinFunction(self): + """Builtin types don't work, like they do in PyCrust. This is because they have a null __doc__ string in Jython.""" + method = 'len(' + + (name, argspec, tip) = introspect.getCallTip(method, locals()) + + self.assertEquals('len', name) + + if not sys.platform.startswith('java'): + # next line worked with Python 2.2, fails with Python 2.3 and 2.5 on OS X, probably need newer introspect, inspect or dis + # self.assertEquals('len(object) -> integer', argspec) + self.assertEquals('', argspec) + self.assertEquals('len(object) -> integer\n\nReturn the number of items of a sequence or mapping.', tip) + + +if __name__ == '__main__': + unittest.main() diff --git a/jythonconsole/tip.py b/jythonconsole/tip.py new file mode 100644 index 0000000..7b76e7f --- /dev/null +++ b/jythonconsole/tip.py @@ -0,0 +1,47 @@ +from java.awt import Color, Dimension +from javax.swing import JWindow, JTextArea, JScrollPane + +__author__ = "Don Coleman " +__cvsid__ = "$Id: tip.py,v 1.3 2003/05/01 03:43:53 dcoleman Exp $" + +class Tip(JWindow): + """ + Window which provides the user with information about the method. + For Python, this shows arguments, and the documention + For Java, this shows the signature(s) and return type + """ + MAX_HEIGHT = 300 + MAX_WIDTH = 400 + + def __init__(self, frame): + JWindow.__init__(self, frame) + self.textarea = JTextArea() + # TODO put this color with all the other colors + self.textarea.setBackground(Color(225,255,255)) + self.textarea.setEditable(0) + self.jscrollpane = JScrollPane(self.textarea) + self.getContentPane().add(self.jscrollpane) + + def setText(self, tip): + self.textarea.setText(tip) + self.textarea.setCaretPosition(0) + #print >> sys.stderr, self.textarea.getPreferredScrollableViewportSize() + self.setSize(self.getPreferredSize()) + + def getPreferredSize(self): + # need to add a magic amount to the size to avoid scrollbars + # I'm sure there's a better way to do this + MAGIC = 20 + size = self.textarea.getPreferredScrollableViewportSize() + height = size.height + MAGIC + width = size.width + MAGIC + if height > Tip.MAX_HEIGHT: + height = Tip.MAX_HEIGHT + if width > Tip.MAX_WIDTH: + width = Tip.MAX_WIDTH + return Dimension(width, height) + + def showTip(self, tip, displayPoint): + self.setLocation(displayPoint) + self.setText(tip) + self.show() diff --git a/md.py b/md.py new file mode 100644 index 0000000..7bb16fc --- /dev/null +++ b/md.py @@ -0,0 +1,9 @@ +from com.nomagic.uml2.ext.jmi.helpers import StereotypesHelper as halp +from com.nomagic.magicdraw.core import Application + +app = Application.getInstance() +proj = app.getProject() +mod = proj.getModel() +sysml = halp.getProfile(proj, "SysML Profile") +req = halp.getStereotype(proj, "Requirement") + diff --git a/mdjythonconsole.py b/mdjythonconsole.py new file mode 100644 index 0000000..a20cbc2 --- /dev/null +++ b/mdjythonconsole.py @@ -0,0 +1,68 @@ +import sys +from javax.swing import JFrame, JScrollPane +""" + much as i was looking forward to rewriting the whole terminal experience, + somebody already did: + http://code.google.com/p/jythonconsole/ + let us grab the most recent version here: + wget http://jythonconsole.googlecode.com/files/jythonconsole-0.0.7.zip + unzip jythonconsole-0.0.7.zip + mv jythonconsole-0.0.7 jythonconsole + touch jythonconsole/__init__.py + you'll then have to add it to the jython path (see below). +""" + +# by default, MagicDraw seems to add +# $MD_HOME/Lib +# to the jython path when it means to add +# $MD_HOME/plugins/com.nomagic.magicdraw.jpython/jython2.5.1/Lib +# let us fix this. +real_jython_lib_path = sys.currentWorkingDir + "/plugins/com.nomagic.magicdraw.jpython/jython2.5.1/Lib" +if real_jython_lib_path not in sys.path: + sys.path.append(sys.currentWorkingDir + "/plugins/com.nomagic.magicdraw.jpython/jython2.5.1/Lib") + +# also, the location of this script is not added. let us fix this as well so we can import things +# TODO: automate finding this location. +# PROBLEM: __file__ is not populated, and inspect is not available +script_path = "/home/gtpmasevm/MagicDrawJythonTerminal" +if script_path not in sys.path: + sys.path.append(script_path) + +from jythonconsole.console import Console + +from com.nomagic.magicdraw.core import Application + +class MDConsole(Console): + + # BANNER = Console.BANNER + ["MagicDraw OpenAPI Version %s" % Application.getInstance().openAPIVersion] + def __init__(self, namespace={}): + """ + Create a MagicDraw Jython Console. + namespace is an optional and should be a dictionary or Map + """ + + # populate with the application and current project + namespace['app'] = Application.getInstance() + # TODO: probably need to listen for this changing, huh? + namespace['proj'] = namespace['app'].getProject() + # TODO: probably need to listen for this changing, huh? + namespace['model'] = namespace['proj'].getModel() + + Console.__init__(self, namespace) + +class MDJythonFrame(JFrame): + """ This is just like the default JythonFrame, but without the MagicDraw-killing feature + """ + def __init__(self): + self.title = "MagicDraw Jython" + self.size = (600, 400) + +def main(namespace={}): + frame = MDJythonFrame() + console = MDConsole(namespace) + frame.getContentPane().add(JScrollPane(console.text_pane)) + frame.visible = True + +if __name__ == "__main__": + main() +