Skip to content

Commit

Permalink
Make all functions handle regions
Browse files Browse the repository at this point in the history
  • Loading branch information
storax committed Dec 30, 2016
1 parent 9de9ef5 commit 87ba4ab
Show file tree
Hide file tree
Showing 8 changed files with 690 additions and 428 deletions.
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ An example on how to expose a simple function to add arguments::

class MyProvider(reddel_server.ProviderBase)
@reddel_server.red_src()
@reddel_server.red_type(["def"])
def add_arg(self, red, index, arg):
@reddel_server.red_validate([reddel_server.OptionalRegionValidator(),
reddel_server.SingleNodeValidator(),
reddel_server.TypeValidator(["def"])])
def add_arg(self, red, start, end, index, arg):
red.arguments.insert(index, arg)
return red

Expand All @@ -93,7 +95,7 @@ Start the reddel server from Emacs::
>>> ;; make sure myprovidermod is in a directory within the PYTHONPATH
>>> (epc:call-sync my-epc 'add_provider '("myprovidermod.MyProvider"))

>>> (message (epc:call-sync my-epc 'add_arg '("def foo(arg1, arg3): pass" 1 "arg2")))
>>> (message (epc:call-sync my-epc 'add_arg '("def foo(arg1, arg3): pass" nil nil 1 "arg2")))
"def foo(arg1, arg2, arg3): pass"

Redbaron provides a lossless format, so even formatting and comments are preserved.
Expand Down
17 changes: 12 additions & 5 deletions src/reddel_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
* :func:`red_src <reddel_server.red_src>`
* :func:`red_validate <reddel_server.red_validate>`
* :func:`red_type <reddel_server.red_type>`
These decorators are the mini framework that allows the server to tell the client what actions are available for
a given piece of code.
Expand All @@ -77,7 +76,6 @@
* :func:`redwraps <reddel_server.redwraps>`
* :func:`red_src <reddel_server.red_src>`
* :func:`red_validate <reddel_server.red_validate>`
* :func:`red_type <reddel_server.red_type>`
* :func:`get_parents <reddel_server.get_parents>`
* :func:`get_node_of_region <reddel_server.get_node_of_region>`
Expand All @@ -89,12 +87,19 @@
E.g. if the source is a function, reddel can report to Emacs which functions can be applied to
functions and Emacs can use the information to dynamically build a UI.
Validators can transform the source as well (see :meth:`transform <reddel_server.BaronTypeValidator.transform>`).
Validators can transform the source as well.
The transformed source is passed onto the next validator when you use :func:`reddel_server.red_validate`.
All validators provided by ``reddel_server`` can be used as mix-ins.
When you create your own validator and you inherit from multiple builtin ones then
they are effectively combined since all of them perform the appropriate super call.
See:
* :class:`ValidatorInterface <reddel_server.ValidatorInterface>`
* :class:`BaronTypeValidator <reddel_server.BaronTypeValidator>`
* :class:`OptionalRegionValidator <reddel_server.OptionalRegionValidator>`
* :class:`MandatoryRegionValidator <reddel_server.MandatoryRegionValidator>`
* :class:`SingleNodeValidator <reddel_server.SingleNodeValidator>`
* :class:`TypeValidator <reddel_server.TypeValidator>`
----------
Exceptions
Expand All @@ -118,6 +123,7 @@

from .exceptions import *
from .provider import *
from .redlib import *
from .redprovider import *
from .server import *
from .validators import *
Expand All @@ -126,6 +132,7 @@
server.__all__ +
provider.__all__ +
validators.__all__ +
redprovider.__all__)
redprovider.__all__ +
redlib.__all__)

__version__ = "0.1.0"
140 changes: 140 additions & 0 deletions src/reddel_server/redlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""RedBaron specific library of functions"""
import collections

import redbaron

__all__ = ['Position', 'Parent', 'get_parents', 'get_node_of_region']


class Position(collections.namedtuple('Position', ['row', 'column'])):
"""Describes a position in the source code by line number and character position in that line.
:param row: the line number
:type row: :class:`int`
:param column: the position in the line
:type column: :class:`int`
"""
__slots__ = ()


class Parent(collections.namedtuple('Parent', ['identifier', 'start', 'end'])):
"""Represents a node type with the bounding location.
:param identifier: the node type
:type identifier: :class:`str`
:param start: the position where the node begins in the code
:type start: :class:`Position`
:param end: the position where the node ends in the code
:type end: :class:`Position`
"""
__slots__ = ()


def get_parents(red):
"""Yield the parents of the given red node
:param red: the red baron source
:type red: :class:`redbaron.base_nodes.Node` | :class:`redbaron.RedBaron`
:returns: each parent of the given node
:rtype: Generator[:class:`redbaron.base_nodes.Node`]
:raises: None
"""
current = red.parent
while current:
yield current
current = current.parent


def get_node_of_region(red, start, end):
"""Get the node that contains the given region
:param red: the red baron source
:type red: :class:`redbaron.RedBaron`
:param start: position of the beginning of the region
:type start: :class:`Position`
:param end: position of the end of the region
:type end: :class:`Position`
:returns: the node that contains the region
:rtype: :class:`redbaron.base_nodes.Node`
First the nodes at start and end are gathered. Then the common parent is selected.
If the common parent is a list, the minimum slice is used.
This makes it easier for the user because he can partially select nodes and
still gets what he most likely intended to get.
For example if your region partially selects several lines in a for loop
and you want to extract them (``|`` shows the region bounds)::
for i in range(10):
a = 1 + 2
b |= 4
c = 5
d =| 7
then we expect to get back::
b = 4
c = 5
d = 7
Note that the leading tab is missing because it doesn't belong to the ``b = 4`` node.
.. doctest::
>>> import redbaron
>>> import reddel_server
>>> testsrc = ("for i in range(10):\\n"
... " a = 1 + 2\\n"
... " b = 4\\n"
... " c = 5\\n"
... " d = 7\\n")
>>> start = (3, 7)
>>> end = (5, 8)
>>> reddel_server.get_node_of_region(redbaron.RedBaron(testsrc), start, end).dumps()
'b = 4\\n c = 5\\n d = 7'
You can also partially select a list:
.. doctest::
>>> testsrc = "[1, 2, 3, 4, 5, 6]"
>>> start = (1, 8) # "3"
>>> end = (1, 14) # "5"
>>> reddel_server.get_node_of_region(redbaron.RedBaron(testsrc), start, end).dumps()
'3, 4, 5'
"""
snode = red.find_by_position(start)
enode = red.find_by_position(end)
if snode == enode:
return snode
snodes = [snode] + list(get_parents(snode))
enodes = [enode] + list(get_parents(enode))
previous = red
for snode, enode in list(zip(reversed(snodes), reversed(enodes))):
# both list of parents should end with the root node
# so we iterate over the parents in reverse until we
# reach a parent that is not common.
# the previous parent then has to encapsulate both
if snode != enode:
if hasattr(previous, "node_list"):
# For lists we want the minimum slice of the list.
# E.g. the region spans over 2 functions in a big module
# the common parent would be the root node.
# We only want the part containing the 2 functions and not everything.
# Unfortunately we might loose some information if we slice the proxy list.
# When we use node_list then we keep things like new lines etc.
try:
sindex = previous.node_list.index(snode)
eindex = previous.node_list.index(enode)
except ValueError:
# if one of the nodes is not in the list, it means it's not part of
# previous value. E.g. previous is a function definition and snode
# is part of the arguments while enode is part of the function body.
# in that case we take the whole previous node (e.g. the whole function)
pass
else:
previous = redbaron.NodeList(previous.node_list[sindex:eindex + 1])
break
previous = snode
return previous

0 comments on commit 87ba4ab

Please sign in to comment.