summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2011-08-08 15:41:56 +0100
committerLars Wirzenius <liw@liw.fi>2011-08-08 15:41:56 +0100
commita22c2dccaf10e610f9e34c724446fffe2465588c (patch)
tree9d991ff8a7d8321d8be16fbb2998e2b063d2ba70
parentd0aa60910d20bf6129f9c7cba5ea54c0f4806218 (diff)
parent9aaf91edeb34c585c765c8663965b61b0db6ad65 (diff)
downloadsummain-a22c2dccaf10e610f9e34c724446fffe2465588c.tar.gz
Add JSON and CSV output formats.
-rw-r--r--NEWS6
-rwxr-xr-xsummain137
-rw-r--r--summainlib.py39
-rw-r--r--summainlib_tests.py34
4 files changed, 134 insertions, 82 deletions
diff --git a/NEWS b/NEWS
index 5022fa6..1c8623d 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,12 @@ NEWS file for summain
=====================
+Version 0.10, released UNRELEASED
+---------------------------------
+
+* Added support for JSON and CSV output formats, in addition to the default
+ RFC822-like one. (See `--output-format` option.)
+
Version 0.9, released 2011-07-24
--------------------------------
diff --git a/summain b/summain
index 7082594..026a2ae 100755
--- a/summain
+++ b/summain
@@ -16,12 +16,90 @@
import cliapp
+import csv
+import json
import os
import sys
import summainlib
+class OutputFormat(object):
+
+ keys = ['Mtime', 'Mode', 'Ino', 'Dev', 'Nlink', 'Size',
+ 'Uid', 'Username', 'Gid', 'Group', 'Target']
+
+ def __init__(self, output, checksums, objects):
+ self.output = output
+ self.checksums = checksums
+ self.objects = objects
+
+ def write(self):
+ for name, o in self.objects:
+ self.write_object(name, o)
+
+ def write_object(self, name, o):
+ raise NotImplemented()
+
+
+class Rfc822(OutputFormat):
+
+ def write_object(self, name, o):
+ keys = self.keys + self.checksums
+ values = [('Name', name)]
+ values += [(k, o[k]) for k in keys if o[k] != '']
+ record = ''.join('%s: %s\n' % (k, v) for k, v in values if v != '')
+ self.output.write('%s\n' % record)
+
+
+class CSV(OutputFormat):
+
+ def __init__(self, output, checksums, objects):
+ OutputFormat.__init__(self, output, checksums, objects)
+ self.writer = csv.writer(output)
+ self.wrote_headings = False
+
+ def write_object(self, name, o):
+ keys = self.keys + self.checksums
+
+ if not self.wrote_headings:
+ self.writer.writerow(['Name'] + keys)
+ self.wrote_headings = True
+
+ values = [name] + [o[k] or '' for k in keys]
+ self.writer.writerow(values)
+
+
+class GeneratorList(list):
+
+ def __init__(self, gen):
+ self.gen = gen
+
+ def __len__(self):
+ return 1
+
+ def __iter__(self):
+ return iter(self.gen)
+
+
+class Json(OutputFormat):
+
+ def write(self):
+ gen = GeneratorList(self.dictify(name, o) for name, o in self.objects)
+ json.dump(gen, self.output, sort_keys=True, indent=1)
+ self.output.write('\n')
+
+ def dictify(self, name, o):
+ keys = self.keys + self.checksums
+
+ values = { 'Name': name }
+ for k in keys:
+ if o[k] != '':
+ values[k] = o[k]
+
+ return values
+
+
class Summain(cliapp.Application):
def add_settings(self):
@@ -39,6 +117,9 @@ class Summain(cliapp.Application):
'MD5, SHA1, SHA224, SHA256, SHA384, SHA512; '
'use once per checksum type '
'(default is SHA1)')
+ self.settings.choice(['output-format', 'f'],
+ ['rfc822', 'csv', 'json'],
+ 'choose output format (rfc822, csv, json)')
def files(self, root):
if os.path.isdir(root):
@@ -51,6 +132,12 @@ class Summain(cliapp.Application):
yield root
def process_args(self, args):
+ checksums = [x.upper()
+ for x in self.settings['checksum'] or ['SHA1']]
+ fmt = self.new_formatter(checksums, self.find_roots(args))
+ fmt.write()
+
+ def find_roots(self, roots):
relative = self.settings['relative-paths']
exclude = self.settings['exclude']
nn = summainlib.NumberNormalizer()
@@ -58,20 +145,44 @@ class Summain(cliapp.Application):
pn = summainlib.PathNormalizer(self.settings['secret'])
else:
pn = summainlib.SamePath()
- checksums = [x.upper()
- for x in self.settings['checksum'] or ['SHA1']]
- o = summainlib.FilesystemObject('.', nn, pn, exclude, checksums)
- for checksum in checksums:
- try:
- o[checksum]
- except KeyError:
- raise cliapp.AppException('Unknown checksum %s' % checksum)
- for root in args:
+
+ for root in roots:
for filename in self.files(root):
- o = summainlib.FilesystemObject(filename, nn, pn, exclude,
- checksums)
- self.output.write(o.format(root if relative else None))
- self.output.write('\n')
+ o = summainlib.FilesystemObject(filename, nn, pn, exclude)
+ if relative:
+ name = self.relative_path(root, o)
+ else:
+ name = o['Name']
+ yield name, o
+
+ def relative_path(self, root, o):
+ '''Return a path that is relative to root, if possible.
+
+ If pathname does not start with root, then return it
+ unmodified.
+
+ '''
+
+ if root.endswith(os.sep):
+ root2 = root
+ else:
+ root2 = root + os.sep
+ pathname = o['Name']
+ if pathname.startswith(root2):
+ return pathname[len(root2):]
+ elif pathname == root and o.isdir():
+ return '.'
+ else:
+ return pathname
+
+ def new_formatter(self, checksums, objects):
+ table = {
+ 'rfc822': Rfc822,
+ 'csv': CSV,
+ 'json': Json,
+ }
+ return table[self.settings['output-format']](self.output, checksums,
+ objects)
Summain(version=summainlib.__version__).run()
diff --git a/summainlib.py b/summainlib.py
index 5376e00..34dad47 100644
--- a/summainlib.py
+++ b/summainlib.py
@@ -112,13 +112,12 @@ class FilesystemObject(object):
'''
- def __init__(self, filename, nn, pn, exclude, checksums,
+ def __init__(self, filename, nn, pn, exclude,
stat_result=None, sha1=None, sha224=None,
sha256=None, sha384=None, sha512=None,
md5=None, open_file=None, readlink=None):
self._filename = filename
self._exclude = set(self._normalize_key(k) for k in exclude)
- self._checksums = checksums
self._pn = pn
self._nn = nn
self._md5 = md5 or hashlib.md5()
@@ -232,41 +231,7 @@ class FilesystemObject(object):
raise KeyError(key)
return self.values.get(key, '')
- def _isdir(self):
+ def isdir(self): # pragma: no cover
'''Is this a directory?'''
-
return stat.S_ISDIR(int(self['Mode'], 8))
- def relative_path(self, root):
- '''Return a path that is relative to root, if possible.
-
- If pathname does not start with root, then return it
- unmodified.
-
- '''
-
- if root.endswith(os.sep):
- root2 = root
- else:
- root2 = root + os.sep
- pathname = self['Name']
- if pathname.startswith(root2):
- return pathname[len(root2):]
- elif pathname == root and self._isdir():
- return '.'
- else:
- return pathname
-
- def format(self, root=None): # pragma: no cover
- if root is None:
- name = self['Name']
- else:
- name = self.relative_path(root)
-
- keys = (['Mtime', 'Mode', 'Ino', 'Dev', 'Nlink', 'Size',
- 'Uid', 'Username', 'Gid', 'Group', 'Target'] +
- self._checksums)
- values = [('Name', name)]
- values += [(k, self[k]) for k in keys if self[k] != '']
- return ''.join('%s: %s\n' % (k, v) for k, v in values if v != '')
-
diff --git a/summainlib_tests.py b/summainlib_tests.py
index 6015541..4ec3da5 100644
--- a/summainlib_tests.py
+++ b/summainlib_tests.py
@@ -85,13 +85,12 @@ class FilesystemObjectTests(unittest.TestCase):
self.nn = summainlib.NumberNormalizer()
self.pn = summainlib.SamePath()
self.exclude = []
- self.checksums = ['SHA1']
def new(self, name, mode=None):
if mode is not None:
self.st.st_mode = mode
return summainlib.FilesystemObject(name, self.nn, self.pn,
- self.exclude, self.checksums,
+ self.exclude,
stat_result=self.st,
sha1=FakeChecksummer(),
sha224=FakeChecksummer(),
@@ -168,35 +167,6 @@ class FilesystemObjectTests(unittest.TestCase):
def test_formats_target_correctly_for_regular_file(self):
self.assertEqual(self.new('foo')['Target'], '')
-
- def test_does_not_output_size_for_directory(self):
- fso = self.new('foo', mode=stat.S_IFDIR | 0777)
- output = fso.format()
- self.assert_('Size:' not in output)
-
- def test_relative_path_returns_path_if_not_starting_with_root(self):
- fso = self.new('/foo/bar')
- self.assertEqual(fso.relative_path('/yo'), '/foo/bar')
-
- def test_relative_path_returns_partial_path_if_starting_with_root(self):
- fso = self.new('/foo/bar')
- self.assertEqual(fso.relative_path('/foo'), 'bar')
-
- def test_relative_path_returns_dot_if_same_as_root_and_dir(self):
- fso = self.new('/foo/bar', mode=stat.S_IFDIR)
- self.assertEqual(fso.relative_path('/foo/bar'), '.')
-
- def test_relative_path_returns_path_if_same_as_root_and_not_dir(self):
- fso = self.new('/foo/bar', mode=stat.S_IFREG)
- self.assertEqual(fso.relative_path('/foo/bar'), '/foo/bar')
-
- def test_relative_path_returns_path_if_root_is_slashless_prefix(self):
- fso = self.new('/foobar', mode=stat.S_IFREG)
- self.assertEqual(fso.relative_path('/foo'), '/foobar')
-
- def test_relative_path_returns_partial_path_if_root_ends_in_slash(self):
- fso = self.new('/foo/bar', mode=stat.S_IFREG)
- self.assertEqual(fso.relative_path('/foo/'), 'bar')
def test_excludes_unwanted_fields_from_output(self):
self.exclude = ['mtime']
@@ -224,7 +194,7 @@ class FilesystemObjectNormalizedNumbersTests(unittest.TestCase):
st_uid=0, st_gid=0)
self.ino += 1
return summainlib.FilesystemObject(name, self.nn, self.pn,
- self.exclude, self.checksums,
+ self.exclude,
stat_result=st,
sha1=FakeChecksummer(),
sha224=FakeChecksummer(),