summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-30 23:03:33 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-30 23:03:33 -0400
commitf44248b2890aab70633ce12209710e6de84638bd (patch)
tree0f94a61d0090c70f84b40829de377425b3044649
parent176153315bbd4ae3ec8542a5fc6704041d7de342 (diff)
downloadpasslib-f44248b2890aab70633ce12209710e6de84638bd.tar.gz
assorted bugfixes & additional test coverage
-rw-r--r--admin/benchmarks.py2
-rw-r--r--passlib/exc.py17
-rw-r--r--passlib/handlers/django.py2
-rw-r--r--passlib/handlers/scram.py2
-rw-r--r--passlib/tests/test_apps.py4
-rw-r--r--passlib/tests/test_handlers.py152
-rw-r--r--passlib/tests/test_hosts.py2
-rw-r--r--passlib/tests/test_registry.py18
-rw-r--r--passlib/tests/test_utils.py30
-rw-r--r--passlib/utils/__init__.py103
-rw-r--r--passlib/utils/compat.py1
11 files changed, 190 insertions, 143 deletions
diff --git a/admin/benchmarks.py b/admin/benchmarks.py
index beca3dd..4e4f9bb 100644
--- a/admin/benchmarks.py
+++ b/admin/benchmarks.py
@@ -166,7 +166,7 @@ def test_context_update():
"test speed of CryptContext.update()"
kwds = dict(
schemes = [ "sha512_crypt", "sha256_crypt", "md5_crypt",
- "des_crypt", "unix_fallback" ],
+ "des_crypt", "unix_disabled" ],
deprecated = [ "des_crypt" ],
sha512_crypt__min_rounds=4000,
)
diff --git a/passlib/exc.py b/passlib/exc.py
index a4a9899..3fd64fe 100644
--- a/passlib/exc.py
+++ b/passlib/exc.py
@@ -85,9 +85,6 @@ class PasslibRuntimeWarning(PasslibWarning):
class PasslibSecurityWarning(PasslibWarning):
"""Special warning issued when Passlib encounters something
that might affect security.
-
- The main reason this is issued is when Passlib's pure-python bcrypt
- backend is used, to warn that it's 20x too slow to acheive real security.
"""
#==========================================================================
@@ -104,7 +101,7 @@ def _get_name(handler):
return handler.name if handler else "<unnamed>"
#----------------------------------------------------------------
-# encrypt/verify parameter errors
+# generic helpers
#----------------------------------------------------------------
def type_name(value):
"return pretty-printed string containing name of value's type"
@@ -126,25 +123,33 @@ def ExpectedStringError(value, param):
"error message when param was supposed to be unicode or bytes"
return ExpectedTypeError(value, "unicode or bytes", param)
+#----------------------------------------------------------------
+# encrypt/verify parameter errors
+#----------------------------------------------------------------
def MissingDigestError(handler=None):
"raised when verify() method gets passed config string instead of hash"
name = _get_name(handler)
return ValueError("expected %s hash, got %s config string instead" %
(name, name))
+def NullPasswordError(handler=None):
+ "raised by OS crypt() supporting hashes, which forbid NULLs in password"
+ name = _get_name(handler)
+ return ValueError("%s does not allow NULL bytes in password" % name)
+
#----------------------------------------------------------------
# errors when parsing hashes
#----------------------------------------------------------------
def InvalidHashError(handler=None):
"error raised if unrecognized hash provided to handler"
- raise ValueError("not a valid %s hash" % _get_name(handler))
+ return ValueError("not a valid %s hash" % _get_name(handler))
def MalformedHashError(handler=None, reason=None):
"error raised if recognized-but-malformed hash provided to handler"
text = "malformed %s hash" % _get_name(handler)
if reason:
text = "%s (%s)" % (text, reason)
- raise ValueError(text)
+ return ValueError(text)
def ZeroPaddedRoundsError(handler=None):
"error raised if hash was recognized but contained zero-padded rounds field"
diff --git a/passlib/handlers/django.py b/passlib/handlers/django.py
index 3885d5c..229b885 100644
--- a/passlib/handlers/django.py
+++ b/passlib/handlers/django.py
@@ -224,7 +224,7 @@ class django_pbkdf2_sha256(DjangoVariableHash):
if isinstance(secret, unicode):
secret = secret.encode("utf-8")
hash = pbkdf2(secret, self.salt.encode("ascii"), self.rounds,
- keylen=-1, prf=self._prf)
+ keylen=None, prf=self._prf)
return b64encode(hash).rstrip().decode("ascii")
class django_pbkdf2_sha1(django_pbkdf2_sha256):
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index e423a1c..6e1b144 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -15,7 +15,7 @@ from passlib.exc import PasslibHashWarning
from passlib.utils import ab64_decode, ab64_encode, consteq, saslprep, \
to_native_str, xor_bytes, splitcomma
from passlib.utils.compat import b, bytes, bascii_to_str, iteritems, \
- itervalues, PY3, u, unicode
+ PY3, u, unicode
from passlib.utils.pbkdf2 import pbkdf2, get_prf, norm_hash_name
import passlib.utils.handlers as uh
#pkg
diff --git a/passlib/tests/test_apps.py b/passlib/tests/test_apps.py
index 5632087..5f0c10c 100644
--- a/passlib/tests/test_apps.py
+++ b/passlib/tests/test_apps.py
@@ -23,6 +23,10 @@ class AppsTest(TestCase):
# they mainly try to ensure no typos
# or dynamic behavior foul-ups.
+ def test_master_context(self):
+ ctx = apps.master_context
+ self.assertGreater(len(ctx.schemes()), 50)
+
def test_custom_app_context(self):
ctx = apps.custom_app_context
self.assertEqual(ctx.schemes(), ("sha512_crypt", "sha256_crypt"))
diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py
index a90daa3..d6d6ede 100644
--- a/passlib/tests/test_handlers.py
+++ b/passlib/tests/test_handlers.py
@@ -6,14 +6,15 @@ from __future__ import with_statement
#core
import hashlib
import logging; log = logging.getLogger(__name__)
+import os
import warnings
#site
#pkg
from passlib import hash
from passlib.utils import repeat_string
from passlib.utils.compat import irange, PY3, u, get_method_function
-from passlib.tests.utils import TestCase, HandlerCase, create_backend_case, \
- enable_option, b, catch_warnings, UserHandlerMixin, randintgauss
+from passlib.tests.utils import TestCase, HandlerCase, skipUnless, \
+ TEST_MODE, b, catch_warnings, UserHandlerMixin, randintgauss, EncodingHandlerMixin
#module
#=========================================================
@@ -33,7 +34,7 @@ def get_handler_case(scheme):
handler = get_crypt_handler(scheme)
if hasattr(handler, "backends") and not hasattr(handler, "wrapped"):
backend = handler.get_backend()
- name = "%s_%s_test" % (backend, scheme)
+ name = "%s_%s_test" % (scheme, backend)
else:
name = "%s_test" % scheme
return globals()[name]
@@ -133,7 +134,7 @@ class _bcrypt_test(HandlerCase):
'$2a$05$Z17AXnnlpzddNUvnC6cZNOSwMA/8oNiKnHTHTwLlBijfucQQlHjaG'),
]
- if enable_option("cover"):
+ if TEST_MODE("full"):
#
# add some extra tests related to 2/2a
#
@@ -153,8 +154,8 @@ class _bcrypt_test(HandlerCase):
])
known_correct_configs = [
- ('$2a$10$Z17AXnnlpzddNUvnC6cZNO', UPASS_TABLE,
- '$2a$10$Z17AXnnlpzddNUvnC6cZNOl54vBeVTewdrxohbPtcwl.GEZFTGjHe'),
+ ('$2a$04$uM6csdM8R9SXTex/gbTaye', UPASS_TABLE,
+ '$2a$04$uM6csdM8R9SXTex/gbTayezuvzFEufYGd2uB6of7qScLjQ4GwcD4G'),
]
known_unidentified_hashes = [
@@ -368,10 +369,8 @@ class _bcrypt_test(HandlerCase):
hash.bcrypt._no_backends_msg() #call this for coverage purposes
#create test cases for specific backends
-pybcrypt_bcrypt_test = create_backend_case(_bcrypt_test, "pybcrypt")
-bcryptor_bcrypt_test = create_backend_case(_bcrypt_test, "bcryptor")
-os_crypt_bcrypt_test = create_backend_case(_bcrypt_test, "os_crypt")
-builtin_bcrypt_test = create_backend_case(_bcrypt_test, "builtin")
+bcrypt_pybcrypt_test, bcrypt_bcryptor_test, bcrypt_os_crypt_test, bcrypt_builtin_test = \
+ _bcrypt_test.create_backend_cases(["pybcrypt", "bcryptor", "os_crypt", "builtin"])
#=========================================================
#bigcrypt
@@ -397,14 +396,23 @@ class bigcrypt_test(HandlerCase):
]
known_unidentified_hashes = [
- # one char short
+ # one char short (10 % 11)
"qiyh4XPJGsOZ2MEAyLkfWqe"
+
+ # one char too many (1 % 11)
+ "f8.SVpL2fvwjkAnxn8/rgTkwvrif6bjYB5cd"
]
# omit des_crypt from known_other since it's a valid bigcrypt hash too.
known_other_hashes = [row for row in HandlerCase.known_other_hashes
if row[0] != "des_crypt"]
+ def test_90_internal(self):
+ # check that _norm_checksum() also validates checksum size.
+ # (current code uses regex in parser)
+ self.assertRaises(ValueError, hash.bigcrypt, use_defaults=True,
+ checksum=u('yh4XPJGsOZ'))
+
#=========================================================
#bsdi crypt
#=========================================================
@@ -460,8 +468,8 @@ class _bsdi_crypt_test(HandlerCase):
super(_bsdi_crypt_test, self).setUp()
warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd.*")
-os_crypt_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "os_crypt")
-builtin_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "builtin")
+bsdi_crypt_os_crypt_test, bsdi_crypt_builtin_test = \
+ _bsdi_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
# cisco pix
@@ -602,10 +610,9 @@ class cisco_type7_test(HandlerCase):
handler(salt=None, use_defaults=True)
self.assertRaises(TypeError, handler, salt='abc')
self.assertRaises(ValueError, handler, salt=-10)
- with catch_warnings(record=True) as wlog:
+ with self.assertWarningList("salt/offset must be.*"):
h = handler(salt=100, relaxed=True)
- self.consumeWarningList(wlog, ["salt/offset must be.*"])
- self.assertEqual(h.salt, 52)
+ self.assertEqual(h.salt, 52)
#=========================================================
# crypt16
@@ -686,11 +693,8 @@ class _des_crypt_test(HandlerCase):
# darwin?
)
- def test_90_invalid_secret_chars(self):
- self.assertRaises(ValueError, self.do_encrypt, 'sec\x00t')
-
-os_crypt_des_crypt_test = create_backend_case(_des_crypt_test, "os_crypt")
-builtin_des_crypt_test = create_backend_case(_des_crypt_test, "builtin")
+des_crypt_os_crypt_test, des_crypt_builtin_test = \
+ _des_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
#django
@@ -955,6 +959,9 @@ class django_bcrypt_test(HandlerCase, _DjangoHelper):
return None
return ident
+django_bcrypt_test = skipUnless(hash.bcrypt.has_backend(),
+ "no bcrypt backends available")(django_bcrypt_test)
+
#=========================================================
#fshp
#=========================================================
@@ -1097,10 +1104,18 @@ class htdigest_test(UserHandlerMixin, HandlerCase):
'4dabed2727d583178777fab468dd1f17'),
]
+ known_unidentified_hashes = [
+ # bad char \/ - currently rejecting upper hex chars, may change
+ '939e7578edAe3c518a452acee763bce9',
+
+ # bad char \/
+ '939e7578edxe3c518a452acee763bce9',
+ ]
+
def test_80_user(self):
raise self.skipTest("test case doesn't support 'realm' keyword")
- def _insert_user(self, kwds, secret):
+ def populate_context(self, secret, kwds):
"insert username into kwds"
if isinstance(secret, tuple):
secret, user, realm = secret
@@ -1176,6 +1191,7 @@ class ldap_salted_sha1_test(HandlerCase):
]
class ldap_plaintext_test(HandlerCase):
+ # TODO: integrate EncodingHandlerMixin
handler = hash.ldap_plaintext
known_correct_hashes = [
("password", 'password'),
@@ -1226,8 +1242,8 @@ class _ldap_md5_crypt_test(HandlerCase):
'{CRYPT}$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!',
]
-os_crypt_ldap_md5_crypt_test = create_backend_case(_ldap_md5_crypt_test, "os_crypt")
-builtin_ldap_md5_crypt_test = create_backend_case(_ldap_md5_crypt_test, "builtin")
+ldap_md5_crypt_os_crypt_test, ldap_md5_crypt_builtin_test = \
+ _ldap_md5_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
#ldap_pbkdf2_{digest}
@@ -1266,7 +1282,7 @@ class ldap_pbkdf2_test(TestCase):
#=========================================================
# lanman
#=========================================================
-class lmhash_test(HandlerCase):
+class lmhash_test(EncodingHandlerMixin, HandlerCase):
handler = hash.lmhash
secret_size = 14
secret_case_insensitive = True
@@ -1290,23 +1306,30 @@ class lmhash_test(HandlerCase):
# ensures cp437 used for unicode
(u('ENCYCLOP\xC6DIA'), 'fed6416bffc9750d48462b9d7aaac065'),
(u('encyclop\xE6dia'), 'fed6416bffc9750d48462b9d7aaac065'),
- ]
- # TODO: test encoding keyword.
+ # test various encoding values
+ ((u("\xC6"), None), '25d8ab4a0659c97aaad3b435b51404ee'),
+ ((u("\xC6"), "cp437"), '25d8ab4a0659c97aaad3b435b51404ee'),
+ ((u("\xC6"), "latin-1"), '184eecbbe9991b44aad3b435b51404ee'),
+ ((u("\xC6"), "utf-8"), '00dd240fcfab20b8aad3b435b51404ee'),
+ ]
known_unidentified_hashes = [
# bad char in otherwise correct hash
'855c3697d9979e78ac404c4ba2c6653X',
]
- # override default list since lmhash uses cp437 as default encoding
- stock_passwords = [
- u("test"),
- b("test"),
- u("\u00AC\u00BA"),
- ]
-
- fuzz_password_alphabet = u('qwerty1234<>.@*#! \u00AC')
+ def test_90_raw(self):
+ "test lmhash.raw() method"
+ from binascii import unhexlify
+ from passlib.utils.compat import str_to_bascii
+ lmhash = self.handler
+ for secret, hash in self.known_correct_hashes:
+ kwds = {}
+ secret = self.populate_context(secret, kwds)
+ data = unhexlify(str_to_bascii(hash))
+ self.assertEqual(lmhash.raw(secret, **kwds), data)
+ self.assertRaises(TypeError, lmhash.raw, 1)
#=========================================================
#md5 crypt
@@ -1361,8 +1384,8 @@ class _md5_crypt_test(HandlerCase):
# darwin?
)
-os_crypt_md5_crypt_test = create_backend_case(_md5_crypt_test, "os_crypt")
-builtin_md5_crypt_test = create_backend_case(_md5_crypt_test, "builtin")
+md5_crypt_os_crypt_test, md5_crypt_builtin_test = \
+ _md5_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
# msdcc 1 & 2
@@ -1543,8 +1566,9 @@ class mssql2000_test(HandlerCase):
]
known_malformed_hashes = [
- # non-hex char ---\/
- '0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3',
+ # non-hex char -----\/
+ b('0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
+ u('0x01005B200543327G2E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B332752E1BC2E7C5DF0F9EBFE486E9BEE063E8D3B3'),
]
class mssql2005_test(HandlerCase):
@@ -1847,6 +1871,17 @@ class pbkdf2_sha1_test(HandlerCase):
'$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc'),
]
+ known_malformed_hashes = [
+ # zero padded rounds field
+ '$pbkdf2$01212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc',
+
+ # empty rounds field
+ '$pbkdf2$$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc',
+
+ # too many field
+ '$pbkdf2$1212$THDqatpidANpadlLeTeOEg$HV3oi1k5C5LQCgG1BMOL.BX4YZc$',
+ ]
+
class pbkdf2_sha256_test(HandlerCase):
handler = hash.pbkdf2_sha256
known_correct_hashes = [
@@ -1971,6 +2006,7 @@ class phpass_test(HandlerCase):
#plaintext
#=========================================================
class plaintext_test(HandlerCase):
+ # TODO: integrate EncodingHandlerMixin
handler = hash.plaintext
accepts_all_hashes = True
@@ -2092,11 +2128,15 @@ class scram_test(HandlerCase):
def test_90_algs(self):
"test parsing of 'algs' setting"
+ defaults = dict(salt=b('A')*10, rounds=1000)
def parse(algs, **kwds):
- return self.handler(algs=algs, use_defaults=True, **kwds).algs
+ for k in defaults:
+ kwds.setdefault(k, defaults[k])
+ return self.handler(algs=algs, **kwds).algs
# None -> default list
- self.assertEqual(parse(None), ["sha-1","sha-256","sha-512"])
+ self.assertEqual(parse(None, use_defaults=True), hash.scram.default_algs)
+ self.assertRaises(TypeError, parse, None)
# strings should be parsed
self.assertEqual(parse("sha1"), ["sha-1"])
@@ -2107,6 +2147,7 @@ class scram_test(HandlerCase):
# sha-1 required
self.assertRaises(ValueError, parse, ["sha-256"])
+ self.assertRaises(ValueError, parse, algs=[], use_defaults=True)
# alg names must be < 10 chars
self.assertRaises(ValueError, parse, ["sha-1","shaxxx-190"])
@@ -2115,6 +2156,18 @@ class scram_test(HandlerCase):
self.assertRaises(RuntimeError, parse, ['sha-1'],
checksum={"sha-1": b("\x00"*20)})
+ def test_90_checksums(self):
+ "test internal parsing of 'checksum' keyword"
+ # check non-bytes checksum values are rejected
+ self.assertRaises(TypeError, self.handler, use_defaults=True,
+ checksum={'sha-1': u('X')*20})
+
+ # check sha-1 is required
+ self.assertRaises(ValueError, self.handler, use_defaults=True,
+ checksum={'sha-256': b('X')*32})
+
+ # XXX: anything else that's not tested by the other code already?
+
def test_91_extract_digest_info(self):
"test scram.extract_digest_info()"
edi = self.handler.extract_digest_info
@@ -2296,8 +2349,8 @@ class _sha1_crypt_test(HandlerCase):
darwin=False,
)
-os_crypt_sha1_crypt_test = create_backend_case(_sha1_crypt_test, "os_crypt")
-builtin_sha1_crypt_test = create_backend_case(_sha1_crypt_test, "builtin")
+sha1_crypt_os_crypt_test, sha1_crypt_builtin_test = \
+ _sha1_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
#roundup
@@ -2367,7 +2420,7 @@ class _sha256_crypt_test(HandlerCase):
(u('with unic\u00D6de'), '$5$rounds=1000$IbG0EuGQXw5EkMdP$LQ5AfPf13KufFsKtmazqnzSGZ4pxtUNw3woQ.ELRDF4'),
]
- if enable_option("cover"):
+ if TEST_MODE("full"):
# builtin alg was changed in 1.6, and had possibility of fencepost
# errors near rounds that are multiples of 42. these hashes test rounds
# 1004..1012 (42*24=1008 +/- 4) to ensure no mistakes were made.
@@ -2390,6 +2443,9 @@ class _sha256_crypt_test(HandlerCase):
# zero-padded rounds
'$5$rounds=010428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3',
+
+ # extra "$"
+ '$5$rounds=10428$uy/jIAhCetNCTtb0$YWvUOXbkqlqhyoPMpN8BMe.ZGsGx2aBvxTvDFI613c3$',
]
known_correct_configs = [
@@ -2433,8 +2489,8 @@ class _sha256_crypt_test(HandlerCase):
# darwin ?,
)
-os_crypt_sha256_crypt_test = create_backend_case(_sha256_crypt_test, "os_crypt")
-builtin_sha256_crypt_test = create_backend_case(_sha256_crypt_test, "builtin")
+sha256_crypt_os_crypt_test, sha256_crypt_builtin_test = \
+ _sha256_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
#test sha512-crypt
@@ -2514,8 +2570,8 @@ class _sha512_crypt_test(HandlerCase):
platform_crypt_support = _sha256_crypt_test.platform_crypt_support
-os_crypt_sha512_crypt_test = create_backend_case(_sha512_crypt_test, "os_crypt")
-builtin_sha512_crypt_test = create_backend_case(_sha512_crypt_test, "builtin")
+sha512_crypt_os_crypt_test, sha512_crypt_builtin_test = \
+ _sha512_crypt_test.create_backend_cases(["os_crypt","builtin"])
#=========================================================
#sun md5 crypt
diff --git a/passlib/tests/test_hosts.py b/passlib/tests/test_hosts.py
index 33b2451..aebe28e 100644
--- a/passlib/tests/test_hosts.py
+++ b/passlib/tests/test_hosts.py
@@ -49,8 +49,6 @@ class HostsTest(TestCase):
self.check_unix_disabled(ctx)
def test_bsd_contexts(self):
- warnings.filterwarnings("ignore",
- "SECURITY WARNING: .*pure-python bcrypt.*")
for ctx in [
hosts.freebsd_context,
hosts.openbsd_context,
diff --git a/passlib/tests/test_registry.py b/passlib/tests/test_registry.py
index c076845..87c994f 100644
--- a/passlib/tests/test_registry.py
+++ b/passlib/tests/test_registry.py
@@ -16,7 +16,7 @@ from passlib import hash, registry
from passlib.registry import register_crypt_handler, register_crypt_handler_path, \
get_crypt_handler, list_crypt_handlers, _unload_handler_name as unload_handler_name
import passlib.utils.handlers as uh
-from passlib.tests.utils import TestCase, mktemp, catch_warnings
+from passlib.tests.utils import TestCase, catch_warnings
#module
log = getLogger(__name__)
@@ -88,6 +88,14 @@ class RegistryTest(TestCase):
self.assertTrue('dummy_0' not in paths)
self.assertFalse(hasattr(hash, 'dummy_0'))
+ # check invalid names are rejected
+ self.assertRaises(ValueError, register_crypt_handler_path,
+ "dummy_0", ".test_registry")
+ self.assertRaises(ValueError, register_crypt_handler_path,
+ "dummy_0", __name__ + ":dummy_0:xxx")
+ self.assertRaises(ValueError, register_crypt_handler_path,
+ "dummy_0", __name__ + ":dummy_0.xxx")
+
#try lazy load
register_crypt_handler_path('dummy_0', __name__)
self.assertTrue('dummy_0' in list_crypt_handlers())
@@ -155,16 +163,24 @@ class RegistryTest(TestCase):
class dummy_1(uh.StaticHandler):
name = "dummy_1"
+ # without available handler
self.assertRaises(KeyError, get_crypt_handler, "dummy_1")
self.assertIs(get_crypt_handler("dummy_1", None), None)
+ # already loaded handler
register_crypt_handler(dummy_1)
self.assertIs(get_crypt_handler("dummy_1"), dummy_1)
with catch_warnings():
warnings.filterwarnings("ignore", "handler names should be lower-case, and use underscores instead of hyphens:.*", UserWarning)
+
+ # already loaded handler, using incorrect name
self.assertIs(get_crypt_handler("DUMMY-1"), dummy_1)
+ # lazy load of unloaded handler, using incorrect name
+ register_crypt_handler_path('dummy_0', __name__)
+ self.assertIs(get_crypt_handler("DUMMY-0"), dummy_0)
+
#=========================================================
#EOF
#=========================================================
diff --git a/passlib/tests/test_utils.py b/passlib/tests/test_utils.py
index 4b03b0c..6a64b98 100644
--- a/passlib/tests/test_utils.py
+++ b/passlib/tests/test_utils.py
@@ -12,8 +12,8 @@ import warnings
#pkg
#module
from passlib.utils.compat import b, bytes, bascii_to_str, irange, PY2, PY3, u, \
- unicode, join_bytes
-from passlib.tests.utils import TestCase, Params as ak, enable_option, catch_warnings
+ unicode, join_bytes, SUPPORTS_DIR_METHOD
+from passlib.tests.utils import TestCase, catch_warnings
def hb(source):
return unhexlify(b(source))
@@ -26,6 +26,19 @@ class MiscTest(TestCase):
#NOTE: could test xor_bytes(), but it's exercised well enough by pbkdf2 test
+ def test_compat(self):
+ "test compat's lazymodule"
+ from passlib.utils import compat
+ # "<module 'passlib.utils.compat' from 'passlib/utils/compat.pyc'>"
+ self.assertRegex(repr(compat),
+ r"^<module 'passlib.utils.compat' from '.*?'>$")
+
+ # test synthentic dir()
+ dir(compat)
+ if SUPPORTS_DIR_METHOD:
+ self.assertTrue('UnicodeIO' in dir(compat))
+ self.assertTrue('irange' in dir(compat))
+
def test_classproperty(self):
from passlib.utils import classproperty
@@ -50,15 +63,12 @@ class MiscTest(TestCase):
self.assertTrue(".. deprecated::" in test_func.__doc__)
- with catch_warnings(record=True) as wlog:
+ with self.assertWarningList(dict(category=DeprecationWarning,
+ message="the function passlib.tests.test_utils.test_func() "
+ "is deprecated as of Passlib 1.6, and will be "
+ "removed in Passlib 1.8."
+ )):
self.assertEqual(test_func(1,2), (1,2))
- self.consumeWarningList(wlog,[
- dict(category=DeprecationWarning,
- message="the function passlib.tests.test_utils.test_func() "
- "is deprecated as of Passlib 1.6, and will be "
- "removed in Passlib 1.8."
- ),
- ])
def test_memoized_property(self):
from passlib.utils import memoized_property
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 4e29259..9593abb 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -12,7 +12,7 @@ import math
import os
import sys
import random
-if JYTHON:
+if JYTHON: # pragma: no cover -- runtime detection
# Jython 2.5.2 lacks stringprep module -
# see http://bugs.jython.org/issue1758320
try:
@@ -31,7 +31,7 @@ from warnings import warn
from passlib.exc import ExpectedStringError
from passlib.utils.compat import add_doc, b, bytes, join_bytes, join_byte_values, \
join_byte_elems, exc_err, irange, imap, PY3, u, \
- join_unicode, unicode, byte_elem_value, PY_MIN_32
+ join_unicode, unicode, byte_elem_value, PY_MIN_32, next_method_attr
#local
__all__ = [
# constants
@@ -138,9 +138,9 @@ def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True,
"""decorator to deprecate a function.
:arg msg: optional msg, default chosen if omitted
- :kwd deprecated: release where function was first deprecated
- :kwd removed: release where function will be removed
- :kwd replacement: name/instructions for replacement function.
+ :kwd deprecated: version when function was first deprecated
+ :kwd removed: version when function will be removed
+ :kwd replacement: alternate name / instructions for replacing this function.
:kwd updoc: add notice to docstring (default ``True``)
"""
if msg is None:
@@ -156,31 +156,36 @@ def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True,
msg += ", use %s instead" % replacement
msg += "."
def build(func):
- kwds = dict(
+ opts = dict(
mod=func.__module__,
name=func.__name__,
deprecated=deprecated,
removed=removed,
)
if _is_method:
- state = [None]
- else:
- state = [msg % kwds]
- def wrapper(*args, **kwds):
- text = state[0]
- if text is None:
+ def wrapper(*args, **kwds):
+ tmp = opts.copy()
klass = args[0].__class__
- kwds.update(klass=klass.__name__, mod=klass.__module__)
- text = state[0] = msg % kwds
- warn(text, DeprecationWarning, stacklevel=2)
- return func(*args, **kwds)
+ tmp.update(klass=klass.__name__, mod=klass.__module__)
+ warn(msg % tmp, DeprecationWarning, stacklevel=2)
+ return func(*args, **kwds)
+ else:
+ text = msg % opts
+ def wrapper(*args, **kwds):
+ warn(text, DeprecationWarning, stacklevel=2)
+ return func(*args, **kwds)
update_wrapper(wrapper, func)
if updoc and (deprecated or removed) and wrapper.__doc__:
- txt = "as of Passlib %s" % (deprecated,) if deprecated else ""
- if removed:
- if txt:
- txt += ", and "
- txt += "will be removed in Passlib %s" % (removed,)
+ txt = deprecated or ''
+ if removed or replacement:
+ txt += "\n "
+ if removed:
+ txt += "and will be removed in version %s" % (removed,)
+ if replacement:
+ if removed:
+ txt += ", "
+ txt += "use %s instead" % replacement
+ txt += "."
wrapper.__doc__ += "\n.. deprecated:: %s\n" % (txt,)
return wrapper
return build
@@ -190,59 +195,14 @@ def deprecated_method(msg=None, deprecated=None, removed=None, updoc=True,
"""decorator to deprecate a method.
:arg msg: optional msg, default chosen if omitted
- :kwd deprecated: release where function was first deprecated
- :kwd removed: release where function will be removed
- :kwd replacement: name/instructions for replacement method.
+ :kwd deprecated: version when method was first deprecated
+ :kwd removed: version when method will be removed
+ :kwd replacement: alternate name / instructions for replacing this method.
:kwd updoc: add notice to docstring (default ``True``)
"""
return deprecated_function(msg, deprecated, removed, updoc, replacement,
_is_method=True)
-##def relocated_function(target, msg=None, name=None, deprecated=None, mod=None,
-## removed=None, updoc=True):
-## """constructor to create alias for relocated function.
-##
-## :arg target: import path to target
-## :arg msg: optional msg, default chosen if omitted
-## :kwd deprecated: release where function was first deprecated
-## :kwd removed: release where function will be removed
-## :kwd updoc: add notice to docstring (default ``True``)
-## """
-## target_mod, target_name = target.rsplit(".",1)
-## if mod is None:
-## import inspect
-## mod = inspect.currentframe(1).f_globals["__name__"]
-## if not name:
-## name = target_name
-## if msg is None:
-## msg = ("the function %(mod)s.%(name)s() has been moved to "
-## "%(target_mod)s.%(target_name)s(), the old location is deprecated")
-## if deprecated:
-## msg += " as of Passlib %(deprecated)s"
-## if removed:
-## msg += ", and will be removed in Passlib %(removed)s"
-## msg += "."
-## msg %= dict(
-## mod=mod,
-## name=name,
-## target_mod=target_mod,
-## target_name=target_name,
-## deprecated=deprecated,
-## removed=removed,
-## )
-## state = [None]
-## def wrapper(*args, **kwds):
-## warn(msg, DeprecationWarning, stacklevel=2)
-## func = state[0]
-## if func is None:
-## module = __import__(target_mod, fromlist=[target_name], level=0)
-## func = state[0] = getattr(module, target_name)
-## return func(*args, **kwds)
-## wrapper.__module__ = mod
-## wrapper.__name__ = name
-## wrapper.__doc__ = msg
-## return wrapper
-
class memoized_property(object):
"""decorator which invokes method once, then replaces attr with result"""
def __init__(self, func):
@@ -908,10 +868,7 @@ class Base64Engine(object):
if tail == 1:
#only 6 bits left, can't encode a whole byte!
raise ValueError("input string length cannot be == 1 mod 4")
- if PY3:
- next_value = imap(self._decode64, source).__next__
- else:
- next_value = imap(self._decode64, source).next
+ next_value = getattr(imap(self._decode64, source), next_method_attr)
try:
return join_byte_values(self._decode_bytes(next_value, chunks, tail))
except KeyError:
diff --git a/passlib/utils/compat.py b/passlib/utils/compat.py
index 94ec9bb..02b808b 100644
--- a/passlib/utils/compat.py
+++ b/passlib/utils/compat.py
@@ -10,6 +10,7 @@ PY27 = sys.version_info[:2] == (2,7) # supports last 2.x release
PY_MIN_32 = sys.version_info >= (3,2) # py 3.2 or later
# __dir__() added in py2.6
+# NOTE: testing shows pypy1.5 doesn't either; but added somewhere <= 1.8
SUPPORTS_DIR_METHOD = not PY_MAX_25
#=============================================================================