diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 00:31:08 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-27 00:31:08 -0400 |
commit | 41b93836a0a19ad20c5e387d0609eb571de48190 (patch) | |
tree | 92aa3228176bd7dcda5289d27b403c365bc17392 | |
parent | ea0df30de9d73011bd299b69a9b15a1094d009cf (diff) | |
download | passlib-41b93836a0a19ad20c5e387d0609eb571de48190.tar.gz |
cleaned up the registry code -- less redundant data, more type checks
-rw-r--r-- | passlib/hash.py | 25 | ||||
-rw-r--r-- | passlib/registry.py | 395 | ||||
-rw-r--r-- | passlib/tests/test_context.py | 7 | ||||
-rw-r--r-- | passlib/tests/test_context_deprecated.py | 7 | ||||
-rw-r--r-- | passlib/tests/test_registry.py | 1 |
5 files changed, 222 insertions, 213 deletions
diff --git a/passlib/hash.py b/passlib/hash.py index e787b0e..7290e11 100644 --- a/passlib/hash.py +++ b/passlib/hash.py @@ -1,28 +1,27 @@ -"""passlib.hash stub +"""passlib.hash - proxy object mapping hash scheme names -> handlers -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. +Note +==== +This module does not actually contain any hashes. This file +is a stub that replaces itself with a proxy object. - the actually implementations of hashes (at least, those built into passlib) - are stored in the passlib.handlers subpackage. +This proxy object (passlib.registry._PasslibRegistryProxy) +handles lazy-loading hashes as they are requested. + +The actual implementation of the various hashes is store elsewhere, +mainly in the submodules of the ``passlib.handlers`` package. """ #NOTE: could support 'non-lazy' version which just imports # all schemes known to list_crypt_handlers() #========================================================= -#import special proxy object as 'passlib.hash' module +# import proxy object and replace this module #========================================================= -#import proxy object, and replace this module with it. -#this should cause any import commands to return that object, -#not this module from passlib.registry import _proxy import sys -sys.modules['passlib.hash'] = _proxy -del sys, _proxy +sys.modules[__name__] = _proxy #========================================================= #eoc diff --git a/passlib/registry.py b/passlib/registry.py index 627ebe9..20a3a08 100644 --- a/passlib/registry.py +++ b/passlib/registry.py @@ -2,17 +2,14 @@ #========================================================= #imports #========================================================= -#core -import inspect +# core import re import logging; log = logging.getLogger(__name__) from warnings import warn -#site -#libs -from passlib.exc import PasslibWarning +# pkg +from passlib.exc import ExpectedTypeError, PasslibWarning from passlib.utils import is_crypt_handler -#pkg -#local +# local __all__ = [ "register_crypt_handler_path", "register_crypt_handler", @@ -21,9 +18,9 @@ __all__ = [ ] #========================================================= -#registry proxy object +# proxy object used in place of 'passlib.hash' module #========================================================= -class PasslibRegistryProxy(object): +class _PasslibRegistryProxy(object): """proxy module passlib.hash this module is in fact an object which lazy-loads @@ -44,123 +41,147 @@ class PasslibRegistryProxy(object): def __setattr__(self, attr, value): if attr.startswith("_"): - #NOTE: this is required for GAE, - # since it tries to set passlib.hash.__loader__ + # writing to private attributes should behave normally. + # (required so GAE can write to the __loader__ attribute). object.__setattr__(self, attr, value) else: - register_crypt_handler(value, name=attr) + # writing to public attributes should be treated + # as attempting to register a handler. + register_crypt_handler(value, _attr=attr) def __repr__(self): return "<proxy module 'passlib.hash'>" def __dir__(self): - #add in handlers that will be lazy-loaded, - #otherwise this is std dir implementation + # this adds in lazy-loaded handler names, + # otherwise this is the standard dir() implementation. attrs = set(dir(self.__class__)) attrs.update(self.__dict__) - attrs.update(_handler_locations) + attrs.update(_locations) return sorted(attrs) - #========================================================= - #eoc - #========================================================= +# create single instance - available publically as 'passlib.hash' +_proxy = _PasslibRegistryProxy() -#singleton instance - available publically as 'passlib.hash' -_proxy = PasslibRegistryProxy() +#========================================================= +# internal registry state +#========================================================= -#========================================================== -#internal registry state -#========================================================== +# singleton uses to detect omitted keywords +_UNSET = object() -#: dict mapping name -> handler for all loaded handlers. uses proxy's dict so they stay in sync. +# dict mapping name -> loaded handlers (just uses proxy object's internal dict) _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.handlers.md5_crypt", "apr_md5_crypt"), - "atlassian_pbkdf2_sha1": - ("passlib.handlers.pbkdf2", "atlassian_pbkdf2_sha1"), - "bcrypt": ("passlib.handlers.bcrypt", "bcrypt"), - "bigcrypt": ("passlib.handlers.des_crypt", "bigcrypt"), - "bsd_nthash": ("passlib.handlers.windows", "bsd_nthash"), - "bsdi_crypt": ("passlib.handlers.des_crypt", "bsdi_crypt"), - "cisco_pix": ("passlib.handlers.cisco", "cisco_pix"), - "cisco_type7": ("passlib.handlers.cisco", "cisco_type7"), - "cta_pbkdf2_sha1": ("passlib.handlers.pbkdf2", "cta_pbkdf2_sha1"), - "crypt16": ("passlib.handlers.des_crypt", "crypt16"), - "des_crypt": ("passlib.handlers.des_crypt", "des_crypt"), - "django_salted_sha1": - ("passlib.handlers.django", "django_salted_sha1"), - "django_salted_md5":("passlib.handlers.django", "django_salted_md5"), - "django_des_crypt": ("passlib.handlers.django", "django_des_crypt"), - "django_disabled": ("passlib.handlers.django", "django_disabled"), - "dlitz_pbkdf2_sha1":("passlib.handlers.pbkdf2", "dlitz_pbkdf2_sha1"), - "fshp": ("passlib.handlers.fshp", "fshp"), - "grub_pbkdf2_sha512": - ("passlib.handlers.pbkdf2", "grub_pbkdf2_sha512"), - "hex_md4": ("passlib.handlers.digests", "hex_md4"), - "hex_md5": ("passlib.handlers.digests", "hex_md5"), - "hex_sha1": ("passlib.handlers.digests", "hex_sha1"), - "hex_sha256": ("passlib.handlers.digests", "hex_sha256"), - "hex_sha512": ("passlib.handlers.digests", "hex_sha512"), - "htdigest": ("passlib.handlers.digests", "htdigest"), - "ldap_plaintext": ("passlib.handlers.ldap_digests","ldap_plaintext"), - "ldap_md5": ("passlib.handlers.ldap_digests","ldap_md5"), - "ldap_sha1": ("passlib.handlers.ldap_digests","ldap_sha1"), - "ldap_hex_md5": ("passlib.handlers.roundup", "ldap_hex_md5"), - "ldap_hex_sha1": ("passlib.handlers.roundup", "ldap_hex_sha1"), - "ldap_salted_md5": ("passlib.handlers.ldap_digests","ldap_salted_md5"), - "ldap_salted_sha1": ("passlib.handlers.ldap_digests","ldap_salted_sha1"), - "ldap_des_crypt": ("passlib.handlers.ldap_digests","ldap_des_crypt"), - "ldap_bsdi_crypt": ("passlib.handlers.ldap_digests","ldap_bsdi_crypt"), - "ldap_md5_crypt": ("passlib.handlers.ldap_digests","ldap_md5_crypt"), - "ldap_bcrypt": ("passlib.handlers.ldap_digests","ldap_bcrypt"), - "ldap_sha1_crypt": ("passlib.handlers.ldap_digests","ldap_sha1_crypt"), - "ldap_sha256_crypt":("passlib.handlers.ldap_digests","ldap_sha256_crypt"), - "ldap_sha512_crypt":("passlib.handlers.ldap_digests","ldap_sha512_crypt"), - "ldap_pbkdf2_sha1": ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha1"), - "ldap_pbkdf2_sha256": - ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha256"), - "ldap_pbkdf2_sha512": - ("passlib.handlers.pbkdf2", "ldap_pbkdf2_sha512"), - "lmhash": ("passlib.handlers.windows", "lmhash"), - "md5_crypt": ("passlib.handlers.md5_crypt", "md5_crypt"), - "msdcc": ("passlib.handlers.windows", "msdcc"), - "msdcc2": ("passlib.handlers.windows", "msdcc2"), - "mssql2000": ("passlib.handlers.mssql", "mssql2000"), - "mssql2005": ("passlib.handlers.mssql", "mssql2005"), - "mysql323": ("passlib.handlers.mysql", "mysql323"), - "mysql41": ("passlib.handlers.mysql", "mysql41"), - "nthash": ("passlib.handlers.windows", "nthash"), - "oracle10": ("passlib.handlers.oracle", "oracle10"), - "oracle11": ("passlib.handlers.oracle", "oracle11"), - "pbkdf2_sha1": ("passlib.handlers.pbkdf2", "pbkdf2_sha1"), - "pbkdf2_sha256": ("passlib.handlers.pbkdf2", "pbkdf2_sha256"), - "pbkdf2_sha512": ("passlib.handlers.pbkdf2", "pbkdf2_sha512"), - "phpass": ("passlib.handlers.phpass", "phpass"), - "plaintext": ("passlib.handlers.misc", "plaintext"), - "postgres_md5": ("passlib.handlers.postgres", "postgres_md5"), - "roundup_plaintext":("passlib.handlers.roundup", "roundup_plaintext"), - "scram": ("passlib.handlers.scram", "scram"), - "sha1_crypt": ("passlib.handlers.sha1_crypt", "sha1_crypt"), - "sha256_crypt": ("passlib.handlers.sha2_crypt", "sha256_crypt"), - "sha512_crypt": ("passlib.handlers.sha2_crypt", "sha512_crypt"), - "sun_md5_crypt": ("passlib.handlers.sun_md5_crypt","sun_md5_crypt"), - "unix_disabled": ("passlib.handlers.misc", "unix_disabled"), - "unix_fallback": ("passlib.handlers.misc", "unix_fallback"), -} - -#: master regexp for detecting valid handler names -_name_re = re.compile("^[a-z][_a-z0-9]{2,}$") - -#: names which aren't allowed for various reasons (mainly keyword conflicts in CryptContext) -_forbidden_names = frozenset(["onload", "policy", "context", "all", "default", "none"]) - -#========================================================== -#registry frontend functions -#========================================================== +# dict mapping names -> import path for lazy loading. +# * import path should be "module.path" or "module.path:attr" +# * if attr omitted, "name" used as default. +_locations = dict( + # NOTE: this is a hardcoded list of the handlers built into passlib, + # applications should call register_crypt_handler_path() + apr_md5_crypt = "passlib.handlers.md5_crypt", + atlassian_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + bcrypt = "passlib.handlers.bcrypt", + bigcrypt = "passlib.handlers.des_crypt", + bsd_nthash = "passlib.handlers.windows", + bsdi_crypt = "passlib.handlers.des_crypt", + cisco_pix = "passlib.handlers.cisco", + cisco_type7 = "passlib.handlers.cisco", + cta_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + crypt16 = "passlib.handlers.des_crypt", + des_crypt = "passlib.handlers.des_crypt", + django_salted_sha1 = "passlib.handlers.django", + django_salted_md5 = "passlib.handlers.django", + django_des_crypt = "passlib.handlers.django", + django_disabled = "passlib.handlers.django", + dlitz_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + fshp = "passlib.handlers.fshp", + grub_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + hex_md4 = "passlib.handlers.digests", + hex_md5 = "passlib.handlers.digests", + hex_sha1 = "passlib.handlers.digests", + hex_sha256 = "passlib.handlers.digests", + hex_sha512 = "passlib.handlers.digests", + htdigest = "passlib.handlers.digests", + ldap_plaintext = "passlib.handlers.ldap_digests", + ldap_md5 = "passlib.handlers.ldap_digests", + ldap_sha1 = "passlib.handlers.ldap_digests", + ldap_hex_md5 = "passlib.handlers.roundup", + ldap_hex_sha1 = "passlib.handlers.roundup", + ldap_salted_md5 = "passlib.handlers.ldap_digests", + ldap_salted_sha1 = "passlib.handlers.ldap_digests", + ldap_des_crypt = "passlib.handlers.ldap_digests", + ldap_bsdi_crypt = "passlib.handlers.ldap_digests", + ldap_md5_crypt = "passlib.handlers.ldap_digests", + ldap_bcrypt = "passlib.handlers.ldap_digests", + ldap_sha1_crypt = "passlib.handlers.ldap_digests", + ldap_sha256_crypt = "passlib.handlers.ldap_digests", + ldap_sha512_crypt = "passlib.handlers.ldap_digests", + ldap_pbkdf2_sha1 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha256 = "passlib.handlers.pbkdf2", + ldap_pbkdf2_sha512 = "passlib.handlers.pbkdf2", + lmhash = "passlib.handlers.windows", + md5_crypt = "passlib.handlers.md5_crypt", + msdcc = "passlib.handlers.windows", + msdcc2 = "passlib.handlers.windows", + mssql2000 = "passlib.handlers.mssql", + mssql2005 = "passlib.handlers.mssql", + mysql323 = "passlib.handlers.mysql", + mysql41 = "passlib.handlers.mysql", + nthash = "passlib.handlers.windows", + oracle10 = "passlib.handlers.oracle", + oracle11 = "passlib.handlers.oracle", + pbkdf2_sha1 = "passlib.handlers.pbkdf2", + pbkdf2_sha256 = "passlib.handlers.pbkdf2", + pbkdf2_sha512 = "passlib.handlers.pbkdf2", + phpass = "passlib.handlers.phpass", + plaintext = "passlib.handlers.misc", + postgres_md5 = "passlib.handlers.postgres", + roundup_plaintext = "passlib.handlers.roundup", + scram = "passlib.handlers.scram", + sha1_crypt = "passlib.handlers.sha1_crypt", + sha256_crypt = "passlib.handlers.sha2_crypt", + sha512_crypt = "passlib.handlers.sha2_crypt", + sun_md5_crypt = "passlib.handlers.sun_md5_crypt", + unix_disabled = "passlib.handlers.misc", + unix_fallback = "passlib.handlers.misc", +) + +# master regexp for detecting valid handler names +_name_re = re.compile("^[a-z][a-z0-9_]+[a-z0-9]$") + +# names which aren't allowed for various reasons +# (mainly keyword conflicts in CryptContext) +_forbidden_names = frozenset(["onload", "policy", "context", "all", + "default", "none", "auto"]) + +#========================================================= +# registry frontend functions +#========================================================= +def _validate_handler_name(name): + """helper to validate handler name + + :raises ValueError: + * if empty name + * if name not lower case + * if name contains double underscores + * if name is reserved (e.g. ``context``, ``all``). + """ + if not name: + raise ValueError("handler name cannot be empty: %r" % (name,)) + if name.lower() != name: + raise ValueError("name must be lower-case: %r" % (name,)) + if not _name_re.match(name): + raise ValueError("invalid name (must be 3+ characters, " + " begin with a-z, and contain only underscore, a-z, " + "0-9): %r" % (name,)) + if '__' in name: + raise ValueError("name may not contain double-underscores: %r" % + (name,)) + if name in _forbidden_names: + raise ValueError("that name is not allowed: %r" % (name,)) + return True + def register_crypt_handler_path(name, path): """register location to lazy-load handler when requested. @@ -187,38 +208,23 @@ def register_crypt_handler_path(name, path): >>> from passlib.registry import registry_crypt_handler_path >>> registry_crypt_handler_path("myhash", "myapp.helpers:MyHash") """ - global _handler_locations - if ':' in path: - modname, modattr = path.split(":") - else: - modname, modattr = path, name - _handler_locations[name] = (modname, modattr) + # validate name + _validate_handler_name(name) -def _validate_handler_name(name): - """helper to validate handler name + # validate path + if path.startswith("."): + raise ValueError("path cannot start with '.'") + if ':' in path: + if path.count(':') > 1: + raise ValueError("path cannot have more than one ':'") + if path.index('.', path.index(':')) > -1: + raise ValueError("path cannot have '.' to right of ':'") - :raises ValueError: - * if empty name - * if name not lower case - * if name contains double underscores - * if name is reserved (e.g. ``context``, ``all``). - """ - if not name: - raise ValueError("handler name cannot be empty: %r" % (name,)) - if name.lower() != name: - raise ValueError("name must be lower-case: %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,)) - if '__' in name: - raise ValueError("name may not contain double-underscores: %r" % - (name,)) - if name in _forbidden_names: - raise ValueError("that name is not allowed: %r" % (name,)) - return True + # store location + _locations[name] = path + log.debug("registered path to %r handler: %r", name, path) -def register_crypt_handler(handler, force=False, name=None): +def register_crypt_handler(handler, force=False, _attr=None): """register password hash handler. this method immediately registers a handler with the internal passlib registry, @@ -226,7 +232,7 @@ def register_crypt_handler(handler, force=False, name=None): :arg handler: the password hash handler to register :param force: force override of existing handler (defaults to False) - :param name: + :param _attr: [internal kwd] if specified, ensures ``handler.name`` matches this value, or raises :exc:`ValueError`. @@ -241,39 +247,37 @@ def register_crypt_handler(handler, force=False, name=None): if a (different) handler was already registered with the same name, and ``force=True`` was not specified. """ - global _handlers, _name_re - - #validate handler + # 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 + # TODO: make an official interface + raise ExpectedTypeError(handler, "password hash handler", "handler") + if not handler: + raise AssertionError("``bool(handler)`` must be True") + + # validate name + name = handler.name _validate_handler_name(name) + if _attr and _attr != name: + raise ValueError("handlers must be stored only under their own name") - #check for existing handler + # check for existing handler other = _handlers.get(name) if other: if other is handler: - return #already registered - if force: - log.warning("overriding previous handler registered to name %r: %r", name, other) + log.debug("same %r handler already registered: %r", name, handler) + return + elif force: + log.warning("overriding previously registered %r handler: %r", + name, other) else: - raise KeyError("a handler has already registered for the name %r: %r (use force=True to override)" % (name, other)) + raise KeyError("another %r handler has already been registered: %r" % + (name, other)) - #register handler in dict + # register handler _handlers[name] = handler - log.debug("registered crypt handler %r: %r", name, handler) - -_NOTSET = object() + log.debug("registered %r handler: %r", name, handler) -def get_crypt_handler(name, default=_NOTSET): +def get_crypt_handler(name, default=_UNSET): """return handler for specified password hash scheme. this method looks up a handler for the specified scheme. @@ -287,51 +291,55 @@ def get_crypt_handler(name, default=_NOTSET): :returns: handler attached to name, or default value (if specified). """ - global _handlers, _handler_locations - - #check if handler is already loaded + # check if handler is already loaded try: return _handlers[name] except KeyError: pass - #normalize name (and if changed, check dict again) + # normalize name (and if changed, check dict again) assert isinstance(name, str), "name must be str instance" alt = name.replace("-","_").lower() if alt != name: warn("handler names should be lower-case, and use underscores instead " - "of hyphens: %r => %r" % (name, alt), PasslibWarning) + "of hyphens: %r => %r" % (name, alt), PasslibWarning, + stacklevel=2) name = alt - #check if handler loaded - handler = _handlers.get(name) - if handler: - return handler - - #check if lazy load mapping has been specified for this driver - route = _handler_locations.get(name) - if route: - modname, modattr = route + # try to load using new name + try: + return _handlers[name] + except KeyError: + pass + + # check if lazy load mapping has been specified for this driver + path = _locations.get(name) + if path: + if ':' in path: + modname, modattr = path.split(":") + else: + modname, modattr = path, name + ##log.debug("loading %r handler from path: '%s:%s'", name, modname, modattr) - #try to load the module - any import errors indicate runtime config, usually + # try to load the module - any import errors indicate runtime config, usually # either missing package, or bad path provided to register_crypt_handler_path() - mod = __import__(modname, None, None, [modattr], 0) + mod = __import__(modname, fromlist=[modattr], level=0) - #first check if importing module triggered register_crypt_handler(), - #(this is discouraged due to it's magical implicitness) + # first check if importing module triggered register_crypt_handler(), + # (this is discouraged due to it's magical implicitness) handler = _handlers.get(name) if handler: - #XXX: issue deprecation warning here? + # XXX: issue deprecation warning here? assert is_crypt_handler(handler), "unexpected object: name=%r object=%r" % (name, handler) return handler - #then get real handler & register it + # then get real handler & register it handler = getattr(mod, modattr) - register_crypt_handler(handler, name=name) + register_crypt_handler(handler, _attr=name) return handler - #fail! - if default is _NOTSET: + # fail! + if default is _UNSET: raise KeyError("no crypt handler found for algorithm: %r" % (name,)) else: return default @@ -343,15 +351,14 @@ def list_crypt_handlers(loaded_only=False): :returns: list of names of all known handlers """ - global _handlers, _handler_locations names = set(_handlers) if not loaded_only: - names.update(_handler_locations) + names.update(_locations) return sorted(names) #NOTE: these two functions mainly exist just for the unittests... -def has_crypt_handler(name, loaded_only=False): +def _has_crypt_handler(name, loaded_only=False): """check if handler name is known. this is only useful for two cases: @@ -362,8 +369,7 @@ def has_crypt_handler(name, loaded_only=False): :arg name: name of handler :param loaded_only: if ``True``, returns False if handler exists but hasn't been loaded """ - global _handlers, _handler_locations - return (name in _handlers) or (not loaded_only and name in _handler_locations) + return (name in _handlers) or (not loaded_only and name in _locations) def _unload_handler_name(name, locations=True): """unloads a handler from the registry. @@ -381,13 +387,10 @@ def _unload_handler_name(name, locations=True): :arg name: name of handler to unload :param locations: if False, won't purge registered handler locations (default True) """ - global _handlers, _handler_locations - if name in _handlers: del _handlers[name] - - if locations and name in _handler_locations: - del _handler_locations[name] + if locations and name in _locations: + del _locations[name] #========================================================= # eof diff --git a/passlib/tests/test_context.py b/passlib/tests/test_context.py index d039ac6..28ce208 100644 --- a/passlib/tests/test_context.py +++ b/passlib/tests/test_context.py @@ -29,8 +29,11 @@ from passlib.utils.compat import irange, u import passlib.utils.handlers as uh from passlib.tests.utils import TestCase, mktemp, catch_warnings, \ gae_env, set_file -from passlib.registry import register_crypt_handler_path, has_crypt_handler, \ - _unload_handler_name as unload_handler_name, get_crypt_handler +from passlib.registry import (register_crypt_handler_path, + _has_crypt_handler as has_crypt_handler, + _unload_handler_name as unload_handler_name, + get_crypt_handler, + ) #module log = getLogger(__name__) #========================================================= diff --git a/passlib/tests/test_context_deprecated.py b/passlib/tests/test_context_deprecated.py index f6d33d8..1662076 100644 --- a/passlib/tests/test_context_deprecated.py +++ b/passlib/tests/test_context_deprecated.py @@ -30,8 +30,11 @@ from passlib.utils.compat import irange, u import passlib.utils.handlers as uh from passlib.tests.utils import TestCase, mktemp, catch_warnings, \ gae_env, set_file -from passlib.registry import register_crypt_handler_path, has_crypt_handler, \ - _unload_handler_name as unload_handler_name +from passlib.registry import (register_crypt_handler_path, + _has_crypt_handler as has_crypt_handler, + _unload_handler_name as unload_handler_name, + get_crypt_handler, + ) #module log = getLogger(__name__) diff --git a/passlib/tests/test_registry.py b/passlib/tests/test_registry.py index 3f18271..ce9466d 100644 --- a/passlib/tests/test_registry.py +++ b/passlib/tests/test_registry.py @@ -156,6 +156,7 @@ class RegistryTest(TestCase): name = "dummy_1" self.assertRaises(KeyError, get_crypt_handler, "dummy_1") + self.asssertIs(get_crypt_handler("dummy_1", None), None) register_crypt_handler(dummy_1) self.assertIs(get_crypt_handler("dummy_1"), dummy_1) |