Skip to content

Commit

Permalink
|\ merge from release-0.7.1, released as 0.7.1.
Browse files Browse the repository at this point in the history
  • Loading branch information
StyXman committed Feb 10, 2016
2 parents c12d6bc + a33a3ae commit 1b27afd
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 42 deletions.
12 changes: 10 additions & 2 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
ayrton (0.7) UNRELEASED; urgency=medium
ayrton (0.7.1) unstable; urgency=medium

* Iterable parameters to executables are expanded in situ, so `foo(..., i, ...)` is expanded to `foo (..., i[0], i[1], ...` and `foo(..., k=i, ...)` is expanded to `foo (..., k=i[0], k=i[1], ...`.
* `-x|--trace` allows for minimal execution tracing.
* `-xx|--trace-with-linenos` allows for execution tracing that also prints the line number.

-- Marcos Dione <[email protected]> Wed, 10 Feb 2016 10:14:17 +0100

ayrton (0.7) unstable; urgency=medium

* Send data to/from the remote via another `ssh` channel, which is more stable than using `stdin`.
* Stabilized a lot all tests, specially those using a mocked stdout for getting test validation.
Expand All @@ -11,7 +19,7 @@ ayrton (0.7) UNRELEASED; urgency=medium
* `argv` is handled at the last time possible, allowing it being passed from test invoction.
* `shift` complains on negative values.
* Lazy `pprint()`, so debug statemens do not do useless work.
* `stdin/out/err` handling in `remote()` is done by a single thread.
* `stdin/out/err` handling in `remote()` is done by a single thread.
* Modify a lot the local terminal when in `remote()` so, among other things, we have no local echo.
* Properly pass the terminal type and size to the remote. These last three features allows programs like `vi` be run in the remote.
* Paved the road to make `remote()`s more like `Command()`s.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ If you're unsure on how this apply to your interpreted programs, check
Currently `ayrton` is under heavy development, so if you're following it and
clone it, use the branch `develop`.

# Instalation
# Installation

`ayrton` depends on two pieces of code. Python is the most obvious; it has been
developed in its version 3.3. Python 3.2 is not enough, sorry. On the other hand,
Expand Down Expand Up @@ -217,14 +217,15 @@ A: (Short version) We think nobody provides all of `ayrton`'s features.
A: ... [`sh`](https://amoffat.github.io/sh/)? Well, we started with `sh` as a basis
of `ayrton`, but its buffered output didn't allow us to run editors and other TIU's.

A: ... [`xonsh`](http://xonsh.org/)? `xonsh` keeps environment variables well
separated from Python ones; it even has a Python mode and a 'subprocess' mode;
A: ... [`xonsh`](http://xonsh.org/)? `xonsh` keeps environment variables in a different
namespace than the Python ones; it even has a Python mode and a 'subprocess' mode
(although lots of Python characteristics can be used in the subprocess mode and vice versa);
and is more oriented to being a shell. `ayrton` aims directly in the opposite
direction.

A: ... [`plumbum`](https://plumbum.readthedocs.org/en/latest/)? You could say that we
independently thought of its piping and redirection syntax (but in reality we just
based ours on `bash`'s). Still, the fact that you fisrt build pipes and then execute
based ours on `bash`'s). Still, the fact that you first build pipes and then execute
them looks weird for a SysAdmin.

A: ... [`fabric`](http://www.fabfile.org/)? `fabric` is the only one that has remote
Expand Down
85 changes: 73 additions & 12 deletions ayrton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import logging
import dis
import traceback
import linecache

# patch logging so we have debug2 and debug3
import ayrton.utils
Expand All @@ -46,7 +47,16 @@
from ayrton.parser.astcompiler.astbuilder import ast_from_node
from ayrton.ast_pprinter import pprint

__version__= '0.7'
__version__= '0.7.1'


class ExecParams:
def __init__ (self, **kwargs):
# defaults
self.trace= False
self.linenos= False

self.__dict__.update (kwargs)


def parse (script, file_name=''):
Expand All @@ -67,7 +77,8 @@ def polute (self):
# weed out some stuff
for weed in ('copyright', '__doc__', 'help', '__package__', 'credits',
'license', '__name__', 'quit', 'exit'):
del self[weed]
if weed in self:
del self[weed]

# these functions will be loaded from each module and put in the globals
# tuples (src, dst) renames function src to dst
Expand Down Expand Up @@ -145,39 +156,54 @@ def __init__ (self, g=None, l=None, **kwargs):

self.options= {}
self.pending_children= []
self.file_name= None
self.script= None
self.params= ExecParams ()

# HACK to update the singleton
# this might break if we implement subinstances
global runner
runner= self


def run_file (self, file_name, argv=None):
def run_file (self, file_name, argv=None, params=None):
# it's a pity that parse() does not accept a file as input
# so we could avoid reading the whole file
# and now we read it anyways in the case of tracing
logger.debug ('running from file %s', file_name)

f= open (file_name)
script= f.read ()
f.close ()

return self.run_script (script, file_name, argv)
return self.run_script (script, file_name, argv, params)


def run_script (self, script, file_name, argv=None):
def run_script (self, script, file_name, argv=None, params=None):
logger.debug ('running script:\n-----------\n%s\n-----------', script)
self.file_name= file_name
self.script= script.split ('\n')

# up to this point the script is the whole script in one string
# because parse() needs it that way
tree= parse (script, file_name)
# TODO: self.locals?
tree= CrazyASTTransformer (self.globals, file_name).modify (tree)

return self.run_tree (tree, file_name, argv)
return self.run_tree (tree, file_name, argv, params)


def run_tree (self, tree, file_name, argv=None):
def run_tree (self, tree, file_name, argv=None, params=None):
logger.debug2 ('AST: %s', ast.dump (tree))
logger.debug2 ('code: \n%s', pprint (tree))

return self.run_code (compile (tree, file_name, 'exec'), file_name, argv)
if params is not None:
# we delay this assignment down to here because run_file(),
# run_script() and run_tree() are entry points
self.params= params

code= compile (tree, file_name, 'exec')
return self.run_code (code, file_name, argv)


def run_code (self, code, file_name, argv=None):
Expand Down Expand Up @@ -238,11 +264,16 @@ def run_code (self, code, file_name, argv=None):
error= None
try:
logger.debug3 ('globals for script: %s', ayrton.utils.dump_dict (self.globals))
if self.params.trace:
sys.settrace (self.global_tracer)

exec (code, self.globals, self.locals)
except Exception as e:
logger.debug ('script finished by Exception')
logger.debug (traceback.format_exc ())
error= e
finally:
sys.settrace (None)

logger.debug3 ('globals at script exit: %s', ayrton.utils.dump_dict (self.globals))
logger.debug3 ('locals at script exit: %s', ayrton.utils.dump_dict (self.locals))
Expand All @@ -261,19 +292,49 @@ def wait_for_pending_children (self):
child.wait ()


def global_tracer (self, frame, event, arg):
""""""
logger.debug2 ('global_tracer: %s', event)
if event in ('call', 'line'):
return self.local_tracer
else:
return None


def local_tracer (self, frame, event, arg):
if event=='line':
file_name= frame.f_code.co_filename
if self.params.trace_all or file_name==self.file_name:
lineno= frame.f_lineno

line= linecache.getline (file_name, lineno).rstrip ()
if line=='':
line= self.script[lineno-1].rstrip () # line numbers start at 1

logger.debug2 ('trace e: %s, f: %s, n: %d, l: %s', event, file_name, lineno, line)
if self.params.linenos:
print ("+ [%6d] %s" % (lineno, line), file=sys.stderr)
else:
print ("+ %s" % line, file=sys.stderr)


def run_tree (tree, g, l):
"""main entry point for remote()"""
runner= Ayrton (g=g, l=l)
return runner.run_tree (tree, 'unknown_tree')

def run_file_or_script (script=None, file_name='script_from_command_line', argv=None,
**kwargs):
def run_file_or_script (script=None, file_name='script_from_command_line',
argv=None, params=None, **kwargs):
"""Main entry point for bin/ayrton and unittests."""
runner= Ayrton (**kwargs)

if params is None:
params= ExecParams ()

if script is None:
v= runner.run_file (file_name, argv)
v= runner.run_file (file_name, argv, params)
else:
v= runner.run_script (script, file_name, argv)
v= runner.run_script (script, file_name, argv, params)

return v

Expand Down
7 changes: 5 additions & 2 deletions ayrton/castt.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ def func_name2dotted_exec (node):

complete_name= str (pprint (node))

while type (node) in (Attribute, Subscript):
node= node.value
while type (node) in (Attribute, Subscript, Call):
if type (node) in (Attribute, Subscript):
node= node.value
elif type (node) in (Call, ):
node= node.func

return (node.id, complete_name)

Expand Down
29 changes: 23 additions & 6 deletions ayrton/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ def resolve_program(program):

return path

def isiterable (o):
"""Returns True if o is iterable but not str/bytes type."""
# TODO: what about Mappings?
return ( isinstance (o, Iterable) and
not isinstance (o, (bytes, str)) )

class Command:
default_options= dict (
_in_tty= False,
Expand Down Expand Up @@ -342,7 +348,12 @@ def prepare_args (self, cmd, args, kwargs):
if isinstance (arg, o):
self.prepare_arg (ans, arg.key, arg.value)
else:
ans.append (str (arg))
if isiterable (arg):
# a sequence type that is not string like
for elem in arg:
ans.append (str (elem))
else:
ans.append (str (arg))

for k, v in kwargs.items ():
self.prepare_arg (ans, k, v)
Expand All @@ -351,12 +362,18 @@ def prepare_args (self, cmd, args, kwargs):

def prepare_arg (self, seq, name, value):
if value!=False:
seq.append (name)
if isiterable (value):
for elem in value:
seq.append (name)
seq.append (str (elem))

else:
seq.append (name)

# this is not the same as 'not value'
# because value can have any, well, value of any kind
if value!=True:
seq.append (str (value))
# this is not the same as 'not value'
# because value can have any, well, value of any kind
if value!=True:
seq.append (str (value))
else:
# TODO: --no-option?
pass
Expand Down
5 changes: 4 additions & 1 deletion ayrton/parser/astcompiler/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def handle_suite(self, suite_node):
def handle_if_stmt(self, if_node):
child_count = len(if_node.children)
if child_count == 4:
# simple if-then
test = self.handle_expr(if_node.children[1])
suite = self.handle_suite(if_node.children[3])
new_node = ast.If (test, suite, [])
Expand All @@ -353,6 +354,7 @@ def handle_if_stmt(self, if_node):
return new_node
otherwise_string = if_node.children[4].value
if otherwise_string == "else":
# if-then-else
test = self.handle_expr(if_node.children[1])
suite = self.handle_suite(if_node.children[3])
else_suite = self.handle_suite(if_node.children[6])
Expand All @@ -361,6 +363,7 @@ def handle_if_stmt(self, if_node):
new_node.col_offset = if_node.col_offset
return new_node
elif otherwise_string == "elif":
# if-then-elif...
elif_count = child_count - 4
after_elif = if_node.children[elif_count + 1]
if after_elif.type == tokens.NAME and \
Expand All @@ -369,7 +372,7 @@ def handle_if_stmt(self, if_node):
elif_count -= 3
else:
has_else = False
elif_count /= 4
elif_count //= 4
if has_else:
last_elif = if_node.children[-6]
last_elif_test = self.handle_expr(last_elif)
Expand Down
9 changes: 6 additions & 3 deletions ayrton/tests/test_ayrton.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,15 @@ def setUp (self):
self.runner= ayrton.Ayrton ()


def doTest (self, script, expected=None, argv=None):
def doTest (self, file_name, expected=None, argv=None):
if argv is not None:
argv.insert (0, script)
argv.insert (0, file_name)
else:
argv= [ file_name ]

logger.debug (argv)
result= self.runner.run_file (os.path.join ('ayrton/tests/scripts', script), argv=argv)
result= self.runner.run_file (os.path.join ('ayrton/tests/scripts', file_name),
argv=argv)
if expected is not None:
self.assertEqual (result, expected)

Expand Down
7 changes: 7 additions & 0 deletions ayrton/tests/test_castt.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ def testDottedSubscriptComplex (self):
# this is a very strange but possible executable name
self.assertEqual (combined, 'argv[3].split[:54]')

def testDottedCall (self):
single, combined= castt.func_name2dotted_exec (parse_expression ('str (foo).strip ()'))

self.assertEqual (single, 'str')
# this is a very strange but possible executable name
self.assertEqual (combined, 'str (foo, ).strip ()')

class TestWeirdErrors (unittest.TestCase):
check_attrs= check_attrs

Expand Down
24 changes: 24 additions & 0 deletions ayrton/tests/test_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,27 @@ def foo (self):

# runner.options['errexit']= True
pass

class HelperFunctions (unittest.TestCase):
def setUp (self):
self.c= Command ('/bin/true')

def testPrepareArgsSimple (self):
self.assertEqual (self.c.prepare_args ('foo', [], {}),
[ 'foo' ])

def testPrepareArgsSingleArg (self):
self.assertEqual (self.c.prepare_args ('foo', ['bar'], {}),
[ 'foo', 'bar' ])

def testPrepareArgsSingleKwarg (self):
self.assertEqual (self.c.prepare_args ('foo', [], { 'bar': 'quux' }),
[ 'foo', 'bar', 'quux' ])

def testPrepareArgsIterableArg (self):
self.assertEqual (self.c.prepare_args ('foo', [ [ 1, 2 ] ], {}),
[ 'foo', '1', '2' ])

def testPrepareArgsIterableKwarg (self):
self.assertEqual (self.c.prepare_args ('foo', [], { 'bar': [ 1, 2 ]}),
[ 'foo', 'bar', '1', 'bar', '2' ])
9 changes: 9 additions & 0 deletions ayrton/tests/test_pyparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,12 @@ def test_comp (self):

def test_comp_if (self):
self.parse ('[ x for x in foo() if x ]')


def test_elif (self):
self.parse ('''if True:
pass
elif False:
pass
else:
pass''')
Loading

0 comments on commit 1b27afd

Please sign in to comment.