Fixed #3747 -- cache extensions on x.509 objects (#3769)
* Fixed #3747 -- cache extensions on x.509 objects
* be kind to cpython, save a dict lookup
* flake8
* changelog
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 28addfd..cc993be 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -35,6 +35,13 @@
and
:meth:`~cryptography.hazmat.primitives.asymmetric.dh.DHParameters.parameter_bytes`
.
+* The ``extensions`` attribute on :class:`~cryptography.x509.Certificate`,
+ :class:`~cryptography.x509.CertificateSigningRequest`,
+ :class:`~cryptography.x509.CertificateRevocationList`, and
+ :class:`~cryptography.x509.RevokedCertificate` now caches the computed
+ ``Extensions`` object. There should be no performance change, just a
+ performance improvement for programs accessing the ``extensions`` attribute
+ multiple times.
1.9 - 2017-05-29
~~~~~~~~~~~~~~~~
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
index a04d6d5..5bf0438 100644
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -126,7 +126,7 @@
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 @@
)
)
- @property
+ @utils.cached_property
def extensions(self):
return _REVOKED_CERTIFICATE_EXTENSION_PARSER.parse(
self._backend, self._x509_revoked
@@ -334,7 +334,7 @@
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 @@
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
index d28dc71..efb12e2 100644
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -145,3 +145,17 @@
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)
diff --git a/tests/test_cryptography_utils.py b/tests/test_cryptography_utils.py
index 037d11c..290e161 100644
--- 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