Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding .get methods #205

Merged
merged 11 commits into from
Jul 23, 2015
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.5.1
-----
* Machines: Added ``.get()`` method for checking several commands.

1.5.0
-----
* Removed support for Python 2.5. (Travis CI does not support it anymore)
Expand Down
22 changes: 20 additions & 2 deletions docs/local_commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,31 @@ and serves as a factory for command objects::
<LocalCommand c:\windows\notepad.exe>

If you don't specify a full path, the program is searched for in your system's ``PATH`` (and if no
match is found, an exception is raised). Otherwise, the full path is used as given. Once you have
a ``Command`` object, you can execute it like a normal function::
match is found, a ``CommandNotFound`` exception is raised). Otherwise, the full path is used as given.
Once you have a ``Command`` object, you can execute it like a normal function::

>>> ls()
'README.rst\nplumbum\nsetup.py\ntests\ntodo.txt\n'
>>> ls("-a")
'.\n..\n.git\n.gitignore\n.project\n.pydevproject\nREADME.rst\n[...]'

.. _fallbacks:

If you use the ``.get()`` method instead of ``[]``, you can include fallbacks to try if the
first command does not exist on the machine. This can be used to get one of several
equivalent commands, or it can be used to check for common locations of a command if
not in the path. For example::

pandoc = local.get('pandoc',
'~/AppData/Local/Pandoc/pandoc.exe',
'/Program Files/Pandoc/pandoc.exe',
'/Program Files (x86)/Pandoc/pandoc.exe')

An exception is still raised if none of the commands are found. Unlike ``[]`` access,
an exception will be raised if the executable does not exist.

.. versionadded:: 1.5.1

.. _import-hack:

With just a touch of magic, you can *import* commands from the mock module ``cmd``, like so::
Expand Down Expand Up @@ -249,3 +266,4 @@ we run the command chain. As you can see, ``|`` and the backslashes have been qu
them from executing on the first-level shell; this way, they would safey get to the
second-level shell.


45 changes: 45 additions & 0 deletions plumbum/machines/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import with_statement
from plumbum.commands.processes import CommandNotFound


class BaseMachine(object):
"""This is a base class for other machines. It contains common code to
all machines in Plumbum."""


def get(self, cmd, *othercommands):
"""This works a little like the ``.get`` method with dict's, only
it supports an unlimited number of arguments, since later arguments
are tried as commands and could also fail. It
will try to call the first command, and if that is not found,
it will call the next, etc. Will raise if no file named for the
executable if a path is given, unlike ``[]`` access.

Usage::

best_zip = local.get('pigz','gzip')
"""
try:
command = self[cmd]
if not command.executable.exists():
raise CommandNotFound(cmd,command.executable)
else:
return command
except CommandNotFound:
if othercommands:
return self.get(othercommands[0],*othercommands[1:])
else:
raise

def __contains__(self, cmd):
"""Tests for the existance of the command, e.g., ``"ls" in plumbum.local``.
``cmd`` can be anything acceptable by ``__getitem__``.
"""
try:
self[cmd]
except CommandNotFound:
return False
else:
return True


15 changes: 3 additions & 12 deletions plumbum/machines/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from plumbum.lib import ProcInfo, IS_WIN32, six
from plumbum.commands.daemons import win32_daemonize, posix_daemonize
from plumbum.commands.processes import iter_lines
from plumbum.machines.base import BaseMachine
from plumbum.machines.env import BaseEnv

if sys.version_info >= (3, 2):
Expand Down Expand Up @@ -111,7 +112,7 @@ def popen(self, args = (), cwd = None, env = None, **kwargs):
#===================================================================================================
# Local Machine
#===================================================================================================
class LocalMachine(object):
class LocalMachine(BaseMachine):
"""The *local machine* (a singleton object). It serves as an entry point to everything
related to the local machine, such as working directory and environment manipulation,
command creation, etc.
Expand Down Expand Up @@ -192,6 +193,7 @@ def __getitem__(self, cmd):

ls = local["ls"]
"""

if isinstance(cmd, LocalPath):
return LocalCommand(cmd)
elif not isinstance(cmd, RemotePath):
Expand All @@ -204,17 +206,6 @@ def __getitem__(self, cmd):
else:
raise TypeError("cmd must not be a RemotePath: %r" % (cmd,))

def __contains__(self, cmd):
"""Tests for the existance of the command, e.g., ``"ls" in plumbum.local``.
``cmd`` can be anything acceptable by ``__getitem__``.
"""
try:
self[cmd]
except CommandNotFound:
return False
else:
return True

def _popen(self, executable, argv, stdin = PIPE, stdout = PIPE, stderr = PIPE,
cwd = None, env = None, new_session = False, **kwargs):
if new_session:
Expand Down
14 changes: 2 additions & 12 deletions plumbum/machines/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from plumbum.lib import _setdoc, ProcInfo, six
from plumbum.machines.local import LocalPath
from tempfile import NamedTemporaryFile
from plumbum.machines.base import BaseMachine
from plumbum.machines.env import BaseEnv
from plumbum.path.remote import RemotePath, RemoteWorkdir, StatRes

Expand Down Expand Up @@ -127,7 +128,7 @@ def __getattr__(self, name):
raise ClosedRemoteMachine("%r has been closed" % (self._obj,))


class BaseRemoteMachine(object):
class BaseRemoteMachine(BaseMachine):
"""Represents a *remote machine*; serves as an entry point to everything related to that
remote machine, such as working directory and environment manipulation, command creation,
etc.
Expand Down Expand Up @@ -236,17 +237,6 @@ def __getitem__(self, cmd):
else:
raise TypeError("cmd must not be a LocalPath: %r" % (cmd,))

def __contains__(self, cmd):
"""Tests for the existance of the command, e.g., ``"ls" in remote_machine``.
``cmd`` can be anything acceptable by ``__getitem__``.
"""
try:
self[cmd]
except CommandNotFound:
return False
else:
return True

@property
def python(self):
"""A command that represents the default remote python interpreter"""
Expand Down
16 changes: 14 additions & 2 deletions tests/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from plumbum import CommandNotFound, ProcessExecutionError, ProcessTimedOut
from plumbum.fs.atomic import AtomicFile, AtomicCounterFile, PidFile
from plumbum.path import RelativePath
import plumbum


if not hasattr(unittest, "skipIf"):
Expand Down Expand Up @@ -93,8 +94,12 @@ def test_read_write(self):

class LocalMachineTest(unittest.TestCase):
def test_getattr(self):
import plumbum
self.assertEqual(getattr(plumbum.cmd, 'does_not_exist', 1), 1)
pb = plumbum
self.assertEqual(getattr(pb.cmd, 'does_not_exist', 1), 1)
ls_cmd1 = pb.cmd.non_exist1N9 if hasattr(pb.cmd, 'non_exist1N9') else pb.cmd.ls
ls_cmd2 = getattr(pb.cmd, 'non_exist1N9', pb.cmd.ls)
self.assertEqual(str(ls_cmd1), str(local['ls']))
self.assertEqual(str(ls_cmd2), str(local['ls']))

def test_imports(self):
from plumbum.cmd import ls
Expand All @@ -110,6 +115,13 @@ def test_imports(self):
else:
self.fail("from plumbum.cmd import non_exist1N9")

def test_get(self):
self.assertEqual(str(local['ls']),str(local.get('ls')))
self.assertEqual(str(local['ls']),str(local.get('non_exist1N9', 'ls')))
self.assertRaises(CommandNotFound, lambda: local.get("non_exist1N9"))
self.assertRaises(CommandNotFound, lambda: local.get("non_exist1N9", "non_exist1N8"))
self.assertRaises(CommandNotFound, lambda: local.get("non_exist1N9", "/tmp/non_exist1N8"))

def test_cwd(self):
from plumbum.cmd import ls
self.assertEqual(local.cwd, os.getcwd())
Expand Down
16 changes: 11 additions & 5 deletions tests/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def wrapper(*args, **kwargs):
class RemotePathTest(unittest.TestCase):
def _connect(self):
return SshMachine(TEST_HOST)


def test_basename(self):
name = RemotePath(self._connect(), "/some/long/path/to/file.txt").basename
Expand All @@ -44,7 +43,7 @@ def test_dirname(self):
name = RemotePath(self._connect(), "/some/long/path/to/file.txt").dirname
self.assertTrue(isinstance(name, RemotePath))
self.assertEqual("/some/long/path/to", str(name))

def test_suffix(self):
p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt")
p2 = RemotePath(self._connect(), "file.tar.gz")
Expand All @@ -58,7 +57,7 @@ def test_suffix(self):
strcmp(p2.with_suffix(".other", 2), RemotePath(self._connect(), "file.other"))
strcmp(p2.with_suffix(".other", 0), RemotePath(self._connect(), "file.tar.gz.other"))
strcmp(p2.with_suffix(".other", None), RemotePath(self._connect(), "file.other"))

def test_newname(self):
p1 = RemotePath(self._connect(), "/some/long/path/to/file.txt")
p2 = RemotePath(self._connect(), "file.tar.gz")
Expand Down Expand Up @@ -212,6 +211,13 @@ def test_tunnel(self):

p.communicate()

def test_get(self):
with self._connect() as rem:
self.assertEqual(str(rem['ls']),str(rem.get('ls')))
self.assertEqual(str(rem['ls']),str(rem.get('not_a_valid_process_234','ls')))
self.assertTrue('ls' in rem)
self.assertFalse('not_a_valid_process_234' in rem)

def test_list_processes(self):
with self._connect() as rem:
self.assertTrue(list(rem.list_processes()))
Expand Down Expand Up @@ -266,7 +272,7 @@ def test_sshpass(self):
class TestParamikoMachine(unittest.TestCase, BaseRemoteMachineTest):
def _connect(self):
return ParamikoMachine(TEST_HOST, missing_host_policy = paramiko.AutoAddPolicy())

def test_tunnel(self):
with self._connect() as rem:
p = rem.python["-c", self.TUNNEL_PROG].popen()
Expand All @@ -275,7 +281,7 @@ def test_tunnel(self):
except ValueError:
print(p.communicate())
raise

s = rem.connect_sock(port)
s.send(six.b("world"))
data = s.recv(100)
Expand Down