Permalink
Browse files

update docs, version; move remote paths implementation into the remot…

…e machine class
  • Loading branch information...
1 parent 1d6fe6e commit 4f9dda9f91b07228d806a61cbc4d171d2da9282c @tomerfiliba committed Dec 7, 2012
View
3 CHANGELOG.rst
@@ -6,6 +6,9 @@
See also: `#43 <https://github.com/tomerfiliba/plumbum/issues/43>`_
* The "import hack" has moved to the package's ``__init__.py``, to make it importable directly
`#45 <https://github.com/tomerfiliba/plumbum/issues/45>`_
+* Paths now support ``chmod`` (on POSIX platform) `#49 <https://github.com/tomerfiliba/plumbum/pull/49>`_
+* The argument name of a ``SwitchAttr`` can now be given to it (defaults to ``VALUE``)
+ `#46 <https://github.com/tomerfiliba/plumbum/pull/46>`_
1.0.1
-----
View
16 README.rst
@@ -18,7 +18,6 @@ Now let's see some code!
Cheat Sheet
-----------
-
**Basics** ::
>>> from plumbum import local
@@ -29,11 +28,11 @@ Cheat Sheet
u'build.py\ndist\ndocs\nLICENSE\nplumbum\nREADME.rst\nsetup.py\ntests\ntodo.txt\n'
>>> notepad = local["c:\\windows\\notepad.exe"]
>>> notepad() # Notepad window pops up
- u''
+ u'' # Notepad window is closed by user, command returns
Instead of writing ``xxx = local["xxx"]`` for every program you wish to use, you can
-also import commands::
-
+also :ref:`import commands <import-hack>`:
+
>>> from plumbum.cmd import grep, wc, cat, head
>>> grep
LocalCommand(<LocalPath /bin/grep>)
@@ -83,8 +82,12 @@ also import commands::
lo Link encap:Local Loopback
UP LOOPBACK RUNNING MTU:16436 Metric:1
-**Remote commands (over SSH)** ::
-
+**Remote commands (over SSH)**
+
+Supports `openSSH <http://www.openssh.org/>`_-compatible clients,
+`PuTTY <http://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ (on Windows)
+and `Paramiko <https://github.com/paramiko/paramiko/>`_ (a pure-Python implementation of SSH2) ::
+
>>> from plumbum import SshMachine
>>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa")
>>> r_ls = remote["ls"]
@@ -112,6 +115,7 @@ also import commands::
print "Include dirs:", self.include_dirs
print "Compiling:", srcfiles
+
if __name__ == "__main__":
MyCompiler.run()
View
8 docs/_cheatsheet.rst
@@ -62,8 +62,12 @@ also :ref:`import commands <import-hack>`:
lo Link encap:Local Loopback
UP LOOPBACK RUNNING MTU:16436 Metric:1
-**Remote commands (over SSH)** ::
-
+**Remote commands (over SSH)**
+
+Supports `openSSH <http://www.openssh.org/>`_-compatible clients,
+`PuTTY <http://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ (on Windows)
+and `Paramiko <https://github.com/paramiko/paramiko/>`_ (a pure-Python implementation of SSH2) ::
+
>>> from plumbum import SshMachine
>>> remote = SshMachine("somehost", user = "john", keyfile = "/path/to/idrsa")
>>> r_ls = remote["ls"]
View
2 docs/api/remote_machine.rst
@@ -5,7 +5,7 @@ plumbum.remote_machine
:special-members:
plumbum.paramiko_machine
-======================
+========================
.. automodule:: plumbum.paramiko_machine
:members:
:special-members:
View
2 docs/cli.rst
@@ -110,7 +110,7 @@ for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call
def log_to_file(self, filename):
pass
- Will bind the add the switch function to ``--log-to-file``.
+ Will bind the switch function to ``--log-to-file``.
Arguments
^^^^^^^^^
View
2 docs/index.rst
@@ -132,4 +132,4 @@ the two libraries go in different directions, where Plumbum attempts to provide
wholesome approach.
Plumbum also pays tribute to `Rotem Yaari <https://github.com/vmalloc/>`_ who suggested a
-library code-named ``pyplatform`` for that very same purpose, but which had never materialized.
+library code-named ``pyplatform`` for that very purpose, but which had never materialized.
View
13 docs/remote.rst
@@ -22,6 +22,7 @@ Or as a context-manager::
... pass
.. note::
+
``SshMachine`` requires ``ssh`` (``openSSH`` or compatible) installed on your system in order
to connect to remote machines. Alternatively, you can use the pure-Python implementation of
:ref:`ParamikoMachine <guide-paramiko-machine>`.
@@ -35,6 +36,14 @@ Much like the :ref:`local object <guide-local-machine>`, remote machines expose
upload/download files, etc. You may also refer to :class:`the full API
<plumbum.remote_machine.SshMachine>`, as this guide will only survey the features.
+.. note::
+
+ `PuTTY <http://www.chiark.greenend.org.uk/~sgtatham/putty/>`_ users on Windows should use
+ the dedicated :class:`PuttyMachine <plumbum.remote_machine.PuttyMachine>` instead of
+ ``SshMachine``. See also :ref:`ParamikoMachine <guide-paramiko-machine>`.
+
+ .. versionadded:: 1.0.1
+
Working Directory and Environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``cwd`` and ``env`` attributes represent the remote machine's working directory and environment
@@ -140,8 +149,8 @@ Which is even more efficient (no need to send data back and forth over SSH).
.. _guide-paramiko-machine:
-Paramiko Remote Machine
------------------------
+Paramiko Machine
+----------------
.. versionadded:: 1.1
``SshMachine`` relies on the system's ``ssh`` client to run commands; this means that for each
View
3 plumbum/__init__.py
@@ -39,9 +39,10 @@
from plumbum.path import Path
from plumbum.local_machine import local, LocalPath
from plumbum.remote_machine import SshMachine, RemotePath, PuttyMachine
-from plumbum.version import version as __version__
+from plumbum.version import version
__author__ = "Tomer Filiba (tomerfiliba@gmail.com)"
+__version__ = version
#===================================================================================================
View
2 plumbum/cli.py
@@ -295,7 +295,7 @@ class MyApp(Application):
def __init__(self, *values, **kwargs):
self.case_sensitive = kwargs.pop("case_sensitive", False)
if kwargs:
- raise TypeError("got unexpected keyword argument(s)", kwargs.keys())
+ raise TypeError("got unexpected keyword argument(s): %r" % (kwargs.keys(),))
self.values = dict(((v if self.case_sensitive else v.lower()), v) for v in values)
def __repr__(self):
return "{%s}" % (", ".join(repr(v) for v in self.values.values()))
View
33 plumbum/commands.py
@@ -238,11 +238,12 @@ def popen(self, args = (), **kwargs):
"""Spawns the given command, returning a ``Popen``-like object.
.. note::
- When processes run in the **background** (either via ``popen`` or
- :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
- causing them to hang. If you know a process produces output, be sure to consume it
- every once in a while, using a monitoring thread/reactor in the background.
- For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
+
+ When processes run in the **background** (either via ``popen`` or
+ :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
+ causing them to hang. If you know a process produces output, be sure to consume it
+ every once in a while, using a monitoring thread/reactor in the background.
+ For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
:param args: Any arguments to be passed to the process (a tuple)
@@ -519,11 +520,12 @@ class BG(ExecutionModifier):
future = sleep[5] & BG(7) # a future expecting an exit code of 7
.. note::
- When processes run in the **background** (either via ``popen`` or
- :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
- causing them to hang. If you know a process produces output, be sure to consume it
- every once in a while, using a monitoring thread/reactor in the background.
- For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
+
+ When processes run in the **background** (either via ``popen`` or
+ :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
+ causing them to hang. If you know a process produces output, be sure to consume it
+ every once in a while, using a monitoring thread/reactor in the background.
+ For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
"""
__slots__ = []
def __rand__(self, cmd):
@@ -540,11 +542,12 @@ def __rand__(self, cmd):
future = sleep[5] & BG(7) # a future expecting an exit code of 7
.. note::
- When processes run in the **background** (either via ``popen`` or
- :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
- causing them to hang. If you know a process produces output, be sure to consume it
- every once in a while, using a monitoring thread/reactor in the background.
- For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
+
+ When processes run in the **background** (either via ``popen`` or
+ :class:`& BG <plumbum.commands.BG>`), their stdout/stderr pipes might fill up,
+ causing them to hang. If you know a process produces output, be sure to consume it
+ every once in a while, using a monitoring thread/reactor in the background.
+ For more info, see `#48 <https://github.com/tomerfiliba/plumbum/issues/48>`_
"""
class FG(ExecutionModifier):
View
22 plumbum/paramiko_machine.py
@@ -8,6 +8,7 @@
from plumbum.lib import _setdoc
from plumbum.local_machine import LocalPath
from plumbum.remote_path import RemotePath
+from plumbum.commands import shquote
logger = logging.getLogger("plumbum.paramiko")
@@ -245,6 +246,27 @@ def connect_sock(self, dport, dhost = "localhost", ipv6 = False):
chan = self._client.get_transport().open_channel('direct-tcpip', (dhost, dport), srcaddr)
return SocketCompatibleChannel(chan)
+ #
+ # Path implementation
+ #
+ def _path_listdir(self, fn):
+ return self.sftp.listdir(str(fn))
+
+ def _path_read(self, fn):
+ f = self.sftp.open(str(fn), 'rb')
+ data = f.read()
+ f.close()
+ #if self.encoding and isinstance(data, bytes) and not isinstance(data, str):
+ # data = data.decode(self.encoding)
+ return data
+ def _path_write(self, fn, data):
+ if self.encoding and isinstance(data, str) and not isinstance(data, bytes):
+ data = data.encode(self.encoding)
+ f = self.sftp.open(str(fn), 'wb')
+ f.write(data)
+ f.close()
+
+
###################################################################################################
# Make paramiko.Channel adhere to the socket protocol, namely, send and recv should fail
View
18 plumbum/path.py
@@ -8,6 +8,24 @@ def __new__(cls, val, name = None):
self.name = name
return self
+class StatRes(object):
+ """POSIX-like stat result"""
+ def __init__(self, tup):
+ self._tup = tuple(tup)
+ def __getitem__(self, index):
+ return self._tup[index]
+ mode = property(lambda self: self[0])
+ ino = property(lambda self: self[1])
+ dev = property(lambda self: self[2])
+ nlink = property(lambda self: self[3])
+ uid = property(lambda self: self[4])
+ gid = property(lambda self: self[5])
+ size = property(lambda self: self[6])
+ atime = property(lambda self: self[7])
+ mtime = property(lambda self: self[8])
+ ctime = property(lambda self: self[9])
+
+
class Path(object):
"""An abstraction over file system paths. This class is abstract, and the two implementations
are :class:`LocalPath <plumbum.local_machine.LocalPath>` and
View
71 plumbum/remote_machine.py
@@ -5,6 +5,8 @@
from plumbum.session import ShellSession
from plumbum.lib import _setdoc
from plumbum.local_machine import local, BaseEnv, LocalPath
+from plumbum.path import StatRes
+from tempfile import NamedTemporaryFile
class Workdir(RemotePath):
@@ -277,6 +279,69 @@ def tempdir(self):
finally:
dir.delete()
+ #
+ # Path implementation
+ #
+ def _path_listdir(self, fn):
+ files = self._session.run("ls -a %s" % (shquote(fn),))[1].splitlines()
+ files.remove(".")
+ files.remove("..")
+ return files
+ def _path_glob(self, fn, pattern):
+ matches = self._session.run("for fn in %s/%s; do echo $fn; done" % (fn, pattern))[1].splitlines()
+ if len(matches) == 1 and not self._path_stat(matches[0]):
+ return [] # pattern expansion failed
+ return matches
+
+ def _path_getuid(self, fn):
+ return self._session.run("stat -c '%u,%U' " + shquote(fn))[1].strip().split(",")
+ def _path_getgid(self, fn):
+ return self._session.run("stat -c '%g,%G' " + shquote(fn))[1].strip().split(",")
+ def _path_stat(self, fn):
+ rc, out, _ = self._session.run("stat -c '%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z' " + shquote(fn),
+ retcode = None)
+ if rc != 0:
+ return None
+ statres = out.strip().split(",")
+ text_mode = statres.pop(0).lower()
+ res = StatRes(statres)
+ res.text_mode = text_mode
+ return res
+
+ def _path_delete(self, fn):
+ self._session.run("rm -rf %s" % (shquote(fn),))
+ def _path_move(self, src, dst):
+ self._session.run("mv %s %s" % (shquote(src), shquote(dst)))
+ def _path_copy(self, src, dst):
+ self._session.run("cp -r %s %s" % (shquote(src), shquote(dst)))
+ def _path_mkdir(self, fn):
+ self._session.run("mkdir -p %s" % (shquote(fn),))
+ def _path_chmod(self, mode, fn):
+ self._session.run("chmod %o %s" % (mode, shquote(fn)))
+ def _path_chown(self, fn, owner, group, recursive):
+ args = ["chown"]
+ if recursive:
+ args.append("-R")
+ if owner is not None and group is not None:
+ args.append("%s:%s" % (owner, group))
+ elif owner is not None:
+ args.append(str(owner))
+ elif group is not None:
+ args.append(":%s" % (group,))
+ args.append(shquote(fn))
+ self._session.run(" ".join(args))
+
+ def _path_read(self, fn):
+ return self["cat"](fn)
+ def _path_write(self, fn, data):
+ if self.encoding and isinstance(data, str) and not isinstance(data, bytes):
+ data = data.encode(self.encoding)
+ with NamedTemporaryFile() as f:
+ f.write(data)
+ f.flush()
+ f.seek(0)
+ self.upload(f.name, fn)
+
class SshTunnel(object):
"""An object representing an SSH tunnel (created by
@@ -452,7 +517,9 @@ def upload(self, src, dst):
class PuttyMachine(SshMachine):
"""
PuTTY-flavored SSH connection. The programs ``plink`` and ``pscp`` are expected to
- be in the path (or you may supply
+ be in the path (or you may provide your own ``ssh_command`` and ``scp_command``)
+
+ Arguments are the same as for :class:`plumbum.remote_machine.SshMachine`
"""
def __init__(self, host, user = None, port = None, keyfile = None, ssh_command = None,
scp_command = None, ssh_opts = (), scp_opts = (), encoding = "utf8"):
@@ -468,7 +535,7 @@ def __init__(self, host, user = None, port = None, keyfile = None, ssh_command =
ssh_opts, scp_opts)
def __str__(self):
- return "ssh(putty)://%s" % (self._fqhost,)
+ return "putty-ssh://%s" % (self._fqhost,)
@_setdoc(BaseRemoteMachine)
def session(self, isatty = False):
View
84 plumbum/remote_path.py
@@ -1,11 +1,9 @@
from __future__ import with_statement
-import os
import errno
import six
from tempfile import NamedTemporaryFile
from plumbum.path import Path, FSUser
from plumbum.lib import _setdoc
-from plumbum.commands import shquote
if not six.PY3:
@@ -62,13 +60,13 @@ def dirname(self):
@property
@_setdoc(Path)
def uid(self):
- uid, name = self.remote._session.run("stat -c '%u,%U' " + shquote(self))[1].strip().split(",")
+ uid, name = self.remote._path_getuid(self)
return FSUser(int(uid), name)
@property
@_setdoc(Path)
def gid(self):
- gid, name = self.remote._session.run("stat -c '%g,%G' " + shquote(self))[1].strip().split(",")
+ gid, name = self.remote._path_getgid(self)
return FSUser(int(gid), name)
def _get_info(self):
@@ -82,115 +80,85 @@ def join(self, *parts):
def list(self):
if not self.isdir():
return []
- files = self.remote._session.run("ls -a %s" % (self,))[1].splitlines()
- files.remove(".")
- files.remove("..")
- return [self.join(fn) for fn in files]
+ return [self.join(fn) for fn in self.remote._path_listdir(self)]
@_setdoc(Path)
def isdir(self):
- res = self._stat(self)
+ res = self.remote._path_stat(self)
if not res:
return False
- return res[0] in ("directory")
+ return res.text_mode == "directory"
@_setdoc(Path)
def isfile(self):
- res = self._stat(self)
+ res = self.remote._path_stat(self)
if not res:
return False
- return res[0] in ("regular file", "regular empty file")
+ return res.text_mode in ("regular file", "regular empty file")
@_setdoc(Path)
def exists(self):
- return self._stat(self) is not None
-
- def _stat(self, path):
- rc, out, _ = self.remote._session.run(
- "stat -c '%F,%f,%i,%d,%h,%u,%g,%s,%X,%Y,%Z' " + shquote(path), retcode = None)
- if rc != 0:
- return None
- statres = out.strip().split(",")
- mode = statres.pop(0).lower()
- return mode, os.stat_result(statres)
+ return self.remote._path_stat(self) is not None
@_setdoc(Path)
def stat(self):
- res = self._stat(self)
+ res = self.remote._path_stat(self)
if res is None:
raise OSError(errno.ENOENT)
- return res[1]
+ return res
@_setdoc(Path)
def glob(self, pattern):
- matches = self.remote._session.run("for fn in %s/%s; do echo $fn; done" % (self, pattern))[1].splitlines()
- if len(matches) == 1 and not self._stat(matches[0]):
- return [] # pattern expansion failed
- return [RemotePath(self.remote, m) for m in matches]
+ return [RemotePath(self.remote, m) for m in self.remote._path_glob(self, pattern)]
@_setdoc(Path)
def delete(self):
if not self.exists():
return
- self.remote._session.run("rm -rf %s" % (shquote(self),))
+ self.remote._path_delete(self)
@_setdoc(Path)
def move(self, dst):
if isinstance(dst, RemotePath) and dst.remote is not self.remote:
raise TypeError("dst points to a different remote machine")
elif not isinstance(dst, six.string_types):
raise TypeError("dst must be a string or a RemotePath (to the same remote machine)")
- self.remote._session.run("mv %s %s" % (shquote(self), shquote(dst)))
+ self.remote._path_move(self, dst)
@_setdoc(Path)
def copy(self, dst, override = False):
if isinstance(dst, RemotePath):
if dst.remote is not self.remote:
raise TypeError("dst points to a different remote machine")
elif not isinstance(dst, six.string_types):
- raise TypeError("dst must be a string or a RemotePath (to the same remote machine)", repr(dst))
+ raise TypeError("dst must be a string or a RemotePath (to the same remote machine), "
+ "got %r" % (dst,))
if override:
if isinstance(dst, six.string_types):
dst = RemotePath(self.remote, dst)
dst.remove()
- self.remote._session.run("cp -r %s %s" % (shquote(self), shquote(dst)))
+ self.remote._path_copy(self, dst)
@_setdoc(Path)
def mkdir(self):
- self.remote._session.run("mkdir -p %s" % (shquote(self),))
+ self.remote._path_mkdir(self)
@_setdoc(Path)
def read(self):
- return self.remote["cat"](self)
-
+ return self.remote._path_read(self)
@_setdoc(Path)
def write(self, data):
- if self.remote.encoding and isinstance(data, str) and not isinstance(data, bytes):
- data = data.encode(self.remote.encoding)
- with NamedTemporaryFile() as f:
- f.write(data)
- f.flush()
- f.seek(0)
- self.remote.upload(f.name, self)
+ self.remote._path_write(self, data)
@_setdoc(Path)
def chown(self, owner=None, group=None, recursive=None):
- args = ["chown"]
- if recursive is None:
- recursive = self.isdir()
- if recursive:
- args.append("-R")
- if owner is not None and group is not None:
- args.append("%s:%s" % (owner, group))
- elif owner is not None:
- args.append(str(owner))
- elif group is not None:
- args.append(":%s" % (group,))
- args.append(shquote(self))
- self.remote._session.run(" ".join(args))
-
+ self.remote._path_chown(self, owner, group, self.isdir() if recursive is None else recursive)
@_setdoc(Path)
def chmod(self, mode):
- args = ["chmod", '%o' % mode, shquote(self)]
- self.remote._session.run(" ".join(args))
+ self.remote._path_chmod(mode, self)
+
+
+
+
+
View
2 plumbum/version.py
@@ -1,3 +1,3 @@
version = (1, 1, 0)
version_string = "1.1.0"
-release_date = "2012.12.01"
+release_date = "2012.12.15"
View
39 tests/test_remote.py
@@ -71,7 +71,21 @@ class BaseRemoteMachineTest(object):
s2.close()
s.close()
"""
-
+
+ def test_basic(self):
+ with self._connect() 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):
with self._connect() as rem:
rem.upload("test_remote.py", "/tmp")
@@ -115,21 +129,6 @@ class RemoteMachineTest(unittest.TestCase, BaseRemoteMachineTest):
def _connect(self):
return SshMachine(TEST_HOST)
- def test_remote(self):
- with self._connect() 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_tunnel(self):
with self._connect() as rem:
p = (rem.python["-u"] << self.TUNNEL_PROG).popen()
@@ -158,14 +157,6 @@ class TestParamikoMachine(unittest.TestCase, BaseRemoteMachineTest):
def _connect(self):
return ParamikoMachine(TEST_HOST, missing_host_policy = paramiko.AutoAddPolicy())
- def test_remote(self):
- with self._connect() as rem:
- r_ssh = rem["ssh"]
- r_ls = rem["ls"]
- r_grep = rem["grep"]
-
- self.assertTrue(".bashrc" in r_ls("-a").splitlines())
-
def test_tunnel(self):
with self._connect() as rem:
p = rem.python["-c", self.TUNNEL_PROG].popen()

0 comments on commit 4f9dda9

Please sign in to comment.