shacl2code 0.0.11__tar.gz → 0.0.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/test.yaml +5 -0
  2. {shacl2code-0.0.11 → shacl2code-0.0.13}/PKG-INFO +1 -2
  3. {shacl2code-0.0.11 → shacl2code-0.0.13}/pyproject.toml +0 -1
  4. shacl2code-0.0.13/src/shacl2code/context.py +277 -0
  5. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/common.py +9 -0
  6. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/templates/jsonschema.j2 +21 -5
  7. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/templates/python.j2 +74 -26
  8. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/model.py +1 -1
  9. shacl2code-0.0.13/src/shacl2code/version.py +1 -0
  10. shacl2code-0.0.13/tests/__init__.py +0 -0
  11. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/conftest.py +4 -5
  12. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/context.j2 +2 -2
  13. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/model/test-context.json +65 -7
  14. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/model/test.ttl +24 -1
  15. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/roundtrip.json +14 -2
  16. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/jsonschema/test-context.json +105 -12
  17. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/jsonschema/test.json +88 -0
  18. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/python/test-context.py +107 -35
  19. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/python/test.py +85 -20
  20. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/raw/test-context.txt +4 -0
  21. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/raw/test.txt +3 -1
  22. shacl2code-0.0.13/tests/http.py +74 -0
  23. shacl2code-0.0.13/tests/test_context.py +136 -0
  24. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_jsonschema.py +41 -5
  25. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_model_source.py +60 -7
  26. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_python.py +60 -5
  27. shacl2code-0.0.11/src/shacl2code/context.py +0 -167
  28. shacl2code-0.0.11/src/shacl2code/version.py +0 -1
  29. shacl2code-0.0.11/tests/test_context.py +0 -144
  30. {shacl2code-0.0.11 → shacl2code-0.0.13}/.flake8 +0 -0
  31. {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/coverage-generate.yaml +0 -0
  32. {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/coverage-report.yaml +0 -0
  33. {shacl2code-0.0.11 → shacl2code-0.0.13}/.github/workflows/publish.yaml +0 -0
  34. {shacl2code-0.0.11 → shacl2code-0.0.13}/.gitignore +0 -0
  35. {shacl2code-0.0.11 → shacl2code-0.0.13}/LICENSE +0 -0
  36. {shacl2code-0.0.11 → shacl2code-0.0.13}/README.md +0 -0
  37. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/__init__.py +0 -0
  38. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/__main__.py +0 -0
  39. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/__init__.py +0 -0
  40. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/jinja.py +0 -0
  41. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/jsonschema.py +0 -0
  42. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/lang.py +0 -0
  43. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/lang/python.py +0 -0
  44. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/main.py +0 -0
  45. {shacl2code-0.0.11 → shacl2code-0.0.13}/src/shacl2code/urlcontext.py +0 -0
  46. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/abort.j2 +0 -0
  47. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-id.j2 +0 -0
  48. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-node-kind.ttl +0 -0
  49. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-pattern-class.ttl +0 -0
  50. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-pattern-integer.ttl +0 -0
  51. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/bad-reference.jsonld +0 -0
  52. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/context-url.j2 +0 -0
  53. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/missing-range.ttl +0 -0
  54. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-inline.json +0 -0
  55. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-ref-after.json +0 -0
  56. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/bad-object-type-ref-before.json +0 -0
  57. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/python/links.json +0 -0
  58. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/data/raw.j2 +0 -0
  59. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/expect/make_expect.py +0 -0
  60. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_cli.py +0 -0
  61. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_common_jinja.py +0 -0
  62. {shacl2code-0.0.11 → shacl2code-0.0.13}/tests/test_common_prefix.py +0 -0
@@ -22,10 +22,15 @@ jobs:
22
22
  uses: actions/setup-python@v4
23
23
  with:
24
24
  python-version: ${{ matrix.python-version }}
25
+ - name: Setup Node
26
+ uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
25
29
  - name: Install dependencies
26
30
  run: |
27
31
  python -m pip install --upgrade pip
28
32
  pip install build
33
+ npm install -g jsonld-cli
29
34
  - name: Build package
30
35
  run: |
31
36
  python -m build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: shacl2code
3
- Version: 0.0.11
3
+ Version: 0.0.13
4
4
  Summary: Convert SHACL model file to code bindings
5
5
  Project-URL: Homepage, https://github.com/JPEWdev/shacl2code
6
6
  Project-URL: Repository, https://github.com/JPEWdev/shacl2code.git
@@ -25,7 +25,6 @@ Requires-Dist: flake8>=7.0.0; extra == 'dev'
25
25
  Requires-Dist: jsonschema>=4.21.1; extra == 'dev'
26
26
  Requires-Dist: pyshacl>=0.25.0; extra == 'dev'
27
27
  Requires-Dist: pytest-cov>=4.1; extra == 'dev'
28
- Requires-Dist: pytest-server-fixtures>=1.7; extra == 'dev'
29
28
  Requires-Dist: pytest>=7.4; extra == 'dev'
30
29
  Description-Content-Type: text/markdown
31
30
 
@@ -31,7 +31,6 @@ dev = [
31
31
  "pyshacl >= 0.25.0",
32
32
  "pytest >= 7.4",
33
33
  "pytest-cov >= 4.1",
34
- "pytest-server-fixtures >= 1.7",
35
34
  ]
36
35
 
37
36
  [project.urls]
@@ -0,0 +1,277 @@
1
+ # Copyright (c) 2024 Joshua Watt
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ import re
6
+ from contextlib import contextmanager
7
+
8
+
9
+ def foreach_context(contexts):
10
+ for ctx in contexts:
11
+ for name, value in ctx.items():
12
+ yield name, value
13
+
14
+
15
+ class Context(object):
16
+ def __init__(self, contexts=[]):
17
+ self.contexts = [c for c in contexts if c]
18
+ self.__vocabs = []
19
+ self.__expanded_iris = {}
20
+ self.__expanded_ids = {}
21
+ self.__expanded_vocabs = {}
22
+ self.__compacted_iris = {}
23
+ self.__compacted_ids = {}
24
+ self.__compacted_vocabs = {}
25
+
26
+ @contextmanager
27
+ def vocab_push(self, vocab):
28
+ if not vocab:
29
+ yield self
30
+ return
31
+
32
+ self.__vocabs.append(vocab)
33
+ try:
34
+ yield self
35
+ finally:
36
+ self.__vocabs.pop()
37
+
38
+ def __vocab_key(self):
39
+ if not self.__vocabs:
40
+ return ""
41
+
42
+ return self.__vocabs[-1]
43
+
44
+ def __get_vocab_contexts(self):
45
+ contexts = []
46
+
47
+ for v in self.__vocabs:
48
+ for name, value in foreach_context(self.contexts):
49
+ if (
50
+ isinstance(value, dict)
51
+ and value["@type"] == "@vocab"
52
+ and v == self.expand_iri(value["@id"])
53
+ ):
54
+ if "@context" in value:
55
+ contexts.insert(0, value["@context"])
56
+
57
+ contexts.extend(self.contexts)
58
+
59
+ return contexts
60
+
61
+ def __choose_possible(
62
+ self,
63
+ term,
64
+ default,
65
+ contexts,
66
+ *,
67
+ vocab=False,
68
+ base=False,
69
+ exact=False,
70
+ prefix=False,
71
+ ):
72
+ def remove_prefix(_id, value):
73
+ expanded_id = self.expand_iri(_id)
74
+ expanded_value = self.expand_iri(value)
75
+
76
+ possible = set()
77
+ if expanded_id.startswith(expanded_value):
78
+ tmp_id = _id[len(expanded_value) :]
79
+ possible.add(tmp_id)
80
+ return possible
81
+
82
+ def helper(term):
83
+ possible = set()
84
+ for name, value in foreach_context(contexts):
85
+ if name == "@vocab":
86
+ if vocab:
87
+ possible |= remove_prefix(term, value)
88
+ continue
89
+
90
+ if name == "@base":
91
+ if base:
92
+ possible |= remove_prefix(term, value)
93
+ continue
94
+
95
+ if isinstance(value, dict):
96
+ value = value["@id"]
97
+
98
+ if term == self.expand_iri(value):
99
+ if exact and name not in possible:
100
+ possible.add(name)
101
+ possible |= helper(name)
102
+ continue
103
+
104
+ if not prefix:
105
+ continue
106
+
107
+ if term.startswith(value) and value.endswith("/"):
108
+ tmp_id = name + ":" + term[len(value) :].lstrip("/")
109
+ if tmp_id not in possible:
110
+ possible.add(tmp_id)
111
+ possible |= helper(tmp_id)
112
+ continue
113
+
114
+ if term.startswith(value + ":") and self.expand_iri(value).endswith(
115
+ "/"
116
+ ):
117
+ tmp_id = name + term[len(value) :]
118
+ if tmp_id not in possible:
119
+ possible.add(tmp_id)
120
+ possible |= helper(tmp_id)
121
+ continue
122
+
123
+ return possible
124
+
125
+ possible = helper(term)
126
+
127
+ if not possible:
128
+ return default
129
+
130
+ # To select from the possible identifiers, choose the one that has the
131
+ # least context (fewest ":"), then the shortest, and finally
132
+ # alphabetically
133
+ possible = list(possible)
134
+ possible.sort(key=lambda p: (p.count(":"), len(p), p))
135
+ return possible[0]
136
+
137
+ def compact_iri(self, iri):
138
+ if iri not in self.__compacted_iris:
139
+ self.__compacted_iris[iri] = self.__choose_possible(
140
+ iri,
141
+ iri,
142
+ self.contexts,
143
+ exact=True,
144
+ prefix=True,
145
+ )
146
+
147
+ return self.__compacted_iris[iri]
148
+
149
+ def compact_id(self, _id):
150
+ if ":" not in _id:
151
+ return _id
152
+
153
+ if _id not in self.__compacted_ids:
154
+ self.__compacted_ids[_id] = self.__choose_possible(
155
+ _id,
156
+ _id,
157
+ self.contexts,
158
+ base=True,
159
+ prefix=True,
160
+ )
161
+
162
+ return self.__compacted_ids[_id]
163
+
164
+ def compact_vocab(self, term, vocab=None):
165
+ with self.vocab_push(vocab):
166
+ v = self.__vocab_key()
167
+ if v in self.__compacted_vocabs and term in self.__compacted_vocabs[v]:
168
+ return self.__compacted_vocabs[v][term]
169
+
170
+ compact = self.__choose_possible(
171
+ term,
172
+ None,
173
+ self.__get_vocab_contexts(),
174
+ vocab=True,
175
+ exact=True,
176
+ )
177
+ if compact is not None:
178
+ self.__compacted_vocabs.setdefault(v, {})[term] = self.compact_id(
179
+ compact
180
+ )
181
+ return compact
182
+
183
+ # If unable to compact with a vocabulary, compact as an ID
184
+ return self.compact_id(term)
185
+
186
+ def expand_iri(self, iri):
187
+ if iri not in self.__expanded_iris:
188
+ self.__expanded_iris[iri] = self.__expand(
189
+ iri,
190
+ self.contexts,
191
+ exact=True,
192
+ prefix=True,
193
+ )
194
+
195
+ return self.__expanded_iris[iri]
196
+
197
+ def expand_id(self, _id):
198
+ if _id not in self.__expanded_ids:
199
+ self.__expanded_ids[_id] = self.__expand(
200
+ _id,
201
+ self.contexts,
202
+ base=True,
203
+ prefix=True,
204
+ )
205
+
206
+ return self.__expanded_ids[_id]
207
+
208
+ def expand_vocab(self, term, vocab=None):
209
+ with self.vocab_push(vocab):
210
+ v = self.__vocab_key()
211
+ if v not in self.__expanded_vocabs or term not in self.__expanded_vocabs[v]:
212
+ value = self.__expand(
213
+ term,
214
+ self.__get_vocab_contexts(),
215
+ vocab=True,
216
+ exact=True,
217
+ )
218
+ self.__expanded_vocabs.setdefault(v, {})[term] = self.expand_id(value)
219
+
220
+ return self.__expanded_vocabs[v][term]
221
+
222
+ def __expand(
223
+ self,
224
+ term,
225
+ contexts,
226
+ *,
227
+ base=False,
228
+ exact=False,
229
+ prefix=False,
230
+ vocab=False,
231
+ ):
232
+ def helper(term):
233
+ vocabs = []
234
+ bases = []
235
+ prefixes = []
236
+ exacts = []
237
+ is_short = not re.match(r"[^:]+:", term)
238
+
239
+ for name, value in foreach_context(contexts):
240
+ if name == "@vocab":
241
+ if vocab and is_short:
242
+ vocabs.append(helper(value))
243
+ continue
244
+
245
+ if name == "@base":
246
+ if base and is_short:
247
+ bases.append(value)
248
+ continue
249
+
250
+ if isinstance(value, dict):
251
+ value = value["@id"]
252
+
253
+ if term == name:
254
+ if exact:
255
+ exacts.append(helper(value))
256
+ continue
257
+
258
+ if prefix:
259
+ prefixes.append(name)
260
+
261
+ for e in exacts:
262
+ return e
263
+
264
+ if ":" in term:
265
+ p, suffix = term.split(":", 1)
266
+ for name in prefixes:
267
+ if p == name:
268
+ p = self.expand_iri(p)
269
+ if p.endswith("/"):
270
+ return p + suffix
271
+
272
+ for value in vocabs + bases:
273
+ return value + term
274
+
275
+ return term
276
+
277
+ return helper(term)
@@ -114,6 +114,14 @@ class BasicJinjaRender(object):
114
114
  d.sort()
115
115
  return d
116
116
 
117
+ def get_all_named_individuals(cls):
118
+ ni = set(i._id for i in cls.named_individuals)
119
+
120
+ for d in get_all_derived(cls):
121
+ ni |= set(i._id for i in classes.get(d).named_individuals)
122
+
123
+ return ni
124
+
117
125
  classes = ObjectList(model.classes)
118
126
  concrete_classes = ObjectList(
119
127
  list(c for c in model.classes if not c.is_abstract)
@@ -132,6 +140,7 @@ class BasicJinjaRender(object):
132
140
 
133
141
  env = {
134
142
  "get_all_derived": get_all_derived,
143
+ "get_all_named_individuals": get_all_named_individuals,
135
144
  **self.get_extra_env(),
136
145
  }
137
146
 
@@ -59,7 +59,7 @@
59
59
  {#- Classes are divided into 2 parts. The properties are separated into a separate object #}
60
60
  {#- so that a object can references the properties of its parent without needing the const #}
61
61
  {#- @type tag #}
62
- {%- if not class.is_abstract %}
62
+ {%- if not class.is_abstract or class.is_extensible %}
63
63
  "{{ varname(*class.clsname) }}": {
64
64
  "allOf": [
65
65
  {
@@ -69,13 +69,21 @@
69
69
  {%- endif %}
70
70
  "properties": {
71
71
  "{{ class.id_property or "@id" }}": { "$ref": "#/$defs/{{ class.node_kind.split("#")[-1] }}" },
72
- "{{ context.compact("@type") }}": {
72
+ "{{ context.compact_iri("@type") }}": {
73
+ {#- Abstract Extensible classes are weird; any type _except_ the specific class type is allowed #}
74
+ {%- if class.is_abstract and class.is_extensible %}
75
+ "allOf": [
76
+ { "$ref": "#/$defs/IRI" },
77
+ { "not": { "const": "{{ context.compact_vocab(class._id) }}" } }
78
+ ]
79
+ {%- else %}
73
80
  "oneOf": [
74
81
  {%- if class.is_extensible %}
75
82
  { "$ref": "#/$defs/IRI" },
76
83
  {%- endif %}
77
84
  { "const": "{{ context.compact_vocab(class._id) }}" }
78
85
  ]
86
+ {%- endif %}
79
87
  }
80
88
  }{%- if class.node_kind == SH.IRI %},
81
89
  "required": ["{{ class.id_property or "@id" }}"]
@@ -86,9 +94,14 @@
86
94
  },
87
95
  {%- endif %}
88
96
  "{{ varname(*class.clsname) }}_derived": {
89
- {%- set ns = namespace(external_ref=False, local_ref=False, any_ref=False, json_refs=[]) %}
97
+ {%- set ns = namespace(json_refs=[], named_individuals=[]) %}
90
98
  {%- for d in get_all_derived(class) + [class._id] %}
91
99
  {%- set ns.json_refs = ns.json_refs + ["#/$defs/" + varname(*classes.get(d).clsname)] %}
100
+ {%- for n in classes.get(d).named_individuals %}
101
+ {%- if context.compact_iri(n._id) != n._id %}
102
+ {%- set ns.named_individuals = ns.named_individuals + [context.compact_iri(n._id)] %}
103
+ {%- endif %}
104
+ {%- endfor %}
92
105
  {%- endfor %}
93
106
  "anyOf": [
94
107
  {%- if ns.json_refs %}
@@ -104,6 +117,9 @@
104
117
  ]
105
118
  },
106
119
  {%- endif %}
120
+ {%- for n in ns.named_individuals %}
121
+ { "const": "{{ n }}" },
122
+ {%- endfor %}
107
123
  { "$ref": "#/$defs/BlankNodeOrIRI" }
108
124
  ]
109
125
  },
@@ -251,7 +267,7 @@
251
267
  "SHACLClass": {
252
268
  "type": "object",
253
269
  "properties": {
254
- "{{ context.compact("@type") }}": {
270
+ "{{ context.compact_iri("@type") }}": {
255
271
  "oneOf": [
256
272
  { "$ref": "#/$defs/IRI" },
257
273
  {
@@ -264,7 +280,7 @@
264
280
  ]
265
281
  }
266
282
  },
267
- "required": ["{{ context.compact("@type") }}"]
283
+ "required": ["{{ context.compact_iri("@type") }}"]
268
284
  },
269
285
  "AnyClass": {
270
286
  "anyOf": [
@@ -220,13 +220,34 @@ class FloatProp(Property):
220
220
  return decoder.read_float()
221
221
 
222
222
 
223
- class ObjectProp(Property):
223
+ class IRIProp(Property):
224
+ def __init__(self, context=[], *, pattern=None):
225
+ super().__init__(pattern=pattern)
226
+ self.context = context
227
+
228
+ def compact(self, value):
229
+ for iri, compact in self.context:
230
+ if value == iri:
231
+ return compact
232
+ return None
233
+
234
+ def expand(self, value):
235
+ for iri, compact in self.context:
236
+ if value == compact:
237
+ return iri
238
+ return None
239
+
240
+ def iri_values(self):
241
+ return (iri for iri, _ in self.context)
242
+
243
+
244
+ class ObjectProp(IRIProp):
224
245
  """
225
246
  A scalar SHACL object property of a SHACL object
226
247
  """
227
248
 
228
- def __init__(self, cls, required):
229
- super().__init__()
249
+ def __init__(self, cls, required, context=[]):
250
+ super().__init__(context)
230
251
  self.cls = cls
231
252
  self.required = required
232
253
 
@@ -264,7 +285,7 @@ class ObjectProp(Property):
264
285
  raise ValueError("Object cannot be None")
265
286
 
266
287
  if isinstance(value, str):
267
- encoder.write_iri(value)
288
+ encoder.write_iri(value, self.compact(value))
268
289
  return
269
290
 
270
291
  return value.encode(encoder, state)
@@ -274,6 +295,8 @@ class ObjectProp(Property):
274
295
  if iri is None:
275
296
  return self.cls.decode(decoder, objectset=objectset)
276
297
 
298
+ iri = self.expand(iri) or iri
299
+
277
300
  if objectset is None:
278
301
  return iri
279
302
 
@@ -442,36 +465,27 @@ class ListProp(Property):
442
465
  return ListProxy(self.prop, data=data)
443
466
 
444
467
 
445
- class EnumProp(Property):
468
+ class EnumProp(IRIProp):
446
469
  VALID_TYPES = str
447
470
 
448
471
  def __init__(self, values, *, pattern=None):
449
- super().__init__(pattern=pattern)
450
- self.values = values
472
+ super().__init__(values, pattern=pattern)
451
473
 
452
474
  def validate(self, value):
453
475
  super().validate(value)
454
476
 
455
- valid_values = (iri for iri, _ in self.values)
477
+ valid_values = self.iri_values()
456
478
  if value not in valid_values:
457
479
  raise ValueError(
458
480
  f"'{value}' is not a valid value. Choose one of {' '.join(valid_values)}"
459
481
  )
460
482
 
461
483
  def encode(self, encoder, value, state):
462
- for iri, compact in self.values:
463
- if iri == value:
464
- encoder.write_enum(value, self, compact)
465
- return
466
-
467
- encoder.write_enum(value, self)
484
+ encoder.write_enum(value, self, self.compact(value))
468
485
 
469
486
  def decode(self, decoder, *, objectset=None):
470
487
  v = decoder.read_enum(self)
471
- for iri, compact in self.values:
472
- if v == compact:
473
- return iri
474
- return v
488
+ return self.expand(v) or v
475
489
 
476
490
 
477
491
  class NodeKind(Enum):
@@ -506,6 +520,8 @@ def register(type_iri, *, compact_type=None, abstract=False):
506
520
  SHACLObject.CLASSES[key] = c
507
521
 
508
522
  def decorator(c):
523
+ global NAMED_INDIVIDUALS
524
+
509
525
  assert issubclass(
510
526
  c, SHACLObject
511
527
  ), f"{c.__name__} is not derived from SHACLObject"
@@ -518,6 +534,8 @@ def register(type_iri, *, compact_type=None, abstract=False):
518
534
  if compact_type:
519
535
  add_class(compact_type, c)
520
536
 
537
+ NAMED_INDIVIDUALS |= set(c.NAMED_INDIVIDUALS.values())
538
+
521
539
  # Registration is deferred until the first instance of class is created
522
540
  # so that it has access to any other defined class
523
541
  c._NEEDS_REG = True
@@ -527,6 +545,7 @@ def register(type_iri, *, compact_type=None, abstract=False):
527
545
 
528
546
 
529
547
  register_lock = threading.Lock()
548
+ NAMED_INDIVIDUALS = set()
530
549
 
531
550
 
532
551
  @functools.total_ordering
@@ -537,7 +556,7 @@ class SHACLObject(object):
537
556
  IS_ABSTRACT = True
538
557
 
539
558
  def __init__(self, **kwargs):
540
- if self.__class__.IS_ABSTRACT:
559
+ if self._is_abstract():
541
560
  raise NotImplementedError(
542
561
  f"{self.__class__.__name__} is abstract and cannot be implemented"
543
562
  )
@@ -559,6 +578,9 @@ class SHACLObject(object):
559
578
  for k, v in kwargs.items():
560
579
  setattr(self, k, v)
561
580
 
581
+ def _is_abstract(self):
582
+ return self.__class__.IS_ABSTRACT
583
+
562
584
  @classmethod
563
585
  def _register_props(cls):
564
586
  cls._add_property("_id", StringProp(), iri="@id")
@@ -875,11 +897,20 @@ class SHACLExtensibleObject(object):
875
897
  CLOSED = False
876
898
 
877
899
  def __init__(self, typ=None, **kwargs):
878
- super().__init__(**kwargs)
879
900
  if typ:
880
901
  self.__dict__["_obj_TYPE"] = (typ, None)
881
902
  else:
882
903
  self.__dict__["_obj_TYPE"] = (self._OBJ_TYPE, self._OBJ_COMPACT_TYPE)
904
+ super().__init__(**kwargs)
905
+
906
+ def _is_abstract(self):
907
+ # Unknown classes are assumed to not be abstract so that they can be
908
+ # deserialized
909
+ typ = self.__dict__["_obj_TYPE"][0]
910
+ if typ in self.__class__.CLASSES:
911
+ return self.__class__.CLASSES[typ].IS_ABSTRACT
912
+
913
+ return False
883
914
 
884
915
  @classmethod
885
916
  def _make_object(cls, typ):
@@ -1079,6 +1110,8 @@ class SHACLObjectSet(object):
1079
1110
  return self._link()
1080
1111
 
1081
1112
  def _link(self):
1113
+ global NAMED_INDIVIDUALS
1114
+
1082
1115
  self.missing_ids = set()
1083
1116
  visited = set()
1084
1117
 
@@ -1101,6 +1134,9 @@ class SHACLObjectSet(object):
1101
1134
  obj_by_id[_id] = obj
1102
1135
  self.obj_by_id = obj_by_id
1103
1136
 
1137
+ # Named individuals aren't considered missing
1138
+ self.missing_ids -= NAMED_INDIVIDUALS
1139
+
1104
1140
  return self.missing_ids
1105
1141
 
1106
1142
  def find_by_id(self, _id, default=None):
@@ -1493,14 +1529,14 @@ class JSONLDDecoder(Decoder):
1493
1529
 
1494
1530
  def object_keys(self):
1495
1531
  for key in self.data.keys():
1496
- if key in ("@type", "{{ context.compact('@type') }}"):
1532
+ if key in ("@type", "{{ context.compact_iri('@type') }}"):
1497
1533
  continue
1498
1534
  if self.root and key == "@context":
1499
1535
  continue
1500
1536
  yield key
1501
1537
 
1502
1538
  def read_object(self):
1503
- typ = self.__get_value("@type", "{{ context.compact('@type') }}")
1539
+ typ = self.__get_value("@type", "{{ context.compact_iri('@type') }}")
1504
1540
  if typ is not None:
1505
1541
  return typ, self
1506
1542
 
@@ -1692,7 +1728,7 @@ class JSONLDEncoder(Encoder):
1692
1728
  @contextmanager
1693
1729
  def write_object(self, o, _id, needs_id):
1694
1730
  self.data = {
1695
- "{{ context.compact('@type') }}": o.COMPACT_TYPE or o.TYPE,
1731
+ "{{ context.compact_iri('@type') }}": o.COMPACT_TYPE or o.TYPE,
1696
1732
  }
1697
1733
  if needs_id:
1698
1734
  self.data[o.ID_ALIAS or "@id"] = _id
@@ -1816,7 +1852,7 @@ class JSONLDInlineEncoder(Encoder):
1816
1852
  self._write_comma()
1817
1853
 
1818
1854
  self.write("{")
1819
- self.write_string("{{ context.compact('@type') }}")
1855
+ self.write_string("{{ context.compact_iri('@type') }}")
1820
1856
  self.write(":")
1821
1857
  self.write_string(o.COMPACT_TYPE or o.TYPE)
1822
1858
  self.comma = True
@@ -1946,7 +1982,7 @@ CONTEXT_URLS = [
1946
1982
  #{{ (" " + l).rstrip() }}
1947
1983
  {%- endfor %}
1948
1984
  {%- endif %}
1949
- @register("{{ class._id }}"{%- if context.compact(class._id) != class._id %}, compact_type="{{ context.compact(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }})
1985
+ @register("{{ class._id }}"{%- if context.compact_iri(class._id) != class._id %}, compact_type="{{ context.compact_iri(class._id) }}"{%- endif %}, abstract={{ class.is_abstract }})
1950
1986
  class {{ varname(*class.clsname) }}(
1951
1987
  {%- if class.is_extensible -%}
1952
1988
  SHACLExtensibleObject{{", "}}
@@ -1997,7 +2033,19 @@ class {{ varname(*class.clsname) }}(
1997
2033
  {%- endfor %}
1998
2034
  ])
1999
2035
  {%- elif prop.class_id -%}
2000
- ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %})
2036
+ {%- set ctx = [] %}
2037
+ {%- for value in get_all_named_individuals(classes.get(prop.class_id)) %}
2038
+ {%- if context.compact_vocab(value, prop.path) != value %}
2039
+ {{- ctx.append((value, context.compact_vocab(value, prop.path))) or "" }}
2040
+ {%- endif %}
2041
+ {%- endfor -%}
2042
+ ObjectProp({{ varname(*classes.get(prop.class_id).clsname) }}, {% if prop.min_count and not is_list %}True{% else %}False{% endif %}{% if ctx %}, context=[
2043
+ {%- for value, compact in ctx %}
2044
+ ("{{ value }}", "{{ compact }}"),
2045
+ {%- endfor %}
2046
+ ],
2047
+ {%- endif -%}
2048
+ )
2001
2049
  {%- else -%}
2002
2050
  {% if not prop.datatype in DATATYPE_CLASSES -%}
2003
2051
  {{ abort("Unknown data type " + prop.datatype) -}}
@@ -280,7 +280,7 @@ class Model(object):
280
280
  """
281
281
  _id = str(_id)
282
282
  if _id not in self.compact_ids:
283
- self.compact_ids[_id] = self.context.compact(_id)
283
+ self.compact_ids[_id] = self.context.compact_iri(_id)
284
284
 
285
285
  if self.compact_ids[_id] == _id and fallback is not None:
286
286
  return fallback
@@ -0,0 +1 @@
1
+ VERSION = "0.0.13"
File without changes