From 7917ba0dd12901d88137d3f8b487307eda38d326 Mon Sep 17 00:00:00 2001
From: Jordan Cook <jordan.cook@pioneer.com>
Date: Sat, 9 Apr 2022 13:33:32 -0500
Subject: [PATCH] Add compatibility with cattrs 21.1+, and clean up preconf
 module a bit

---
 requests_cache/__init__.py            |  2 +-
 requests_cache/serializers/preconf.py | 89 +++++++++++++++------------
 2 files changed, 49 insertions(+), 42 deletions(-)

diff --git a/requests_cache/__init__.py b/requests_cache/__init__.py
index 87781ba..835586c 100644
--- a/requests_cache/__init__.py
+++ b/requests_cache/__init__.py
@@ -17,4 +17,4 @@ try:
     from .session 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 @@ class that raises an ``ImportError`` at initialization time instead of at import
 """
 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')
-
-    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
-        )
+    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)
+
 
+try:
+    import itsdangerous  # noqa: F401
 except ImportError as e:
     signer_stage = get_placeholder_class(e)
     safe_pickle_serializer = get_placeholder_class(e)
 
 
-# BSON serializer
-try:
+def _get_bson_functions():
+    """Handle different function names between pymongo's bson and standalone bson"""
     try:
-        from bson import decode as _bson_loads
-        from bson import encode as _bson_dumps
+        import pymongo  # noqa: F401
+
+        return {'dumps': 'encode', 'loads': 'decode'}
     except ImportError:
-        from bson import dumps as _bson_dumps
-        from bson import loads as _bson_loads
+        return {'dumps': 'dumps', 'loads': 'loads'}
+
+
+# BSON serializer
+try:
+    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)
-- 
2.35.1

