Skip to content

Commit

Permalink
|\ merge from release-0.2, released as 0.2.
Browse files Browse the repository at this point in the history
  • Loading branch information
StyXman committed Sep 14, 2013
2 parents 32d33f6 + 813e35f commit c83f9ae
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 24 deletions.
10 changes: 10 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
ayrton (0.2) unstable; urgency=low

* New function `options()` is similar to `bash`'s `set` command. So far
only the `errexit` and its short versions is accepted.
* The `ssh()` context manager was renamed to `remote()`. See NEWS.rst.
* New function `shitf()` similar to `bash`'s command of the same name.
See the docs.

-- Marcos Dione <[email protected]> Sat, 14 Sep 2013 17:59:27 +0200

ayrton (0.1.2) unstable; urgency=low

* RunninCommand.exit_code is a property, not a function. Closes #13.
Expand Down
7 changes: 7 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ayrton (0.2) UNRELEASED; urgency=low

* The `ssh()` context manager was renamed to `remote()` so the `ssh`
executable is stills reachable from code. This was due to the fact
that `ssh` is too complex to mimic.

-- Marcos Dione <[email protected]> Fri, 13 Sep 2013 13:47:26 +0200
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The cherry on top of the cake, or more like the melon of top of the cupcake, is
(semi) transparent remote execution. This is achieved with the following construct:

a= 42
with ssh ('localhost') as streams:
with remote ('localhost') as streams:
foo= input ()
print (foo)
# we can also access variables already in the scope
Expand All @@ -163,8 +163,8 @@ The cherry on top of the cake, or more like the melon of top of the cupcake, is
print (o.readlines ())


The body of the `with ssh(): ...` statement is actually executed in a remote
machine after connecting via `ssh`. The `ssh()` context manager accepts the
The body of the `with remote(): ...` statement is actually executed in a remote
machine after connecting via `ssh`. The `remote()` context manager accepts the
same parameters as `paramiko`'s
[`SSHClient.connect()`](http://docs.paramiko.org/paramiko.SSHClient-class.html#connect)
method.
Expand Down
36 changes: 29 additions & 7 deletions ayrton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from ast import fix_missing_locations, Import, alias
import pickle

__version__= '0.1.2'
__version__= '0.2'

class RunningCommandWrapper (sh.RunningCommand):
def _handle_exit_code (self, code):
Expand All @@ -50,10 +50,16 @@ def __bool__ (self):
# instead of going to stdout
Capture= (42, )

class CommandFailed (Exception):
def __init__ (self, code):
self.code= code

class CommandWrapper (sh.Command):
# this class changes the behaviour of sh.Command
# so is more shell scripting freindly
def __call__ (self, *args, **kwargs):
global runner

if ('_out' in kwargs.keys () and kwargs['_out']==Capture and
not '_tty_out' in kwargs.keys ()):
# for capturing, the default is to not simulate a tty
Expand All @@ -70,13 +76,17 @@ def __call__ (self, *args, **kwargs):
kwargs[std]= None

# mess with the environ
global runner
kwargs['_env']= runner.environ.os_environ

return super ().__call__ (*args, **kwargs)
ans= super ().__call__ (*args, **kwargs)

if runner.options.get ('errexit', False) and not bool (ans):
raise CommandFailed (ans.exit_code)

return ans

class Environment (object):
def __init__ (self, globals=None, locals=None):
def __init__ (self, globals=None, locals=None, **kwargs):
super ().__init__ ()

if globals is None:
Expand All @@ -94,6 +104,14 @@ def __init__ (self, globals=None, locals=None):
polute (self.ayrton_builtins)
self.os_environ= os.environ.copy ()

# now polute the locals with kwargs
for k, v in kwargs.items ():
# BUG: this sucks
if k=='argv':
self.ayrton_builtins['argv']= v
else:
self.locals[k]= v

def __getitem__ (self, k):
strikes= 0
for d in (self.locals, self.globals, self.os_environ,
Expand Down Expand Up @@ -127,7 +145,7 @@ class CrazyASTTransformer (ast.NodeTransformer):
def visit_With (self, node):
call= node.items[0].context_expr
# TODO: more checks
if call.func.id=='ssh':
if call.func.id=='remote':
# capture the body and put it as the first argument to ssh()
# but within a module, and already pickled;
# otherwise we need to create an AST for the call of all the
Expand All @@ -152,6 +170,8 @@ class Ayrton (object):
def __init__ (self, script=None, file=None, tree=None, globals=None,
locals=None, **kwargs):
if script is None and file is not None:
# it's a pity that compile() does not accept a file as input
# so we could avoid reading the whole file
script= open (file).read ()
else:
file= 'arg_to_main'
Expand All @@ -160,8 +180,9 @@ def __init__ (self, script=None, file=None, tree=None, globals=None,
tree= ast.parse (script)
tree= CrazyASTTransformer().visit (tree)

self.options= {}
self.source= compile (tree, file, 'exec')
self.environ= Environment (globals, locals)
self.environ= Environment (globals, locals, **kwargs)

def run (self):
exec (self.source, self.environ.globals, self.environ)
Expand All @@ -179,7 +200,8 @@ def polute (d):
'_k', '_p', '_r', '_s', '_u', '_w', '_x', '_L',
'_N', '_S', '_nt', '_ot' ],
'ayrton.expansion': [ 'bash', ],
'ayrton.functions': [ 'cd', 'export', 'run', 'ssh', 'unset', ],
'ayrton.functions': [ 'cd', 'export', 'option', 'remote', 'run', 'shift',
'source', 'unset', ],
'ayrton': [ 'Capture', ],
'sh': [ 'CommandNotFound', ],
}
Expand Down
49 changes: 43 additions & 6 deletions ayrton/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,31 @@ def export (**kwargs):
ayrton.runner.environ.globals[k]= str (v)
ayrton.runner.environ.os_environ[k]= str (v)

def run (path, *args, **kwargs):
c= ayrton.CommandWrapper._create (path)
return c (*args, **kwargs)
option_map= dict (
e= 'errexit',
)

class ssh (object):
def option (option, value=True):
if len (option)==2:
if option[0]=='-':
value= True
elif option[0]=='+':
value= False
else:
# TODO: syntax error:
pass
option= option_map[option[1]]

ayrton.runner.options[option]= value

class remote (object):
# TODO: inherit CommandWrapper?
# TODO: see foo.txt
"Uses the same arguments as paramiko.SSHClient.connect ()"
def __init__ (self, ast, *args, **kwargs):
def __init__ (self, ast, hostname, *args, **kwargs):
# actually, it's not a proper ast, it's the pickle of such thing
self.ast= ast
self.hostname= hostname
self.args= args
self.python_only= False
if '_python_only' in kwargs:
Expand All @@ -64,7 +78,7 @@ def __init__ (self, ast, *args, **kwargs):
def __enter__ (self):
self.client= paramiko.SSHClient ()
self.client.load_host_keys (bash ('~/.ssh/known_hosts')[0])
self.client.connect (*self.args, **self.kwargs)
self.client.connect (self.hostname, *self.args, **self.kwargs)
# get the locals from the runtime
# we can't really export the globals: it's full of unpicklable things
# so send an empty environment
Expand Down Expand Up @@ -110,6 +124,29 @@ def __enter__ (self):
def __exit__ (self, *args):
pass

def run (path, *args, **kwargs):
c= ayrton.CommandWrapper._create (path)
return c (*args, **kwargs)

def shift (n=1):
# we start at 1 becasuse 0 is the script's path
# this closely follows bash's behavior
if n==1:
ans= ayrton.runner.environ.ayrton_builtins['argv'].pop (1)
elif n>1:
ans= [ ayrton.runner.environ.ayrton_builtins['argv'].pop (1)
for i in range (n) ]
else:
# TODO
pass

return ans

def source (file):
sub_runner= ayrton.Ayrton (file=file)
sub_runner.run ()
ayrton.runner.environ.locals.update (sub_runner.environ.locals)

def unset (*args):
for k in args:
if k in ayrton.runner.environ.globals.keys ():
Expand Down
1 change: 1 addition & 0 deletions ayrton/tests/source.ay
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a= 42
43 changes: 43 additions & 0 deletions ayrton/tests/test_ayrton.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def testExitCodeNOK (self):
ayrton.main ('if not false (): print ("yes!")')
self.assertEqual (self.a.buffer.getvalue (), b'yes!\n')

def testOptionErrexit (self):
self.assertRaises (ayrton.CommandFailed,
ayrton.main, '''option ('errexit')
false ()
''')

def testOptionMinus_e (self):
self.assertRaises (ayrton.CommandFailed,
ayrton.main, '''option ('-e')
false ()
''')

def testOptionPlus_e (self):
ayrton.main ('''option ('+e')
false ()
''')

class MiscTests (unittest.TestCase):
setUp= setUpMockStdout
tearDown= tearDownMockStdout
Expand Down Expand Up @@ -169,3 +186,29 @@ def testRename (self):
def testWithCd (self):
ayrton.main ('import os.path\nwith cd ("bin"):\n print (os.path.split (pwd ())[-1])')
self.assertEqual (self.a.buffer.getvalue (), b'bin\n')

def testShift (self):
ayrton.main ('a= shift (); print (a)', argv=['test_script.ay', '42'])
self.assertEqual (self.a.buffer.getvalue (), b'42\n')

def testShifts (self):
ayrton.main ('a= shift (2); print (a)', argv=['test_script.ay', '42', '27'])
self.assertEqual (self.a.buffer.getvalue (), b"['42', '27']\n")

def testSource (self):
ayrton.main ('source ("ayrton/tests/source.ay"); print (a)')
self.assertEqual (self.a.buffer.getvalue (), b'42\n')

# SSH_CLIENT='127.0.0.1 55524 22'
# SSH_CONNECTION='127.0.0.1 55524 127.0.0.1 22'
# SSH_TTY=/dev/pts/14

def testRemote (self):
ayrton.main ('''a= 42
with remote ('localhost', allow_agent=False) as s:
print (SSH_CLIENT)
print (s[1].readlines ())''')
expected1= b'''[b'127.0.0.1 '''
expected2= b''' 22\\n']\n'''
self.assertEqual (self.a.buffer.getvalue ()[:len (expected1)], expected1)
self.assertEqual (self.a.buffer.getvalue ()[-len (expected2):], expected2)
2 changes: 1 addition & 1 deletion doc/examples/arc.ay
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

where= argv[2]

with ssh (where, allow_agent=False) as foo:
with remote (where, allow_agent=False) as foo:
# notice how we can reference to the local's argv
what= argv[1]
# we assume Audacious is running on display :0
Expand Down
5 changes: 4 additions & 1 deletion doc/examples/with_ssh.ay → doc/examples/with_remote.ay
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import random

a= 42

with ssh ('localhost', allow_agent=False) as s:
with remote ('localhost', allow_agent=False) as s:
import random

b= input ('gimme a number, bro!\n')
Expand All @@ -15,3 +15,6 @@ i.write (bytes ('%d\n' % random.randint (10, 100), 'ascii'))

print (o.readlines ())
print (e.readlines ())

for x in i, o, e:
x.close ()
Loading

0 comments on commit c83f9ae

Please sign in to comment.