diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c74020e1ac86277b8db522c89a90960b4fe67965
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,15 @@
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v4.1.0
+  hooks:
+  - id: trailing-whitespace
+  - id: end-of-file-fixer
+  - id: mixed-line-ending
+    args: ['--fix=lf']
+    description: Forces to replace line ending by the UNIX 'lf' character.
+- repo: https://github.com/psf/black
+  rev: 22.1.0
+  hooks:
+  - id: black
+    language_version: python3
+    args: [-t, py310]
diff --git a/LICENSE b/LICENSE
index 89f8850e9d23f4145c26bef43f5684fb90214b09..60d8589928abc32e382317adb33df2d7c95bfa1d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2019 Alexandre Ferland
+Copyright (c) 2022 Alexandre Ferland
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..16ef367f11f82fc981a1f442250a7d663ccf2808
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+.PHONY: help dev clean update test lint pre-commit
+
+VENV_NAME?=venv
+VENV_ACTIVATE=. $(VENV_NAME)/bin/activate
+PYTHON=${VENV_NAME}/bin/python3
+
+.DEFAULT: help
+help:
+	@echo "make dev"
+	@echo "       prepare development environment, use only once"
+	@echo "make clean"
+	@echo "       delete development environment"
+	@echo "make update"
+	@echo "       update dependencies"
+	@echo "make test"
+	@echo "       run tests"
+	@echo "make lint"
+	@echo "       run black"
+	@echo "make pre-commit"
+	@echo "       run pre-commit hooks"
+
+dev:
+	make venv
+
+venv: $(VENV_NAME)/bin/activate
+$(VENV_NAME)/bin/activate:
+	test -d $(VENV_NAME) || virtualenv -p python3 $(VENV_NAME)
+	${PYTHON} -m pip install -U pip
+	${PYTHON} -m pip install -r dev_requirements.txt
+	$(VENV_NAME)/bin/pre-commit install
+	touch $(VENV_NAME)/bin/activate
+
+clean:
+	rm -rf venv
+
+update:
+	${PYTHON} -m pip install -U -r dev_requirements.txt
+	$(VENV_NAME)/bin/pre-commit install
+
+test: venv
+	${PYTHON} -m pytest
+
+lint: venv
+	$(VENV_NAME)/bin/black -t py310 --exclude $(VENV_NAME) .
+
+pre-commit: venv
+	$(VENV_NAME)/bin/pre-commit
diff --git a/README.md b/README.md
index fc09040e6053e0c56af7ad4542e8090c11518f6d..126c350111282eeea9dd1b908986c57f8ccbddfd 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,20 @@
-Flask-SimpleLDAP
+Flask-SimpleLDAP [![Build Status](https://app.travis-ci.com/alexferl/flask-simpleldap.svg?branch=master)](https://app.travis-ci.com/alexferl/flask-simpleldap)
 ================
 
-[![Build Status](https://travis-ci.com/alexferl/flask-simpleldap.svg?branch=master)](https://travis-ci.com/alexferl/flask-simpleldap)
-
 Flask-SimpleLDAP provides LDAP authentication for Flask.
 
-Flask-SimpleLDAP is compatible with and tested on Python 3.5, 3.6 and 3.7.
+Flask-SimpleLDAP is compatible with and tested on Python 3.7+.
 
 Quickstart
 ----------
 
 First, install Flask-SimpleLDAP:
-    
-    $ pip install flask-simpleldap
-    
+
+```shell
+pip install flask-simpleldap
+```
+
+
 Flask-SimpleLDAP depends, and will install for you, recent versions of Flask
 (0.12.4 or later) and [python-ldap](https://python-ldap.org/).
 Please consult the [python-ldap installation instructions](https://www.python-ldap.org/en/latest/installing.html) if you get an error during installation.
@@ -27,19 +28,19 @@ from flask import Flask, g
 from flask_simpleldap import LDAP
 
 app = Flask(__name__)
-#app.config['LDAP_HOST'] = 'ldap.example.org'  # defaults to localhost
-app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org'
-app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org'
-app.config['LDAP_PASSWORD'] = 'password'
+# app.config["LDAP_HOST"] = "ldap.example.org"  # defaults to localhost
+app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+app.config["LDAP_PASSWORD"] = "password"
 
 ldap = LDAP(app)
 
-@app.route('/')
+@app.route("/")
 @ldap.basic_auth_required
 def index():
-    return 'Welcome, {0}!'.format(g.ldap_username)
+    return "Welcome, {0}!".format(g.ldap_username)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
 ```
 
@@ -54,8 +55,8 @@ Once you get the basic example working, check out the more complex ones:
 * [examples/groups](examples/groups) demostrates using:
   * `@ldap.login_required` for form/cookie-based auth, instead of basic HTTP authentication.
   * `@ldap.group_required()` to restrict access to pages based on the user's LDAP groups.
-* [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's 
-[application factories](http://flask.pocoo.org/docs/patterns/appfactories/) 
+* [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's
+[application factories](http://flask.pocoo.org/docs/patterns/appfactories/)
 and [blueprints](http://flask.pocoo.org/docs/blueprints/).
 
 
@@ -63,7 +64,7 @@ OpenLDAP
 --------
 
 Add the ``LDAP`` instance to your code and depending on your OpenLDAP
-configuration, add the following at least LDAP_USER_OBJECT_FILTER and 
+configuration, add the following at least LDAP_USER_OBJECT_FILTER and
 LDAP_USER_OBJECT_FILTER.
 
 ```python
@@ -73,31 +74,31 @@ from flask_simpleldap import LDAP
 app = Flask(__name__)
 
 # Base
-app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication'
-app.config['LDAP_HOST'] = 'openldap.example.org'
-app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org'
-app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org'
-app.config['LDAP_PASSWORD'] = 'password'
+app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication"
+app.config["LDAP_HOST"] = "openldap.example.org"
+app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org"
+app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org"
+app.config["LDAP_PASSWORD"] = "password"
 
-# OpenLDAP 
-app.config['LDAP_OBJECTS_DN'] = 'dn'
-app.config['LDAP_OPENLDAP'] = True
-app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))'
+# OpenLDAP
+app.config["LDAP_OBJECTS_DN"] = "dn"
+app.config["LDAP_OPENLDAP"] = True
+app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))"
 
 # Groups
-app.config['LDAP_GROUP_MEMBERS_FIELD'] = "uniquemember"
-app.config['LDAP_GROUP_OBJECT_FILTER'] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
-app.config['LDAP_GROUP_MEMBER_FILTER'] = "(&(cn=*)(objectclass=groupOfUniqueNames)(uniquemember=%s))"
-app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn"
+app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember"
+app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER"] = "(&(cn=*)(objectclass=groupOfUniqueNames)(uniquemember=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn"
 
 ldap = LDAP(app)
 
-@app.route('/')
+@app.route("/")
 @ldap.basic_auth_required
 def index():
-    return 'Welcome, {0}!'.format(g.ldap_username)
+    return "Welcome, {0}!".format(g.ldap_username)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
 ```
 
diff --git a/dev_requirements.txt b/dev_requirements.txt
index 0c497390497e75680475272faef62d8a87fe9c4d..32cd95d5224e4ae22d7ed5a0aabc3dca78de8abb 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,3 +1,5 @@
+black==22.1.0
+pre-commit==2.17.0
 python-ldap==3.2.0  # here instead of requirements.txt so rtfd can build
 Sphinx==2.1.2
 
diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py
index 33f47449c11d241e36c83d7f95d819bbe56e2c8f..0dcf53b7542b7093e2bd89615e532023605417eb 100644
--- a/docs/_themes/flask_theme_support.py
+++ b/docs/_themes/flask_theme_support.py
@@ -1,7 +1,19 @@
 # flasky extensions.  flasky pygments style based on tango style
 from pygments.style import Style
-from pygments.token import Keyword, Name, Comment, String, Error, \
-     Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
+from pygments.token import (
+    Keyword,
+    Name,
+    Comment,
+    String,
+    Error,
+    Number,
+    Operator,
+    Generic,
+    Whitespace,
+    Punctuation,
+    Other,
+    Literal,
+)
 
 
 class FlaskyStyle(Style):
@@ -10,77 +22,68 @@ class FlaskyStyle(Style):
 
     styles = {
         # No corresponding class for the following:
-        #Text:                     "", # class:  ''
-        Whitespace:                "underline #f8f8f8",      # class: 'w'
-        Error:                     "#a40000 border:#ef2929", # class: 'err'
-        Other:                     "#000000",                # class 'x'
-
-        Comment:                   "italic #8f5902", # class: 'c'
-        Comment.Preproc:           "noitalic",       # class: 'cp'
-
-        Keyword:                   "bold #004461",   # class: 'k'
-        Keyword.Constant:          "bold #004461",   # class: 'kc'
-        Keyword.Declaration:       "bold #004461",   # class: 'kd'
-        Keyword.Namespace:         "bold #004461",   # class: 'kn'
-        Keyword.Pseudo:            "bold #004461",   # class: 'kp'
-        Keyword.Reserved:          "bold #004461",   # class: 'kr'
-        Keyword.Type:              "bold #004461",   # class: 'kt'
-
-        Operator:                  "#582800",   # class: 'o'
-        Operator.Word:             "bold #004461",   # class: 'ow' - like keywords
-
-        Punctuation:               "bold #000000",   # class: 'p'
-
+        # Text:                     "", # class:  ''
+        Whitespace: "underline #f8f8f8",  # class: 'w'
+        Error: "#a40000 border:#ef2929",  # class: 'err'
+        Other: "#000000",  # class 'x'
+        Comment: "italic #8f5902",  # class: 'c'
+        Comment.Preproc: "noitalic",  # class: 'cp'
+        Keyword: "bold #004461",  # class: 'k'
+        Keyword.Constant: "bold #004461",  # class: 'kc'
+        Keyword.Declaration: "bold #004461",  # class: 'kd'
+        Keyword.Namespace: "bold #004461",  # class: 'kn'
+        Keyword.Pseudo: "bold #004461",  # class: 'kp'
+        Keyword.Reserved: "bold #004461",  # class: 'kr'
+        Keyword.Type: "bold #004461",  # class: 'kt'
+        Operator: "#582800",  # class: 'o'
+        Operator.Word: "bold #004461",  # class: 'ow' - like keywords
+        Punctuation: "bold #000000",  # class: 'p'
         # because special names such as Name.Class, Name.Function, etc.
         # are not recognized as such later in the parsing, we choose them
         # to look the same as ordinary variables.
-        Name:                      "#000000",        # class: 'n'
-        Name.Attribute:            "#c4a000",        # class: 'na' - to be revised
-        Name.Builtin:              "#004461",        # class: 'nb'
-        Name.Builtin.Pseudo:       "#3465a4",        # class: 'bp'
-        Name.Class:                "#000000",        # class: 'nc' - to be revised
-        Name.Constant:             "#000000",        # class: 'no' - to be revised
-        Name.Decorator:            "#888",           # class: 'nd' - to be revised
-        Name.Entity:               "#ce5c00",        # class: 'ni'
-        Name.Exception:            "bold #cc0000",   # class: 'ne'
-        Name.Function:             "#000000",        # class: 'nf'
-        Name.Property:             "#000000",        # class: 'py'
-        Name.Label:                "#f57900",        # class: 'nl'
-        Name.Namespace:            "#000000",        # class: 'nn' - to be revised
-        Name.Other:                "#000000",        # class: 'nx'
-        Name.Tag:                  "bold #004461",   # class: 'nt' - like a keyword
-        Name.Variable:             "#000000",        # class: 'nv' - to be revised
-        Name.Variable.Class:       "#000000",        # class: 'vc' - to be revised
-        Name.Variable.Global:      "#000000",        # class: 'vg' - to be revised
-        Name.Variable.Instance:    "#000000",        # class: 'vi' - to be revised
-
-        Number:                    "#990000",        # class: 'm'
-
-        Literal:                   "#000000",        # class: 'l'
-        Literal.Date:              "#000000",        # class: 'ld'
-
-        String:                    "#4e9a06",        # class: 's'
-        String.Backtick:           "#4e9a06",        # class: 'sb'
-        String.Char:               "#4e9a06",        # class: 'sc'
-        String.Doc:                "italic #8f5902", # class: 'sd' - like a comment
-        String.Double:             "#4e9a06",        # class: 's2'
-        String.Escape:             "#4e9a06",        # class: 'se'
-        String.Heredoc:            "#4e9a06",        # class: 'sh'
-        String.Interpol:           "#4e9a06",        # class: 'si'
-        String.Other:              "#4e9a06",        # class: 'sx'
-        String.Regex:              "#4e9a06",        # class: 'sr'
-        String.Single:             "#4e9a06",        # class: 's1'
-        String.Symbol:             "#4e9a06",        # class: 'ss'
-
-        Generic:                   "#000000",        # class: 'g'
-        Generic.Deleted:           "#a40000",        # class: 'gd'
-        Generic.Emph:              "italic #000000", # class: 'ge'
-        Generic.Error:             "#ef2929",        # class: 'gr'
-        Generic.Heading:           "bold #000080",   # class: 'gh'
-        Generic.Inserted:          "#00A000",        # class: 'gi'
-        Generic.Output:            "#888",           # class: 'go'
-        Generic.Prompt:            "#745334",        # class: 'gp'
-        Generic.Strong:            "bold #000000",   # class: 'gs'
-        Generic.Subheading:        "bold #800080",   # class: 'gu'
-        Generic.Traceback:         "bold #a40000",   # class: 'gt'
+        Name: "#000000",  # class: 'n'
+        Name.Attribute: "#c4a000",  # class: 'na' - to be revised
+        Name.Builtin: "#004461",  # class: 'nb'
+        Name.Builtin.Pseudo: "#3465a4",  # class: 'bp'
+        Name.Class: "#000000",  # class: 'nc' - to be revised
+        Name.Constant: "#000000",  # class: 'no' - to be revised
+        Name.Decorator: "#888",  # class: 'nd' - to be revised
+        Name.Entity: "#ce5c00",  # class: 'ni'
+        Name.Exception: "bold #cc0000",  # class: 'ne'
+        Name.Function: "#000000",  # class: 'nf'
+        Name.Property: "#000000",  # class: 'py'
+        Name.Label: "#f57900",  # class: 'nl'
+        Name.Namespace: "#000000",  # class: 'nn' - to be revised
+        Name.Other: "#000000",  # class: 'nx'
+        Name.Tag: "bold #004461",  # class: 'nt' - like a keyword
+        Name.Variable: "#000000",  # class: 'nv' - to be revised
+        Name.Variable.Class: "#000000",  # class: 'vc' - to be revised
+        Name.Variable.Global: "#000000",  # class: 'vg' - to be revised
+        Name.Variable.Instance: "#000000",  # class: 'vi' - to be revised
+        Number: "#990000",  # class: 'm'
+        Literal: "#000000",  # class: 'l'
+        Literal.Date: "#000000",  # class: 'ld'
+        String: "#4e9a06",  # class: 's'
+        String.Backtick: "#4e9a06",  # class: 'sb'
+        String.Char: "#4e9a06",  # class: 'sc'
+        String.Doc: "italic #8f5902",  # class: 'sd' - like a comment
+        String.Double: "#4e9a06",  # class: 's2'
+        String.Escape: "#4e9a06",  # class: 'se'
+        String.Heredoc: "#4e9a06",  # class: 'sh'
+        String.Interpol: "#4e9a06",  # class: 'si'
+        String.Other: "#4e9a06",  # class: 'sx'
+        String.Regex: "#4e9a06",  # class: 'sr'
+        String.Single: "#4e9a06",  # class: 's1'
+        String.Symbol: "#4e9a06",  # class: 'ss'
+        Generic: "#000000",  # class: 'g'
+        Generic.Deleted: "#a40000",  # class: 'gd'
+        Generic.Emph: "italic #000000",  # class: 'ge'
+        Generic.Error: "#ef2929",  # class: 'gr'
+        Generic.Heading: "bold #000080",  # class: 'gh'
+        Generic.Inserted: "#00A000",  # class: 'gi'
+        Generic.Output: "#888",  # class: 'go'
+        Generic.Prompt: "#745334",  # class: 'gp'
+        Generic.Strong: "bold #000000",  # class: 'gs'
+        Generic.Subheading: "bold #800080",  # class: 'gu'
+        Generic.Traceback: "bold #a40000",  # class: 'gt'
     }
diff --git a/docs/conf.py b/docs/conf.py
index 8f32b24f97f4c5cd8cabd796ee68ced61589eaaf..47beca9dec4346648f408599823f124746c84323 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,214 +20,218 @@ from mock import Mock as MagicMock
 class Mock(MagicMock):
     @classmethod
     def __getattr__(cls, name):
-            return Mock()
+        return Mock()
 
-MOCK_MODULES = ['ldap']
+
+MOCK_MODULES = ["ldap"]
 sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath(".."))
 
 # -- General configuration -----------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx.ext.intersphinx',
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'Flask-SimpleLDAP'
-copyright = u'2019, Alexandre Ferland'
+project = "Flask-SimpleLDAP"
+copyright = "2022, Alexandre Ferland"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '1.4.0'
+version = "1.4.0"
 # The full version, including alpha/beta/rc tags.
-release = '1.4.0'
+release = "1.4.0"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
 
 
 # -- Options for HTML output ----------------------------------------------
 
-sys.path.append(os.path.abspath('_themes'))
-html_theme_path = ['_themes']
-html_theme = 'flask_small'
+sys.path.append(os.path.abspath("_themes"))
+html_theme_path = ["_themes"]
+html_theme = "flask_small"
 html_theme_options = {
-    'index_logo': '',  #TODO
-    'github_fork': 'admiralobvious/flask-simpleldap',
-    }
+    "index_logo": "",  # TODO
+    "github_fork": "alexferl/flask-simpleldap",
+}
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'Flask-SimpleLDAPdoc'
+htmlhelp_basename = "Flask-SimpleLDAPdoc"
 
 
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # The paper size ('letterpaper' or 'a4paper').
+    #'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    #'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    #'preamble': '',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-  ('index', 'Flask-SimpleLDAP.tex', u'Flask-SimpleLDAP Documentation',
-   u'Alexandre Ferland', 'manual'),
+    (
+        "index",
+        "Flask-SimpleLDAP.tex",
+        "Flask-SimpleLDAP Documentation",
+        "Alexandre Ferland",
+        "manual",
+    ),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output ---------------------------------------
@@ -235,12 +239,17 @@ latex_documents = [
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'flask-simpleldap', u'Flask-SimpleLDAP Documentation',
-     [u'Alexandre Ferland'], 1)
+    (
+        "index",
+        "flask-simpleldap",
+        "Flask-SimpleLDAP Documentation",
+        ["Alexandre Ferland"],
+        1,
+    )
 ]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -249,23 +258,29 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'Flask-SimpleLDAP', u'Flask-SimpleLDAP Documentation',
-   u'Alexandre Ferland', 'Flask-SimpleLDAP', 'One line description of project.',
-   'Miscellaneous'),
+    (
+        "index",
+        "Flask-SimpleLDAP",
+        "Flask-SimpleLDAP Documentation",
+        "Alexandre Ferland",
+        "Flask-SimpleLDAP",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False
 
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
+intersphinx_mapping = {"http://docs.python.org/": None}
diff --git a/docs/index.rst b/docs/index.rst
index 2ab3e2c0473223473e538b2c411c98244de37bb8..24b23457e32335c92622ab3ff1f3e0cdb4f6ddb7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,37 +14,38 @@ Quickstart
 
 First, install Flask-SimpleLDAP:
 
-.. code-block:: bash
+    .. code-block:: bash
 
-    $ pip install flask-simpleldap
+      pip install flask-simpleldap
 
 Flask-SimpleLDAP depends, and will install for you, recent versions of Flask
 (0.12.4 or later) and pyldap. Flask-SimpleLDAP is compatible
-with and tested on Python 3.5, 3.6 and 3.7.
+with and tested on Python 3.7+.
 
 Next, add a :class:`~flask_simpleldap.LDAP` to your code and at least the three
 required configuration options:
 
-.. code-block:: python
+    .. code-block:: python
 
-    from flask import Flask
-    from flask_simpleldap import LDAP
+      from flask import Flask
+      from flask_simpleldap import LDAP
 
-    app = Flask(__name__)
-    app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org'
-    app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org'
-    app.config['LDAP_PASSWORD'] = 'password'
+      app = Flask(__name__)
+      # app.config["LDAP_HOST"] = "ldap.example.org"  # defaults to localhost
+      app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+      app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+      app.config["LDAP_PASSWORD"] = "password"
 
-    ldap = LDAP(app)
+      ldap = LDAP(app)
 
-    @app.route('/ldap')
-    @ldap.login_required
-    def ldap_protected():
-        return 'Success!'
-    
-    
-    if __name__ == '__main__':
-        app.run()
+      @app.route("/ldap")
+      @ldap.login_required
+      def ldap_protected():
+          return "Success!"
+
+
+      if __name__ == "__main__":
+          app.run()
 
 
 Configuration
@@ -122,40 +123,62 @@ History
 
 Changes:
 
+- 1.4.0 July 16, 2019
+
+  - This release drops support for `Python 2.7 <https://pythonclock.org/>`_. If you're still on Python 2.7, you can use `v1.3.3 <https://github.com/alexferl/flask-simpleldap/releases/tag/v1.3.3>`_.
+
+  - Fixes:
+
+    - `#62 <https://github.com/admiralobvious/flask-simpleldap/issues/62>`_ get_object_details returning None
+
 - 1.3.0 July 14, 2019
+
   - Thanks to the contributors, this release fixes issues related to bind_user and fixes some issues related to filtering.
-    `#51 <https://github.com/admiralobvious/flask-simpleldap/pull/51>`_ Referral chasing crash
-    `#54 <https://github.com/admiralobvious/flask-simpleldap/pull/54>`_ Fixes #44 - Error in bind_user method, also fixes #60 and #61
-    `#56 <https://github.com/admiralobvious/flask-simpleldap/pull/56>`_ OpenLDAP section has Incorrect LDAP_GROUP_OBJECT_FILTER
-    `#57 <https://github.com/admiralobvious/flask-simpleldap/pull/57>`_ next vaule: Priority use request.full_path
-    `#59 <https://github.com/admiralobvious/flask-simpleldap/pull/59>`_ get_object_details to take query_filter and fallback to LDAP_USER_OBJECT_FILTER or LDAP_GROUP_OBJECT_FILTER
+
+    - `#51 <https://github.com/admiralobvious/flask-simpleldap/pull/51>`_ Referral chasing crash
+
+    - `#54 <https://github.com/admiralobvious/flask-simpleldap/pull/54>`_ Fixes #44 - Error in bind_user method, also fixes #60 and #61
+
+    - `#56 <https://github.com/admiralobvious/flask-simpleldap/pull/56>`_ OpenLDAP section has Incorrect LDAP_GROUP_OBJECT_FILTER
+
+    - `#57 <https://github.com/admiralobvious/flask-simpleldap/pull/57>`_ next vaule: Priority use request.full_path
+
+    - `#59 <https://github.com/admiralobvious/flask-simpleldap/pull/59>`_ get_object_details to take query_filter and fallback to LDAP_USER_OBJECT_FILTER or LDAP_GROUP_OBJECT_FILTER
 
 
 - 1.2.0 September 26, 2017
+
   - Changed get_group_members() and get_user_groups() returning strings instead of bytes in PY3.
 
 - 1.1.2 July 17, 2017
+
   - Merge GitHub PR `#30 <https://github.com/admiralobvious/flask-simpleldap/pull/30>`_,
     Fix for python3
   - Fix decoding bytes in PY3 for @ldap.group_required.
 
 - 1.1.1 April 10, 2017
+
   - Merge GitHub pull `#26 <https://github.com/admiralobvious/flask-simpleldap/pull/26>`_,
     Fix set_option call to LDAP for SSL CERT
 
 - 1.1.0 June 7, 2016
+
   - Add the ability the pass any valid pyldap config options via the LDAP_CUSTOM_OPTIONS configuration directive.
 
 - 1.0.1 June 5, 2016
+
   - Fix ldap filter import.
 
 - 1.0.0 June 4, 2016
+
   - Python 3.x support. Switched from python-ldap to pyldap which is a fork with Python 3.x support.
 
 - 0.4.0: September 5, 2015
+
   - Added support for OpenLDAP directories. Thanks to `@jm66 <https://github.com/jm66>`_ on GitHub.
 
 - 0.3.0: January 21, 2015
+
   - Fix Github issue `#10 <https://github.com/admiralobvious/flask-simpleldap/issues/10>`_,
     Redirect users back to the page they originally requested after authenticating
 
@@ -163,14 +186,17 @@ Changes:
     Only trust .bind_user() with a non-empty password
 
 - 0.2.0: December 7, 2014
+
   - Added HTTP Basic Authentication. Thanks to `@OptiverTimAll <https://github.com/optivertimall>`_ on GitHub.
   - Fix GitHub issue `#4 <https://github.com/admiralobvious/flask-simpleldap/issues/4>`_,
     User or group queries are vulnerable to LDAP injection.
     Make sure you update your filters to use '%s' instead of the old '{}'!
 
 - 0.1.1: September 6, 2014
+
   - Fix GitHub issue `#3 <https://github.com/admiralobvious/flask-simpleldap/issues/3>`_,
     Not compatible with uppercase distinguished names.
 
 - 0.1: August 9, 2014
+
   - Initial Release
diff --git a/examples/basic_auth/app.py b/examples/basic_auth/app.py
index e9e745f30d2a47a219656d57018c9956f7d26ab8..2cff66f82767528f958d9bc2106fde2485ad235b 100644
--- a/examples/basic_auth/app.py
+++ b/examples/basic_auth/app.py
@@ -2,17 +2,19 @@ from flask import Flask, g
 from flask_simpleldap import LDAP
 
 app = Flask(__name__)
-#app.config['LDAP_HOST'] = 'ldap.example.org'  # defaults to localhost
-app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org'
-app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org'
-app.config['LDAP_PASSWORD'] = 'password'
+# app.config['LDAP_HOST'] = 'ldap.example.org'  # defaults to localhost
+app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+app.config["LDAP_PASSWORD"] = "password"
 
 ldap = LDAP(app)
 
-@app.route('/')
+
+@app.route("/")
 @ldap.basic_auth_required
 def index():
-    return 'Welcome, {0}!'.format(g.ldap_username)
+    return "Welcome, {0}!".format(g.ldap_username)
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
diff --git a/examples/basic_auth/app_oldap.py b/examples/basic_auth/app_oldap.py
index 99aa789ae2ce5031063a9f4aef1efd38a64a8886..89edb076d92d74690b446147871b00f64ef3fdc8 100644
--- a/examples/basic_auth/app_oldap.py
+++ b/examples/basic_auth/app_oldap.py
@@ -4,31 +4,35 @@ from flask_simpleldap import LDAP
 app = Flask(__name__)
 
 # Base
-app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication'
-app.config['LDAP_HOST'] = 'openldap.example.org'
-app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org'
-app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org'
-app.config['LDAP_PASSWORD'] = 'password'
+app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication"
+app.config["LDAP_HOST"] = "openldap.example.org"
+app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org"
+app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org"
+app.config["LDAP_PASSWORD"] = "password"
 
 # OpenLDAP
-app.config['LDAP_OPENLDAP'] = True
-app.config['LDAP_OBJECTS_DN'] = 'dn'
-app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))'
+app.config["LDAP_OPENLDAP"] = True
+app.config["LDAP_OBJECTS_DN"] = "dn"
+app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))"
 
 # Groups configuration
-app.config['LDAP_GROUP_MEMBERS_FIELD'] = 'uniquemember'
-app.config['LDAP_GROUP_OBJECT_FILTER'] = '(&(objectclass=groupOfUniqueNames)(cn=%s))'
-app.config['LDAP_GROUPS_OBJECT_FILTER'] = 'objectclass=groupOfUniqueNames'
-app.config['LDAP_GROUP_FIELDS'] = ['cn', 'entryDN', 'member', 'description']
-app.config['LDAP_GROUP_MEMBER_FILTER'] = '(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))'
-app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn"
+app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember"
+app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
+app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames"
+app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"]
+app.config[
+    "LDAP_GROUP_MEMBER_FILTER"
+] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn"
 
 ldap = LDAP(app)
 
-@app.route('/')
+
+@app.route("/")
 @ldap.basic_auth_required
 def index():
-    return 'Welcome, {0}!'.format(g.ldap_username)
+    return "Welcome, {0}!".format(g.ldap_username)
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
diff --git a/examples/blueprints/blueprints/app.py b/examples/blueprints/blueprints/app.py
index 13e5652fb077d4569b07cb802f91a2fe9f88034b..506b02c43b0a2e138106664435425418a952555e 100644
--- a/examples/blueprints/blueprints/app.py
+++ b/examples/blueprints/blueprints/app.py
@@ -5,10 +5,7 @@ from .extensions import ldap
 from .core import core
 from .foo import foo
 
-DEFAULT_BLUEPRINTS = (
-    core,
-    foo
-)
+DEFAULT_BLUEPRINTS = (core, foo)
 
 
 def create_app(config=None, app_name=None, blueprints=None):
@@ -33,11 +30,11 @@ def register_hooks(app):
     @app.before_request
     def before_request():
         g.user = None
-        if 'user_id' in session:
+        if "user_id" in session:
             # This is where you'd query your database to get the user info.
             g.user = {}
             # Create a global with the LDAP groups the user is a member of.
-            g.ldap_groups = ldap.get_user_groups(user=session['user_id'])
+            g.ldap_groups = ldap.get_user_groups(user=session["user_id"])
 
 
 def register_blueprints(app, blueprints):
diff --git a/examples/blueprints/blueprints/config.py b/examples/blueprints/blueprints/config.py
index 8156f297e4aa3f2932c80f9cf51451a2635e374a..8c0a5f392742d6b5c9510f28975ae66a743b6b14 100644
--- a/examples/blueprints/blueprints/config.py
+++ b/examples/blueprints/blueprints/config.py
@@ -2,14 +2,14 @@ import ldap
 
 
 class BaseConfig(object):
-    PROJECT = 'foo'
-    SECRET_KEY = 'dev key'
+    PROJECT = "foo"
+    SECRET_KEY = "dev key"
     DEBUG = True
 
     # LDAP
-    LDAP_HOST = 'ldap.example.org'
-    LDAP_BASE_DN = 'OU=users,dc=example,dc=org'
-    LDAP_USERNAME = 'CN=user,OU=Users,DC=example,DC=org'
-    LDAP_PASSWORD = 'password'
-    LDAP_LOGIN_VIEW = 'core.login'
+    LDAP_HOST = "ldap.example.org"
+    LDAP_BASE_DN = "OU=users,dc=example,dc=org"
+    LDAP_USERNAME = "CN=user,OU=Users,DC=example,DC=org"
+    LDAP_PASSWORD = "password"
+    LDAP_LOGIN_VIEW = "core.login"
     LDAP_CUSTOM_OPTIONS = {ldap.OPT_REFERRALS: 0}
diff --git a/examples/blueprints/blueprints/core/views.py b/examples/blueprints/blueprints/core/views.py
index cbb4dd5795bf1a4d584807fbd79dd6f5d3e97c12..e812bf791ff496d7fd154d4f37434925e4be05e1 100644
--- a/examples/blueprints/blueprints/core/views.py
+++ b/examples/blueprints/blueprints/core/views.py
@@ -1,41 +1,41 @@
 from flask import Blueprint, g, request, session, redirect, url_for
 from ..extensions import ldap
 
-core = Blueprint('core', __name__)
+core = Blueprint("core", __name__)
 
 
-@core.route('/')
+@core.route("/")
 @ldap.login_required
 def index():
-    return 'Successfully logged in!'
+    return "Successfully logged in!"
 
 
-@core.route('/login', methods=['GET', 'POST'])
+@core.route("/login", methods=["GET", "POST"])
 def login():
     if g.user:
-        return redirect(url_for('index'))
-    if request.method == 'POST':
-        user = request.form['user']
-        passwd = request.form['passwd']
+        return redirect(url_for("index"))
+    if request.method == "POST":
+        user = request.form["user"]
+        passwd = request.form["passwd"]
         test = ldap.bind_user(user, passwd)
-        if test is None or passwd == '':
-            return 'Invalid credentials'
+        if test is None or passwd == "":
+            return "Invalid credentials"
         else:
-            session['user_id'] = request.form['user']
-            return redirect('/')
+            session["user_id"] = request.form["user"]
+            return redirect("/")
     return """<form action="" method="post">
                 user: <input name="user"><br>
                 password:<input type="password" name="passwd"><br>
                 <input type="submit" value="Submit"></form>"""
 
 
-@core.route('/group')
-@ldap.group_required(groups=['Web Developers', 'QA'])
+@core.route("/group")
+@ldap.group_required(groups=["Web Developers", "QA"])
 def group():
-    return 'Group restricted page'
+    return "Group restricted page"
 
 
-@core.route('/logout')
+@core.route("/logout")
 def logout():
-    session.pop('user_id', None)
-    return redirect(url_for('index'))
+    session.pop("user_id", None)
+    return redirect(url_for("index"))
diff --git a/examples/blueprints/blueprints/extensions.py b/examples/blueprints/blueprints/extensions.py
index 95498892652aecfdb2ad649a27df569fed61c01a..df61de2ab0b1b973ca6dbdc3b72cd2976942b023 100644
--- a/examples/blueprints/blueprints/extensions.py
+++ b/examples/blueprints/blueprints/extensions.py
@@ -1,2 +1,3 @@
 from flask_simpleldap import LDAP
+
 ldap = LDAP()
diff --git a/examples/blueprints/blueprints/foo/views.py b/examples/blueprints/blueprints/foo/views.py
index 41505187939852ae96c7cecb6fda2386e0d4a61b..c5f86d02b5f2fb5f8f00acf704501ec2028b3631 100644
--- a/examples/blueprints/blueprints/foo/views.py
+++ b/examples/blueprints/blueprints/foo/views.py
@@ -1,10 +1,10 @@
 from flask import Blueprint
 from ..extensions import ldap
 
-foo = Blueprint('foo', __name__, url_prefix='/foo')
+foo = Blueprint("foo", __name__, url_prefix="/foo")
 
 
-@foo.route('/group')
-@ldap.group_required(groups=['Web Developers', 'QA'])
+@foo.route("/group")
+@ldap.group_required(groups=["Web Developers", "QA"])
 def group():
-    return 'Group restricted page in foo module'
+    return "Group restricted page in foo module"
diff --git a/examples/groups/app.py b/examples/groups/app.py
index 747d6065ffeee7b0961829898c4283e5577d3985..0ea563262edf0c571ad84776a730810d8e27d10e 100644
--- a/examples/groups/app.py
+++ b/examples/groups/app.py
@@ -3,14 +3,14 @@ from flask import Flask, g, request, session, redirect, url_for
 from flask_simpleldap import LDAP
 
 app = Flask(__name__)
-app.secret_key = 'dev key'
+app.secret_key = "dev key"
 app.debug = True
 
-app.config['LDAP_HOST'] = 'ldap.example.org'
-app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org'
-app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org'
-app.config['LDAP_PASSWORD'] = 'password'
-app.config['LDAP_CUSTOM_OPTIONS'] = {l.OPT_REFERRALS: 0}
+app.config["LDAP_HOST"] = "ldap.example.org"
+app.config["LDAP_BASE_DN"] = "OU=users,dc=example,dc=org"
+app.config["LDAP_USERNAME"] = "CN=user,OU=Users,DC=example,DC=org"
+app.config["LDAP_PASSWORD"] = "password"
+app.config["LDAP_CUSTOM_OPTIONS"] = {l.OPT_REFERRALS: 0}
 
 ldap = LDAP(app)
 
@@ -18,49 +18,49 @@ ldap = LDAP(app)
 @app.before_request
 def before_request():
     g.user = None
-    if 'user_id' in session:
+    if "user_id" in session:
         # This is where you'd query your database to get the user info.
         g.user = {}
         # Create a global with the LDAP groups the user is a member of.
-        g.ldap_groups = ldap.get_user_groups(user=session['user_id'])
+        g.ldap_groups = ldap.get_user_groups(user=session["user_id"])
 
 
-@app.route('/')
+@app.route("/")
 @ldap.login_required
 def index():
-    return 'Successfully logged in!'
+    return "Successfully logged in!"
 
 
-@app.route('/login', methods=['GET', 'POST'])
+@app.route("/login", methods=["GET", "POST"])
 def login():
     if g.user:
-        return redirect(url_for('index'))
-    if request.method == 'POST':
-        user = request.form['user']
-        passwd = request.form['passwd']
+        return redirect(url_for("index"))
+    if request.method == "POST":
+        user = request.form["user"]
+        passwd = request.form["passwd"]
         test = ldap.bind_user(user, passwd)
-        if test is None or passwd == '':
-            return 'Invalid credentials'
+        if test is None or passwd == "":
+            return "Invalid credentials"
         else:
-            session['user_id'] = request.form['user']
-            return redirect('/')
+            session["user_id"] = request.form["user"]
+            return redirect("/")
     return """<form action="" method="post">
                 user: <input name="user"><br>
                 password:<input type="password" name="passwd"><br>
                 <input type="submit" value="Submit"></form>"""
 
 
-@app.route('/group')
-@ldap.group_required(groups=['Web Developers', 'QA'])
+@app.route("/group")
+@ldap.group_required(groups=["Web Developers", "QA"])
 def group():
-    return 'Group restricted page'
+    return "Group restricted page"
 
 
-@app.route('/logout')
+@app.route("/logout")
 def logout():
-    session.pop('user_id', None)
-    return redirect(url_for('index'))
+    session.pop("user_id", None)
+    return redirect(url_for("index"))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
diff --git a/examples/groups/app_oldap.py b/examples/groups/app_oldap.py
index 36677f11b91d97fe32e0b06b671e638081432c85..4fd547a7c8da866a5d5168179a50c26040660882 100644
--- a/examples/groups/app_oldap.py
+++ b/examples/groups/app_oldap.py
@@ -2,25 +2,27 @@ from flask import Flask, g, request, session, redirect, url_for
 from flask_simpleldap import LDAP
 
 app = Flask(__name__)
-app.secret_key = 'dev key'
+app.secret_key = "dev key"
 app.debug = True
 
-app.config['LDAP_OPENLDAP'] = True
-app.config['LDAP_OBJECTS_DN'] = 'dn'
-app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication'
-app.config['LDAP_HOST'] = 'openldap.example.org'
-app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org'
-app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org'
-app.config['LDAP_PASSWORD'] = 'password'
-app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))'
+app.config["LDAP_OPENLDAP"] = True
+app.config["LDAP_OBJECTS_DN"] = "dn"
+app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication"
+app.config["LDAP_HOST"] = "openldap.example.org"
+app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org"
+app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org"
+app.config["LDAP_PASSWORD"] = "password"
+app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))"
 
 # Group configuration
-app.config['LDAP_GROUP_MEMBERS_FIELD'] = 'uniquemember'
-app.config['LDAP_GROUP_OBJECT_FILTER'] = '(&(objectclass=groupOfUniqueNames)(cn=%s))'
-app.config['LDAP_GROUPS_OBJECT_FILTER'] = 'objectclass=groupOfUniqueNames'
-app.config['LDAP_GROUP_FIELDS'] = ['cn', 'entryDN', 'member', 'description']
-app.config['LDAP_GROUP_MEMBER_FILTER'] = '(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))'
-app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn"
+app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember"
+app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))"
+app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames"
+app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"]
+app.config[
+    "LDAP_GROUP_MEMBER_FILTER"
+] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))"
+app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn"
 
 ldap = LDAP(app)
 
@@ -28,49 +30,49 @@ ldap = LDAP(app)
 @app.before_request
 def before_request():
     g.user = None
-    if 'user_id' in session:
+    if "user_id" in session:
         # This is where you'd query your database to get the user info.
         g.user = {}
         # Create a global with the LDAP groups the user is a member of.
-        g.ldap_groups = ldap.get_user_groups(user=session['user_id'])
+        g.ldap_groups = ldap.get_user_groups(user=session["user_id"])
 
 
-@app.route('/')
+@app.route("/")
 @ldap.login_required
 def index():
-    return 'Successfully logged in!'
+    return "Successfully logged in!"
 
 
-@app.route('/login', methods=['GET', 'POST'])
+@app.route("/login", methods=["GET", "POST"])
 def login():
     if g.user:
-        return redirect(url_for('index'))
-    if request.method == 'POST':
-        user = request.form['user']
-        passwd = request.form['passwd']
+        return redirect(url_for("index"))
+    if request.method == "POST":
+        user = request.form["user"]
+        passwd = request.form["passwd"]
         test = ldap.bind_user(user, passwd)
-        if test is None or passwd == '':
-            return 'Invalid credentials'
+        if test is None or passwd == "":
+            return "Invalid credentials"
         else:
-            session['user_id'] = request.form['user']
-            return redirect('/')
+            session["user_id"] = request.form["user"]
+            return redirect("/")
     return """<form action="" method="post">
                 user: <input name="user"><br>
                 password:<input type="password" name="passwd"><br>
                 <input type="submit" value="Submit"></form>"""
 
 
-@app.route('/group')
-@ldap.group_required(groups=['web-developers'])
+@app.route("/group")
+@ldap.group_required(groups=["web-developers"])
 def group():
-    return 'Group restricted page'
+    return "Group restricted page"
 
 
-@app.route('/logout')
+@app.route("/logout")
 def logout():
-    session.pop('user_id', None)
-    return redirect(url_for('index'))
+    session.pop("user_id", None)
+    return redirect(url_for("index"))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     app.run()
diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py
index 7eff2c8d191b22ed7f6282b9085c2f4b6e9469cf..1d3867d9c3a202b809dc0a2193c1f92514614f01 100644
--- a/flask_simpleldap/__init__.py
+++ b/flask_simpleldap/__init__.py
@@ -2,10 +2,9 @@ import re
 from functools import wraps
 import ldap
 from ldap import filter as ldap_filter
-from flask import abort, current_app, g, make_response, redirect, url_for, \
-    request
+from flask import abort, current_app, g, make_response, redirect, url_for, request
 
-__all__ = ['LDAP']
+__all__ = ["LDAP"]
 
 
 class LDAPException(RuntimeError):
@@ -32,51 +31,50 @@ class LDAP(object):
         :param flask.Flask app: the application to configure for use with
            this :class:`~LDAP`
         """
-        app.config.setdefault('LDAP_HOST', 'localhost')
-        app.config.setdefault('LDAP_PORT', 389)
-        app.config.setdefault('LDAP_SCHEMA', 'ldap')
-        app.config.setdefault('LDAP_USERNAME', None)
-        app.config.setdefault('LDAP_PASSWORD', None)
-        app.config.setdefault('LDAP_TIMEOUT', 10)
-        app.config.setdefault('LDAP_USE_SSL', False)
-        app.config.setdefault('LDAP_USE_TLS', False)
-        app.config.setdefault('LDAP_REQUIRE_CERT', False)
-        app.config.setdefault('LDAP_CERT_PATH', '/path/to/cert')
-        app.config.setdefault('LDAP_BASE_DN', None)
-        app.config.setdefault('LDAP_OBJECTS_DN', 'distinguishedName')
-        app.config.setdefault('LDAP_USER_FIELDS', [])
-        app.config.setdefault('LDAP_USER_OBJECT_FILTER',
-                              '(&(objectclass=Person)(userPrincipalName=%s))')
-        app.config.setdefault('LDAP_USER_GROUPS_FIELD', 'memberOf')
-        app.config.setdefault('LDAP_GROUP_FIELDS', [])
-        app.config.setdefault('LDAP_GROUPS_OBJECT_FILTER', 'objectclass=Group')
-        app.config.setdefault('LDAP_GROUP_OBJECT_FILTER',
-                              '(&(objectclass=Group)(userPrincipalName=%s))')
-        app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member')
-        app.config.setdefault('LDAP_LOGIN_VIEW', 'login')
-        app.config.setdefault('LDAP_REALM_NAME', 'LDAP authentication')
-        app.config.setdefault('LDAP_OPENLDAP', False)
-        app.config.setdefault('LDAP_GROUP_MEMBER_FILTER', '*')
-        app.config.setdefault('LDAP_GROUP_MEMBER_FILTER_FIELD', '*')
-        app.config.setdefault('LDAP_CUSTOM_OPTIONS', None)
-
-        if app.config['LDAP_USE_SSL'] or app.config['LDAP_USE_TLS']:
-            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
-                            ldap.OPT_X_TLS_NEVER)
-
-        if app.config['LDAP_REQUIRE_CERT']:
-            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
-                            ldap.OPT_X_TLS_DEMAND)
-            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,
-                            app.config['LDAP_CERT_PATH'])
-
-        for option in ['USERNAME', 'PASSWORD', 'BASE_DN']:
-            if app.config['LDAP_{0}'.format(option)] is None:
-                raise LDAPException('LDAP_{0} cannot be None!'.format(option))
+        app.config.setdefault("LDAP_HOST", "localhost")
+        app.config.setdefault("LDAP_PORT", 389)
+        app.config.setdefault("LDAP_SCHEMA", "ldap")
+        app.config.setdefault("LDAP_USERNAME", None)
+        app.config.setdefault("LDAP_PASSWORD", None)
+        app.config.setdefault("LDAP_TIMEOUT", 10)
+        app.config.setdefault("LDAP_USE_SSL", False)
+        app.config.setdefault("LDAP_USE_TLS", False)
+        app.config.setdefault("LDAP_REQUIRE_CERT", False)
+        app.config.setdefault("LDAP_CERT_PATH", "/path/to/cert")
+        app.config.setdefault("LDAP_BASE_DN", None)
+        app.config.setdefault("LDAP_OBJECTS_DN", "distinguishedName")
+        app.config.setdefault("LDAP_USER_FIELDS", [])
+        app.config.setdefault(
+            "LDAP_USER_OBJECT_FILTER", "(&(objectclass=Person)(userPrincipalName=%s))"
+        )
+        app.config.setdefault("LDAP_USER_GROUPS_FIELD", "memberOf")
+        app.config.setdefault("LDAP_GROUP_FIELDS", [])
+        app.config.setdefault("LDAP_GROUPS_OBJECT_FILTER", "objectclass=Group")
+        app.config.setdefault(
+            "LDAP_GROUP_OBJECT_FILTER", "(&(objectclass=Group)(userPrincipalName=%s))"
+        )
+        app.config.setdefault("LDAP_GROUP_MEMBERS_FIELD", "member")
+        app.config.setdefault("LDAP_LOGIN_VIEW", "login")
+        app.config.setdefault("LDAP_REALM_NAME", "LDAP authentication")
+        app.config.setdefault("LDAP_OPENLDAP", False)
+        app.config.setdefault("LDAP_GROUP_MEMBER_FILTER", "*")
+        app.config.setdefault("LDAP_GROUP_MEMBER_FILTER_FIELD", "*")
+        app.config.setdefault("LDAP_CUSTOM_OPTIONS", None)
+
+        if app.config["LDAP_USE_SSL"] or app.config["LDAP_USE_TLS"]:
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+
+        if app.config["LDAP_REQUIRE_CERT"]:
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
+            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, app.config["LDAP_CERT_PATH"])
+
+        for option in ["USERNAME", "PASSWORD", "BASE_DN"]:
+            if app.config["LDAP_{0}".format(option)] is None:
+                raise LDAPException("LDAP_{0} cannot be None!".format(option))
 
     @staticmethod
     def _set_custom_options(conn):
-        options = current_app.config['LDAP_CUSTOM_OPTIONS']
+        options = current_app.config["LDAP_CUSTOM_OPTIONS"]
         if options:
             for k, v in options.items():
                 conn.set_option(k, v)
@@ -90,15 +88,19 @@ class LDAP(object):
         """
 
         try:
-            conn = ldap.initialize('{0}://{1}:{2}'.format(
-                current_app.config['LDAP_SCHEMA'],
-                current_app.config['LDAP_HOST'],
-                current_app.config['LDAP_PORT']))
-            conn.set_option(ldap.OPT_NETWORK_TIMEOUT,
-                            current_app.config['LDAP_TIMEOUT'])
+            conn = ldap.initialize(
+                "{0}://{1}:{2}".format(
+                    current_app.config["LDAP_SCHEMA"],
+                    current_app.config["LDAP_HOST"],
+                    current_app.config["LDAP_PORT"],
+                )
+            )
+            conn.set_option(
+                ldap.OPT_NETWORK_TIMEOUT, current_app.config["LDAP_TIMEOUT"]
+            )
             conn = self._set_custom_options(conn)
             conn.protocol_version = ldap.VERSION3
-            if current_app.config['LDAP_USE_TLS']:
+            if current_app.config["LDAP_USE_TLS"]:
                 conn.start_tls_s()
             return conn
         except ldap.LDAPError as e:
@@ -116,8 +118,8 @@ class LDAP(object):
         conn = self.initialize
         try:
             conn.simple_bind_s(
-                current_app.config['LDAP_USERNAME'],
-                current_app.config['LDAP_PASSWORD'])
+                current_app.config["LDAP_USERNAME"], current_app.config["LDAP_PASSWORD"]
+            )
             return conn
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e.args))
@@ -148,15 +150,17 @@ class LDAP(object):
             return
         try:
             conn = self.initialize
-            _user_dn = user_dn.decode('utf-8') \
-                if isinstance(user_dn, bytes) else user_dn
+            _user_dn = (
+                user_dn.decode("utf-8") if isinstance(user_dn, bytes) else user_dn
+            )
             conn.simple_bind_s(_user_dn, password)
             return True
         except ldap.LDAPError:
             return
 
-    def get_object_details(self, user=None, group=None, query_filter=None,
-                           dn_only=False):
+    def get_object_details(
+        self, user=None, group=None, query_filter=None, dn_only=False
+    ):
         """Returns a ``dict`` with the object's (user or group) details.
 
         :param str user: Username of the user object you want details for.
@@ -169,33 +173,35 @@ class LDAP(object):
         fields = None
         if user is not None:
             if not dn_only:
-                fields = current_app.config['LDAP_USER_FIELDS']
-            query_filter = query_filter or \
-                current_app.config['LDAP_USER_OBJECT_FILTER']
+                fields = current_app.config["LDAP_USER_FIELDS"]
+            query_filter = query_filter or current_app.config["LDAP_USER_OBJECT_FILTER"]
             query = ldap_filter.filter_format(query_filter, (user,))
         elif group is not None:
             if not dn_only:
-                fields = current_app.config['LDAP_GROUP_FIELDS']
-            query_filter = query_filter or \
-                current_app.config['LDAP_GROUP_OBJECT_FILTER']
+                fields = current_app.config["LDAP_GROUP_FIELDS"]
+            query_filter = (
+                query_filter or current_app.config["LDAP_GROUP_OBJECT_FILTER"]
+            )
             query = ldap_filter.filter_format(query_filter, (group,))
         conn = self.bind
         try:
-            records = conn.search_s(current_app.config['LDAP_BASE_DN'],
-                                    ldap.SCOPE_SUBTREE, query, fields)
+            records = conn.search_s(
+                current_app.config["LDAP_BASE_DN"], ldap.SCOPE_SUBTREE, query, fields
+            )
             conn.unbind_s()
             result = {}
-            if records and\
-               records[0][0] is not None and isinstance(records[0][1], dict):
+            if (
+                records
+                and records[0][0] is not None
+                and isinstance(records[0][1], dict)
+            ):
                 if dn_only:
-                    if current_app.config['LDAP_OPENLDAP']:
+                    if current_app.config["LDAP_OPENLDAP"]:
                         if records:
                             return records[0][0]
                     else:
-                        if current_app.config['LDAP_OBJECTS_DN'] \
-                                in records[0][1]:
-                            dn = records[0][1][
-                                current_app.config['LDAP_OBJECTS_DN']]
+                        if current_app.config["LDAP_OBJECTS_DN"] in records[0][1]:
+                            dn = records[0][1][current_app.config["LDAP_OBJECTS_DN"]]
                             return dn[0]
                 for k, v in list(records[0][1].items()):
                     result[k] = v
@@ -217,17 +223,21 @@ class LDAP(object):
         """
         conn = self.bind
         try:
-            fields = fields or current_app.config['LDAP_GROUP_FIELDS']
-            if current_app.config['LDAP_OPENLDAP']:
+            fields = fields or current_app.config["LDAP_GROUP_FIELDS"]
+            if current_app.config["LDAP_OPENLDAP"]:
                 records = conn.search_s(
-                    current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
-                    current_app.config['LDAP_GROUPS_OBJECT_FILTER'],
-                    fields)
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_GROUPS_OBJECT_FILTER"],
+                    fields,
+                )
             else:
                 records = conn.search_s(
-                    current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
-                    current_app.config['LDAP_GROUPS_OBJECT_FILTER'],
-                    fields)
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_GROUPS_OBJECT_FILTER"],
+                    fields,
+                )
             conn.unbind_s()
             if records:
                 if dn_only:
@@ -248,42 +258,51 @@ class LDAP(object):
 
         conn = self.bind
         try:
-            if current_app.config['LDAP_OPENLDAP']:
-                fields = \
-                    [str(current_app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'])]
+            if current_app.config["LDAP_OPENLDAP"]:
+                fields = [str(current_app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"])]
                 records = conn.search_s(
-                    current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
                     ldap_filter.filter_format(
-                        current_app.config['LDAP_GROUP_MEMBER_FILTER'],
-                        (self.get_object_details(user, dn_only=True),)),
-                    fields)
+                        current_app.config["LDAP_GROUP_MEMBER_FILTER"],
+                        (self.get_object_details(user, dn_only=True),),
+                    ),
+                    fields,
+                )
             else:
                 records = conn.search_s(
-                    current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
+                    current_app.config["LDAP_BASE_DN"],
+                    ldap.SCOPE_SUBTREE,
                     ldap_filter.filter_format(
-                        current_app.config['LDAP_USER_OBJECT_FILTER'],
-                        (user,)),
-                    [current_app.config['LDAP_USER_GROUPS_FIELD']])
+                        current_app.config["LDAP_USER_OBJECT_FILTER"], (user,)
+                    ),
+                    [current_app.config["LDAP_USER_GROUPS_FIELD"]],
+                )
 
             conn.unbind_s()
             if records:
-                if current_app.config['LDAP_OPENLDAP']:
-                    group_member_filter = \
-                        current_app.config['LDAP_GROUP_MEMBER_FILTER_FIELD']
+                if current_app.config["LDAP_OPENLDAP"]:
+                    group_member_filter = current_app.config[
+                        "LDAP_GROUP_MEMBER_FILTER_FIELD"
+                    ]
                     record_list = [record[1] for record in records]
                     record_dicts = [
-                        record for record in record_list if isinstance(record, dict)]
-                    groups = [item.get([group_member_filter][0])[0]
-                              for item in record_dicts]
+                        record for record in record_list if isinstance(record, dict)
+                    ]
+                    groups = [
+                        item.get([group_member_filter][0])[0] for item in record_dicts
+                    ]
                     return groups
                 else:
-                    if current_app.config['LDAP_USER_GROUPS_FIELD'] in \
-                            records[0][1]:
+                    if current_app.config["LDAP_USER_GROUPS_FIELD"] in records[0][1]:
                         groups = records[0][1][
-                            current_app.config['LDAP_USER_GROUPS_FIELD']]
-                        result = [re.findall(b'(?:cn=|CN=)(.*?),', group)[0]
-                                  for group in groups]
-                        result = [r.decode('utf-8') for r in result]
+                            current_app.config["LDAP_USER_GROUPS_FIELD"]
+                        ]
+                        result = [
+                            re.findall(b"(?:cn=|CN=)(.*?),", group)[0]
+                            for group in groups
+                        ]
+                        result = [r.decode("utf-8") for r in result]
                         return result
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e.args))
@@ -298,17 +317,20 @@ class LDAP(object):
         conn = self.bind
         try:
             records = conn.search_s(
-                current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
+                current_app.config["LDAP_BASE_DN"],
+                ldap.SCOPE_SUBTREE,
                 ldap_filter.filter_format(
-                    current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,)),
-                [current_app.config['LDAP_GROUP_MEMBERS_FIELD']])
+                    current_app.config["LDAP_GROUP_OBJECT_FILTER"], (group,)
+                ),
+                [current_app.config["LDAP_GROUP_MEMBERS_FIELD"]],
+            )
             conn.unbind_s()
             if records:
-                if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in \
-                        records[0][1]:
+                if current_app.config["LDAP_GROUP_MEMBERS_FIELD"] in records[0][1]:
                     members = records[0][1][
-                        current_app.config['LDAP_GROUP_MEMBERS_FIELD']]
-                    members = [m.decode('utf-8') for m in members]
+                        current_app.config["LDAP_GROUP_MEMBERS_FIELD"]
+                    ]
+                    members = [m.decode("utf-8") for m in members]
                     return members
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e.args))
@@ -316,8 +338,8 @@ class LDAP(object):
     @staticmethod
     def error(e):
         e = e[0]
-        if 'desc' in e:
-            return e['desc']
+        if "desc" in e:
+            return e["desc"]
         else:
             return e
 
@@ -337,12 +359,12 @@ class LDAP(object):
         @wraps(func)
         def wrapped(*args, **kwargs):
             if g.user is None:
-                next_path=request.full_path or request.path
-                if next_path == '/?':
-                    return redirect(
-                        url_for(current_app.config['LDAP_LOGIN_VIEW']))
-                return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'],
-                                        next=next_path))
+                next_path = request.full_path or request.path
+                if next_path == "/?":
+                    return redirect(url_for(current_app.config["LDAP_LOGIN_VIEW"]))
+                return redirect(
+                    url_for(current_app.config["LDAP_LOGIN_VIEW"], next=next_path)
+                )
             return func(*args, **kwargs)
 
         return wrapped
@@ -367,8 +389,11 @@ class LDAP(object):
             def wrapped(*args, **kwargs):
                 if g.user is None:
                     return redirect(
-                        url_for(current_app.config['LDAP_LOGIN_VIEW'],
-                                next=request.full_path or request.path))
+                        url_for(
+                            current_app.config["LDAP_LOGIN_VIEW"],
+                            next=request.full_path or request.path,
+                        )
+                    )
                 match = [group for group in groups if group in g.ldap_groups]
                 if not match:
                     abort(403)
@@ -395,9 +420,8 @@ class LDAP(object):
         """
 
         def make_auth_required_response():
-            response = make_response('Unauthorized', 401)
-            response.www_authenticate.set_basic(
-                current_app.config['LDAP_REALM_NAME'])
+            response = make_response("Unauthorized", 401)
+            response.www_authenticate.set_basic(current_app.config["LDAP_REALM_NAME"])
             return response
 
         @wraps(func)
@@ -412,13 +436,14 @@ class LDAP(object):
             # with an empty password, even if you supply a non-anonymous user
             # ID, causing .bind_user() to return True. Therefore, only accept
             # non-empty passwords.
-            if req_username in ['', None] or req_password in ['', None]:
-                current_app.logger.debug('Got a request without auth data')
+            if req_username in ["", None] or req_password in ["", None]:
+                current_app.logger.debug("Got a request without auth data")
                 return make_auth_required_response()
 
             if not self.bind_user(req_username, req_password):
-                current_app.logger.debug('User {0!r} gave wrong '
-                                         'password'.format(req_username))
+                current_app.logger.debug(
+                    "User {0!r} gave wrong " "password".format(req_username)
+                )
                 return make_auth_required_response()
 
             g.ldap_username = req_username
diff --git a/requirements.txt b/requirements.txt
index 88522a19cea56db4923c1b520071e24113327383..b78790e9723f372dd5b88ba54a18e8b69d3e30e2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-Flask==1.1.1
-mock==3.0.5  # for ci
+Flask==2.0.2
+mock==4.0.3  # for ci
diff --git a/setup.py b/setup.py
index 84c3c61d7ef584f2b943e3a625614a6bf728f734..3897a387d01d5cb9feaed202eeb465e0feaa2ba1 100644
--- a/setup.py
+++ b/setup.py
@@ -6,33 +6,35 @@ LDAP authentication extension for Flask
 """
 from setuptools import setup
 
+from pathlib import Path
+
+this_directory = Path(__file__).parent
+long_description = (this_directory / "README.md").read_text()
+
 
 setup(
-    name='Flask-SimpleLDAP',
-    version='1.4.0',
-    url='https://github.com/admiralobvious/flask-simpleldap',
-    license='MIT',
-    author='Alexandre Ferland',
-    author_email='aferlandqc@gmail.com',
-    description='LDAP authentication extension for Flask',
-    long_description=__doc__,
-    packages=['flask_simpleldap'],
+    name="Flask-SimpleLDAP",
+    version="1.4.0",
+    url="https://github.com/alexferl/flask-simpleldap",
+    license="MIT",
+    author="Alexandre Ferland",
+    author_email="me@alexferl.com",
+    description="LDAP authentication extension for Flask",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    packages=["flask_simpleldap"],
     zip_safe=False,
     include_package_data=True,
-    platforms='any',
-    install_requires=[
-        'Flask>=0.12.4',
-        'python-ldap>=3.0.0'
-    ],
+    platforms="any",
+    install_requires=["Flask>=0.12.4", "python-ldap>=3.0.0"],
     classifiers=[
-        'Environment :: Web Environment',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Topic :: Software Development :: Libraries :: Python Modules'
-    ]
+        "Environment :: Web Environment",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+    ],
 )
-