Command-Tools 0.1a2
('Framework for building python programs that are easily testable.',)
Command-Tools
-----------
Command-Tools is a framework for easily building python programs
that are testable without system side effects. For that, two assets
are provided:
- An abstraction layer that sits between the program logic and code
that usually talks to the operating system.
- A DSL for declaring constructs common to command line programs such
as options and positional arguments(built upon the optparse module)
The 'System' layer has an interface similar to the 'os' and 'sys'
modules, but frees the programmer from talking to these modules directly.
This has an important advantage: When testing, this layer is replaced
by a smart mock that behaves like a posix system for simple operations
like manipulating files and writing output. This mock layer uses python
data structures like 'dict' and 'StringIO' to keep state that
is affected by program execution. These data structures are also used
to verify if the program behavior is as expected.
Installation
------------
```bash
$ easy_install command-tools
```
Motivation
----------
The usual approach for testing code in isolation is using mocks for
recording calls/returning stubs. The disadvantage is that sometimes
the programmer ends up replicating a lot of the logic being tested when
the code is complex and invokes many external systems.
The approach offered here makes testing easier, for as long as the
code being tested uses the abstraction layer for 'system calls',
all assertions are made against the already-provided 'SystemMock'
class.
Besides providing these testing helpers, Command-Tools also does
a lot of grunt work when building command-line programs, like
collecting options/arguments interactively or via command switches.
Example
-------
This is a simple version of the unix 'cp' program for copying
files/directories:
```python
#!/usr/bin/env python
import sys
from command_tools import Command, Arg, Opt
class CopyCommand(Command):
# Positional arguments (required for program execution)
source = Arg(1, help='Source file/directory',
ask='What is the source file/directory?')
target = Arg(2, help='Target file/directory',
ask='What is the target file/directory?')
# Options, support most parameters from the optparse module
recursive = Opt('-R', action='store_true',
help='Copy directories recursively')
quiet = Opt('-q', action='store_false', dest='verbose',
help='Supress normal output')
def run(self, os, source, target, recursive=False, verbose=True):
source = os.join_path(os.pwd(), source)
target = os.join_path(os.pwd(), target)
if not os.exists(source):
os.errl("Source directory does not exist!")
os.exit(1)
basename = os.basename(source)
if os.exists(target):
target = os.join_path(target, basename)
if not os.isdir(source):
if os.exists(target) and os.isdir(target):
target = os.join_path(target, basename)
os.cp(source, target)
return
if os.exists(target):
os.errl("Target directory already exists")
os.exit(1)
os.mkdir(target)
for tree in os.walk(source):
base = tree[0]
tgt_base = base[len(source)+1:]
tgt_base = os.join_path(target, tgt_base)
subdirs = tree[1]
files = tree[2]
for f in files:
src = os.join_path(base, f)
tgt = os.join_path(tgt_base, f)
os.cp(src, tgt)
if verbose:
os.outl("'%s' -> '%s'" % (src, tgt))
if not recursive:
break
for d in subdirs:
src_dir = os.join_path(base, d)
tgt_dir = os.join_path(tgt_base, d)
os.mkdir(tgt_dir)
if verbose:
os.outl("'%s' -> '%s'" % (src_dir, tgt_dir))
if __name__ == '__main__':
cmd = CopyCommand()
cmd(sys.argv[1:])
```
Output from '-h':
```
Usage: cp.py [options] SOURCE TARGET
Positional arguments:
SOURCE Source file/directory
TARGET Target file/directory
Options:
-q, --quiet Supress normal output
-R, --recursive Copy directories recursively
-h, --help show this help message and exit
```
A few test cases:
```python
from cp import CopyCommand
from command_tools.system import SystemMock, Exit
class TestCopyCommand(object):
def setup(self):
self.os = SystemMock()
self.os._filesystem = {
'usr': {
'bin': {
'vim': 'Programmer best friend',
'sed': 'Non-interactive text editor',
'zsh': 'Best shell for unix systems'
},
'local': {
'bin': 'File'
}
},
'etc': {
'passwd': 'System users',
'group': 'System groups',
'apt': {
'apt.conf': 'apt configuration',
'apt.conf.d': {
'01.conf': 'conf 1',
'02.conf': 'conf 2'
}
}
},
'srv': {}
}
self.tgt = CopyCommand(progname='cp')
def test_copy_non_recursively(self):
os = self.os
self.tgt(['/etc', '/srv'], os=os)
assert set(os.ls('/srv')) == set(['etc'])
assert set(os.ls('/srv/etc')) == set(['passwd', 'group'])
def test_copy_recursively(self):
os = self.os
self.tgt(['/etc', '/srv', '-R'], os=os)
assert set(os.ls('/srv')) == set(['etc'])
assert set(os.ls('/srv/etc')) == set(['passwd', 'group', 'apt'])
assert set(os.ls('/srv/etc/apt')) == \
set(['apt.conf.d', 'apt.conf'])
assert set(os.ls('/srv/etc/apt/apt.conf.d')) == \
set(['01.conf', '02.conf'])
def test_verbose_output(self):
os = self.os
self.tgt(['/usr/bin', '/srv'], os=os)
e = set(["'/usr/bin/vim' -> '/srv/bin/vim'",
"'/usr/bin/sed' -> '/srv/bin/sed'",
"'/usr/bin/zsh' -> '/srv/bin/zsh'", ''])
assert e == set(os.stdout.getvalue().split('\n'))
def test_quiet_output(self):
os = self.os
self.tgt(['/usr/bin', '/srv', '-q'], os=os)
assert not os.stdout.getvalue()
def test_source_doesnt_exist(self):
os = self.os
try:
self.tgt(['/inexistent', '/srv'], os=os)
assert False
except Exit, ex:
expected = "Source directory does not exist!\n"
assert expected == os.stderr.getvalue()
```
More examples can be seen in the 'tests' directory.
| File | Type | Py Version | Uploaded on | Size | # downloads |
|---|---|---|---|---|---|
| Command-Tools-0.1a2.tar.gz (md5) | Source | 2012-01-19 | 16KB | 176 | |
- Author: Thiago de Arruda
- Home Page: http://github.com/tarruda/command-tools
- Keywords: command command-line utilties
- License: BSD
- Package Index Owner: tarruda
- DOAP record: Command-Tools-0.1a2.xml
