diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-30 23:03:33 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-30 23:03:33 -0400 |
commit | f44248b2890aab70633ce12209710e6de84638bd (patch) | |
tree | 0f94a61d0090c70f84b40829de377425b3044649 | |
parent | 176153315bbd4ae3ec8542a5fc6704041d7de342 (diff) | |
download | passlib-f44248b2890aab70633ce12209710e6de84638bd.tar.gz |
assorted bugfixes & additional test coverage
-rw-r--r-- | admin/benchmarks.py | 2 | ||||
-rw-r--r-- | passlib/exc.py | 17 | ||||
-rw-r--r-- | passlib/handlers/django.py | 2 | ||||
-rw-r--r-- | passlib/handlers/scram.py | 2 | ||||
-rw-r--r-- | passlib/tests/test_apps.py | 4 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 152 | ||||
-rw-r--r-- | passlib/tests/test_hosts.py | 2 | ||||
-rw-r--r-- | passlib/tests/test_registry.py | 18 | ||||
-rw-r--r-- | passlib/tests/test_utils.py | 30 | ||||
-rw-r--r-- | passlib/utils/__init__.py | 103 | ||||
-rw-r--r-- | passlib/utils/compat.py | 1 |
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 #============================================================================= |