summaryrefslogtreecommitdiff
path: root/passlib
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-03-21 19:54:55 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-03-21 19:54:55 -0400
commitf6c3fb3c0878cc74a4467828418ffb3ccd0082b2 (patch)
tree855db1dae542695bd3a57c9a43860467a3ebfe77 /passlib
parenta8bd4647cabe847ee728944428962e947c6df7bd (diff)
downloadpasslib-f6c3fb3c0878cc74a4467828418ffb3ccd0082b2.tar.gz
cleaned up registry code, added more UTs.
Diffstat (limited to 'passlib')
-rw-r--r--passlib/base.py194
-rw-r--r--passlib/hash.py9
-rw-r--r--passlib/tests/_test_bad_register.py14
-rw-r--r--passlib/tests/test_base.py161
-rw-r--r--passlib/tests/test_utils.py13
-rw-r--r--passlib/unix.py2
-rw-r--r--passlib/utils/__init__.py21
7 files changed, 310 insertions, 104 deletions
diff --git a/passlib/base.py b/passlib/base.py
index 0284181..465bb21 100644
--- a/passlib/base.py
+++ b/passlib/base.py
@@ -27,16 +27,15 @@ from warnings import warn
#site
from pkg_resources import resource_string
#libs
-##import passlib.drivers.as _hmod
from passlib.utils import Undef, is_crypt_handler, splitcomma, rng
#pkg
#local
__all__ = [
- #global registry
- 'register_crypt_handler',
- 'register_crypt_location',
- 'get_crypt_handler',
- 'list_crypt_handlers'
+ #registry interface
+ "register_crypt_handler_path",
+ "register_crypt_handler",
+ "get_crypt_handler",
+ "list_crypt_handlers",
#contexts
'CryptPolicy',
@@ -44,9 +43,9 @@ __all__ = [
]
#=========================================================
-#proxy object
+#registry proxy object
#=========================================================
-class PasslibHashProxy(object):
+class PasslibRegistryProxy(object):
"""proxy module passlib.hash
this module is in fact an object which lazy-loads
@@ -58,35 +57,45 @@ class PasslibHashProxy(object):
def __getattr__(self, attr):
if attr.startswith("_"):
- raise AttributeError, "unknown attribute: %r" % (attr,)
+ raise AttributeError, "missing attribute: %r" % (attr,)
handler = get_crypt_handler(attr, None)
- if handler is None:
+ if handler:
+ return handler
+ else:
raise AttributeError, "unknown password hash: %r" % (attr,)
- setattr(self, attr, handler)
- return handler
+
+ def __setattr__(self, attr, value):
+ register_crypt_handler(value, name=attr)
def __repr__(self):
return "<proxy module 'passlib.hash'>"
def __dir__(self):
- "report list of all actual attrs, PLUS all known algorithms that haven't been loaded"
+ #add in handlers that will be lazy-loaded,
+ #otherwise this is std dir implementation
attrs = set(dir(self.__class__))
attrs.update(self.__dict__)
- attrs.update(_driver_locations)
+ attrs.update(_handler_locations)
return sorted(attrs)
-#NOTE: this is inserted into sys.modules by passlib/__init__.py
-_hashmod = PasslibHashProxy()
+ #=========================================================
+ #eoc
+ #=========================================================
-#=========================================================
-#global registry
-#=========================================================
+#singleton instance
+_proxy = PasslibRegistryProxy()
-#: dict mapping hash names -> loaded driver objects (uses passlib.hash.__dict__ so the two will always be in sync)
-_drivers = _hashmod.__dict__
+#==========================================================
+#internal registry state
+#==========================================================
-#: dict mapping hash names -> (module name, None | class name) that should be location of driver
-_driver_locations = {
+#: dict mapping name -> handler for all loaded handlers. uses proxy's dict so they stay in sync.
+_handlers = _proxy.__dict__
+
+#: dict mapping name -> (module path, attribute) for lazy-loading of handlers
+_handler_locations = {
+ #NOTE: this is a hardcoded list of the handlers built into passlib,
+ #applications should call register_crypt_handler_location() to add their own
"apr_md5_crypt": ("passlib.drivers.md5_crypt", "apr_md5_crypt"),
"bcrypt": ("passlib.drivers.bcrypt", "bcrypt"),
"bigcrypt": ("passlib.drivers.des_crypt", "bigcrypt"),
@@ -118,54 +127,103 @@ _driver_locations = {
"unix_fallback": ("passlib.drivers.misc", "unix_fallback"),
}
-def register_crypt_location(name, path):
- "register location to lazy-load driver when requested"
- global _driver_locations
+#: master regexp for detecting valid handler names
+_name_re = re.compile("^[a-z][_a-z0-9]{2,}$")
+
+#==========================================================
+#registry frontend functions
+#==========================================================
+def register_crypt_handler_path(name, path):
+ """register location to lazy-load handler when requested.
+
+ custom hashes may be registered via :func:`register_crypt_handler`,
+ or they may be registered by this function,
+ which will delay actually importing and loading the handler
+ until a call to :func:`get_crypt_handler` is made for the specified name.
+
+ :arg name: name of handler
+ :arg path: module import path
+
+ the specified module path should contain a password hash handler
+ called :samp:`{name}`, or the path may contain a semicolon,
+ specifying the module and module attribute to use.
+ """
+ global _handler_locations
if ':' in path:
modname, modattr = path.split(":")
else:
- modname = path
- modattr = None
- _driver_locations[name] = (modname, modattr)
+ modname, modattr = path, name
+ _handler_locations[name] = (modname, modattr)
+
+def register_crypt_handler(handler, force=False, name=None):
+ """register password hash handler.
-def register_crypt_handler(obj, force=False):
- "register CryptHandler handler"
- global _drivers
+ this method registers a handler with the internal passlib registry,
+ so that it will be returned by :func:`get_crypt_handler` when requested.
- #validate obj
- if not is_crypt_handler(obj):
- raise TypeError, "object does not appear to be a CryptHandler: %r" % (obj,)
- assert obj, "CryptHandlers must be boolean True: %r" % (obj,)
+ :arg handler: the password hash handler to register
+ :param force: force override of existing handler (defaults to False)
+
+ :raises KeyError:
+ if a (different) handler was already registered with
+ the same name, and ``force=True`` was not specified.
+ """
+ global _handlers, _name_re
+
+ #validate handler
+ if not is_crypt_handler(handler):
+ raise TypeError, "object does not appear to be a crypt handler: %r" % (handler,)
+ assert handler, "crypt handlers must be boolean True: %r" % (handler,)
+
+ #if name specified, make sure it matched
+ #(this is mainly used as a check to help __setattr__)
+ if name:
+ if name != handler.name:
+ raise ValueError, "handlers must be stored only under their own name"
+ else:
+ name = handler.name
#validate name
- name = obj.name
if not name:
raise ValueError, "name is null: %r" % (name,)
if name.lower() != name:
raise ValueError, "name must be lower-case: %r" % (name,)
- if re.search("[^_a-z0-9]",name):
- raise ValueError, "invalid characters in name (only underscore, a-z, 0-9 allowed): %r" % (name,)
+ if not _name_re.match(name):
+ raise ValueError, "invalid characters in name (must be 3+ characters, begin with a-z, and contain only underscore, a-z, 0-9): %r" % (name,)
#check for existing handler
- other = _drivers.get(name)
+ other = _handlers.get(name)
if other:
- if other is obj:
+ if other is handler:
return #already registered
if force:
log.warning("overriding previous handler registered to name %r: %r", name, other)
else:
- raise ValueError, "handler already registered for name %r: %r" % (name, other)
+ raise KeyError, "a handler has already registered for the name %r: %r (use force=True to override)" % (name, other)
- #put handler into hash module
- _drivers[name] = obj
- log.info("registered crypt handler %r: %r", name, obj)
+ #register handler in dict
+ _handlers[name] = handler
+ log.info("registered crypt handler %r: %r", name, handler)
def get_crypt_handler(name, default=Undef):
- "resolve crypt algorithm name"
- global _drivers, _driver_locations
+ """return handler for specified password hash scheme.
+
+ this method looks up a handler for the specified scheme.
+ if the handler is not already loaded,
+ it checks if the location of one is known (:func:`register_crypt_handler`)
+ and loads it first.
+
+ :arg name: name of handler to return
+ :param default: if specified, returns default value if no handler found.
+
+ :raises KeyError: if no handler matching that name is found, and no default specified
+
+ :returns: handler attached to name, or default if specified
+ """
+ global _handlers, _handler_locations
#check if handler loaded
- handler = _drivers.get(name)
+ handler = _handlers.get(name, None)
if handler:
return handler
@@ -176,56 +234,48 @@ def get_crypt_handler(name, default=Undef):
name = alt
#check if handler loaded
- handler = _drivers.get(name)
+ handler = _handlers.get(name)
if handler:
return handler
#check if lazy load mapping has been specified for this driver
- route = _driver_locations.get(name)
+ route = _handler_locations.get(name)
if route:
modname, modattr = route
#try to load the module - any import errors indicate runtime config,
- # either missing packages, or bad path provided to register_crypt_location()
+ # either missing packages, or bad path provided to register_crypt_handler_path()
mod = __import__(modname, None, None, ['dummy'], 0)
#first check if importing module triggered register_crypt_handler(),
- #though this is discouraged due to it's magical implicitness
- handler = _drivers.get(name)
+ #(though this is discouraged due to it's magical implicitness)
+ handler = _handlers.get(name)
if handler:
#XXX: issue deprecation warning here?
assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler)
return handler
- #if attribute specified, assume *that's* the handler, otherwise assume the module itself.
- if modattr:
- handler = getattr(mod, modattr)
- else:
- handler = mod
-
- #XXX: can this ever happen under legitimate circumstances?
- if handler.name != name:
- raise RuntimeError, "handler name does not match expected name: %r vs %r" % (handler.name, name)
-
- #run through register_crypt_handler, to validate it
- register_crypt_handler(handler)
-
+ #then get real handler & register it
+ handler = getattr(mod, modattr)
+ register_crypt_handler(handler, name=name)
return handler
- #TODO: check egg entry points under name "passlib.hash"
-
#fail!
if default is Undef:
raise KeyError, "no crypt handler found for algorithm: %r" % (name,)
else:
return default
-def list_crypt_handlers():
- "return sorted list of all known crypt algorithm names"
- return filter(lambda x: not x.startswith("_"), dir(_hashmod))
+def list_crypt_handlers(loaded_only=False):
+ "return sorted list of all known crypt handler names"
+ global _handlers, _handler_locations
+ names = set(_handlers)
+ if not loaded_only:
+ names.update(_handler_locations)
+ return sorted(names)
#=========================================================
-#policy
+#crypt policy
#=========================================================
def _parse_policy_key(key):
"helper to normalize & parse policy keys; returns ``(category, name, option)``"
diff --git a/passlib/hash.py b/passlib/hash.py
index d0d3520..a246996 100644
--- a/passlib/hash.py
+++ b/passlib/hash.py
@@ -3,7 +3,8 @@
NOTE:
this module does not actually contain any hashes.
this file is a stub which is replaced by a proxy object,
- which lazy-loads hashes as requested by calling get_crypt_handler().
+ which lazy-loads hashes as requested.
+
the actually implementations of hashes (at least, those built into passlib)
are stored in the passlib.drivers subpackage.
"""
@@ -18,10 +19,10 @@ NOTE:
#import proxy object, and replace this module with it.
#this should cause any import commands to return that object,
#not this module
-from passlib.base import _hashmod as hash
+from passlib.base import _proxy
import sys
-sys.modules['passlib.hash'] = hash
-del sys, hash
+sys.modules['passlib.hash'] = _proxy
+del sys, _proxy
#=========================================================
#eoc
diff --git a/passlib/tests/_test_bad_register.py b/passlib/tests/_test_bad_register.py
new file mode 100644
index 0000000..8554ee5
--- /dev/null
+++ b/passlib/tests/_test_bad_register.py
@@ -0,0 +1,14 @@
+"helper for method in test_base.py"
+
+from passlib.base import register_crypt_handler
+from passlib.utils.drivers import BaseHash
+
+class dummy_bad(BaseHash):
+ name = "dummy_bad"
+ setting_kwds = ()
+
+class alt_dummy_bad(BaseHash):
+ name = "dummy_bad"
+ setting_kwds = ()
+
+register_crypt_handler(alt_dummy_bad)
diff --git a/passlib/tests/test_base.py b/passlib/tests/test_base.py
index b3a1071..60bb9ba 100644
--- a/passlib/tests/test_base.py
+++ b/passlib/tests/test_base.py
@@ -9,10 +9,13 @@ from logging import getLogger
import os
import time
import warnings
+import sys
#site
#pkg
-from passlib import base, hash
-from passlib.base import CryptContext, CryptPolicy
+from passlib import hash, base
+from passlib.base import CryptContext, CryptPolicy, \
+ register_crypt_handler, register_crypt_handler_path, \
+ get_crypt_handler, list_crypt_handlers
from passlib.utils.drivers import BaseHash
from passlib.tests.utils import TestCase, mktemp, catch_warnings
from passlib.drivers.md5_crypt import md5_crypt as AnotherHash
@@ -21,25 +24,89 @@ from passlib.tests.test_utils_drivers import UnsaltedHash, SaltedHash
log = getLogger(__name__)
#=========================================================
-#proxy
+#test registry
#=========================================================
-class MiscTest(TestCase):
+class dummy_0(BaseHash):
+ name = "dummy_0"
+ setting_kwds = ()
+
+class alt_dummy_0(BaseHash):
+ name = "dummy_0"
+ setting_kwds = ()
+
+dummy_x = 1
+
+def unload_handler_name(name):
+ if hasattr(hash, name):
+ delattr(hash, name)
+
+ #NOTE: this messes w/ internals of registry, shouldn't be used publically.
+ paths = base._handler_locations
+ if name in paths:
+ del paths[name]
+
+class RegistryTest(TestCase):
+
+ case_prefix = "passlib registry"
def tearDown(self):
- if hasattr(hash, "dummy_1"):
- del hash.dummy_1
+ for name in ("dummy_0", "dummy_1", "dummy_x", "dummy_bad"):
+ unload_handler_name(name)
def test_hash_proxy(self):
+ "test passlib.hash proxy object"
dir(hash)
repr(hash)
self.assertRaises(AttributeError, getattr, hash, 'fooey')
+ def test_register_crypt_handler_path(self):
+ "test register_crypt_handler_path()"
+
+ #NOTE: this messes w/ internals of registry, shouldn't be used publically.
+ paths = base._handler_locations
+
+ #check namespace is clear
+ self.assertTrue('dummy_0' not in paths)
+ self.assertFalse(hasattr(hash, 'dummy_0'))
+
+ #try lazy load
+ register_crypt_handler_path('dummy_0', 'passlib.tests.test_base')
+ self.assertTrue('dummy_0' in list_crypt_handlers())
+ self.assertTrue('dummy_0' not in list_crypt_handlers(loaded_only=True))
+ self.assertIs(hash.dummy_0, dummy_0)
+ self.assertTrue('dummy_0' in list_crypt_handlers(loaded_only=True))
+ unload_handler_name('dummy_0')
+
+ #try lazy load w/ alt
+ register_crypt_handler_path('dummy_0', 'passlib.tests.test_base:alt_dummy_0')
+ self.assertIs(hash.dummy_0, alt_dummy_0)
+ unload_handler_name('dummy_0')
+
+ #check lazy load w/ wrong type fails
+ register_crypt_handler_path('dummy_x', 'passlib.tests.test_base')
+ self.assertRaises(TypeError, get_crypt_handler, 'dummy_x')
+
+ #check lazy load w/ wrong name fails
+ register_crypt_handler_path('alt_dummy_0', 'passlib.tests.test_base')
+ self.assertRaises(ValueError, get_crypt_handler, "alt_dummy_0")
+
+ #TODO: check lazy load which calls register_crypt_handler (warning should be issued)
+ sys.modules.pop("passlib.tests._test_bad_register", None)
+ register_crypt_handler_path("dummy_bad", "passlib.tests._test_bad_register")
+ with catch_warnings():
+ warnings.filterwarnings("ignore", "xxxxxxxxxx", DeprecationWarning)
+ h = get_crypt_handler("dummy_bad")
+ from passlib.tests import _test_bad_register as tbr
+ self.assertIs(h, tbr.alt_dummy_bad)
+
def test_register_crypt_handler(self):
- self.assertRaises(TypeError, base.register_crypt_handler, {})
+ "test register_crypt_handler()"
- self.assertRaises(ValueError, base.register_crypt_handler, BaseHash)
- self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="AB_CD")))
- self.assertRaises(ValueError, base.register_crypt_handler, type('x', (BaseHash,), dict(name="ab-cd")))
+ self.assertRaises(TypeError, register_crypt_handler, {})
+
+ self.assertRaises(ValueError, register_crypt_handler, BaseHash)
+ self.assertRaises(ValueError, register_crypt_handler, type('x', (BaseHash,), dict(name="AB_CD")))
+ self.assertRaises(ValueError, register_crypt_handler, type('x', (BaseHash,), dict(name="ab-cd")))
class dummy_1(BaseHash):
name = "dummy_1"
@@ -47,34 +114,34 @@ class MiscTest(TestCase):
class dummy_1b(BaseHash):
name = "dummy_1"
- self.assertTrue('dummy_1' not in base.list_crypt_handlers())
+ self.assertTrue('dummy_1' not in list_crypt_handlers())
- base.register_crypt_handler(dummy_1)
- base.register_crypt_handler(dummy_1)
- self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1)
+ register_crypt_handler(dummy_1)
+ register_crypt_handler(dummy_1)
+ self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
- self.assertRaises(ValueError, base.register_crypt_handler, dummy_1b)
- self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1)
+ self.assertRaises(KeyError, register_crypt_handler, dummy_1b)
+ self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
- base.register_crypt_handler(dummy_1b, force=True)
- self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1b)
+ register_crypt_handler(dummy_1b, force=True)
+ self.assertIs(get_crypt_handler("dummy_1"), dummy_1b)
- self.assertTrue('dummy_1' in base.list_crypt_handlers())
+ self.assertTrue('dummy_1' in list_crypt_handlers())
def test_get_crypt_handler(self):
+ "test get_crypt_handler()"
class dummy_1(BaseHash):
name = "dummy_1"
- self.assertRaises(KeyError, base.get_crypt_handler, "dummy_1")
- self.assertIs(base.get_crypt_handler("dummy_1", None), None)
+ self.assertRaises(KeyError, get_crypt_handler, "dummy_1")
- base.register_crypt_handler(dummy_1)
- self.assertIs(base.get_crypt_handler("dummy_1"), dummy_1)
+ register_crypt_handler(dummy_1)
+ self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
with catch_warnings():
warnings.filterwarnings("ignore", "handler names be lower-case, and use underscores instead of hyphens:.*", UserWarning)
- self.assertIs(base.get_crypt_handler("DUMMY-1"), dummy_1)
+ self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1)
#=========================================================
#
@@ -183,6 +250,26 @@ sha512_crypt.min_rounds = 45000
sha512_crypt__min_rounds=45000,
)
+ #-----------------------------------------------------
+ #sample 4 - category specific
+ #-----------------------------------------------------
+ sample_config_4s = """
+[passlib]
+schemes = sha512_crypt
+all.vary_rounds = 10%
+default.sha512_crypt.max_rounds = 20000
+admin.all.vary_rounds = 5%
+admin.sha512_crypt.max_rounds = 40000
+"""
+
+ sample_config_4pd = dict(
+ schemes = [ "sha512_crypt" ],
+ all__vary_rounds = "10%",
+ sha512_crypt__max_rounds = 20000,
+ admin__all__vary_rounds = "5%",
+ admin__sha512_crypt__max_rounds = 40000,
+ )
+
#=========================================================
#constructors
#=========================================================
@@ -232,6 +319,9 @@ sha512_crypt.min_rounds = 45000
policy = CryptPolicy.from_string(self.sample_config_1s)
self.assertEquals(policy.to_dict(), self.sample_config_1pd)
+ policy = CryptPolicy.from_string(self.sample_config_4s)
+ self.assertEquals(policy.to_dict(), self.sample_config_4pd)
+
def test_03_from_source(self):
"test CryptPolicy.from_source() constructor"
@@ -370,10 +460,26 @@ sha512_crypt.min_rounds = 45000
max_rounds = 50000,
))
+ p4 = CryptPolicy.from_string(self.sample_config_4s)
+ self.assertEquals(p4.get_options("sha512_crypt"), dict(
+ vary_rounds="10%",
+ max_rounds=20000,
+ ))
+
+ self.assertEquals(p4.get_options("sha512_crypt", "user"), dict(
+ vary_rounds="10%",
+ max_rounds=20000,
+ ))
+
+ self.assertEquals(p4.get_options("sha512_crypt", "admin"), dict(
+ vary_rounds="5%",
+ max_rounds=40000,
+ ))
+
def test_14_handler_is_deprecated(self):
"test handler_is_deprecated() method"
pa = CryptPolicy(**self.sample_config_1pd)
- pb = pa.replace(deprecated=["des_crypt", "bsdi_crypt"])
+ pb = pa.replace(deprecated=["des_crypt", "bsdi_crypt"], admin__context__deprecated=["des_crypt"])
self.assert_(not pa.handler_is_deprecated("des_crypt"))
self.assert_(not pa.handler_is_deprecated(hash.bsdi_crypt))
@@ -383,6 +489,11 @@ sha512_crypt.min_rounds = 45000
self.assert_(pb.handler_is_deprecated(hash.bsdi_crypt))
self.assert_(not pb.handler_is_deprecated("sha512_crypt"))
+ #check categories as well
+ self.assertTrue(pb.handler_is_deprecated("des_crypt", "user"))
+ self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin"))
+ self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "admin"))
+
#TODO: test this.
##def test_gen_min_verify_time(self):
## "test get_min_verify_time() method"
diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py
index 382500e..ad9dfaf 100644
--- a/passlib/tests/test_utils.py
+++ b/passlib/tests/test_utils.py
@@ -11,14 +11,25 @@ import random
#module
from passlib import utils
from passlib.base import CryptContext
-from passlib.utils import h64, des
+from passlib.utils import h64, des, Undef
from passlib.utils.md4 import md4
from passlib.tests.utils import TestCase, Params as ak, enable_option
+
#=========================================================
#byte funcs
#=========================================================
class UtilsTest(TestCase):
+ def test_undef(self):
+ "test Undef singleton"
+ self.assertEqual(repr(Undef), "<Undef>")
+ self.assertFalse(Undef==None,)
+ self.assertFalse(Undef==Undef,)
+ self.assertFalse(Undef==True,)
+ self.assertTrue(Undef!=None,)
+ self.assertTrue(Undef!=Undef,)
+ self.assertTrue(Undef!=True,)
+
def test_list_to_bytes(self):
self.assertFunctionResults(utils.list_to_bytes, [
#standard big endian
diff --git a/passlib/unix.py b/passlib/unix.py
index 30bf955..6942033 100644
--- a/passlib/unix.py
+++ b/passlib/unix.py
@@ -4,7 +4,7 @@
#imports
#=========================================================
#pkg
-from passlib.base import CryptContext, register_crypt_handler
+from passlib.base import CryptContext
from passlib.utils.drivers import CryptHandler
#local
__all__ = [
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 619d492..00f27da 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -101,7 +101,26 @@ class classproperty(object):
## update_wrapper(wrapper, func)
## return classmethod(wrapper)
-Undef = object() #singleton used as default kwd value in some functions
+#NOTE: Undef is only used in *one* place now, could just remove it
+
+class UndefType(object):
+ _undef = None
+
+ def __new__(cls):
+ if cls._undef is None:
+ cls._undef = object.__new__(cls)
+ return cls._undef
+
+ def __repr__(self):
+ return '<Undef>'
+
+ def __eq__(self, other):
+ return False
+
+ def __ne__(self, other):
+ return True
+
+Undef = UndefType() #singleton used as default kwd value in some functions
#==========================================================
#protocol helpers