> > What do you think? > > Shouldn't be too hard. I'll see if I can find the time to implement > it.
There. I've put together a prototype implementation. But whatever you do, do not commit this one yet! It's still way too immature, and I'd appreciate some feedback before I finish up on this one.
My thoughts:* Is the constructor / other method separation a good idea? I think so, but I'm not sure... I can't think of any rational examples for why it would be a bad idea... I don't think I'd ever want empty mutables as default arg values to my methods...
* My old script assign parameters to attributes should not move the cursor after inserting the new lines. If it would leave the cursor alone, it would work better in conjunction with these and other context sensitive assistants that operate on method def lines.
* For constructors, should I combine this with my old hack, so that one assist proposal does all the arg None checking and assignment to attributes? Is it a clever idea to let one propsal do so much? Next question: What should I name such a monster? "Do the usual thing"?
* I also noticed a design flaw in RegexBasedAssistProposal. The newline delimiter should be added to self.vars already in the isValid() method. This is fixed in the attached implementation.
Any pointers and suggestions are welcome. Cheers! /Joel
"""Quick Assistant: Regex based proposals. This module combines AssistProposal, regexes and string formatting to provide a way of swiftly coding your own custom Quick Assistant proposals. These proposals are ready for instatiation and registering with assist_proposal.register_proposal(): AssignToAttributeOfSelf, AssignEmptyDictToVarIfNone, AssignEmptyDictToVarIfNone and AssignAttributeOfSelfToVarIfNone. Using these as examples it should be straightforward to code your own regex driven Quick Assistant proposals. """ __author__ = """Joel Hedlund <joel.hedlund at gmail.com>""" __version__ = "1.0.0" __copyright__ = '''Available under the same conditions as PyDev. See PyDev license for details. http://pydev.sourceforge.net ''' import re from org.python.pydev.core.docutils import PySelection [EMAIL PROTECTED] from org.python.pydev.editor.actions import PyAction [EMAIL PROTECTED] import assist_proposal # For older python versions. True, False = 1,0 class RegexBasedAssistProposal(assist_proposal.AssistProposal): """Base class for regex driven Quick Assist proposals. More docs available in base class source. New class data members ====================== regex = re.compile(r'^(?P<initial>\s*)(?P<name>\w+)\s*$'): <regex> Must .match() current line for .isValid() to return true. Any named groups will be available in self.vars. template = "%(initial)sprint 'Hello World!'": <str> This will replace what's currently on the line on .apply(). May use string formatters with names from self.vars. base_vars = {}: <dict <str>:<str>> Used to initiallize self.vars. New instance data members ========================= vars = <dict <str>:<str>> Variables used with self.template to produce the code that replaces the current line. This will contain values from self.base_vars, all named groups in self.regex, as well with these two additional ones: 'indent': the static indentation string 'newline': the line delimiter string selection, current_line, editor, offset: Same as the corresponding args to .isValid(). """ template = "" base_vars = {} regex = re.compile(r'^(?P<initial>\s*)(?P<name>\w+)\s*$') def isValid(self, selection, current_line, editor, offset): """Is this proposal applicable to this line of code? If current_line .match():es against self.regex then we will store a lot of information on the match and environment, and return True. Otherwise return False. IN: pyselection: <PySelection> The current selection. Highly useful. current_line: <str> The text on the current line. editor: <PyEdit> The current editor. offset: <int> The current position in the editor. OUT: Boolean. Is the proposal applicable in the current situation? """ m = self.regex.match(current_line) if not m: return False self.vars = {'indent': PyAction.getStaticIndentationString(editor), 'newline': PyAction.getDelimiter(editor.getDocument())} self.vars.update(self.base_vars) self.vars.update(m.groupdict()) self.selection = selection self.current_line = current_line self.editor = editor self.offset = offset return True def apply(self, document): """Replace the current line with the populated template. IN: document: <IDocument> The edited document. OUT: None. """ sNewCode = self.template % self.vars # Move to insert point: iStartLineOffset = self.selection.getLineOffset() iEndLineOffset = iStartLineOffset + len(self.current_line) self.editor.setSelection(iEndLineOffset, 0) self.selection = PySelection(self.editor) # Replace the old code with the new assignment expression: self.selection.replaceLineContentsToSelection(sNewCode) class AssignToAttributeOfSelf(RegexBasedAssistProposal): """Assign variable to attribute of self. Effect ====== Generates code that assigns a variable to attribute of self with the same name. Valid when ========== When the current line contains exactly one alphanumeric word. No check is performed to see if the word is defined or valid in any other way. Use case ======== It's often a good idea to use the same names in args, variables and data members. This keeps the terminology consistent. This way customer_id should always contain a customer id, and any other variants are misspellings that probably will lead to bugs. This proposal helps you do this by assigning variables to data members with the same name. """ description = "Assign to attribute of self" tag = "ASSIGN_VARIABLE_TO_ATTRIBUTE_OF_SELF" regex = re.compile(r'^(?P<initial> {8}\s*)(?P<name>\w+)\s*$') template = "%(initial)sself.%(name)s = %(name)s" class AssignDefaultToVarIfNone(RegexBasedAssistProposal): """Assign default value to variable if None. This is a base class intended for subclassing. Effect ====== Generates code that tests if a variable is none, and if so, assigns a default value to it. Valid when ========== When the current line contains exactly one alphanumeric word. No check is performed to see if the word is defined or valid in any other way. Use case ======== It's generally a bad idea to use mutable objects as default values to methods and functions. The common way around it is to use None as the default value, check the arg in the fuction body, and then assign the desired mutable to it. This proposal does the check/assignment for you. You only need to type the arg name where you want the check, and then activate the Quick Assistant. """ description = "Assign default value to var if None" tag = "ASSIGN_DEFAULT_VALUE_TO_VARIABLE_IF_NONE" regex = re.compile(r'^(?P<initial>\s*)(?P<name>\w+)\s*$') template = ("%(initial)sif %(name)s is None:%(newline)s" "%(initial)s%(indent)s%(name)s = %(value)s") base_vars = {'value': "list()"} class AssignEmptyListToVarIfNone(AssignDefaultToVarIfNone): """Assign empty list to variable if None.""" description = "Assign empty list to var if None" tag = "ASSIGN_EMPTY_LIST_TO_VARIABLE_IF_NONE" priority = 11 class AssignEmptyDictToVarIfNone(AssignEmptyListToVarIfNone): """Assign empty dictionary to variable if None.""" description = "Assign empty dict to var if None" tag = "ASSIGN_EMPTY_DICT_TO_VARIABLE_IF_NONE" base_vars = {'value': "dict()"} priority = 11 class AssignAttributeOfSelfToVarIfNone(AssignDefaultToVarIfNone): """Assign an attribute of self with same name to variable if None. Valid when ========== When the current line contains exactly one alphanumeric word indented by more than 8 spaces. This script does not check if the word is defined or valid in any other way. Use case ======== If a method does something using a data member, but just as well could do the same thing using an argument, it's generally a good idea to let the implementation reflect that. This makes the code more flexible. This is usually done like so: -------------------------- class MyClass: def func(arg = None): if arg is None: arg = self.arg ... -------------------------- This proposal does the check/assignment for you. You only need to type the arg name where you want the check, and then activate the Quick Assistant. """ description = "Assign attribute of self to var if None" tag = "ASSIGN_ATTRIBUTE_OF_SELF_TO_VARIABLE_IF_NONE" regex = re.compile(r'^(?P<initial> {8}\s*)(?P<name>\w+)\s*$') template = ("%(initial)sif %(name)s is None:%(newline)s" "%(initial)s%(indent)s%(name)s = self.%(name)s")
"""Quick Assistant: Quick Assist Assign stuff to default None args. Effect ====== Check method args for None default values and proposes to assign stuff to them. In __init__() methods, this script assigns list() to the args. Otherwise an attribute of self with the same name will be assigned to the arg. Valid when ========== When the current line is the first line of method def statement. Both the def keyword and the opening parenthesis should be on the current line. Installation ============ Place this file in your pydev jython script dir, along with assist_proposal.py and assist_regex_based_proposal.py, open a new editor, and you are ready to go. See the pydev docs if you don't know where your dir is. Use case ======== Bah. I'll write some friendly docs here later. Example ======= Bah. I'll write some friendly docs here later. """ __author__ = """Joel Hedlund <joel.hedlund at gmail.com>""" __version__ = "1.0.0" __copyright__ = '''Available under the same conditions as PyDev. See PyDev license for details. http://pydev.sourceforge.net ''' # # Boring boilerplate preamble code. This can be safely copied to every pydev # jython script that you write. The interesting stuff is further down below. # # For older python versions. True, False = 1,0 # Set to False to inactivate this assist proposal (may require Eclipse restart). USE_THIS_ASSIST_PROPOSAL = True if not USE_THIS_ASSIST_PROPOSAL: raise ExitScriptException() # Set to True to do inefficient stuff that is only useful for debugging # and development purposes. Should always be False if not debugging. DEBUG = True # This is a magic trick that tells the PyDev Extensions editor about the # namespace provided for pydev scripts: if False: from org.python.pydev.editor import PyEdit [EMAIL PROTECTED] cmd = 'command string' editor = PyEdit assert cmd is not None assert editor is not None # We don't need to add the same assist proposal more than once. if not (cmd == 'onCreateActions' or (DEBUG and cmd == 'onSave')): from org.python.pydev.jython import ExitScriptException [EMAIL PROTECTED] raise ExitScriptException() # We want a fresh interpreter if we're debugging this script! if DEBUG and cmd == 'onSave': from org.python.pydev.jython import JythonPlugin [EMAIL PROTECTED] editor.pyEditScripting.interpreter = JythonPlugin.newPythonInterpreter() # # Interesting stuff starts here! # import re from org.python.pydev.core.docutils import PySelection [EMAIL PROTECTED] from org.python.pydev.core.docutils import ParsingUtils [EMAIL PROTECTED] import java.util.StringTokenizer from java.lang import StringBuffer import assist_proposal from assist_regex_based_proposal import RegexBasedAssistProposal def get_argument_definitions(editor): """Return the method arg definitions and the end parenthesis offset. The opening parenthesis of the "def (...)" statement must be on the current line, or else ValueError will be raised. IN: editor: <PyEdit> The current line in this editor will be inspected. OUT: <tuple <tuple <str> 'arg defs', <int> 'end paren offset'> A 2-tuple, where the first item is a tuple of argument definitions, and the second item is the end paren offset. """ oSelection = PySelection(editor) oDocument = editor.getDocument() sLine = oSelection.getCursorLineContents() iParenStart = oSelection.getStartLineOffset() + sLine.index('(') oDummy = StringBuffer() iParenEnd = ParsingUtils.eatPar(oDocument, iParenStart, oDummy) sInsideParenText = oDocument.get()[iParenStart + 1:iParenEnd] lsArgDefs = [] oTokenizer = java.util.StringTokenizer(sInsideParenText, ',') while oTokenizer.hasMoreTokens(): lsArgDefs.append(oTokenizer.nextToken().strip()) return tuple(lsArgDefs), iParenEnd def get_default_none_args(editor): """Return the arguments with default value None, and end paren offset. The opening parenthesis of the "def (...)" statement must be on the current line, or else ValueError will be raised. IN: editor: <PyEdit> The current line in this editor will be inspected. OUT: <tuple <tuple <str> 'arg names', <int> 'end paren offset'> A 2-tuple, where the first item is a tuple of names for arguments whose default value is None, and the second item is the end paren offset. """ lsDefaultNoneArgs = [] tsArgDefs, iParenEnd = get_argument_definitions(editor) for sArgDef in tsArgDefs: lsWords = sArgDef.split('=') if len(lsWords) != 2 or lsWords[1].strip() != 'None': continue lsDefaultNoneArgs.append(lsWords[0].strip()) return tuple(lsDefaultNoneArgs), iParenEnd def skip_docstring(editor, line): """Skip past the docstring whch may or may not start at the given line. IN: editor: <PyEdit> The editor that will be inspected. line: <int> A line index where a docstring may or may not start. OUT: A line index. If a docstring starts at the given line, then the line index for the first line after the docstring will be returned. Otherwise the given line index will be returned. """ oSelection = PySelection(editor) oDocument = editor.getDocument() sLine = oSelection.getLine(line) sStripped = sLine.lstrip() lsStringLiteralStarts = ['"', "'", 'r"', "r'"] for s in lsStringLiteralStarts: if sStripped.startswith(s): break else: return line iDocstrStart = oSelection.getLineOffset(line) + len(sLine) - len(sStripped) if sStripped.startswith('r'): iDocstrStart += 1 oDummy = java.lang.StringBuffer() iDocstrEnd = ParsingUtils.eatLiterals(oDocument, oDummy, iDocstrStart) return oSelection.getLineOfOffset(iDocstrEnd) + 1 class UseAttribOfSelfIfArgsAreDefaultNone(RegexBasedAssistProposal): """Use attributes of self if args are None by default.""" description = "Use attributes of self if args are None by default" tag = "USE_ATTRIBUTES_OF_SELF_IF_ARGS_ARE_NONE_BY_DEFAULT" regex = re.compile("(?P<initial> {4}\s*)def\s*(?!__init__)\w+\s*\(") template = ('%(initial)s%(indent)sif %(arg)s is None:%(newline)s' '%(initial)s%(indent)s%(indent)s%(arg)s = self.%(arg)s') def isValid(self, selection, current_line, editor, offset): if not RegexBasedAssistProposal.isValid(self, selection, current_line, editor, offset): return False lsArgs, iEndParenOffset = get_default_none_args(editor) if not lsArgs: return False self.args = lsArgs iEndParenLine = selection.getLineOfOffset(iEndParenOffset) self.insert_line = skip_docstring(editor, iEndParenLine + 1) - 1 return True def apply(self, document): lsNewCode = [] for sArg in self.args: self.vars['arg'] = sArg try: lsNewCode.append(self.template % self.vars) except: import traceback traceback.print_exc() raise sNewCode = self.vars['newline'].join(lsNewCode) self.selection.addLine(sNewCode, self.insert_line) class UseEmptyListsIfArgsAreDefaultNone(UseAttribOfSelfIfArgsAreDefaultNone): """Use attributes of self if args are None by default.""" description = "Use empty lists if args are None by default" tag = "USE_EMPTY_LISTS_IF_ARGS_ARE_NONE_BY_DEFAULT" regex = re.compile("(?P<initial> {4}\s*)def\s*__init__\s*\(") template = ('%(initial)s%(indent)sif %(arg)s is None:%(newline)s' '%(initial)s%(indent)s%(indent)s%(arg)s = list()') # Test code: class a: def __init__(self, a, b, c = None, d = None): r"""This is a docstring.""" def moo(self, a, b, c = None, d = None, *args, **kw): "moo!" o = UseAttribOfSelfIfArgsAreDefaultNone() assist_proposal.register_proposal(o, DEBUG) o = UseEmptyListsIfArgsAreDefaultNone() assist_proposal.register_proposal(o, DEBUG)
------------------------------------------------------------------------- Take Surveys. Earn Cash. Influence the Future of IT Join SourceForge.net's Techsay panel and you'll get the chance to share your opinions on IT & business topics through brief surveys -- and earn cash http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________ pydev-code mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/pydev-code
