Repository: pyca/cryptography

Issue:
Consider caching parsed extensions in x509.Certificates
Right now they get re-parsed on every access, this can be somewhat expensive, we should consider caching them on first access


Discussion:
FWIW, I measured this a about a 40% win on my test suite of x.509 validations.
What's the absolute improvement?
From 1 second to .5-.6 seconds.
😁 still probably worth doing (and a thing we've talked about previously)

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -126,7 +126,7 @@ def signature_algorithm_oid(self):
         oid = _obj2txt(self._backend, alg[0].algorithm)
         return x509.ObjectIdentifier(oid)
 
-    @property
+    @utils.cached_property
     def extensions(self):
         if self._backend._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER:
             return _CERTIFICATE_EXTENSION_PARSER.parse(
@@ -200,7 +200,7 @@ def revocation_date(self):
             )
         )
 
-    @property
+    @utils.cached_property
     def extensions(self):
         return _REVOKED_CERTIFICATE_EXTENSION_PARSER.parse(
             self._backend, self._x509_revoked
@@ -334,7 +334,7 @@ def __len__(self):
         else:
             return self._backend._lib.sk_X509_REVOKED_num(revoked)
 
-    @property
+    @utils.cached_property
     def extensions(self):
         return _CRL_EXTENSION_PARSER.parse(self._backend, self._x509_crl)
 
@@ -391,7 +391,7 @@ def signature_algorithm_oid(self):
         oid = _obj2txt(self._backend, alg[0].algorithm)
         return x509.ObjectIdentifier(oid)
 
-    @property
+    @utils.cached_property
     def extensions(self):
         x509_exts = self._backend._lib.X509_REQ_get_extensions(self._x509_req)
         return _CSR_EXTENSION_PARSER.parse(self._backend, x509_exts)
diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -145,3 +145,17 @@ def deprecated(value, module_name, message, warning_class):
     if not isinstance(module, _ModuleWithDeprecations):
         sys.modules[module_name] = _ModuleWithDeprecations(module)
     return _DeprecatedValue(value, message, warning_class)
+
+
+def cached_property(func):
+    cached_name = "_cached_{0}".format(func)
+    sentinel = object()
+
+    def inner(instance):
+        cache = getattr(instance, cached_name, sentinel)
+        if cache is not sentinel:
+            return cache
+        result = func(instance)
+        setattr(instance, cached_name, result)
+        return result
+    return property(inner)


Test patch:
diff --git a/tests/test_cryptography_utils.py b/tests/test_cryptography_utils.py
--- a/tests/test_cryptography_utils.py
+++ b/tests/test_cryptography_utils.py
@@ -4,8 +4,55 @@
 
 from __future__ import absolute_import, division, print_function
 
+import pytest
+
 from cryptography import utils
 
 
 def test_int_from_bytes_bytearray():
     assert utils.int_from_bytes(bytearray(b"\x02\x10"), "big") == 528
+
+
+class TestCachedProperty(object):
+    def test_simple(self):
+        accesses = []
+
+        class T(object):
+            @utils.cached_property
+            def t(self):
+                accesses.append(None)
+                return 14
+
+        assert T.t
+        t = T()
+        assert t.t == 14
+        assert len(accesses) == 1
+        assert t.t == 14
+        assert len(accesses) == 1
+
+        t = T()
+        assert t.t == 14
+        assert len(accesses) == 2
+        assert t.t == 14
+        assert len(accesses) == 2
+
+    def test_set(self):
+        accesses = []
+
+        class T(object):
+            @utils.cached_property
+            def t(self):
+                accesses.append(None)
+                return 14
+
+        t = T()
+        with pytest.raises(AttributeError):
+            t.t = None
+        assert len(accesses) == 0
+        assert t.t == 14
+        assert len(accesses) == 1
+        with pytest.raises(AttributeError):
+            t.t = None
+        assert len(accesses) == 1
+        assert t.t == 14
+        assert len(accesses) == 1