Repository: pyca/service-identity

Issue:
VerificationError can't be un-pickled
Ran into this while trying to serialize some errors.

```
import pickle
from service_identity.exceptions import VerificationError
error = VerificationError(errors=[])
pickle.loads(pickle.dumps(error))
```

```
Traceback (most recent call last):
  File "foo.py", line 4, in <module>
    pickle.loads(pickle.dumps(error))
TypeError: __init__() missing 1 required positional argument: 'errors'
```

Maybe it needs auto_exc=True?  :)

Comments:

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6eb6bd7..db08cdd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -67,7 +67,7 @@ jobs:
         run: "check-wheel-contents dist/*.whl"
       - name: "Check long_description"
         run: "python -m twine check dist/*"
-        
+
   install-dev:
     strategy:
       matrix:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 14175b2..3850ab0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,18 +7,18 @@ repos:
         language_version: python3.8
 
   - repo: https://github.com/PyCQA/isort
-    rev: 5.7.0
+    rev: 5.8.0
     hooks:
       - id: isort
         additional_dependencies: [toml]
         language_version: python3.8
 
-  - repo: https://gitlab.com/pycqa/flake8
-    rev: 3.8.4
+  - repo: https://github.com/pycqa/flake8
+    rev: 3.9.0
     hooks:
     - id: flake8
-      language_version: python3.8
       additional_dependencies: [flake8-bugbear]
+      language_version: python3.8
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v3.4.0
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ede0a8b..f3c7262 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,7 +7,7 @@ Versions follow `CalVer <https://calver.org>`_ with a strict backwards compatibi
 The third digit is only for regressions.
 
 
-20.1.0 (UNRELEASED)
+21.1.0 (UNRELEASED)
 -------------------
 
 
@@ -30,7 +30,8 @@ Deprecations:
 Changes:
 ^^^^^^^^
 
-*none*
+- ``service_identity.exceptions.VerificationError`` can now be pickled and is overall more well-behaved as an exception.
+  This raises the requirement of ``attrs`` to 19.1.0.
 
 
 ----
diff --git a/setup.py b/setup.py
index f5e4073..21cca45 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@ CLASSIFIERS = [
     "Topic :: Software Development :: Libraries :: Python Modules",
 ]
 INSTALL_REQUIRES = [
-    "attrs>=17.4.0",
+    "attrs>=19.1.0",
     "ipaddress; python_version<'3.3'",
     "pyasn1-modules",
     # Place pyasn1 after pyasn1-modules to workaround setuptools install bug:
diff --git a/src/service_identity/__init__.py b/src/service_identity/__init__.py
index 28a9223..f9d0e89 100644
--- a/src/service_identity/__init__.py
+++ b/src/service_identity/__init__.py
@@ -12,7 +12,7 @@ from .exceptions import (
 )
 
 
-__version__ = "20.1.0.dev0"
+__version__ = "21.1.0.dev0"
 
 __title__ = "service-identity"
 __description__ = "Service identity verification for pyOpenSSL & cryptography."
diff --git a/src/service_identity/exceptions.py b/src/service_identity/exceptions.py
index 4d103d1..f80c3b3 100644
--- a/src/service_identity/exceptions.py
+++ b/src/service_identity/exceptions.py
@@ -18,7 +18,7 @@ class SubjectAltNameWarning(DeprecationWarning):
     """
 
 
-@attr.s
+@attr.s(auto_exc=True)
 class VerificationError(Exception):
     """
     Service identity verification failed.


Test patch:
diff --git a/tests/test_common.py b/tests/test_common.py
index 5f14c00..aa2744a 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import, division, print_function
 
 import ipaddress
+import pickle
 
 import pytest
 import six
@@ -667,12 +668,32 @@ class TestIsIPAddress(object):
 
 
 class TestVerificationError(object):
-    """
-    The __str__ returns something sane.
-    """
+    def test_repr_str(self):
+        """
+        The __str__ and __repr__ methods return something helpful.
+        """
+        try:
+            raise VerificationError(errors=["foo"])
+        except VerificationError as e:
+            assert repr(e) == str(e)
+            assert str(e) != ""
+
+    @pytest.mark.parametrize("proto", range(0, pickle.HIGHEST_PROTOCOL + 1))
+    @pytest.mark.parametrize(
+        "exc",
+        [
+            VerificationError(errors=[]),
+            VerificationError(errors=[DNSMismatch("example.com")]),
+            VerificationError([]),
+            VerificationError([DNSMismatch("example.com")]),
+        ],
+    )
+    def test_pickle(self, exc, proto):
+        """
+        Exceptions can be pickled and unpickled.
+        """
+        new_exc = pickle.loads(pickle.dumps(exc, proto))
 
-    try:
-        raise VerificationError(errors=["foo"])
-    except VerificationError as e:
-        assert repr(e) == str(e)
-        assert str(e) != ""
+        # Exceptions can't be compared.
+        assert exc.__class__ == new_exc.__class__
+        assert exc.__dict__ == new_exc.__dict__