summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO33
-rw-r--r--doc/HOWTO3
-rw-r--r--lib/thandy/checkJson.py88
-rw-r--r--lib/thandy/tests.py69
4 files changed, 179 insertions, 14 deletions
diff --git a/TODO b/TODO
index 758361f..53e8e88 100644
--- a/TODO
+++ b/TODO
@@ -1,18 +1,19 @@
-03:07 < nickm> ok. tomorrow morning I try to get messaging sorted, and try to
- write up a registry-based exe version checker.
+- get messaging sorted
+o try to write up a registry-based exe version checker.,
+- Decouple install from check: they are not necessarily related.
-- Download improvements.
- - Back off on download failure.
- - Handle full stalled file in download.
+. Download improvements.
+ o Back off on download failure.
+ o Handle full stalled file in download.
- Use if-modified-since on timestamp
-- Write client-side code
- . Decide early if a python implementation will do for v1.
+o Write client-side code
+ o Decide early if a python implementation will do for v1.
o Adjust httplib, urllib2 to use socks4a.
o Check SOCKS package for suitability as basis for socks4a support?
- - Look into best packageing practices
+ o Look into best packaging practices
- . Write code to run, telling another process about status,
+ o Write code to run, telling another process about status,
eventually coming up with a list of packages to install or an
"A-OK" signal.
@@ -21,7 +22,7 @@
o DL-via-Tor
o Install-when-done
o Verbose output
- - quiet output.
+ o quiet output.
X Rendezvous-back with Tor when done.
- Better configurability: let users override mirrors, keys, etc.
@@ -41,9 +42,10 @@
- Testing
- Much bigger unit tests.
-- Bugs Roger keeps noticing
- - you can add the same role to a key twice.
- - wishlist item: thandy-pk dumpkey-all
+o Bugs Roger keeps noticing
+ o you can add the same role to a key twice.
+ o wishlist item: thandy-pk dumpkey-all
+ o Already there: Run "thandy-pk dumpkey" with no arguments
- Confusing tracebacks that could use a check and error message:
@@ -66,6 +68,10 @@ line 343, in decryptSecret
raise thandy.BadPassword()
thandy.BadPassword
+ [ I maintain that BadPassword _is_ an error message. I'll make it say
+ "Password Incorrect", though, so it looks less like a bug.]
+
+
when your ~/.thandy/timestamp_key didn't get the secret key dumped too,
thandy@moria:~$ thandy-server timestamp
Traceback (most recent call last):
@@ -90,3 +96,4 @@ __getattr__
return getattr(self.key, attr)
AttributeError: rsaKey instance has no attribute 'd'
+ [ Gives a more descriptive error now. ]
diff --git a/doc/HOWTO b/doc/HOWTO
index 572b821..71601b1 100644
--- a/doc/HOWTO
+++ b/doc/HOWTO
@@ -99,6 +99,9 @@ or something), run:
thandy-pk dumpkey --include-secret {KEYID}
+You can list as many or as few keys as you want. To dump _all_ keys, just
+omit the KEYID.
+
Generally speaking, you want to figure out what keys get what roles _before_
you start making the first keylist; otherwise you'll probably need to
re-export them.
diff --git a/lib/thandy/checkJson.py b/lib/thandy/checkJson.py
index b5fef3d..84ea9f2 100644
--- a/lib/thandy/checkJson.py
+++ b/lib/thandy/checkJson.py
@@ -6,7 +6,10 @@ import sys
import thandy
class Schema:
+ """A schema matches a set of possible Python objects, of types
+ that are encodable in JSON."""
def matches(self, obj):
+ """Return True if 'obj' matches this schema, False if it doesn't."""
try:
self.checkMatch(obj)
except thandy.FormatException:
@@ -15,10 +18,14 @@ class Schema:
return True
def checkMatch(self, obj):
+ """Raise thandy.FormatException if 'obj' does not match this schema.
+ Abstract method."""
raise NotImplemented()
class Any(Schema):
"""
+ Matches any single object.
+
>>> s = Any()
>>> s.matches("A String")
True
@@ -30,6 +37,8 @@ class Any(Schema):
class RE(Schema):
"""
+ Matches any string that matches a given regular expression.
+
>>> s = RE("h.*d")
>>> s.matches("hello world")
True
@@ -40,12 +49,22 @@ class RE(Schema):
>>> s.matches([33, "Hello"])
False
"""
- def __init__(self, pat=None, modifiers=0, reObj=None, reName="pattern"):
+ def __init__(self, pat=None, modifiers=0, reObj=None, reName=None):
+ """Make a new RE schema
+ pat -- The pattern to match, or None if reObj is provided.
+ modifiers -- Flags to use when compiling the pattern.
+ reObj -- A compiled regular expression object.
+ """
if not reObj:
if not pat.endswith("$"):
pat += "$"
reObj = re.compile(pat, modifiers)
self._re = reObj
+ if reName == None:
+ if pat != None:
+ reName = "pattern /%s/"%pat
+ else:
+ reName = "pattern"
self._reName = reName
def checkMatch(self, obj):
if not isinstance(obj, basestring) or not self._re.match(obj):
@@ -54,6 +73,8 @@ class RE(Schema):
class Str(Schema):
"""
+ Matches a particular string.
+
>>> s = Str("Hi")
>>> s.matches("Hi")
True
@@ -68,6 +89,8 @@ class Str(Schema):
class AnyStr(Schema):
"""
+ Matches any string, but no non-string object.
+
>>> s = AnyStr()
>>> s.matches("")
True
@@ -88,8 +111,36 @@ class AnyStr(Schema):
if not isinstance(obj, basestring):
raise thandy.FormatException("Expected a string; got %r"%obj)
+class OneOf(Schema):
+ """
+ Matches an object that matches any one of several schemas.
+
+ >>> s = OneOf([ListOf(Int()), Str("Hello"), Str("bye")])
+ >>> s.matches(3)
+ False
+ >>> s.matches("bye")
+ True
+ >>> s.matches([])
+ True
+ >>> s.matches([1,2])
+ True
+ >>> s.matches(["Hi"])
+ False
+ """
+ def __init__(self, alternatives):
+ self._subschemas = alternatives
+
+ def checkMatch(self, obj):
+ for m in self._subschemas:
+ if m.matches(obj):
+ return
+
+ raise thandy.FormatException("Object matched no recognized alternative")
+
class ListOf(Schema):
"""
+ Matches a homogenous list of some subschema.
+
>>> s = ListOf(RE("(?:..)*"))
>>> s.matches("hi")
False
@@ -101,6 +152,16 @@ class ListOf(Schema):
True
>>> s.matches(["This", "one", "is not"])
False
+
+ >>> s = ListOf(Int(), minCount=3, maxCount=10)
+ >>> s.matches([3]*2)
+ False
+ >>> s.matches([3]*3)
+ True
+ >>> s.matches([3]*10)
+ True
+ >>> s.matches([3]*11)
+ False
"""
def __init__(self, schema, minCount=0, maxCount=sys.maxint,listName="list"):
self._schema = schema
@@ -123,6 +184,8 @@ class ListOf(Schema):
class Struct(Schema):
"""
+ Matches a non-homogenous list of items.
+
>>> s = Struct([ListOf(AnyStr()), AnyStr(), Str("X")])
>>> s.matches(False)
False
@@ -136,6 +199,18 @@ class Struct(Schema):
False
>>> s.matches([[], "Q", "X", "Y"])
False
+
+ >>> s = Struct([Str("X")], allowMore=True)
+ >>> s.matches([])
+ False
+ >>> s.matches(["X"])
+ True
+ >>> s.matches(["X", "Y"])
+ True
+ >>> s.matches(["X", ["Y", "Z"]])
+ True
+ >>> s.matches([["X"]])
+ False
"""
def __init__(self, subschemas, allowMore=False, structName="list"):
self._subschemas = subschemas[:]
@@ -156,6 +231,10 @@ class Struct(Schema):
class DictOf(Schema):
"""
+ Matches a mapping from items matching a particular key-schema
+ to items matching a value-schema. Note that in JSON, keys must
+ be strings.
+
>>> s = DictOf(RE(r'[aeiou]+'), Struct([AnyStr(), AnyStr()]))
>>> s.matches("")
False
@@ -199,6 +278,9 @@ class Opt:
class Obj(Schema):
"""
+ Matches a dict from specified keys to key-specific types. Unrecognized
+ keys are allowed.
+
>>> s = Obj(a=AnyStr(), bc=Struct([Int(), Int()]))
>>> s.matches({'a':"ZYYY", 'bc':[5,9]})
True
@@ -234,6 +316,8 @@ class Obj(Schema):
class Int(Schema):
"""
+ Matches an integer.
+
>>> s = Int()
>>> s.matches(99)
True
@@ -262,6 +346,8 @@ class Int(Schema):
class Bool(Schema):
"""
+ Matches a boolean.
+
>>> s = Bool()
>>> s.matches(True) and s.matches(False)
True
diff --git a/lib/thandy/tests.py b/lib/thandy/tests.py
index e2fb767..924fc82 100644
--- a/lib/thandy/tests.py
+++ b/lib/thandy/tests.py
@@ -9,9 +9,25 @@ import thandy.keys
import thandy.formats
import thandy.repository
import thandy.checkJson
+import thandy.util
import thandy.tests
+def deltree(top):
+ for dirpath, dirnames, filenames in os.walk(top, topdown=False):
+ for f in filenames:
+ os.unlink(os.path.join(dirpath, f))
+ for d in dirnames:
+ os.rmdir(os.path.join(dirpath, d))
+ os.rmdir(top)
+
+def contents(fn, mode='rb'):
+ f = open(fn, mode)
+ try:
+ return f.read()
+ finally:
+ f.close()
+
class CanonicalEncodingTest(unittest.TestCase):
def test_encode(self):
enc = thandy.formats.encodeCanonical
@@ -47,6 +63,59 @@ class CryptoTests(unittest.TestCase):
ks2.load(passwd)
self.assertEquals(key1.key.n, ks2.getKey(key1.getKeyID()).key.n)
+class UtilTests(unittest.TestCase):
+ def setUp(self):
+ self._dir = tempfile.mkdtemp()
+ def tearDown(self):
+ deltree(self._dir)
+
+ def test_replaceFile(self):
+ fn1 = os.path.join(self._dir, "File1")
+ S1="Why do you curtsey, commoner? I presumed this would be anonymous."
+ S2="I am simply guaranteeing your commitment to my anonymity."
+ # -- WIGU adventures, 24 March 2005.
+ thandy.util.replaceFile(fn1, S1)
+ self.assertEquals(contents(fn1), S1)
+ thandy.util.replaceFile(fn1, S2)
+ self.assertEquals(contents(fn1), S2)
+
+ self.assertEquals(os.listdir(self._dir), [ "File1" ])
+
+ def test_moveFile(self):
+ d = self._dir
+ os.mkdir(os.path.join(d, "subdir"))
+ fn1 = os.path.join(d, "f1")
+ fn2 = os.path.join(d, "f2")
+ fn3 = os.path.join(d, "subdir", "f3")
+ S1="""We monitor all citizens constantly to detect insider baddies!
+ Isn't it wondersome?!"""
+ S2="""Wondersome yes... But could such a tactic instill a sense of
+ distrust and fear in a populace that is overwhelmingly true and
+ pious?"""
+ S3="""I think the fact that we are not currently under siege by
+ unscrupulous minions speaks for itself."""
+ # -- WIGU adventures, 22 January 2004
+
+ thandy.util.replaceFile(fn1, S1)
+ thandy.util.replaceFile(fn2, S2)
+ thandy.util.replaceFile(fn3, S3)
+
+ self.assertEquals(contents(fn1), S1)
+ self.assertTrue(os.path.exists(fn2))
+ self.assertTrue(os.path.exists(fn3))
+
+ thandy.util.moveFile(fn2, fn1)
+ self.assertEquals(contents(fn1), S2)
+ self.assertFalse(os.path.exists(fn2))
+
+ thandy.util.moveFile(fn1, fn3)
+ self.assertEquals(contents(fn3), S2)
+ self.assertFalse(os.path.exists(fn1))
+
+ self.assertEquals(os.listdir(d), ["subdir"])
+ self.assertEquals(os.listdir(os.path.join(d, "subdir")), ["f3"])
+
+
def suite():
suite = unittest.TestSuite()