Add compatibility with cattrs 21.1+, and clean up preconf module a bit
diff --git a/poetry.lock b/poetry.lock
index 5db2e9b..6fe2a64 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -63,11 +63,11 @@
 
 [[package]]
 name = "beautifulsoup4"
-version = "4.10.0"
+version = "4.11.1"
 description = "Screen-scraping library"
 category = "main"
 optional = true
-python-versions = ">3.0.0"
+python-versions = ">=3.6.0"
 
 [package.dependencies]
 soupsieve = ">1.2"
@@ -78,14 +78,14 @@
 
 [[package]]
 name = "boto3"
-version = "1.21.35"
+version = "1.21.37"
 description = "The AWS SDK for Python"
 category = "main"
 optional = true
 python-versions = ">= 3.6"
 
 [package.dependencies]
-botocore = ">=1.24.35,<1.25.0"
+botocore = ">=1.24.37,<1.25.0"
 jmespath = ">=0.7.1,<2.0.0"
 s3transfer = ">=0.5.0,<0.6.0"
 
@@ -94,7 +94,7 @@
 
 [[package]]
 name = "botocore"
-version = "1.24.35"
+version = "1.24.37"
 description = "Low-level, data-driven core of boto 3."
 category = "main"
 optional = true
@@ -122,7 +122,7 @@
 
 [[package]]
 name = "cattrs"
-version = "1.10.0"
+version = "22.1.0"
 description = "Composable complex class support for attrs and dataclasses."
 category = "main"
 optional = false
@@ -130,6 +130,7 @@
 
 [package.dependencies]
 attrs = ">=20"
+exceptiongroup = {version = "*", markers = "python_version <= \"3.10\""}
 typing_extensions = {version = "*", markers = "python_version >= \"3.7\" and python_version < \"3.8\""}
 
 [[package]]
@@ -237,6 +238,17 @@
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
+name = "exceptiongroup"
+version = "1.0.0rc2"
+description = "Backport of PEP 654 (exception groups)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest (>=6)", "coverage (>=6)"]
+
+[[package]]
 name = "execnet"
 version = "1.9.0"
 description = "execnet: rapid multi-Python deployment"
@@ -261,7 +273,7 @@
 
 [[package]]
 name = "furo"
-version = "2022.3.4"
+version = "2022.4.7"
 description = "A clean customisable Sphinx documentation theme."
 category = "main"
 optional = true
@@ -1259,7 +1271,7 @@
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "ef51335c82b4ea675988ed97c3f9f67f24a2cad0c6edec0d1695cd68ed686ffe"
+content-hash = "5cb2e245d8e7642d221b6fb33465920b316e2e8e318db4c833a7ba919ee145d3"
 
 [metadata.files]
 alabaster = [
@@ -1287,23 +1299,23 @@
     {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
 ]
 beautifulsoup4 = [
-    {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
-    {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
+    {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
+    {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
 ]
 boto3 = [
-    {file = "boto3-1.21.35-py3-none-any.whl", hash = "sha256:4fb810755bca4696effbeb0c6f792295795fae52e895638038dff53965f3f423"},
-    {file = "boto3-1.21.35.tar.gz", hash = "sha256:ab6e001ba9de1db986634424abff6c79d938c15d0d2fa3ef95eb0939c120b4f6"},
+    {file = "boto3-1.21.37-py3-none-any.whl", hash = "sha256:1e845aa92b3ad70b954329b98835135c28b3000e322ff8d3fc46a956bdb6e94b"},
+    {file = "boto3-1.21.37.tar.gz", hash = "sha256:013ba57295f05da141e364191dd46f4086e8fe3eb83a3cd09730eeb684ffbab3"},
 ]
 botocore = [
-    {file = "botocore-1.24.35-py3-none-any.whl", hash = "sha256:734aa598af5d6bc0351e6ecce4a91b0b6ccf245febfd8d4de8425211aada5f36"},
-    {file = "botocore-1.24.35.tar.gz", hash = "sha256:36b5422d8f0c312983582b8b4b056c98e1fd6121cb0b2ddb1f67e882e1ae6867"},
+    {file = "botocore-1.24.37-py3-none-any.whl", hash = "sha256:21e164a213beca36033c46026bffa62f2ee2cd2600777271f9a551fb34dba006"},
+    {file = "botocore-1.24.37.tar.gz", hash = "sha256:70c48c4ae3c2b9ec0ca025385979d01f4c7dae4d9a61c82758d4cf7caa7082cd"},
 ]
 bson = [
     {file = "bson-0.5.10.tar.gz", hash = "sha256:d6511b2ab051139a9123c184de1a04227262173ad593429d21e443d6462d6590"},
 ]
 cattrs = [
-    {file = "cattrs-1.10.0-py3-none-any.whl", hash = "sha256:35dd9063244263e63bd0bd24ea61e3015b00272cead084b2c40d788b0f857c46"},
-    {file = "cattrs-1.10.0.tar.gz", hash = "sha256:211800f725cdecedcbcf4c753bbd22d248312b37d130f06045434acb7d9b34e1"},
+    {file = "cattrs-22.1.0-py3-none-any.whl", hash = "sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364"},
+    {file = "cattrs-22.1.0.tar.gz", hash = "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6"},
 ]
 certifi = [
     {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
@@ -1384,6 +1396,10 @@
     {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
     {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
 ]
+exceptiongroup = [
+    {file = "exceptiongroup-1.0.0rc2-py3-none-any.whl", hash = "sha256:83e465152bd0bc2bc40d9b75686854260f86946bb947c652b5cafc31cdff70e7"},
+    {file = "exceptiongroup-1.0.0rc2.tar.gz", hash = "sha256:4d254b05231bed1d43079bdcfe0f1d66c0ab4783e6777a329355f9b78de3ad83"},
+]
 execnet = [
     {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
     {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
@@ -1393,8 +1409,8 @@
     {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"},
 ]
 furo = [
-    {file = "furo-2022.3.4-py3-none-any.whl", hash = "sha256:6c718293ebf87755f0b9f148b1e697c9e3aabd7af955644d4bcaee5ce75db781"},
-    {file = "furo-2022.3.4.tar.gz", hash = "sha256:7660267cc67b2828fd0e17bc07adeb612c47b2eba5a6de07049a1569e6044aa8"},
+    {file = "furo-2022.4.7-py3-none-any.whl", hash = "sha256:7f3e3d2fb977483590f8ecb2c2cd511bd82661b79c18efb24de9558bc9cdf2d7"},
+    {file = "furo-2022.4.7.tar.gz", hash = "sha256:96204ab7cd047e4b6c523996e0279c4c629a8fc31f4f109b2efd470c17f49c80"},
 ]
 identify = [
     {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"},
diff --git a/pyproject.toml b/pyproject.toml
index a39fd4b..ae9fec9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,7 @@
 requests      = "^2.22"    # Needs no introduction
 urllib3       = ">=1.25.5" # Use a slightly newer version than required by requests (for bugfixes)
 attrs         = ">=21.2"   # For response data models
-cattrs        = "^1.8"     # For response serialization
+cattrs        = ">=22.0"   # For response serialization
 platformdirs  = "^2.5"     # For options that use platform-specific user cache dirs
 url-normalize = "^1.4"     # For improved request matching
 
diff --git a/requests_cache/__init__.py b/requests_cache/__init__.py
index b752000..81da8e9 100644
--- a/requests_cache/__init__.py
+++ b/requests_cache/__init__.py
@@ -18,4 +18,4 @@
     from .settings import *
 # Log and ignore ImportErrors, if imported outside a virtualenv (e.g., just to check __version__)
 except ImportError as e:
-    logger.warning(e)
+    logger.warning(e, exc_info=True)
diff --git a/requests_cache/serializers/preconf.py b/requests_cache/serializers/preconf.py
index ed19fb4..cb099b8 100644
--- a/requests_cache/serializers/preconf.py
+++ b/requests_cache/serializers/preconf.py
@@ -1,3 +1,4 @@
+# flake8: noqa: F841
 """The ``cattrs`` library includes a number of `pre-configured converters
 <https://cattrs.readthedocs.io/en/latest/preconf.html>`_ that perform some pre-serialization steps
 required for specific serialization formats.
@@ -14,69 +15,75 @@
 """
 import pickle
 from functools import partial
-
-from cattr.preconf import bson as bson_preconf
-from cattr.preconf import json as json_preconf
-from cattr.preconf import msgpack, orjson, pyyaml, tomlkit, ujson
+from importlib import import_module
 
 from .._utils import get_placeholder_class
 from .cattrs import CattrStage
 from .pipeline import SerializerPipeline, Stage
 
-base_stage = (
-    CattrStage()
-)  #: Base stage for all serializer pipelines (or standalone dict serializer)
+
+def make_stage(preconf_module: str):
+    """Create a preconf serializer stage from a module name, if dependencies are installed"""
+    try:
+        return CattrStage(import_module(preconf_module).make_converter)
+    except ImportError as e:
+        return get_placeholder_class(e)
+
+
+base_stage = CattrStage()  #: Base stage for all serializer pipelines
 dict_serializer = base_stage  #: Partial serializer that unstructures responses into dicts
-bson_preconf_stage = CattrStage(bson_preconf.make_converter)  #: Pre-serialization steps for BSON
-json_preconf_stage = CattrStage(json_preconf.make_converter)  #: Pre-serialization steps for JSON
-msgpack_preconf_stage = CattrStage(msgpack.make_converter)  #: Pre-serialization steps for msgpack
-orjson_preconf_stage = CattrStage(orjson.make_converter)  #: Pre-serialization steps for orjson
-yaml_preconf_stage = CattrStage(pyyaml.make_converter)  #: Pre-serialization steps for YAML
-toml_preconf_stage = CattrStage(tomlkit.make_converter)  #: Pre-serialization steps for TOML
-ujson_preconf_stage = CattrStage(ujson.make_converter)  #: Pre-serialization steps for ultrajson
-pickle_serializer = SerializerPipeline(
-    [base_stage, pickle], is_binary=True
-)  #: Complete pickle serializer
+pickle_serializer = SerializerPipeline([base_stage, pickle], is_binary=True)  #: Pickle serializer
 utf8_encoder = Stage(dumps=str.encode, loads=lambda x: x.decode())  #: Encode to bytes
+bson_preconf_stage = make_stage('cattr.preconf.bson')  #: Pre-serialization steps for BSON
+json_preconf_stage = make_stage('cattr.preconf.json')  #: Pre-serialization steps for JSON
+msgpack_preconf_stage = make_stage('cattr.preconf.msgpack')  #: Pre-serialization steps for msgpack
+orjson_preconf_stage = make_stage('cattr.preconf.orjson')  #: Pre-serialization steps for orjson
+toml_preconf_stage = make_stage('cattr.preconf.tomlkit')  #: Pre-serialization steps for TOML
+ujson_preconf_stage = make_stage('cattr.preconf.ujson')  #: Pre-serialization steps for ultrajson
+yaml_preconf_stage = make_stage('cattr.preconf.pyyaml')  #: Pre-serialization steps for YAML
 
 
 # Safe pickle serializer
-try:
+def signer_stage(secret_key=None, salt='requests-cache') -> Stage:
+    """Create a stage that uses ``itsdangerous`` to add a signature to responses on write, and
+    validate that signature with a secret key on read. Can be used in a
+    :py:class:`.SerializerPipeline` in combination with any other serialization steps.
+    """
     from itsdangerous import Signer
 
-    def signer_stage(secret_key=None, salt='requests-cache') -> Stage:
-        """Create a stage that uses ``itsdangerous`` to add a signature to responses on write, and
-        validate that signature with a secret key on read. Can be used in a
-        :py:class:`.SerializerPipeline` in combination with any other serialization steps.
-        """
-        return Stage(Signer(secret_key=secret_key, salt=salt), dumps='sign', loads='unsign')
+    return Stage(Signer(secret_key=secret_key, salt=salt), dumps='sign', loads='unsign')
 
-    def safe_pickle_serializer(
-        secret_key=None, salt='requests-cache', **kwargs
-    ) -> SerializerPipeline:
-        """Create a serializer that uses ``pickle`` + ``itsdangerous`` to add a signature to
-        responses on write, and validate that signature with a secret key on read.
-        """
-        return SerializerPipeline(
-            [base_stage, pickle, signer_stage(secret_key, salt)], is_binary=True
-        )
 
+def safe_pickle_serializer(secret_key=None, salt='requests-cache', **kwargs) -> SerializerPipeline:
+    """Create a serializer that uses ``pickle`` + ``itsdangerous`` to add a signature to
+    responses on write, and validate that signature with a secret key on read.
+    """
+    return SerializerPipeline([base_stage, pickle, signer_stage(secret_key, salt)], is_binary=True)
+
+
+try:
+    import itsdangerous  # noqa: F401
 except ImportError as e:
     signer_stage = get_placeholder_class(e)
     safe_pickle_serializer = get_placeholder_class(e)
 
 
+def _get_bson_functions():
+    """Handle different function names between pymongo's bson and standalone bson"""
+    try:
+        import pymongo  # noqa: F401
+
+        return {'dumps': 'encode', 'loads': 'decode'}
+    except ImportError:
+        return {'dumps': 'dumps', 'loads': 'loads'}
+
+
 # BSON serializer
 try:
-    try:
-        from bson import decode as _bson_loads
-        from bson import encode as _bson_dumps
-    except ImportError:
-        from bson import dumps as _bson_dumps
-        from bson import loads as _bson_loads
+    import bson
 
     bson_serializer = SerializerPipeline(
-        [bson_preconf_stage, Stage(dumps=_bson_dumps, loads=_bson_loads)], is_binary=True
+        [bson_preconf_stage, Stage(bson, **_get_bson_functions())], is_binary=True
     )  #: Complete BSON serializer; uses pymongo's ``bson`` if installed, otherwise standalone ``bson`` codec
 except ImportError as e:
     bson_serializer = get_placeholder_class(e)