Skip to content

Commit 2c4ec14

Browse files
committed
Fix some ORM model generator issues.
Instead of hard errors issue warnings and attempt to skip over features that cannot be implemented. A partial working set of models is better than none and sometimes is what is needed. Stop allowing multi properties in reflected models, they fundamentally lack an identity and don't mesh with the Object models.
1 parent ad37a6b commit 2c4ec14

File tree

6 files changed

+127
-204
lines changed

6 files changed

+127
-204
lines changed

gel/_testbase.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@
3333
import tempfile
3434
import time
3535
import unittest
36+
import warnings
3637

3738
import gel
3839
from gel import asyncio_client
3940
from gel import blocking_client
40-
from gel.orm.introspection import get_schema_json
41+
from gel.orm.introspection import get_schema_json, GelORMWarning
4142
from gel.orm.sqla import ModelGenerator as SQLAModGen
4243
from gel.orm.django.generator import ModelGenerator as DjangoModGen
4344

@@ -646,17 +647,20 @@ def setUpClass(cls):
646647
if importlib.util.find_spec("psycopg2") is None:
647648
raise unittest.SkipTest("need psycopg2 for ORM tests")
648649

649-
super().setUpClass()
650+
with warnings.catch_warnings():
651+
warnings.simplefilter("ignore", GelORMWarning)
650652

651-
class_set_up = os.environ.get('EDGEDB_TEST_CASES_SET_UP')
652-
if not class_set_up:
653-
# We'll need a temp directory to setup the generated Python
654-
# package
655-
cls.tmpormdir = tempfile.TemporaryDirectory()
656-
sys.path.append(cls.tmpormdir.name)
657-
# Now that the DB is setup, generate the ORM models from it
658-
cls.spec = get_schema_json(cls.client)
659-
cls.setupORM()
653+
super().setUpClass()
654+
655+
class_set_up = os.environ.get('EDGEDB_TEST_CASES_SET_UP')
656+
if not class_set_up:
657+
# We'll need a temp directory to setup the generated Python
658+
# package
659+
cls.tmpormdir = tempfile.TemporaryDirectory()
660+
sys.path.append(cls.tmpormdir.name)
661+
# Now that the DB is setup, generate the ORM models from it
662+
cls.spec = get_schema_json(cls.client)
663+
cls.setupORM()
660664

661665
@classmethod
662666
def setupORM(cls):

gel/orm/cli.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919

2020
import argparse
21+
import warnings
2122

2223
import gel
2324

2425
from gel.codegen.generator import _get_conn_args
25-
from .introspection import get_schema_json
26+
from .introspection import get_schema_json, GelORMWarning
2627
from .sqla import ModelGenerator as SQLAModGen
2728
from .django.generator import ModelGenerator as DjangoModGen
2829

@@ -73,8 +74,15 @@ def main():
7374
args = parser.parse_args()
7475
# setup client
7576
client = gel.create_client(**_get_conn_args(args))
76-
spec = get_schema_json(client)
77-
generate_models(args, spec)
77+
78+
with warnings.catch_warnings(record=True) as wlist:
79+
warnings.simplefilter("always", GelORMWarning)
80+
81+
spec = get_schema_json(client)
82+
generate_models(args, spec)
83+
84+
for w in wlist:
85+
print(w.message)
7886

7987

8088
def generate_models(args, spec):

gel/orm/django/generator.py

+50-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pathlib
22
import re
3+
import warnings
34

4-
from ..introspection import get_mod_and_name, FilePrinter
5+
from ..introspection import get_mod_and_name, GelORMWarning, FilePrinter
56

67

78
GEL_SCALAR_MAP = {
@@ -24,7 +25,7 @@
2425
'cal::local_date': 'DateField',
2526
'cal::local_datetime': 'DateTimeField',
2627
'cal::local_time': 'TimeField',
27-
# all kinds of duration is not supported due to this error:
28+
# all kinds of durations are not supported due to this error:
2829
# iso_8601 intervalstyle currently not supported
2930
}
3031

@@ -53,7 +54,6 @@ class GelPGMeta:
5354
'This is a model reflected from Gel using Postgres protocol.'
5455
'''
5556

56-
FK_RE = re.compile(r'''models\.ForeignKey\((.+?),''')
5757
CLOSEPAR_RE = re.compile(r'\)(?=\s+#|$)')
5858

5959

@@ -83,19 +83,16 @@ def __init__(self, *, out):
8383

8484
def spec_to_modules_dict(self, spec):
8585
modules = {
86-
mod: {} for mod in sorted(spec['modules'])
86+
mod: {'link_tables': {}, 'object_types': {}}
87+
for mod in sorted(spec['modules'])
8788
}
8889

8990
for rec in spec['link_tables']:
9091
mod = rec['module']
91-
if 'link_tables' not in modules[mod]:
92-
modules[mod]['link_tables'] = {}
9392
modules[mod]['link_tables'][rec['table']] = rec
9493

9594
for rec in spec['object_types']:
9695
mod, name = get_mod_and_name(rec['name'])
97-
if 'object_types' not in modules[mod]:
98-
modules[mod]['object_types'] = {}
9996
modules[mod]['object_types'][name] = rec
10097

10198
return modules['default']
@@ -128,10 +125,12 @@ def build_models(self, maps):
128125
# process properties as fields
129126
for prop in rec['properties']:
130127
pname = prop['name']
131-
if pname == 'id':
128+
if pname == 'id' or prop['cardinality'] == 'Many':
132129
continue
133130

134-
mod.props[pname] = self.render_prop(prop)
131+
code = self.render_prop(prop)
132+
if code:
133+
mod.props[pname] = code
135134

136135
# process single links as fields
137136
for link in rec['links']:
@@ -142,7 +141,9 @@ def build_models(self, maps):
142141

143142
lname = link['name']
144143
bklink = mod.get_backlink_name(lname)
145-
mod.links[lname] = self.render_link(link, bklink)
144+
code = self.render_link(link, bklink)
145+
if code:
146+
mod.links[lname] = code
146147

147148
modmap[mod.name] = mod
148149

@@ -153,7 +154,16 @@ def build_models(self, maps):
153154
mod.meta['unique_together'] = "(('source', 'target'),)"
154155

155156
# Only have source and target
156-
_, target = get_mod_and_name(rec['target'])
157+
mtgt, target = get_mod_and_name(rec['target'])
158+
if mtgt != 'default':
159+
# skip this whole link table
160+
warnings.warn(
161+
f'Skipping link {fwname!r}: link target '
162+
f'{rec["target"]!r} is not supported',
163+
GelORMWarning,
164+
)
165+
continue
166+
157167
mod.links['source'] = (
158168
f"LTForeignKey({source!r}, models.DO_NOTHING, "
159169
f"db_column='source', primary_key=True)"
@@ -190,8 +200,11 @@ def render_prop(self, prop):
190200
try:
191201
ftype = GEL_SCALAR_MAP[target]
192202
except KeyError:
193-
raise RuntimeError(
194-
f'Scalar type {target} is not supported')
203+
warnings.warn(
204+
f'Scalar type {target} is not supported',
205+
GelORMWarning,
206+
)
207+
return ''
195208

196209
return f'models.{ftype}({req})'
197210

@@ -201,7 +214,15 @@ def render_link(self, link, bklink=None):
201214
else:
202215
req = ', blank=True, null=True'
203216

204-
_, target = get_mod_and_name(link['target']['name'])
217+
mod, target = get_mod_and_name(link['target']['name'])
218+
219+
if mod != 'default':
220+
warnings.warn(
221+
f'Skipping link {link["name"]!r}: link target '
222+
f'{link["target"]["name"]!r} is not supported',
223+
GelORMWarning,
224+
)
225+
return ''
205226

206227
if bklink:
207228
bklink = f', related_name={bklink!r}'
@@ -215,23 +236,28 @@ def render_models(self, spec):
215236
# Check that there is only "default" module
216237
mods = spec['modules']
217238
if mods[0] != 'default' or len(mods) > 1:
218-
raise RuntimeError(
219-
f"Django reflection doesn't support multiple modules or "
220-
f"non-default modules."
239+
skipped = ', '.join([repr(m) for m in mods if m != 'default'])
240+
warnings.warn(
241+
f"Skipping modules {skipped}: Django reflection doesn't "
242+
f"support multiple modules or non-default modules.",
243+
GelORMWarning,
221244
)
222245
# Check that we don't have multiprops or link properties as they
223246
# produce models without `id` field and Django doesn't like that. It
224247
# causes Django to mistakenly use `source` as `id` and also attempt to
225248
# UPDATE `target` on link tables.
226249
if len(spec['prop_objects']) > 0:
227-
raise RuntimeError(
228-
f"Django reflection doesn't support multi properties as they "
229-
f"produce models without `id` field."
250+
warnings.warn(
251+
f"Skipping multi properties: Django reflection doesn't "
252+
f"support multi properties as they produce models without "
253+
f"`id` field.",
254+
GelORMWarning,
230255
)
231256
if len(spec['link_objects']) > 0:
232-
raise RuntimeError(
233-
f"Django reflection doesn't support link properties as they "
234-
f"produce models without `id` field."
257+
warnings.warn(
258+
f"Skipping link properties: Django reflection doesn't support "
259+
f"link properties as they produce models without `id` field.",
260+
GelORMWarning,
235261
)
236262

237263
maps = self.spec_to_modules_dict(spec)

gel/orm/introspection.py

+33-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import collections
44
import textwrap
5+
import warnings
56

67

78
INTRO_QUERY = '''
@@ -62,6 +63,10 @@
6263
CLEAN_NAME = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
6364

6465

66+
class GelORMWarning(Warning):
67+
pass
68+
69+
6570
def get_sql_name(name):
6671
# Just remove the module name
6772
name = name.rsplit('::', 1)[-1]
@@ -80,12 +85,16 @@ def get_mod_and_name(name):
8085
return name.rsplit('::', 1)
8186

8287

83-
def check_name(name):
88+
def valid_name(name):
8489
# Just remove module separators and check the rest
8590
name = name.replace('::', '')
8691
if not CLEAN_NAME.fullmatch(name):
87-
raise RuntimeError(
88-
f'Non-alphanumeric names are not supported: {name}')
92+
warnings.warn(
93+
f'Skipping {name!r}: non-alphanumeric names are not supported',
94+
GelORMWarning,
95+
)
96+
return False
97+
return True
8998

9099

91100
def get_schema_json(client):
@@ -102,6 +111,23 @@ async def async_get_schema_json(client):
102111
return _process_links(types, modules)
103112

104113

114+
def _skip_invalid_names(spec_list, recurse_into=None):
115+
valid = []
116+
for spec in spec_list:
117+
# skip invalid names
118+
if valid_name(spec['name']):
119+
if recurse_into is not None:
120+
for fname in recurse_into:
121+
if fname not in spec:
122+
continue
123+
spec[fname] = _skip_invalid_names(
124+
spec[fname], recurse_into)
125+
126+
valid.append(spec)
127+
128+
return valid
129+
130+
105131
def _process_links(types, modules):
106132
# Figure out all the backlinks, link tables, and links with link
107133
# properties that require their own intermediate objects.
@@ -110,23 +136,20 @@ def _process_links(types, modules):
110136
link_objects = []
111137
prop_objects = []
112138

139+
# All the names of types, props and links are valid beyond this point.
140+
types = _skip_invalid_names(types, ['properties', 'links'])
113141
for spec in types:
114-
check_name(spec['name'])
115142
type_map[spec['name']] = spec
116143
spec['backlink_renames'] = {}
117144

118-
for prop in spec['properties']:
119-
check_name(prop['name'])
120-
121145
for spec in types:
122146
mod = spec["name"].rsplit('::', 1)[0]
123147
sql_source = get_sql_name(spec["name"])
124148

125149
for prop in spec['properties']:
150+
name = prop['name']
126151
exclusive = prop['exclusive']
127152
cardinality = prop['cardinality']
128-
name = prop['name']
129-
check_name(name)
130153
sql_name = get_sql_name(name)
131154

132155
if cardinality == 'Many':
@@ -158,11 +181,10 @@ def _process_links(types, modules):
158181

159182
for link in spec['links']:
160183
if link['name'] != '__type__':
184+
name = link['name']
161185
target = link['target']['name']
162186
cardinality = link['cardinality']
163187
exclusive = link['exclusive']
164-
name = link['name']
165-
check_name(name)
166188
sql_name = get_sql_name(name)
167189

168190
objtype = type_map[target]
@@ -175,8 +197,6 @@ def _process_links(types, modules):
175197
'has_link_object': False,
176198
})
177199

178-
for prop in link['properties']:
179-
check_name(prop['name'])
180200

181201
link['has_link_object'] = False
182202
# Any link with properties should become its own intermediate

0 commit comments

Comments
 (0)