Skip to content

Commit bf01b8e

Browse files
authored
codegen: add plain initializers (#263)
* test with Python 3.8
1 parent a8a13f9 commit bf01b8e

File tree

10 files changed

+685
-404
lines changed

10 files changed

+685
-404
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
- stage: test
2525
python: "3.7"
2626
dist: xenial
27+
- stage: test
28+
python: "3.8-dev"
29+
dist: xenial
2730
- stage: release-test
2831
python: "2.7"
2932
script: RELEASE_SKIP=head PYVER= ${TRAVIS_BUILD_DIR}/release-test.sh

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ mypy2: ${PYSOURCES}
186186
ln -s $(shell python -c 'from __future__ import print_function; import ruamel.yaml; import os.path; print(os.path.dirname(ruamel.yaml.__file__))') \
187187
typeshed/2and3/ruamel/ ; \
188188
fi # if minimally required ruamel.yaml version is 0.15.99 or greater, than the above can be removed
189-
MYPYPATH=$MYPYPATH:typeshed/2.7:typeshed/2and3 mypy --py2 --disallow-untyped-calls \
189+
MYPYPATH=$$MYPYPATH:typeshed/2.7:typeshed/2and3 mypy --py2 --disallow-untyped-calls \
190190
--warn-redundant-casts \
191191
schema_salad
192192

@@ -197,7 +197,7 @@ mypy3: ${PYSOURCES}
197197
ln -s $(shell python -c 'from __future__ import print_function; import ruamel.yaml; import os.path; print(os.path.dirname(ruamel.yaml.__file__))') \
198198
typeshed/2and3/ruamel/ ; \
199199
fi # if minimally required ruamel.yaml version is 0.15.99 or greater, than the above can be removed
200-
MYPYPATH=$MYPYPATH:typeshed/3:typeshed/2and3 mypy --disallow-untyped-calls \
200+
MYPYPATH=$$MYPYPATH:typeshed/3:typeshed/2and3 mypy --disallow-untyped-calls \
201201
--warn-redundant-casts \
202202
schema_salad
203203

release-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package=schema-salad
77
module=schema_salad
88
slug=${TRAVIS_PULL_REQUEST_SLUG:=common-workflow-language/schema_salad}
99
repo=https://github.com/${slug}.git
10-
run_tests="bin/py.test --pyarg ${module}"
10+
run_tests="bin/py.test --pyargs ${module}"
1111
pipver=8.0.1 # minimum required version of pip
1212
setupver=20.10.1 # minimum required version of setuptools
1313
PYVER=${PYVER:=2.7}

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
typing==3.7.4 ; python_version < "3.5"
2-
ruamel.yaml>=0.12.4, <= 0.15.99
2+
ruamel.yaml>=0.12.4, <= 0.16
33
rdflib==4.2.2
44
rdflib-jsonld==0.4.0
55
mistune>=0.8.1,<0.9

schema_salad/metaschema.py

Lines changed: 588 additions & 358 deletions
Large diffs are not rendered by default.

schema_salad/python_codegen.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
class PythonCodeGen(CodeGenBase):
1616
"""Generation of Python code for a given Schema Salad definition."""
1717
def __init__(self, out):
18-
# type: (IO[str]) -> None
18+
# type: (IO[Any]) -> None
1919
super(PythonCodeGen, self).__init__()
2020
self.out = out
2121
self.current_class_is_abstract = False
@@ -65,7 +65,7 @@ def begin_class(self, # pylint: disable=too-many-arguments
6565
else:
6666
ext = "Savable"
6767

68-
self.out.write("class %s(%s):\n" % (self.safe_name(classname), ext))
68+
self.out.write("class %s(%s):\n" % (classname, ext))
6969

7070
if doc:
7171
self.out.write(' """\n')
@@ -79,30 +79,55 @@ def begin_class(self, # pylint: disable=too-many-arguments
7979
self.out.write(" pass\n\n")
8080
return
8181

82+
safe_inits = ["self"] # type: List[Text]
83+
safe_inits.extend([self.safe_name(f) for f in field_names if f != "class"])
84+
inits_types = ", ".join(["Any"]*(len(safe_inits) -1))
8285
self.out.write(
83-
""" def __init__(self, _doc, baseuri, loadingOptions, docRoot=None):
84-
# type: (Any, Text, LoadingOptions, Optional[Text]) -> None
85-
doc = copy.copy(_doc)
86-
if hasattr(_doc, 'lc'):
87-
doc.lc.data = _doc.lc.data
88-
doc.lc.filename = _doc.lc.filename
89-
errors = []
90-
self.loadingOptions = loadingOptions
86+
" def __init__(" +", ".join(safe_inits) + ", extension_fields=None, loadingOptions=None):\n"
87+
" # type: (" + inits_types + """, Optional[Dict[Text, Any]], Optional[LoadingOptions]) -> None
88+
if extension_fields:
89+
self.extension_fields = extension_fields
90+
else:
91+
self.extension_fields = yaml.comments.CommentedMap()
92+
if loadingOptions:
93+
self.loadingOptions = loadingOptions
94+
else:
95+
self.loadingOptions = LoadingOptions()
9196
""")
97+
field_inits = ""
98+
for name in field_names:
99+
if name == "class":
100+
field_inits +=""" self.class_ = "{}"
101+
""".format(classname)
102+
else:
103+
field_inits +=""" self.{0} = {0}
104+
""".format(self.safe_name(name))
105+
self.out.write(field_inits + '\n'
106+
+"""
107+
@classmethod
108+
def fromDoc(cls, doc, baseuri, loadingOptions, docRoot=None):
109+
# type: (Any, Text, LoadingOptions, Optional[Text]) -> {}
110+
111+
_doc = copy.copy(doc)
112+
if hasattr(doc, 'lc'):
113+
_doc.lc.data = doc.lc.data
114+
_doc.lc.filename = doc.lc.filename
115+
errors = []
116+
""".format(classname))
92117

93118
self.idfield = idfield
94119

95120
self.serializer.write("""
96121
def save(self, top=False, base_url="", relative_uris=True):
97122
# type: (bool, Text, bool) -> Dict[Text, Any]
98-
r = {} # type: Dict[Text, Any]
123+
r = yaml.comments.CommentedMap() # type: Dict[Text, Any]
99124
for ef in self.extension_fields:
100125
r[prefix_url(ef, self.loadingOptions.vocab)] = self.extension_fields[ef]
101126
""")
102127

103128
if "class" in field_names:
104129
self.out.write("""
105-
if doc.get('class') != '{class_}':
130+
if _doc.get('class') != '{class_}':
106131
raise ValidationException("Not a {class_}")
107132
108133
""".format(class_=classname))
@@ -119,14 +144,14 @@ def end_class(self, classname, field_names):
119144
return
120145

121146
self.out.write("""
122-
self.extension_fields = {{}} # type: Dict[Text, Text]
123-
for k in doc.keys():
124-
if k not in self.attrs:
147+
extension_fields = yaml.comments.CommentedMap()
148+
for k in _doc.keys():
149+
if k not in cls.attrs:
125150
if ":" in k:
126151
ex = expand_url(k, u"", loadingOptions, scoped_id=False, vocab_term=False)
127-
self.extension_fields[ex] = doc[k]
152+
extension_fields[ex] = _doc[k]
128153
else:
129-
errors.append(SourceLine(doc, k, str).makeError("invalid field `%s`, expected one of: {attrstr}" % (k)))
154+
errors.append(SourceLine(_doc, k, str).makeError("invalid field `%s`, expected one of: {attrstr}" % (k)))
130155
break
131156
132157
if errors:
@@ -145,7 +170,17 @@ def end_class(self, classname, field_names):
145170

146171
self.serializer.write(" attrs = frozenset({attrs})\n".format(attrs=field_names))
147172

173+
safe_inits = [ self.safe_name(f) for f in field_names if f != "class" ] # type: List[Text]
174+
175+
safe_inits.extend(["extension_fields=extension_fields", "loadingOptions=loadingOptions"])
176+
177+
self.out.write(""" loadingOptions = copy.deepcopy(loadingOptions)
178+
loadingOptions.original_doc = _doc
179+
""")
180+
self.out.write(" return cls(" + ", ".join(safe_inits)+")\n")
181+
148182
self.out.write(self.serializer.getvalue())
183+
149184
self.out.write("\n\n")
150185

151186
prims = {
@@ -206,19 +241,19 @@ def declare_id_field(self, name, fieldtype, doc, optional):
206241
self.declare_field(name, fieldtype, doc, True)
207242

208243
if optional:
209-
opt = """self.{safename} = "_:" + str(uuid.uuid4())""".format(
244+
opt = """{safename} = "_:" + str(uuid.uuid4())""".format(
210245
safename=self.safe_name(name))
211246
else:
212247
opt = """raise ValidationException("Missing {fieldname}")""".format(
213248
fieldname=shortname(name))
214249

215250
self.out.write("""
216-
if self.{safename} is None:
251+
if {safename} is None:
217252
if docRoot is not None:
218-
self.{safename} = docRoot
253+
{safename} = docRoot
219254
else:
220255
{opt}
221-
baseuri = self.{safename}
256+
baseuri = {safename}
222257
""".
223258
format(safename=self.safe_name(name),
224259
fieldname=shortname(name),
@@ -235,30 +270,30 @@ def declare_field(self, name, fieldtype, doc, optional):
235270
return
236271

237272
if optional:
238-
self.out.write(" if '{fieldname}' in doc:\n".format(fieldname=shortname(name)))
273+
self.out.write(" if '{fieldname}' in _doc:\n".format(fieldname=shortname(name)))
239274
spc = " "
240275
else:
241276
spc = ""
242277
self.out.write("""{spc} try:
243-
{spc} self.{safename} = load_field(doc.get('{fieldname}'), {fieldtype}, baseuri, loadingOptions)
278+
{spc} {safename} = load_field(_doc.get('{fieldname}'), {fieldtype}, baseuri, loadingOptions)
244279
{spc} except ValidationException as e:
245-
{spc} errors.append(SourceLine(doc, '{fieldname}', str).makeError(\"the `{fieldname}` field is not valid because:\\n\"+str(e)))
280+
{spc} errors.append(SourceLine(_doc, '{fieldname}', str).makeError(\"the `{fieldname}` field is not valid because:\\n\"+str(e)))
246281
""".
247282
format(safename=self.safe_name(name),
248283
fieldname=shortname(name),
249284
fieldtype=fieldtype.name,
250285
spc=spc))
251286
if optional:
252287
self.out.write(""" else:
253-
self.{safename} = None
288+
{safename} = None
254289
""".format(safename=self.safe_name(name)))
255290

256291
self.out.write("\n")
257292

258293
if name == self.idfield or not self.idfield:
259294
baseurl = 'base_url'
260295
else:
261-
baseurl = "self.%s" % self.safe_name(self.idfield)
296+
baseurl = 'self.{}'.format(self.safe_name(self.idfield))
262297

263298
if fieldtype.is_uri:
264299
self.serializer.write("""

schema_salad/python_codegen_support.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,29 @@ class ValidationException(Exception):
2323
pass
2424

2525
class Savable(object):
26+
@classmethod
27+
def fromDoc(cls, _doc, baseuri, loadingOptions, docRoot=None):
28+
# type: (Any, Text, LoadingOptions, Optional[Text]) -> Savable
29+
pass
30+
2631
def save(self, top=False, base_url="", relative_uris=True):
2732
# type: (bool, Text, bool) -> Dict[Text, Text]
2833
pass
2934

3035
class LoadingOptions(object):
3136
def __init__(self,
32-
fetcher=None, # type: Optional[Fetcher]
33-
namespaces=None, # type: Optional[Dict[Text, Text]]
34-
fileuri=None, # type: Optional[Text]
35-
copyfrom=None, # type: Optional[LoadingOptions]
36-
schemas=None # type: Optional[List[Text]]
37+
fetcher=None, # type: Optional[Fetcher]
38+
namespaces=None, # type: Optional[Dict[Text, Text]]
39+
fileuri=None, # type: Optional[Text]
40+
copyfrom=None, # type: Optional[LoadingOptions]
41+
schemas=None, # type: Optional[List[Text]]
42+
original_doc=None # type: Optional[Any]
3743
): # type: (...) -> None
3844
self.idx = {} # type: Dict[Text, Text]
3945
self.fileuri = fileuri # type: Optional[Text]
4046
self.namespaces = namespaces
4147
self.schemas = schemas
48+
self.original_doc = original_doc
4249
if copyfrom is not None:
4350
self.idx = copyfrom.idx
4451
if fetcher is None:
@@ -104,6 +111,11 @@ def save(val, # type: Optional[Union[Savable, MutableSequence[Sav
104111
return val.save(top=top, base_url=base_url, relative_uris=relative_uris)
105112
if isinstance(val, MutableSequence):
106113
return [save(v, top=False, base_url=base_url, relative_uris=relative_uris) for v in val]
114+
if isinstance(val, MutableMapping):
115+
newdict = {}
116+
for key in val:
117+
newdict[key] = save(val[key], top=False, base_url=base_url, relative_uris=relative_uris)
118+
return newdict
107119
return val
108120

109121
def expand_url(url, # type: Union[str, Text]
@@ -239,14 +251,14 @@ def load(self, doc, baseuri, loadingOptions, docRoot=None):
239251

240252
class _RecordLoader(_Loader):
241253
def __init__(self, classtype):
242-
# type: (type) -> None
254+
# type: (Type[Savable]) -> None
243255
self.classtype = classtype
244256

245257
def load(self, doc, baseuri, loadingOptions, docRoot=None):
246258
# type: (Any, Text, LoadingOptions, Optional[Text]) -> Any
247259
if not isinstance(doc, MutableMapping):
248260
raise ValidationException("Expected a dict")
249-
return self.classtype(doc, baseuri, loadingOptions, docRoot=docRoot)
261+
return self.classtype.fromDoc(doc, baseuri, loadingOptions, docRoot=docRoot)
250262

251263
def __repr__(self): # type: () -> str
252264
return str(self.classtype)

schema_salad/tests/test_cg.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_load():
1919
"type": "string"
2020
}]
2121
}
22-
rs = cg_metaschema.RecordSchema(doc, "http://example.com/", cg_metaschema.LoadingOptions())
22+
rs = cg_metaschema.RecordSchema.fromDoc(doc, "http://example.com/", cg_metaschema.LoadingOptions())
2323
assert "record" == rs.type
2424
assert "http://example.com/#hello" == rs.fields[0].name
2525
assert "Hello test case" == rs.fields[0].doc
@@ -39,15 +39,15 @@ def test_err():
3939
"type": "string"
4040
}
4141
with pytest.raises(cg_metaschema.ValidationException):
42-
rf = cg_metaschema.RecordField(doc, "", cg_metaschema.LoadingOptions())
42+
rf = cg_metaschema.RecordField.fromDoc(doc, "", cg_metaschema.LoadingOptions())
4343

4444
def test_include():
4545
doc = {
4646
"name": "hello",
4747
"doc": [{"$include": "hello.txt"}],
4848
"type": "documentation"
4949
}
50-
rf = cg_metaschema.Documentation(doc, "http://example.com/",
50+
rf = cg_metaschema.Documentation.fromDoc(doc, "http://example.com/",
5151
cg_metaschema.LoadingOptions(fileuri=file_uri(get_data("tests/_"))))
5252
assert "http://example.com/#hello" == rf.name
5353
assert ["hello world!\n"] == rf.doc
@@ -66,7 +66,7 @@ def test_import():
6666
}]
6767
}
6868
lead = file_uri(os.path.normpath(get_data("tests")))
69-
rs = cg_metaschema.RecordSchema(doc, "http://example.com/", cg_metaschema.LoadingOptions(fileuri=lead+"/_"))
69+
rs = cg_metaschema.RecordSchema.fromDoc(doc, "http://example.com/", cg_metaschema.LoadingOptions(fileuri=lead+"/_"))
7070
assert "record" == rs.type
7171
assert lead+"/hellofield.yml#hello" == rs.fields[0].name
7272
assert "hello world!\n" == rs.fields[0].doc
@@ -101,7 +101,7 @@ def test_err2():
101101
}]
102102
}
103103
with pytest.raises(cg_metaschema.ValidationException):
104-
rs = cg_metaschema.RecordSchema(doc, "", cg_metaschema.LoadingOptions())
104+
rs = cg_metaschema.RecordSchema.fromDoc(doc, "", cg_metaschema.LoadingOptions())
105105

106106
def test_idmap():
107107
doc = {
@@ -113,7 +113,7 @@ def test_idmap():
113113
}
114114
}
115115
}
116-
rs = cg_metaschema.RecordSchema(doc, "http://example.com/", cg_metaschema.LoadingOptions())
116+
rs = cg_metaschema.RecordSchema.fromDoc(doc, "http://example.com/", cg_metaschema.LoadingOptions())
117117
assert "record" == rs.type
118118
assert "http://example.com/#hello" == rs.fields[0].name
119119
assert "Hello test case" == rs.fields[0].doc
@@ -134,7 +134,7 @@ def test_idmap2():
134134
"hello": "string"
135135
}
136136
}
137-
rs = cg_metaschema.RecordSchema(doc, "http://example.com/", cg_metaschema.LoadingOptions())
137+
rs = cg_metaschema.RecordSchema.fromDoc(doc, "http://example.com/", cg_metaschema.LoadingOptions())
138138
assert "record" == rs.type
139139
assert "http://example.com/#hello" == rs.fields[0].name
140140
assert rs.fields[0].doc is None

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
install_requires = [
3030
'setuptools',
3131
'requests >= 1.0',
32-
'ruamel.yaml >= 0.12.4, <= 0.15.99',
32+
'ruamel.yaml >= 0.12.4, <= 0.16',
3333
# once the minimum version for ruamel.yaml >= 0.15.99
3434
# then please update the mypy{2,3} targets in the Makefile
3535
'rdflib >= 4.2.2, < 4.3.0',
@@ -80,6 +80,7 @@
8080
"Programming Language :: Python :: 3.5",
8181
"Programming Language :: Python :: 3.6",
8282
"Programming Language :: Python :: 3.7",
83+
"Programming Language :: Python :: 3.8",
8384
"Typing :: Typed",
8485
]
8586
)

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ python =
1616
3.5: py35
1717
3.6: py36
1818
3.7: py37
19-
3.8: py38
19+
3.8-dev: py38
2020

2121
[testenv]
2222
passenv =

0 commit comments

Comments
 (0)