Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

python3 fixes, docs

  • Loading branch information...
commit 2ba2e548cc3104a081958748deb73f2af2613d18 1 parent c02c557
@tomerfiliba authored
View
1  .gitignore
@@ -3,7 +3,6 @@
*.pyc
__pycache__
.settings/
-
/dist
/plumbum.egg-info
/build
View
32 docs/api.rst
@@ -1,14 +1,19 @@
API Reference
=============
-plumbum.localcmd
+plumbum.cli
+-----------
+.. automodule:: plumbum.cli
+ :members:
+
+plumbum.commands
----------------
-.. automodule:: plumbum.localcmd
+.. automodule:: plumbum.commands
:members:
-plumbum.base
-------------
-.. automodule:: plumbum.base
+plumbum.local_machine
+---------------------
+.. automodule:: plumbum.local_machine
:members:
plumbum.path
@@ -16,13 +21,18 @@ plumbum.path
.. automodule:: plumbum.path
:members:
-plumbum.ssh
------------
-.. automodule:: plumbum.ssh
+plumbum.remote_machine
+----------------------
+.. automodule:: plumbum.remote_machine
+ :members:
+
+plumbum.session
+---------------
+.. automodule:: plumbum.session
:members:
-plumbum.remotecmd
------------------
-.. automodule:: plumbum.remotecmd
+plumbum.utils
+---------------
+.. automodule:: plumbum.utils
:members:
View
67 docs/cli.rst
@@ -0,0 +1,67 @@
+Command-Line Interface (CLI)
+============================
+
+The mirror image of executing programs with ease is writing CLI applications with ease.
+Python scripts normally work with ``optparse`` and the more recent ``argparse``, but they
+both offer a quite-limited and a very unintuitive/unpythonic way to work with command-line
+arguments.
+
+``plumbum.cli`` offers a different solution: instead of building parser objects and adding
+switches to them imperatively, you write a class, where methods or attributes correspond
+to switches, and the ``main`` method is the entry-point of the program. A simple program
+might look like so ::
+
+ from plumbum import cli
+
+ class MyApp(cli.Application):
+ verbose = cli.Flag(["v, "verbose"], help = "If given, I will be very talkative")
+
+ def main(self):
+ print "This is my application"
+ if self.verbose:
+ print "Yadda " * 200
+
+ if __name__ == "__main__":
+ MyApp.run()
+
+And here's a more interesting example
+
+Application
+-----------
+* PROGNAME, VERSION, DESCRIPTION, USAGE
+* docstring
+* run, main, help, version
+
+Interdependencies
+-----------------
+* Mandatory
+* Requires
+* Excludes
+
+Switch Groups
+-------------
+
+Tail Arguments
+--------------
+
+
+Attributes
+----------
+* SwitchAttr
+* ToggleAttr
+* Flag
+* CountAttr
+
+Argument Types
+--------------
+* Range
+* Set
+
+
+
+
+
+
+
+
+
View
9 docs/index.rst
@@ -40,11 +40,10 @@ There's much more Plumbum can do, so be sure to read on:
:maxdepth: 2
local_commands
- local_object
- local_path
- ssh
- remote_commands
- remote_path
+ local_machine
+ paths
+ remote_machine
+ cli
api
About
View
1  docs/local_object.rst → docs/local_machine.rst
@@ -1,4 +1,3 @@
-
The Local Object
================
So far we've only seen running local commands, but there's more to the ``local`` object than
View
3  docs/local_path.rst
@@ -1,3 +0,0 @@
-Paths
-=====
-
View
10 docs/paths.rst
@@ -0,0 +1,10 @@
+Local Paths
+===========
+
+
+
+Remote Paths
+============
+
+
+
View
0  docs/remote_commands.rst → docs/remote_machine.rst
File renamed without changes
View
2  docs/remote_path.rst
@@ -1,2 +0,0 @@
-Remote Path
-===========
View
3  docs/ssh.rst
@@ -1,3 +0,0 @@
-SSH
-===
-
View
29 plumbum/cli.py
@@ -1,6 +1,6 @@
import sys
-import inspect
import six
+import inspect
class SwitchError(Exception):
@@ -31,7 +31,7 @@ def __init__(self, **kwargs):
setattr(self, k, v)
def switch(names, argtype = None, argname = None, list = False, mandatory = False, requires = (),
- excludes = (), help = None, overridable = False, group = None):
+ excludes = (), help = None, overridable = False, group = "Switches"):
if isinstance(names, str):
names = [names]
names = [n.lstrip("-") for n in names]
@@ -105,11 +105,11 @@ def __call__(self, obj):
raise ValueError("Not in range [%d..%d]" % (self.start, self.end))
return obj
-class Enum(object):
+class Set(object):
def __init__(self, *values):
self.values = values
def __repr__(self):
- return "Enum(%s)" % (", ".join(self.values))
+ return "Set(%s)" % (", ".join(repr(v) for v in self.values))
def __call__(self, obj):
if obj not in self.values:
raise ValueError("Expected one of %r" % (self.values,))
@@ -122,7 +122,7 @@ def __call__(self, obj):
class Application(object):
PROGNAME = None
DESCRIPTION = None
- VERSION = "0.1"
+ VERSION = "1.0"
USAGE = "Usage: %(executable)s [SWITCHES] %(tailargs)s"
def __init__(self, executable):
@@ -261,29 +261,35 @@ def _parse_args(self, argv):
return swfuncs, tailargs
@classmethod
- def run(cls, argv = sys.argv):
+ def _run(cls, argv):
argv = list(argv)
inst = cls(argv.pop(0))
try:
swfuncs, tailargs = inst._parse_args(list(argv))
except ShowHelp:
inst.help()
- sys.exit(0)
+ return 0
except ShowVersion:
inst.version()
- sys.exit(0)
+ return 0
except SwitchError:
ex = sys.exc_info()[1] # compat
print(ex)
print()
inst.help()
- sys.exit(1)
+ return 1
+
for f, (_, a) in swfuncs.items():
if a is None:
f(inst)
else:
f(inst, a)
retcode = inst.main(*tailargs)
+ return inst, retcode
+
+ @classmethod
+ def run(cls, argv = sys.argv):
+ _, retcode = cls._run(argv)
sys.exit(retcode)
def main(self):
@@ -316,10 +322,7 @@ def help(self): #@ReservedAssignment
by_groups[si.group].append(si)
for grp, swinfos in sorted(by_groups.items(), key = lambda item: item[0]):
- if grp is None:
- print("Switches:")
- if grp is not None:
- print("%s:" % (grp,))
+ print("%s:" % (grp,))
for si in sorted(swinfos, key = lambda si: si.names):
swnames = ", ".join(("-" if len(n) == 1 else "--") + n for n in si.names
View
8 plumbum/local_machine.py
@@ -11,6 +11,7 @@
from plumbum.commands import CommandNotFound, ConcreteCommand
from plumbum.session import ShellSession
from types import ModuleType
+import stat
local_logger = logging.getLogger("plumbum.local")
@@ -295,7 +296,7 @@ def popen(self, args = (), stdin = PIPE, stdout = PIPE, stderr = PIPE, cwd = Non
argv = self.formulate(0, args)
local_logger.debug("Running %r", argv)
proc = Popen(argv, executable = str(self.executable), stdin = stdin, stdout = stdout,
- stderr = stderr, cwd = str(cwd), env = env, **kwargs)
+ stderr = stderr, cwd = str(cwd), env = env, **kwargs) #bufsize = 4096
proc.encoding = self.encoding
proc.argv = argv
return proc
@@ -333,7 +334,10 @@ def _which(cls, progname):
except OSError:
continue
if progname in filelist:
- return filelist[progname]
+ f = filelist[progname]
+ if not IS_WIN32 and not (f.stat().st_mode & stat.S_IXUSR):
+ continue
+ return f
return None
@classmethod
View
8 plumbum/remote_machine.py
@@ -33,10 +33,14 @@ def __str__(self):
@property
def basename(self):
- return str(self).rsplit("/", 1)[0]
+ if not "/" in str(self):
+ return str(self)
+ return str(self).rsplit("/", 1)[1]
@property
def dirname(self):
- raise NotImplementedError()
+ if not "/" in str(self):
+ return str(self)
+ return str(self).rsplit("/", 1)[0]
def _get_info(self):
return (self.remote, self._path)
View
27 plumbum/session.py
@@ -19,15 +19,19 @@ class MarkedPipe(object):
def __init__(self, pipe, marker):
self.pipe = pipe
self.marker = marker
+ if six.PY3:
+ self.marker = bytes(self.marker, "ascii")
def close(self):
- pass
+ self.pipe = None
def readline(self):
+ if self.pipe is None:
+ return six.b("")
line = self.pipe.readline()
- print ("!!", repr(line))
if not line:
raise EOFError()
if line.strip() == self.marker:
- return six.b("")
+ self.pipe = None
+ line = six.b("")
return line
class SessionPopen(object):
@@ -60,6 +64,7 @@ def communicate(self, input = None):
if input:
chunk = input[:1000]
self.stdin.write(chunk)
+ self.stdin.flush()
input = input[1000:]
i = (i + 1) % len(sources)
name, coll, pipe = sources[i]
@@ -108,10 +113,15 @@ def close(self):
return
try:
self.proc.stdin.write(six.b("\nexit\n\n\nexit\n\n"))
- self.proc.stdin.close()
+ self.proc.stdin.flush()
time.sleep(0.05)
except (ValueError, EnvironmentError):
pass
+ for p in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
+ try:
+ p.close()
+ except Exception:
+ pass
try:
self.proc.kill()
except EnvironmentError:
@@ -126,18 +136,19 @@ def popen(self, cmd, retcode = 0):
full_cmd = cmd.formulate(1)
else:
full_cmd = cmd
- marker = six.b("--.END%s.--" % (time.time() * random.random(),))
+ marker = "--.END%s.--" % (time.time() * random.random(),)
if full_cmd.strip():
full_cmd += " ; "
else:
- full_cmd = "echo -n ; "
- full_cmd += "echo $? ; echo %s" % (marker,)
+ full_cmd = "true ; "
+ full_cmd += "echo $? ; echo '%s'" % (marker,)
if not self.isatty:
- full_cmd += " ; echo %s 1>&2" % (marker,)
+ full_cmd += " ; echo '%s' 1>&2" % (marker,)
if self.encoding:
full_cmd = full_cmd.encode(self.encoding)
shell_logger.debug("Running %r", full_cmd)
self.proc.stdin.write(full_cmd + six.b("\n"))
+ self.proc.stdin.flush()
self._current = SessionPopen(full_cmd, self.isatty, self.proc.stdin,
MarkedPipe(self.proc.stdout, marker), MarkedPipe(self.proc.stderr, marker),
self.encoding)
View
0  tests/not-in-path/dummy-executable 100644 → 100755
File mode changed
View
3  tests/test_local.py
@@ -4,9 +4,6 @@
from plumbum import local, FG, BG, ERROUT
from plumbum import CommandNotFound, ProcessExecutionError
-#import logging
-#logging.basicConfig(level=logging.DEBUG)
-
class LocalMachineTest(unittest.TestCase):
def test_imports(self):
View
72 tests/test_remote.py
@@ -1,22 +1,68 @@
+from __future__ import with_statement
+import os
+import socket
import unittest
+import six
+from plumbum import SshMachine, BG
+import time
+#import logging
+#logging.basicConfig(level = logging.DEBUG)
+tunnel_prog = r"""
+import sys, six, socket
+s = socket.socket()
+s.bind(("", 0))
+s.listen(1)
+sys.stdout.write(six.b("%s\n" % (s.getsockname()[1],)))
+sys.stdout.flush()
+s2, _ = s.accept()
+data = s2.recv(100)
+s2.send(six.b("hello ") + data)
+s2.close()
+s.close()
+"""
+
class RemoteMachineTest(unittest.TestCase):
- def test_imports(self):
+ def test_remote(self):
+ with SshMachine("localhost") as rem:
+ r_ssh = rem["ssh"]
+ r_ls = rem["ls"]
+ r_grep = rem["grep"]
+
+ self.assertTrue(".bashrc" in r_ls("-a").splitlines())
+
+ with rem.cwd(os.path.dirname(__file__)):
+ cmd = r_ssh["localhost", "cd", rem.cwd, "&&", r_ls | r_grep["\\.py"]]
+ self.assertTrue("'|'" in str(cmd))
+ self.assertTrue("test_remote.py" in cmd())
+ self.assertTrue("test_remote.py" in [f.basename for f in rem.cwd // "*.py"])
+
+ def test_download_upload(self):
+ pass
+
+ def test_session(self):
pass
+
+ def test_tunnel(self):
+ with SshMachine("localhost") as rem:
+ p = (rem.python["-u"] << tunnel_prog).popen()
+ try:
+ port = int(p.stdout.readline().strip())
+ except ValueError:
+ print(p.communicate())
+ raise
-#if __name__ == "__main__":
-# import logging
-# logging.basicConfig(level = logging.DEBUG)
-# with SshMachine("hollywood.xiv.ibm.com") as r:
-# r.cwd.chdir(r.cwd / "workspace" / "plumbum")
-# #print r.cwd // "*/*.py"
-# r_ssh = r["ssh"]
-# r_ls = r["ls"]
-# r_grep = r["grep"]
-#
-# print (r_ssh["localhost", "cd", r.cwd, "&&", r_ls | r_grep[".py"]])()
-# r.close()
+ with rem.tunnel(12222, port) as tun:
+ time.sleep(0.5)
+ s = socket.socket()
+ s.connect(("localhost", 12222))
+ s.send(six.b("world"))
+ data = s.recv(100)
+ s.close()
+ self.assertEqual(data, six.b("hello world"))
+
+ p.communicate()
View
8 todo.txt
@@ -1,12 +1,8 @@
-* local.which: look only for +x bit
* remote.env: propagate to session and popen
-* python 3
- * encoding issues
- * shell session gets stuck
* tests
- * finish cli
+ * cli
* remote
* utils
* docs
+ * docstrings
* tutorial
- * add docstrings
Please sign in to comment.
Something went wrong with that request. Please try again.