1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3from __future__ import absolute_import
4from __future__ import division
5from __future__ import print_function
6
7
8__doc__ = """
9A Describer is a stateful utility for creating RDF statements in a
10semi-declarative manner. It has methods for creating literal values, rel and
11rev resource relations (somewhat resembling RDFa).
12
13The `rel` and ``rev`` methods return a context manager which sets the current
14about to the referenced resource for the context scope (for use with the
15``with`` statement).
16
17Full example in the ``to_rdf`` method below::
18
19    >>> import datetime
20    >>> from rdflib.graph import Graph
21    >>> from rdflib.namespace import Namespace, RDFS, FOAF
22    >>>
23    >>> ORG_URI = "http://example.org/"
24    >>>
25    >>> CV = Namespace("http://purl.org/captsolo/resume-rdf/0.2/cv#")
26    >>>
27    >>> class Person(object):
28    ...     def __init__(self):
29    ...         self.first_name = u"Some"
30    ...         self.last_name = u"Body"
31    ...         self.username = "some1"
32    ...         self.presentation = u"Just a Python & RDF hacker."
33    ...         self.image = "/images/persons/" + self.username + ".jpg"
34    ...         self.site = "http://example.net/"
35    ...         self.start_date = datetime.date(2009, 9, 4)
36    ...     def get_full_name(self):
37    ...         return u" ".join([self.first_name, self.last_name])
38    ...     def get_absolute_url(self):
39    ...         return "/persons/" + self.username
40    ...     def get_thumbnail_url(self):
41    ...         return self.image.replace('.jpg', '-thumb.jpg')
42    ...
43    ...     def to_rdf(self):
44    ...         graph = Graph()
45    ...         graph.bind('foaf', FOAF)
46    ...         graph.bind('cv', CV)
47    ...         lang = 'en'
48    ...         d = Describer(graph, base=ORG_URI)
49    ...         d.about(self.get_absolute_url()+'#person')
50    ...         d.rdftype(FOAF.Person)
51    ...         d.value(FOAF.name, self.get_full_name())
52    ...         d.value(FOAF.givenName, self.first_name)
53    ...         d.value(FOAF.familyName, self.last_name)
54    ...         d.rel(FOAF.homepage, self.site)
55    ...         d.value(RDFS.comment, self.presentation, lang=lang)
56    ...         with d.rel(FOAF.depiction, self.image):
57    ...             d.rdftype(FOAF.Image)
58    ...             d.rel(FOAF.thumbnail, self.get_thumbnail_url())
59    ...         with d.rev(CV.aboutPerson):
60    ...             d.rdftype(CV.CV)
61    ...             with d.rel(CV.hasWorkHistory):
62    ...                 d.value(CV.startDate, self.start_date)
63    ...                 d.rel(CV.employedIn, ORG_URI+"#company")
64    ...         return graph
65    ...
66    >>> person_graph = Person().to_rdf()
67    >>> expected = Graph().parse(data='''<?xml version="1.0" encoding="utf-8"?>
68    ... <rdf:RDF
69    ...   xmlns:foaf="http://xmlns.com/foaf/0.1/"
70    ...   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
71    ...   xmlns:cv="http://purl.org/captsolo/resume-rdf/0.2/cv#"
72    ...   xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
73    ...   <foaf:Person rdf:about="http://example.org/persons/some1#person">
74    ...     <foaf:name>Some Body</foaf:name>
75    ...     <foaf:givenName>Some</foaf:givenName>
76    ...     <foaf:familyName>Body</foaf:familyName>
77    ...     <foaf:depiction>
78    ...       <foaf:Image
79    ...         rdf:about=
80    ...             "http://example.org/images/persons/some1.jpg">
81    ...         <foaf:thumbnail
82    ...         rdf:resource=
83    ...             "http://example.org/images/persons/some1-thumb.jpg"/>
84    ...       </foaf:Image>
85    ...     </foaf:depiction>
86    ...     <rdfs:comment xml:lang="en">
87    ...             Just a Python &amp; RDF hacker.
88    ...     </rdfs:comment>
89    ...     <foaf:homepage rdf:resource="http://example.net/"/>
90    ...   </foaf:Person>
91    ...   <cv:CV>
92    ...     <cv:aboutPerson
93    ...         rdf:resource="http://example.org/persons/some1#person">
94    ...     </cv:aboutPerson>
95    ...     <cv:hasWorkHistory>
96    ...       <rdf:Description>
97    ...         <cv:startDate
98    ...             rdf:datatype="http://www.w3.org/2001/XMLSchema#date"
99    ...             >2009-09-04</cv:startDate>
100    ...         <cv:employedIn rdf:resource="http://example.org/#company"/>
101    ...       </rdf:Description>
102    ...     </cv:hasWorkHistory>
103    ...   </cv:CV>
104    ... </rdf:RDF>
105    ... ''')
106    >>>
107    >>> from rdflib.compare import isomorphic
108    >>> isomorphic(person_graph, expected)  #doctest: +SKIP
109    True
110"""
111
112from contextlib import contextmanager
113from rdflib.graph import Graph
114from rdflib.namespace import RDF
115from rdflib.term import BNode
116from rdflib.term import Identifier
117from rdflib.term import Literal
118from rdflib.term import URIRef
119
120
121class Describer(object):
122
123    def __init__(self, graph=None, about=None, base=None):
124        if graph is None:
125            graph = Graph()
126        self.graph = graph
127        self.base = base
128        self._subjects = []
129        self.about(about or None)
130
131    def about(self, subject, **kws):
132        """
133        Sets the current subject. Will convert the given object into an
134        ``URIRef`` if it's not an ``Identifier``.
135
136        Usage::
137
138            >>> d = Describer()
139            >>> d._current() #doctest: +ELLIPSIS
140            rdflib.term.BNode(...)
141            >>> d.about("http://example.org/")
142            >>> d._current()
143            rdflib.term.URIRef(u'http://example.org/')
144
145        """
146        kws.setdefault('base', self.base)
147        subject = cast_identifier(subject, **kws)
148        if self._subjects:
149            self._subjects[-1] = subject
150        else:
151            self._subjects.append(subject)
152
153    def value(self, p, v, **kws):
154        """
155        Set a literal value for the given property. Will cast the value to an
156        ``Literal`` if a plain literal is given.
157
158        Usage::
159
160            >>> from rdflib import URIRef
161            >>> from rdflib.namespace import RDF, RDFS
162            >>> d = Describer(about="http://example.org/")
163            >>> d.value(RDFS.label, "Example")
164            >>> d.graph.value(URIRef('http://example.org/'), RDFS.label)
165            rdflib.term.Literal(u'Example')
166
167        """
168        v = cast_value(v, **kws)
169        self.graph.add((self._current(), p, v))
170
171    def rel(self, p, o=None, **kws):
172        """Set an object for the given property. Will convert the given object
173        into an ``URIRef`` if it's not an ``Identifier``. If none is given, a
174        new ``BNode`` is used.
175
176        Returns a context manager for use in a ``with`` block, within which the
177        given object is used as current subject.
178
179        Usage::
180
181            >>> from rdflib import URIRef
182            >>> from rdflib.namespace import RDF, RDFS
183            >>> d = Describer(about="/", base="http://example.org/")
184            >>> _ctxt = d.rel(RDFS.seeAlso, "/about")
185            >>> d.graph.value(URIRef('http://example.org/'), RDFS.seeAlso)
186            rdflib.term.URIRef(u'http://example.org/about')
187
188            >>> with d.rel(RDFS.seeAlso, "/more"):
189            ...     d.value(RDFS.label, "More")
190            >>> (URIRef('http://example.org/'), RDFS.seeAlso,
191            ...         URIRef('http://example.org/more')) in d.graph
192            True
193            >>> d.graph.value(URIRef('http://example.org/more'), RDFS.label)
194            rdflib.term.Literal(u'More')
195
196        """
197
198        kws.setdefault('base', self.base)
199        p = cast_identifier(p)
200        o = cast_identifier(o, **kws)
201        self.graph.add((self._current(), p, o))
202        return self._subject_stack(o)
203
204    def rev(self, p, s=None, **kws):
205        """
206        Same as ``rel``, but uses current subject as *object* of the relation.
207        The given resource is still used as subject in the returned context
208        manager.
209
210        Usage::
211
212            >>> from rdflib import URIRef
213            >>> from rdflib.namespace import RDF, RDFS
214            >>> d = Describer(about="http://example.org/")
215            >>> with d.rev(RDFS.seeAlso, "http://example.net/"):
216            ...     d.value(RDFS.label, "Net")
217            >>> (URIRef('http://example.net/'), RDFS.seeAlso,
218            ...         URIRef('http://example.org/')) in d.graph
219            True
220            >>> d.graph.value(URIRef('http://example.net/'), RDFS.label)
221            rdflib.term.Literal(u'Net')
222
223        """
224        kws.setdefault('base', self.base)
225        p = cast_identifier(p)
226        s = cast_identifier(s, **kws)
227        self.graph.add((s, p, self._current()))
228        return self._subject_stack(s)
229
230    def rdftype(self, t):
231        """
232        Shorthand for setting rdf:type of the current subject.
233
234        Usage::
235
236            >>> from rdflib import URIRef
237            >>> from rdflib.namespace import RDF, RDFS
238            >>> d = Describer(about="http://example.org/")
239            >>> d.rdftype(RDFS.Resource)
240            >>> (URIRef('http://example.org/'),
241            ...     RDF.type, RDFS.Resource) in d.graph
242            True
243
244        """
245        self.graph.add((self._current(), RDF.type, t))
246
247    def _current(self):
248        return self._subjects[-1]
249
250    @contextmanager
251    def _subject_stack(self, subject):
252        self._subjects.append(subject)
253        yield None
254        self._subjects.pop()
255
256
257def cast_value(v, **kws):
258    if not isinstance(v, Literal):
259        v = Literal(v, **kws)
260    return v
261
262
263def cast_identifier(ref, **kws):
264    ref = ref or BNode()
265    if not isinstance(ref, Identifier):
266        ref = URIRef(ref, **kws)
267    return ref
268