Skip to content
Browse files

docs

  • Loading branch information...
1 parent fec50a6 commit 844692af70f1f6f4575dd58e48b2b9e4ed3b1fae @tomerfiliba committed May 11, 2012
Showing with 263 additions and 28 deletions.
  1. +200 −14 docs/cli.rst
  2. +40 −0 docs/remote.rst
  3. +20 −12 plumbum/cli.py
  4. +3 −2 tests/example.py
View
214 docs/cli.rst
@@ -81,12 +81,12 @@ CLI toolkit; it exposes methods of your CLI application as CLI-switches, allowin
invoked from the command line. Let's examine the following toy application::
class MyApp(cli.Application):
- @switch("--log-to-file", str)
+ @cli.switch("--log-to-file", str)
def log_to_file(self, filename):
"""Sets the file into which logs will be emitted"""
logger.addHandler(FileHandle(filename))
- @switch(["-r", "--root"])
+ @cli.switch(["-r", "--root"])
def allow_as_root(self):
"""If given, allow running as root"""
self._allow_root = True
@@ -100,47 +100,233 @@ for instance, ``$ ./myapp.py --log-to-file=/tmp/log`` would translate to a call
``app.log_to_file("/tmp/log")``. After all switches were processed, control passes to ``main``.
.. note::
- Methods' docstrings and argument names will be used to render the help message, keeping your
- code as `DRY <http://en.wikipedia.org/wiki/Don't_repeat_yourself>`_ as possible
+ Methods' docstrings and argument names will be used to render the help message, keeping your
+ code as `DRY <http://en.wikipedia.org/wiki/Don't_repeat_yourself>`_ as possible.
+
+ There's also :func:`autoswitch <plumbum.cli.autoswitch>`, which infers the name of the switch
+ from the function's name, e.g. ::
+
+ @cli.autoswitch(str)
+ def log_to_file(self, filename):
+ pass
+
+ Will bind the add the switch function to ``--log-to-file``.
Arguments
^^^^^^^^^
-As seen in the example above, switch functions may take a single argument.
+As demonstrated in the example above, switch functions may take no arguments (not counting
+``self``) or a single argument argument. If a switch function accepts an argument, it must
+specify the argument's *type*. If you require no special validation, simply pass ``str``;
+otherwise, you may pass any type (or any callable, in fact) that will take a string and convert
+it to a meaningful object. If conversion is not possible, the type (or callable) is expected to
+raise either ``TypeError` or ``ValueError``.
+
+For instance ::
+
+ class MyApp(cli.Application):
+ _port = 8080
+
+ @cli.switch(["-p"], int)
+ def server_port(self, port):
+ self._port = port
+
+ def main(self):
+ print self._port
+
+::
+
+ $ ./example.py -p 17
+ 17
+ $ ./example.py -p foo
+ Argument of -p expected to be <type 'int'>, not 'foo':
+ ValueError("invalid literal for int() with base 10: 'foo'",)
+
+The toolkit includes two additional "types" (or rather, *validators*): ``Range`` and ``Set``.
+``Range`` takes a minimal value and a maximal value and expects an integer in that range
+(inclusive). ``Set`` takes a set of allowed values, and expects the argument to match one of
+these values. Here's an example ::
+
+ class MyApp(cli.Application):
+ _port = 8080
+ _mode = "TCP"
+
+ @cli.switch("-p", cli.Range(1024,65535))
+ def server_port(self, port):
+ self._port = port
+
+ @cli.switch("-m", cli.Set("TCP", "UDP", case_sensitive = False))
+ def server_mode(self, mode):
+ self._mode = mode
+
+ def main(self):
+ print self._port, self._mode
+
+::
+
+ $ ./example.py -p 17
+ Argument of -p expected to be [1024..65535], not '17':
+ ValueError('Not in range [1024..65535]',)
+ $ ./example.py -m foo
+ Argument of -m expected to be Set('udp', 'tcp'), not 'foo':
+ ValueError("Expected one of ['UDP', 'TCP']",)
+
+Repeatable Switches
+^^^^^^^^^^^^^^^^^^^
+Many times, you would like to allow a certain switch to be given multiple times. For instance,
+in ``gcc``, you may give several include directories using ``-I``. By default, switches may
+only be given once, unless you allow multiple occurrences by passing ``list = True`` to the
+``switch`` decorator ::
+
+ class MyApp(cli.Application):
+ _dirs = []
+
+ @cli.switch("-I", str, list = True)
+ def include_dirs(self, dirs):
+ self._dirs = dirs
+
+ def main(self):
+ print self._dirs
-* Range
-* Set
+::
-List
-^^^^
+ $ ./example.py -I/foo/bar -I/usr/include
+ ['/foo/bar', '/usr/include']
+
+.. note::
+ The switch function will be called **only once**, and its argument will be a list of items
Mandatory Switches
^^^^^^^^^^^^^^^^^^
+If a certain switch is required, you can specify this by passing ``mandatory = True`` to the
+``switch`` decorator. The user will not be able to run the program without specifying a value
+for this switch.
Dependencies
^^^^^^^^^^^^
+Many time, the occurrence of a certain switch depends on the occurrence of another, e..g, it
+may not be possible to give ``-x`` without also giving ``-y``. This constraint can be achieved
+by specifying the ``requires`` keyword argument to the ``switch`` decorator; it is a list
+of switch names that this switch depends on. If the required switches are missing, the user
+will not be able to run the program. ::
+
+ class MyApp(cli.Application):
+ @cli.switch("--log-to-file", str)
+ def log_to_file(self, filename):
+ logger.addHandler(logging.FileHandler(filename))
+
+ @cli.switch("--verbose", requires = ["--log-to-file"])
+ def verbose(self):
+ logger.setLevel(logging.DEBUG)
+
+::
+
+ $ ./example --verbose
+ Given --verbose, the following are missing ['log-to-file']
+
+.. warning::
+ Currently, the toolkit doesn't go as far as computing a topological order on the switches given;
+ it invokes the switch functions at an arbitrary order. This will change in future releases.
Mutual Exclusion
^^^^^^^^^^^^^^^^^
+Just as some switches may depend on others, some switches mutually-exclude others. For instance,
+it does not make sense to allow ``--verbose`` and ``--terse``. For this purpose, you can set the
+``excludes`` list in the ``switch`` decorator. ::
+
+ class MyApp(cli.Application):
+ @cli.switch("--log-to-file", str)
+ def log_to_file(self, filename):
+ logger.addHandler(logging.FileHandler(filename))
+
+ @cli.switch("--verbose", requires = ["--log-to-file"], excludes = ["--terse"])
+ def verbose(self):
+ logger.setLevel(logging.DEBUG)
+
+ @cli.switch("--terse", requires = ["--log-to-file"], excludes = ["--verbose"])
+ def terse(self):
+ logger.setLevel(logging.WARNING)
+
+::
+
+ $ ./example --log-to-file=log.txt --verbose --terse
+ Given --verbose, the following are invalid ['--terse']
Grouping
^^^^^^^^
+If you wish to group certain switches together in the help message, you can specify
+``group = "Group Name"``, where ``Group Name`` is any string. When the help message is rendered,
+all the switches that belong to the same group will be grouped together. Note that grouping has
+no other effects on the way switches are processed, but it can help improve the readability of
+the help message.
Switch Attributes
-----------------
-* SwitchAttr
-* Flag
-* CountAttr
+Many times it's desired to simply store a switch's argument in an attribute, or set a flag if
+a certain switch is given. For this purpose, the toolkit provides
+:class:`SwitchAttr <plumbum.cli.SwitchAttr>`, which is `data descriptor
+<http://docs.python.org/howto/descriptor.html>`_ that stores the argument in an instance attribute.
+There are two additional "flavors" of ``SwitchAttr``: ``Flag`` (which toggles its default value
+if the switch is given) and ``CountingAttr`` (which counts the number of occurrences of the switch)
+::
+
+ class MyApp(cli.Application):
+ log_file = cli.SwitchAttr("--log-file", str, default = None)
+ enable_logging = cli.Flag("--no-log", default = True)
+ verbosity_level = cli.CountingAttr("-v")
+
+ def main(self):
+ print self.log_file, self.enable_logging, self.verbosity_level
+
+::
+
+ $ ./example.py -v --log-file=log.txt -v --no-log -vvv
+ log.txt False 5
Main
----
-* arguments
-* varargs
+The ``main()`` method is takes control once all the command-line switches have been processed.
+It may take any number of *positional argument*; for instance, in ``cp -r /foo /bar``,
+``/foo`` and ``/bar`` are the *positional arguments*. The number of positional arguments
+that the program would accept depends on the signature of the method: if the method takes 5
+arguments, 2 of which have default values, then at least 3 positional arguments must be supplied
+by the user and at most 5. If the method also takes varargs (``*args``), the number of
+arguments that may be given is unbound ::
+ class MyApp(cli.Application):
+ def main(self, src, dst, mode = "normal"):
+ print src, dst, mode
+::
+ $ ./example.py /foo /bar
+ /foo /bar normal
+ $ ./example.py /foo /bar spam
+ /foo /bar spam
+ $ ./example.py /foo
+ Expected at least 2 positional arguments, got ['/foo']
+ $ ./example.py /foo /bar spam bacon
+ Expected at most 3 positional arguments, got ['/foo', '/bar', 'spam', 'bacon']
+.. note::
+ The method's signature is also used to generate the help message, e.g. ::
+
+ Usage: [SWITCHES] src dst [mode='normal']
+
+With varargs::
+
+ class MyApp(cli.Application):
+ def main(self, src, dst, *eggs):
+ print src, dst, eggs
+::
+ $ ./example.py a b c d
+ a b ('c', 'd')
+ $ ./example.py --help
+ Usage: [SWITCHES] src dst eggs...
+ Meta-switches:
+ -h, --help Prints this help message and quits
+ -v, --version Prints the program's version and quits
View
40 docs/remote.rst
@@ -2,11 +2,51 @@
Remote
======
+Just like running local commands, Plumbum supports running commands on remote systems, by executing
+them over SSH.
.. _guide-remote-machines:
Remote Machines
---------------
+Forming a connection to a remote machine is very straight forward::
+
+ >>> from plumbum import SshMachine
+ >>> rem = SshMachine("hostname", user = "john", keyfile = "/path/to/idrsa")
+
+Only the ``hostname`` parameter is required, all other parameters are optional. If the host has
+your ``idrsa.pub`` key in its ``authorized_keys`` file, or if you've set up your ``~/.ssh/config``
+to login with some user and ``keyfile``, you can simply use ``rem = SshMachine("hostname")``.
+
+Much like the :ref:`local object <guide-local-machine>`, remote machines expose ``which()``,
+``path()``, ``python``, ``cwd`` and ``env``. You can also run remote commands, create SSH tunnels,
+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.
+
+Working Directory and Environment
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The ``cwd`` and ``env`` attributes can be used to inspect and manipulate the remote machine's
+working directory and environment variables, respectively.
+
+Tunneling
+^^^^^^^^^
+SSH tunneling is a very useful feature of the SSH protocol. It allows you to connect from your
+machine to a remote server process, while having your connection authenticated and encrypted
+out-of-the-box. Say you run on ``machine-A``, and you wish to connect to a server program
+running on ``machine-B``. That server program binds to ``localhost:8888`` (where ``localhost``
+refers naturally to to ``machine-B``). Using Plumbum, you can easily set up a tunnel from
+port 6666 on ``machine-A`` to port 8888 on ``machine-B``::
+
+ >>> tun = rem.tunnel(6666, 8888)
+
+Now you can connect a socket to ``localhost:6666`` (on ``machine-A``, that is), and it will
+be magically forwarded to ``machine-B:8888`` over SSH.
+
+ >>> import socket
+ >>> s = socket.socket()
+ >>> s.connect((""
+
+
.. _guide-remote-commands:
View
32 plumbum/cli.py
@@ -147,6 +147,14 @@ def deco(func):
return func
return deco
+def autoswitch(*args, **kwargs):
+ """A decorator that exposes a function as a switch, "inferring" the name of the switch
+ from the function's name (converting to lower-case, and replacing underscores by hyphens).
+ The arguments are the same as for :func:`switch <plumbum.cli.switch>`."""
+ def deco(func):
+ return switch(func.__name__.replace("_", "-"), *args, **kwargs)(func)
+ return deco
+
#===================================================================================================
# Switch Attributes
#===================================================================================================
@@ -200,12 +208,12 @@ def __init__(self, names, default = False, **kwargs):
def __call__(self, _):
self._value = not self._value
-class CountAttr(SwitchAttr):
+class CountingAttr(SwitchAttr):
"""A specialized `SwitchAttr` that counts the number of occurrences of the switch in
the command line. Usage::
class MyApp(Application):
- verbosity = CountAttr(["-v", "--verbose"], help = "The more, the merrier")
+ verbosity = CountingAttr(["-v", "--verbose"], help = "The more, the merrier")
If ``-v -v -vv`` is given in the command-line, it will result in ``verbosity = 4``.
@@ -257,14 +265,14 @@ class MyApp(Application):
comparison or not. The default is ``True``
"""
def __init__(self, *values, **kwargs):
- self.case_insensitive = kwargs.pop("case_insensitive", True)
+ self.case_sensitive = kwargs.pop("case_sensitive", False)
if kwargs:
raise TypeError("got unexpected keyword argument(s)", kwargs.keys())
- self.values = dict(((v if self.case_insensitive else v.lower()), v) for v in values)
+ self.values = dict(((v if self.case_sensitive else v.lower()), v) for v in values)
def __repr__(self):
- return "Set(%s)" % (", ".join(repr(v) for v in self.values))
+ return "Set(%s)" % (", ".join(repr(v) for v in self.values.values()))
def __call__(self, obj):
- if not self.case_insensitive:
+ if not self.case_sensitive:
obj = obj.lower()
if obj not in self.values:
raise ValueError("Expected one of %r" % (list(self.values.values()),))
@@ -435,14 +443,14 @@ def _parse_args(self, argv):
gotten = set(swfuncs.keys())
for func in gotten:
- missing = requirements[func] - gotten
+ missing = set(f.func for f in requirements[func]) - gotten
if missing:
raise SwitchCombinationError("Given %s, the following are missing %r" %
- (swfuncs[func][0], [swfuncs[f] for f in missing]))
- invalid = exclusions[func] & gotten
+ (swfuncs[func][0], [self._switches_by_func[f].names[0] for f in missing]))
+ invalid = set(f.func for f in exclusions[func]) & gotten
if invalid:
raise SwitchCombinationError("Given %s, the following are invalid %r" %
- (swfuncs[func][0], [swfuncs[f] for f in invalid]))
+ (swfuncs[func][0], [swfuncs[f][0] for f in invalid]))
m_args, m_varargs, _, m_defaults = inspect.getargspec(self.main)
max_args = six.MAXSIZE if m_varargs else len(m_args) - 1
@@ -506,8 +514,8 @@ def help(self): #@ReservedAssignment
m_args, m_varargs, _, m_defaults = inspect.getargspec(self.main)
tailargs = m_args[1:] # skip self
if m_defaults:
- for d, i in enumerate(reversed(m_defaults)):
- tailargs[-i] = "[%s=%r]" % (tailargs[-i], d)
+ for i, d in enumerate(reversed(m_defaults)):
+ tailargs[-i - 1] = "[%s=%r]" % (tailargs[-i - 1], d)
if m_varargs:
tailargs.append("%s..." % (m_varargs,))
tailargs = " ".join(tailargs)
View
5 tests/example.py
@@ -1,8 +1,9 @@
from plumbum import cli
class MyApp(cli.Application):
- PROGNAME = "Foobar"
- VERSION = "7.3"
+ def main(self, src, dst, *eggs):
+ print src, dst, eggs
+
if __name__ == "__main__":
MyApp.run(["", "-h"])

0 comments on commit 844692a

Please sign in to comment.
Something went wrong with that request. Please try again.