Преглед на файлове

De/serialization of all MIME types supported by RDFLib.

Stefano Cossu преди 6 години
родител
ревизия
c64eb391e5
променени са 4 файла, в които са добавени 236 реда и са изтрити 181 реда
  1. 1 1
      lakesuperior/api/resource.py
  2. 64 42
      lakesuperior/endpoints/ldp.py
  3. 1 31
      lakesuperior/model/ldp_factory.py
  4. 170 107
      tests/endpoints/test_ldp.py

+ 1 - 1
lakesuperior/api/resource.py

@@ -298,7 +298,7 @@ def delete(uid, soft=True, inbound=True):
 
     :param string uid: Resource UID.
     :param bool soft: Whether to perform a soft-delete and leave a
-      tombstone resource, or wipe any memory of the resource.
+        tombstone resource, or wipe any memory of the resource.
     """
     # If referential integrity is enforced, grab all inbound relationships
     # to break them.

+ 64 - 42
lakesuperior/endpoints/ldp.py

@@ -9,9 +9,9 @@ from uuid import uuid4
 import arrow
 
 from flask import (
-        Blueprint, g, make_response, render_template,
+        Blueprint, Response, g, make_response, render_template,
         request, send_file)
-from rdflib import Graph
+from rdflib import Graph, plugin, parser#, serializer
 
 from lakesuperior.api import resource as rsrc_api
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
@@ -28,37 +28,40 @@ from lakesuperior.store.ldp_rs.lmdb_store import TxnManager
 from lakesuperior.toolbox import Toolbox
 
 
+DEFAULT_RDF_MIMETYPE = 'text/turtle'
+"""
+Fallback serialization format used when no acceptable formats are specified.
+"""
+
 logger = logging.getLogger(__name__)
+rdf_parsable_mimetypes = {
+    mt.name for mt in plugin.plugins()
+    if mt.kind is parser.Parser and '/' in mt.name
+}
+"""MIMEtypes that can be parsed into RDF."""
 
-# Blueprint for LDP REST API. This is what is usually found under `/rest/` in
-# standard fcrepo4. Here, it is under `/ldp` but initially `/rest` can be kept
-# for backward compatibility.
+rdf_serializable_mimetypes = {
+    #mt.name for mt in plugin.plugins()
+    #if mt.kind is serializer.Serializer and '/' in mt.name
+    'application/n-triples',
+    'application/rdf+xml',
+    'text/turtle',
+    'text/n3',
+}
+"""
+MIMEtypes that RDF can be serialized into.
 
-ldp = Blueprint(
-        'ldp', __name__, template_folder='templates',
-        static_url_path='/static', static_folder='templates/static')
+These are not automatically derived from RDFLib because only triple
+(not quad) serializations are applicable.
+"""
 
 accept_patch = (
     'application/sparql-update',
 )
-accept_rdf = (
-    'application/ld+json',
-    'application/n-triples',
-    'application/rdf+xml',
-    #'application/x-turtle',
-    #'application/xhtml+xml',
-    #'application/xml',
-    #'text/html',
-    'text/n3',
-    #'text/plain',
-    'text/rdf+n3',
-    'text/turtle',
-)
 
 std_headers = {
     'Accept-Patch' : ','.join(accept_patch),
-    'Accept-Post' : ','.join(accept_rdf),
-    #'Allow' : ','.join(allow),
+    'Accept-Post' : ','.join(rdf_parsable_mimetypes),
 }
 
 """Predicates excluded by view."""
@@ -66,6 +69,16 @@ vw_blacklist = {
 }
 
 
+ldp = Blueprint(
+        'ldp', __name__, template_folder='templates',
+        static_url_path='/static', static_folder='templates/static')
+"""
+Blueprint for LDP REST API. This is what is usually found under ``/rest/`` in
+standard fcrepo4. Here, it is under ``/ldp`` but initially ``/rest`` will be
+kept for backward compatibility.
+"""
+
+## ROUTE PRE- & POST-PROCESSING ##
 
 @ldp.url_defaults
 def bp_url_defaults(endpoint, values):
@@ -140,16 +153,20 @@ def get_resource(uid, out_fmt=None):
         return _tombstone_response(e, uid)
     else:
         if out_fmt is None:
+            rdf_mimetype = _best_rdf_mimetype()
             out_fmt = (
                     'rdf'
-                    if isinstance(rsrc, LdpRs) or is_accept_hdr_rdf_parsable()
+                    if isinstance(rsrc, LdpRs) or rdf_mimetype is not None
                     else 'non_rdf')
         out_headers.update(_headers_from_metadata(rsrc))
         uri = g.tbox.uid_to_uri(uid)
         if out_fmt == 'rdf':
+            if locals().get('rdf_mimetype', None) is None:
+                rdf_mimetype = DEFAULT_RDF_MIMETYPE
             ggr = g.tbox.globalize_graph(rsrc.out_graph)
             ggr.namespace_manager = nsm
-            return _negotiate_content(ggr, out_headers, uid=uid, uri=uri)
+            return _negotiate_content(
+                    ggr, rdf_mimetype, out_headers, uid=uid, uri=uri)
         else:
             if not getattr(rsrc, 'local_path', False):
                 return ('{} has no binary content.'.format(rsrc.uid), 404)
@@ -174,6 +191,7 @@ def get_version_info(uid):
 
     :param str uid: UID of resource to retrieve versions for.
     """
+    rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     try:
         gr = rsrc_api.get_version_info(uid)
     except ResourceNotExistsError as e:
@@ -183,7 +201,7 @@ def get_version_info(uid):
     except TombstoneError as e:
         return _tombstone_response(e, uid)
     else:
-        return _negotiate_content(g.tbox.globalize_graph(gr))
+        return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
 
 
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
@@ -194,6 +212,7 @@ def get_version(uid, ver_uid):
     :param str uid: Resource UID.
     :param str ver_uid: Version UID.
     """
+    rdf_mimetype = _best_rdf_mimetype() or DEFAULT_RDF_MIMETYPE
     try:
         gr = rsrc_api.get_version(uid, ver_uid)
     except ResourceNotExistsError as e:
@@ -203,7 +222,7 @@ def get_version(uid, ver_uid):
     except TombstoneError as e:
         return _tombstone_response(e, uid)
     else:
-        return _negotiate_content(g.tbox.globalize_graph(gr))
+        return _negotiate_content(g.tbox.globalize_graph(gr), rdf_mimetype)
 
 
 @ldp.route('/<path:parent_uid>', methods=['POST'], strict_slashes=False)
@@ -225,7 +244,7 @@ def post_resource(parent_uid):
     handling, disposition = set_post_put_params()
     stream, mimetype = _bistream_from_req()
 
-    if LdpFactory.is_rdf_parsable(mimetype):
+    if mimetype in rdf_parsable_mimetypes:
         # If the content is RDF, localize in-repo URIs.
         global_rdf = stream.read()
         rdf_data = g.tbox.localize_payload(global_rdf)
@@ -277,7 +296,7 @@ def put_resource(uid):
     handling, disposition = set_post_put_params()
     stream, mimetype = _bistream_from_req()
 
-    if LdpFactory.is_rdf_parsable(mimetype):
+    if mimetype in rdf_parsable_mimetypes:
         # If the content is RDF, localize in-repo URIs.
         global_rdf = stream.read()
         rdf_data = g.tbox.localize_payload(global_rdf)
@@ -459,7 +478,19 @@ def patch_version(uid, ver_uid):
 
 ## PRIVATE METHODS ##
 
-def _negotiate_content(gr, headers=None, **vw_kwargs):
+def _best_rdf_mimetype():
+    """
+    Check if any of the 'Accept' header values provided is a RDF parsable
+    format.
+    """
+    for accept in request.accept_mimetypes:
+        mimetype = accept[0]
+        if mimetype in rdf_parsable_mimetypes:
+            return mimetype
+    return None
+
+
+def _negotiate_content(gr, rdf_mimetype, headers=None, **vw_kwargs):
     """
     Return HTML or serialized RDF depending on accept headers.
     """
@@ -470,7 +501,9 @@ def _negotiate_content(gr, headers=None, **vw_kwargs):
     else:
         for p in vw_blacklist:
             gr.remove((None, p, None))
-        return (gr.serialize(format='turtle'), headers)
+        return Response(
+                gr.serialize(format=rdf_mimetype), 200, headers,
+                mimetype=rdf_mimetype)
 
 
 def _bistream_from_req():
@@ -534,17 +567,6 @@ def set_post_put_params():
     return handling, disposition
 
 
-def is_accept_hdr_rdf_parsable():
-    """
-    Check if any of the 'Accept' header values provided is a RDF parsable
-    format.
-    """
-    for mimetype in request.accept_mimetypes.values():
-        if LdpFactory.is_rdf_parsable(mimetype):
-            return True
-    return False
-
-
 def parse_repr_options(retr_opts):
     """
     Set options to retrieve IMR.

+ 1 - 31
lakesuperior/model/ldp_factory.py

@@ -3,7 +3,7 @@ import logging
 from pprint import pformat
 from uuid import uuid4
 
-from rdflib import Graph, parser, plugin, serializer
+from rdflib import Graph, parser
 from rdflib.resource import Resource
 from rdflib.namespace import RDF
 
@@ -151,36 +151,6 @@ class LdpFactory:
         return inst
 
 
-    @staticmethod
-    def is_rdf_parsable(mimetype):
-        """
-        Checks whether a MIME type support RDF parsing by a RDFLib plugin.
-
-        :param str mimetype: MIME type to check.
-        """
-        try:
-            plugin.get(mimetype, parser.Parser)
-        except plugin.PluginException:
-            return False
-        else:
-            return True
-
-
-    @staticmethod
-    def is_rdf_serializable(mimetype):
-        """
-        Checks whether a MIME type support RDF serialization by a RDFLib plugin
-
-        :param str mimetype: MIME type to check.
-        """
-        try:
-            plugin.get(mimetype, serializer.Serializer)
-        except plugin.PluginException:
-            return False
-        else:
-            return True
-
-
     @staticmethod
     def mint_uid(parent_uid, path=None):
         """

+ 170 - 107
tests/endpoints/test_ldp.py

@@ -22,15 +22,15 @@ def random_uuid():
 @pytest.mark.usefixtures('client_class')
 @pytest.mark.usefixtures('db')
 class TestLdp:
-    '''
+    """
     Test HTTP interaction with LDP endpoint.
-    '''
+    """
     def test_get_root_node(self):
-        '''
+        """
         Get the root node from two different endpoints.
 
         The test triplestore must be initialized, hence the `db` fixture.
-        '''
+        """
         ldp_resp = self.client.get('/ldp')
         rest_resp = self.client.get('/rest')
 
@@ -39,9 +39,9 @@ class TestLdp:
 
 
     def test_put_empty_resource(self, random_uuid):
-        '''
+        """
         Check response headers for a PUT operation with empty payload.
-        '''
+        """
         resp = self.client.put('/ldp/new_resource')
         assert resp.status_code == 201
         assert resp.data == bytes(
@@ -49,10 +49,10 @@ class TestLdp:
 
 
     def test_put_existing_resource(self, random_uuid):
-        '''
+        """
         Trying to PUT an existing resource should return a 204 if the payload
         is empty.
-        '''
+        """
         path = '/ldp/nonidempotent01'
         put1_resp = self.client.put(path)
         assert put1_resp.status_code == 201
@@ -70,12 +70,12 @@ class TestLdp:
 
 
     def test_put_tree(self, client):
-        '''
+        """
         PUT a resource with several path segments.
 
         The test should create intermediate path segments that are LDPCs,
         accessible to PUT or POST.
-        '''
+        """
         path = '/ldp/test_tree/a/b/c/d/e/f/g'
         self.client.put(path)
 
@@ -91,13 +91,13 @@ class TestLdp:
 
 
     def test_put_nested_tree(self, client):
-        '''
+        """
         Verify that containment is set correctly in nested hierarchies.
 
         First put a new hierarchy and verify that the root node is its
         container; then put another hierarchy under it and verify that the
         first hierarchy is the container of the second one.
-        '''
+        """
         uuid1 = 'test_nested_tree/a/b/c/d'
         uuid2 = uuid1 + '/e/f/g'
         path1 = '/ldp/' + uuid1
@@ -120,9 +120,9 @@ class TestLdp:
 
 
     def test_put_ldp_rs(self, client):
-        '''
+        """
         PUT a resource with RDF payload and verify.
-        '''
+        """
         with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
             self.client.put('/ldp/ldprs01', data=f, content_type='text/turtle')
 
@@ -136,9 +136,9 @@ class TestLdp:
 
 
     def test_put_ldp_nr(self, rnd_img):
-        '''
+        """
         PUT a resource with binary payload and verify checksums.
-        '''
+        """
         rnd_img['content'].seek(0)
         resp = self.client.put('/ldp/ldpnr01', data=rnd_img['content'],
                 headers={
@@ -154,9 +154,9 @@ class TestLdp:
 
 
     def test_put_ldp_nr_multipart(self, rnd_img):
-        '''
+        """
         PUT a resource with a multipart/form-data payload.
-        '''
+        """
         rnd_img['content'].seek(0)
         resp = self.client.put(
             '/ldp/ldpnr02',
@@ -176,11 +176,11 @@ class TestLdp:
 
 
     def test_put_mismatched_ldp_rs(self, rnd_img):
-        '''
+        """
         Verify MIME type / LDP mismatch.
         PUT a LDP-RS, then PUT a LDP-NR on the same location and verify it
         fails.
-        '''
+        """
         path = '/ldp/' + str(uuid.uuid4())
 
         rnd_img['content'].seek(0)
@@ -199,11 +199,11 @@ class TestLdp:
 
 
     def test_put_mismatched_ldp_nr(self, rnd_img):
-        '''
+        """
         Verify MIME type / LDP mismatch.
         PUT a LDP-NR, then PUT a LDP-RS on the same location and verify it
         fails.
-        '''
+        """
         path = '/ldp/' + str(uuid.uuid4())
 
         with open('tests/data/marcel_duchamp_single_subject.ttl', 'rb') as f:
@@ -222,10 +222,10 @@ class TestLdp:
 
 
     def test_missing_reference(self, client):
-        '''
+        """
         PUT a resource with RDF payload referencing a non-existing in-repo
         resource.
-        '''
+        """
         self.client.get('/ldp')
         data = '''
         PREFIX ns: <http://example.org#>
@@ -255,18 +255,18 @@ class TestLdp:
 
 
     def test_post_resource(self, client):
-        '''
+        """
         Check response headers for a POST operation with empty payload.
-        '''
+        """
         res = self.client.post('/ldp/')
         assert res.status_code == 201
         assert 'Location' in res.headers
 
 
     def test_post_ldp_nr(self, rnd_img):
-        '''
+        """
         POST a resource with binary payload and verify checksums.
-        '''
+        """
         rnd_img['content'].seek(0)
         resp = self.client.post('/ldp/', data=rnd_img['content'],
                 headers={
@@ -283,10 +283,10 @@ class TestLdp:
 
 
     def test_post_slug(self):
-        '''
+        """
         Verify that a POST with slug results in the expected URI only if the
         resource does not exist already.
-        '''
+        """
         slug01_resp = self.client.post('/ldp', headers={'slug' : 'slug01'})
         assert slug01_resp.status_code == 201
         assert slug01_resp.headers['location'] == \
@@ -299,17 +299,17 @@ class TestLdp:
 
 
     def test_post_404(self):
-        '''
+        """
         Verify that a POST to a non-existing parent results in a 404.
-        '''
+        """
         assert self.client.post('/ldp/{}'.format(uuid.uuid4()))\
                 .status_code == 404
 
 
     def test_post_409(self, rnd_img):
-        '''
+        """
         Verify that you cannot POST to a binary resource.
-        '''
+        """
         rnd_img['content'].seek(0)
         self.client.put('/ldp/post_409', data=rnd_img['content'], headers={
                 'Content-Disposition' : 'attachment; filename={}'.format(
@@ -318,9 +318,9 @@ class TestLdp:
 
 
     def test_patch_root(self):
-        '''
+        """
         Test patching root node.
-        '''
+        """
         path = '/ldp/'
         self.client.get(path)
         uri = g.webroot + '/'
@@ -338,9 +338,9 @@ class TestLdp:
 
 
     def test_patch(self):
-        '''
+        """
         Test patching a resource.
-        '''
+        """
         path = '/ldp/test_patch01'
         self.client.put(path)
 
@@ -367,9 +367,9 @@ class TestLdp:
 
 
     def test_patch_ssr(self):
-        '''
+        """
         Test patching a resource violating the single-subject rule.
-        '''
+        """
         path = '/ldp/test_patch_ssr'
         self.client.put(path)
 
@@ -398,9 +398,9 @@ class TestLdp:
 
 
     def test_patch_ldp_nr_metadata(self):
-        '''
+        """
         Test patching a LDP-NR metadata resource from the fcr:metadata URI.
-        '''
+        """
         path = '/ldp/ldpnr01'
 
         with open('tests/data/sparql_update/simple_insert.sparql') as data:
@@ -430,9 +430,9 @@ class TestLdp:
 
 
     def test_patch_ldpnr(self):
-        '''
+        """
         Verify that a direct PATCH to a LDP-NR results in a 415.
-        '''
+        """
         with open(
                 'tests/data/sparql_update/delete+insert+where.sparql') as data:
             patch_resp = self.client.patch('/ldp/ldpnr01',
@@ -442,10 +442,10 @@ class TestLdp:
 
 
     def test_patch_invalid_mimetype(self, rnd_img):
-        '''
+        """
         Verify that a PATCH using anything other than an
         `application/sparql-update` MIME type results in an error.
-        '''
+        """
         self.client.put('/ldp/test_patch_invalid_mimetype')
         rnd_img['content'].seek(0)
         ldpnr_resp = self.client.patch('/ldp/ldpnr01/fcr:metadata',
@@ -460,9 +460,9 @@ class TestLdp:
 
 
     def test_delete(self):
-        '''
+        """
         Test delete response codes.
-        '''
+        """
         self.client.put('/ldp/test_delete01')
         delete_resp = self.client.delete('/ldp/test_delete01')
         assert delete_resp.status_code == 204
@@ -472,11 +472,11 @@ class TestLdp:
 
 
     def test_tombstone(self):
-        '''
+        """
         Test tombstone behaviors.
 
         For POST on a tombstone, check `test_resurrection`.
-        '''
+        """
         tstone_resp = self.client.get('/ldp/test_delete01')
         assert tstone_resp.status_code == 410
         assert tstone_resp.headers['Link'] == \
@@ -492,10 +492,10 @@ class TestLdp:
 
 
     def test_delete_recursive(self):
-        '''
+        """
         Test response codes for resources deleted recursively and their
         tombstones.
-        '''
+        """
         child_suffixes = ('a', 'a/b', 'a/b/c', 'a1', 'a1/b1')
         self.client.put('/ldp/test_delete_recursive01')
         for cs in child_suffixes:
@@ -518,9 +518,9 @@ class TestLdp:
 
 
     def test_put_fragments(self):
-        '''
+        """
         Test the correct handling of fragment URIs on PUT and GET.
-        '''
+        """
         with open('tests/data/fragments.ttl', 'rb') as f:
             self.client.put(
                 '/ldp/test_fragment01',
@@ -538,9 +538,9 @@ class TestLdp:
 
 
     def test_patch_fragments(self):
-        '''
+        """
         Test the correct handling of fragment URIs on PATCH.
-        '''
+        """
         self.client.put('/ldp/test_fragment_patch')
 
         with open('tests/data/fragments_insert.sparql', 'rb') as f:
@@ -572,17 +572,80 @@ class TestLdp:
                 : URIRef('http://ex.org/p3') : URIRef('http://ex.org/o3')]
 
 
+@pytest.mark.usefixtures('client_class')
+@pytest.mark.usefixtures('db')
+class TestMimeType:
+    """
+    Test ``Accept`` headers and input & output formats.
+    """
+    def test_accept(self):
+        """
+        Verify the default serialization method.
+        """
+        accept_list = {
+            ('', 'text/turtle'),
+            ('text/turtle', 'text/turtle'),
+            ('application/rdf+xml', 'application/rdf+xml'),
+            ('application/n-triples', 'application/n-triples'),
+            ('application/bogus', 'text/turtle'),
+            (
+                'application/rdf+xml;q=0.5,application/n-triples;q=0.7',
+                'application/n-triples'),
+            (
+                'application/rdf+xml;q=0.5,application/bogus;q=0.7',
+                'application/rdf+xml'),
+            ('application/rdf+xml;q=0.5,text/n3;q=0.7', 'text/n3'),
+        }
+        for mimetype, fmt in accept_list:
+            rsp = self.client.get('/ldp', headers={'Accept': mimetype})
+            assert rsp.mimetype == fmt
+            gr = Graph(identifier=g.webroot + '/').parse(
+                    data=rsp.data, format=fmt)
+
+            assert nsc['fcrepo'].RepositoryRoot in set(gr.objects())
+
+
+    def test_provided_rdf(self):
+        """
+        Test several input RDF serialiation formats.
+        """
+        self.client.get('/ldp')
+        gr = Graph()
+        gr.add((
+            URIRef(g.webroot + '/test_mimetype'), 
+            nsc['dcterms'].title, Literal('Test MIME type.')))
+        test_list = {
+            'application/n-triples',
+            'application/rdf+xml',
+            'text/n3',
+            'text/turtle',
+        }
+        for mimetype in test_list:
+            rdf_data = gr.serialize(format=mimetype)
+            self.client.put('/ldp/test_mimetype', data=rdf_data, headers={
+                'content-type': mimetype})
+
+            rsp = self.client.get('/ldp/test_mimetype')
+            rsp_gr = Graph(identifier=g.webroot + '/test_mimetype').parse(
+                    data=rsp.data, format='text/turtle')
+
+            assert (
+                    URIRef(g.webroot + '/test_mimetype'),
+                    nsc['dcterms'].title, Literal('Test MIME type.')) in rsp_gr
+
+
+
 @pytest.mark.usefixtures('client_class')
 @pytest.mark.usefixtures('db')
 class TestPrefHeader:
-    '''
+    """
     Test various combinations of `Prefer` header.
-    '''
+    """
     @pytest.fixture(scope='class')
     def cont_structure(self):
-        '''
+        """
         Create a container structure to be used for subsequent requests.
-        '''
+        """
         parent_path = '/ldp/test_parent'
         self.client.put(parent_path)
         self.client.put(parent_path + '/child1')
@@ -597,7 +660,7 @@ class TestPrefHeader:
 
 
     def test_put_prefer_handling(self, random_uuid):
-        '''
+        """
         Trying to PUT an existing resource should:
 
         - Return a 204 if the payload is empty
@@ -606,7 +669,7 @@ class TestPrefHeader:
         - Return a 412 (ServerManagedTermError) if the payload is RDF,
           server-managed triples are included and handling is set to 'strict',
           or not set.
-        '''
+        """
         path = '/ldp/put_pref_header01'
         assert self.client.put(path).status_code == 201
         assert self.client.get(path).status_code == 200
@@ -648,9 +711,9 @@ class TestPrefHeader:
 
     # @HOLD Embed children is debated.
     def _disabled_test_embed_children(self, cont_structure):
-        '''
+        """
         verify the "embed children" prefer header.
-        '''
+        """
         parent_path = cont_structure['path']
         cont_resp = cont_structure['response']
         cont_subject = cont_structure['subject']
@@ -686,9 +749,9 @@ class TestPrefHeader:
 
 
     def test_return_children(self, cont_structure):
-        '''
+        """
         verify the "return children" prefer header.
-        '''
+        """
         parent_path = cont_structure['path']
         cont_resp = cont_structure['response']
         cont_subject = cont_structure['subject']
@@ -714,9 +777,9 @@ class TestPrefHeader:
 
 
     def test_inbound_rel(self, cont_structure):
-        '''
+        """
         verify the "inbound relationships" prefer header.
-        '''
+        """
         self.client.put('/ldp/test_target')
         data = '<> <http://ex.org/ns#shoots> <{}> .'.format(
                 g.webroot + '/test_target')
@@ -747,9 +810,9 @@ class TestPrefHeader:
 
 
     def test_srv_mgd_triples(self, cont_structure):
-        '''
+        """
         verify the "server managed triples" prefer header.
-        '''
+        """
         parent_path = cont_structure['path']
         cont_resp = cont_structure['response']
         cont_subject = cont_structure['subject']
@@ -790,9 +853,9 @@ class TestPrefHeader:
 
 
     def test_delete_no_tstone(self):
-        '''
+        """
         Test the `no-tombstone` Prefer option.
-        '''
+        """
         self.client.put('/ldp/test_delete_no_tstone01')
         self.client.put('/ldp/test_delete_no_tstone01/a')
 
@@ -810,14 +873,14 @@ class TestPrefHeader:
 @pytest.mark.usefixtures('client_class')
 @pytest.mark.usefixtures('db')
 class TestVersion:
-    '''
+    """
     Test version creation, retrieval and deletion.
-    '''
+    """
     def test_create_versions(self):
-        '''
+        """
         Test that POSTing multiple times to fcr:versions creates the
         'hasVersions' triple and yields multiple version snapshots.
-        '''
+        """
         self.client.put('/ldp/test_version')
         create_rsp = self.client.post('/ldp/test_version/fcr:versions')
 
@@ -840,9 +903,9 @@ class TestVersion:
 
 
     def test_version_with_slug(self):
-        '''
+        """
         Test a version with a slug.
-        '''
+        """
         self.client.put('/ldp/test_version_slug')
         create_rsp = self.client.post('/ldp/test_version_slug/fcr:versions',
             headers={'slug' : 'v1'})
@@ -858,10 +921,10 @@ class TestVersion:
 
 
     def test_dupl_version(self):
-        '''
+        """
         Make sure that two POSTs with the same slug result in two different
         versions.
-        '''
+        """
         path = '/ldp/test_duplicate_slug'
         self.client.put(path)
         v1_rsp = self.client.post(path + '/fcr:versions',
@@ -877,10 +940,10 @@ class TestVersion:
 
     # @TODO Reverting from version and resurrecting is not fully functional.
     def _disabled_test_revert_version(self):
-        '''
+        """
         Take a version snapshot, update a resource, and then revert to the
         previous vresion.
-        '''
+        """
         rsrc_path = '/ldp/test_revert_version'
         payload1 = '<> <urn:demo:p1> <urn:demo:o1> .'
         payload2 = '<> <urn:demo:p1> <urn:demo:o2> .'
@@ -920,34 +983,34 @@ class TestVersion:
         ]
 
 
-    #def test_resurrection(self):
-    #    '''
-    #    Delete and then resurrect a resource.
+    def test_resurrection(self):
+        """
+        Delete and then resurrect a resource.
 
-    #    Make sure that the resource is resurrected to the latest version.
-    #    '''
-    #    path = '/ldp/test_lazarus'
-    #    self.client.put(path)
+        Make sure that the resource is resurrected to the latest version.
+        """
+        path = '/ldp/test_lazarus'
+        self.client.put(path)
 
-    #    self.client.post(path + '/fcr:versions', headers={'slug': 'v1'})
-    #    self.client.put(
-    #        path, headers={'content-type': 'text/turtle'},
-    #        data=b'<> <urn:demo:p1> <urn:demo:o1> .')
-    #    self.client.post(path + '/fcr:versions', headers={'slug': 'v2'})
-    #    self.client.put(
-    #        path, headers={'content-type': 'text/turtle'},
-    #        data=b'<> <urn:demo:p1> <urn:demo:o2> .')
+        self.client.post(path + '/fcr:versions', headers={'slug': 'v1'})
+        self.client.put(
+            path, headers={'content-type': 'text/turtle'},
+            data=b'<> <urn:demo:p1> <urn:demo:o1> .')
+        self.client.post(path + '/fcr:versions', headers={'slug': 'v2'})
+        self.client.put(
+            path, headers={'content-type': 'text/turtle'},
+            data=b'<> <urn:demo:p1> <urn:demo:o2> .')
 
-    #    self.client.delete(path)
+        self.client.delete(path)
 
-    #    assert self.client.get(path).status_code == 410
+        assert self.client.get(path).status_code == 410
 
-    #    self.client.post(path + '/fcr:tombstone')
+        self.client.post(path + '/fcr:tombstone')
 
-    #    laz_data = self.client.get(path).data
-    #    laz_gr = Graph().parse(data=laz_data, format='turtle')
-    #    assert laz_gr[
-    #        URIRef(g.webroot + '/test_lazarus')
-    #        : URIRef('urn:demo:p1')
-    #        : URIRef('urn:demo:o2')
-    #    ]
+        laz_data = self.client.get(path).data
+        laz_gr = Graph().parse(data=laz_data, format='turtle')
+        assert laz_gr[
+            URIRef(g.webroot + '/test_lazarus')
+            : URIRef('urn:demo:p1')
+            : URIRef('urn:demo:o2')
+        ]