-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathschema.py
441 lines (361 loc) · 14.1 KB
/
schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# -*- coding: utf-8 -*-
from plone.alterego import dynamic
from plone.alterego.interfaces import IDynamicObjectFactory
from plone.behavior.interfaces import IBehavior
from plone.behavior.registration import BehaviorRegistration
from plone.dexterity.interfaces import IContentType
from plone.dexterity.interfaces import IDexterityFTI
from plone.dexterity.interfaces import IDexteritySchema
from plone.dexterity.interfaces import ISchemaInvalidatedEvent
from plone.supermodel.parser import ISchemaPolicy
from plone.supermodel.utils import syncSchema
from plone.synchronize import synchronized
from Products.CMFCore.interfaces import ISiteRoot
from threading import RLock
from zope.component import adapter
from zope.component import getAllUtilitiesRegisteredFor
from zope.component import getUtility
from zope.component import queryUtility
from zope.dottedname.resolve import resolve
from zope.globalrequest import getRequest
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.interface.interface import InterfaceClass
import functools
import logging
import six
import types
import warnings
log = logging.getLogger(__name__)
# Dynamic modules
generated = dynamic.create('plone.dexterity.schema.generated')
transient = types.ModuleType('transient')
_MARKER = dict()
FTI_CACHE_KEY = '__plone_dexterity_fti_cache__'
def invalidate_cache(fti):
fti._p_activate()
fti.__dict__.pop('_v_schema_get', None)
fti.__dict__.pop('_v_schema_behavior_registrations', None)
fti.__dict__.pop('_v_schema_subtypes', None)
fti.__dict__.pop('_v_schema_schema_interfaces', None)
fti.__dict__.pop('_v_schema_modified', None)
fti.__dict__.pop('_v_schema_behavior_schema_interfaces', None)
request = getRequest()
if request:
setattr(request, FTI_CACHE_KEY, None)
def lookup_fti(portal_type, cache=True):
# if its a string lookup fti
if isinstance(portal_type, six.string_types):
# looking up a utility is expensive, using the global request as
# cache is twice as fast
if cache:
request = getRequest()
if request:
fti_cache = getattr(request, FTI_CACHE_KEY, None)
if fti_cache is None:
fti_cache = dict()
setattr(request, FTI_CACHE_KEY, fti_cache)
if portal_type in fti_cache:
fti = fti_cache[portal_type]
else:
fti_cache[portal_type] = fti = queryUtility(
IDexterityFTI,
name=portal_type
)
return fti
return queryUtility(IDexterityFTI, name=portal_type)
if IDexterityFTI.providedBy(portal_type):
# its already an IDexterityFTI instance
return portal_type
raise ValueError(
'portal_type has to either string or IDexterityFTI instance but is '
'{0!r}'.format(portal_type)
)
def volatile(func):
@functools.wraps(func)
def decorator(self, portal_type):
"""lookup fti from portal_type and cache
input can be either a portal_type as string or as the utility instance.
return value is always a FTI-ultiliy or None
"""
# this function is called very often!
# shortcut None input
if portal_type is None:
return func(self, None)
fti = lookup_fti(portal_type, cache=self.cache_enabled)
if fti is None:
return func(self, None)
if self.cache_enabled:
key = '_v_schema_%s' % func.__name__
cache = getattr(fti, key, _MARKER)
if cache is not _MARKER:
mtime, value = cache
if fti._p_mtime == mtime:
return value
value = func(self, fti)
if self.cache_enabled:
setattr(fti, key, (fti._p_mtime, value))
return value
return decorator
class SchemaCache(object):
"""Simple schema cache for FTI based schema information.
This cache will store a Python object reference to the schema, as returned
by fti.lookupSchema(), for any number of portal types. The value will
be cached until the server is restarted or the cache is invalidated or
cleared.
You should only use this if you require bare-metal speed. For almost all
operations, it's safer and easier to do:
>>> fti = getUtility(IDexterityFTI, name=portal_type)
>>> schema = fti.lookupSchema()
The lookupSchema() call is probably as fast as this cache. However, if
you need to avoid the utility lookup, you can use the cache like so:
>>> from plone.dexterity.schema import SCHEMA_CACHE
>>> my_schema = SCHEMA_CACHE.get(portal_type)
The cache uses the FTI's modification time as its invariant.
"""
lock = RLock()
def __init__(self, cache_enabled=True):
self.cache_enabled = cache_enabled
self.invalidations = 0
@volatile
def get(self, fti):
"""main schema
magic! fti is passed in as a string (identifier of fti), then volatile
decorator looks it up and passes the FTI instance in.
"""
if fti is not None:
try:
return fti.lookupSchema()
except (AttributeError, ValueError):
pass
@volatile
def behavior_registrations(self, fti):
"""all behavior behavior registrations of a given fti passed in as
portal_type string (magic see get)
returns a tuple with instances of
``plone.behavior.registration.BehaviorRegistration`` instances
for the given fti.
"""
if fti is None:
return tuple()
registrations = []
for behavior_name in filter(None, fti.behaviors):
registration = queryUtility(IBehavior, name=behavior_name)
if registration is None:
# BBB - this case should be deprecated in v 3.0
warnings.warn(
'No behavior registration found for behavior named "{0}"'
' for factory "{1}"'
' - trying deprecated fallback lookup (will be removed '
'in 3.0)..."'.format(
behavior_name, fti.getId()
),
DeprecationWarning,
)
try:
schema_interface = resolve(behavior_name)
except (ValueError, ImportError):
log.error(
"Error resolving behavior {0} for factory {1}".format(
behavior_name, fti.getId()
)
)
continue
registration = BehaviorRegistration(
title=behavior_name,
description="bbb fallback lookup",
interface=schema_interface,
marker=None,
factory=None
)
registrations.append(registration)
return tuple(registrations)
@volatile
def subtypes(self, fti):
"""all registered marker interfaces of ftis behaviors
XXX: this one does not make much sense and should be deprecated
"""
if fti is None:
return ()
subtypes = []
for behavior_registration in self.behavior_registrations(fti):
if behavior_registration is not None \
and behavior_registration.marker is not None:
subtypes.append(behavior_registration.marker)
return tuple(subtypes)
@volatile
def behavior_schema_interfaces(self, fti):
"""behavior schema interfaces registered for the fti
all schemas from behaviors
"""
if fti is None:
return ()
schemas = []
for behavior_registration in self.behavior_registrations(fti):
if behavior_registration is not None \
and behavior_registration.interface:
schemas.append(behavior_registration.interface)
return tuple(schemas)
@volatile
def schema_interfaces(self, fti):
"""all schema interfaces registered for the fti
main_schema plus schemas from behaviors
"""
if fti is None:
return ()
schemas = []
try:
main_schema = self.get(fti) # main schema
schemas.append(main_schema)
except (ValueError, AttributeError):
pass
for schema in self.behavior_schema_interfaces(fti):
schemas.append(schema)
return tuple(schemas)
@synchronized(lock)
def clear(self):
for fti in getAllUtilitiesRegisteredFor(IDexterityFTI):
self.invalidate(fti)
request = getRequest()
fti_cache = getattr(request, FTI_CACHE_KEY, None)
if fti_cache is not None:
delattr(request, FTI_CACHE_KEY)
@synchronized(lock)
def invalidate(self, fti):
if fti is not None and not IDexterityFTI.providedBy(fti):
# fti is a name, lookup
fti = queryUtility(IDexterityFTI, name=fti)
if fti is not None:
invalidate_cache(fti)
self.invalidations += 1
@volatile
def modified(self, fti):
if fti:
return fti._p_mtime
SCHEMA_CACHE = SchemaCache()
@implementer(ISchemaInvalidatedEvent)
class SchemaInvalidatedEvent(object):
def __init__(self, portal_type):
self.portal_type = portal_type
@adapter(ISchemaInvalidatedEvent)
def invalidate_schema(event):
if event.portal_type:
SCHEMA_CACHE.invalidate(event.portal_type)
else:
SCHEMA_CACHE.clear()
# here starts the code dealing wih dynamic schemas.
class SchemaNameEncoder(object):
"""Schema name encoding
"""
key = (
(' ', '_1_'),
('.', '_2_'),
('-', '_3_'),
('/', '_4_'),
('|', '_5_'),
)
def encode(self, s):
for k, v in self.key:
s = s.replace(k, v)
return s
def decode(self, s):
for k, v in self.key:
s = s.replace(v, k)
return s
def join(self, *args):
return '_0_'.join([self.encode(a) for a in args if a])
def split(self, s):
return [self.decode(a) for a in s.split('_0_')]
def portalTypeToSchemaName(portal_type, schema=u"", prefix=None, suffix=None):
"""Return a canonical interface name for a generated schema interface.
"""
if prefix is None:
prefix = '/'.join(getUtility(ISiteRoot).getPhysicalPath())[1:]
if suffix:
prefix = '|'.join([prefix, suffix])
encoder = SchemaNameEncoder()
return encoder.join(prefix, portal_type, schema)
def schemaNameToPortalType(schemaName):
"""Return a the portal_type part of a schema name
"""
encoder = SchemaNameEncoder()
return encoder.split(schemaName)[1]
def splitSchemaName(schemaName):
"""Return a tuple prefix, portal_type, schemaName
"""
encoder = SchemaNameEncoder()
items = encoder.split(schemaName)
if len(items) == 2:
return items[0], items[1], u""
elif len(items) == 3:
return items[0], items[1], items[2]
else:
raise ValueError("Schema name %s is invalid" % schemaName)
# Dynamic module factory
@implementer(IDynamicObjectFactory)
class SchemaModuleFactory(object):
"""Create dynamic schema interfaces on the fly
"""
lock = RLock()
_transient_SCHEMA_CACHE = {}
@synchronized(lock)
def __call__(self, name, module):
"""Someone tried to load a dynamic interface that has not yet been
created yet. We will attempt to load it from the FTI if we can. If
the FTI doesn't exist, create a temporary marker interface that we
can fill later.
The goal here is to ensure that we create exactly one interface
instance for each name. If we can't find an FTI, we'll cache the
interface so that we don't get a new one with a different id later.
This cache is global, so we synchronise the method with a thread
lock.
Once we have a properly populated interface, we set it onto the
module using setattr(). This means that the factory will not be
invoked again.
"""
try:
prefix, portal_type, schemaName = splitSchemaName(name)
except ValueError:
return None
if name in self._transient_SCHEMA_CACHE:
schema = self._transient_SCHEMA_CACHE[name]
else:
bases = ()
is_default_schema = not schemaName
if is_default_schema:
bases += (IDexteritySchema,)
schema = InterfaceClass(name, bases, __module__=module.__name__)
if is_default_schema:
alsoProvides(schema, IContentType)
if portal_type is not None:
fti = queryUtility(IDexterityFTI, name=portal_type)
else:
fti = None
if fti is None and name not in self._transient_SCHEMA_CACHE:
self._transient_SCHEMA_CACHE[name] = schema
elif fti is not None:
model = fti.lookupModel()
syncSchema(model.schemata[schemaName], schema, sync_bases=True)
# Save this schema in the module - this factory will not be
# called again for this name
if name in self._transient_SCHEMA_CACHE:
del self._transient_SCHEMA_CACHE[name]
log.debug("Dynamic schema generated: %s", name)
setattr(module, name, schema)
return schema
@implementer(ISchemaPolicy)
class DexteritySchemaPolicy(object):
"""Determines how and where imported dynamic interfaces are created.
Note that these schemata are never used directly. Rather, they are merged
into a schema with a proper name and module, either dynamically or
in code.
"""
def module(self, schemaName, tree):
return 'plone.dexterity.schema.transient'
def bases(self, schemaName, tree):
return ()
def name(self, schemaName, tree):
# We use a temporary name whilst the interface is being generated;
# when it's first used, we know the portal_type and site, and can
# thus update it
return '__tmp__' + schemaName