>  > 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

Reply via email to