df40a6fb97e3fb434b78d26d8dce87448eadf29e
[tiramisu.git] / tiramisu / setting.py
1 # -*- coding: utf-8 -*-
2 "sets the options of the configuration objects Config object itself"
3 # Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 # ____________________________________________________________
18 from time import time
19 from copy import copy
20 from logging import getLogger
21 import weakref
22 from .error import (RequirementError, PropertiesOptionError,
23                     ConstError, ConfigError, display_list)
24 from .i18n import _
25
26
27 "Default encoding for display a Config if raise UnicodeEncodeError"
28 default_encoding = 'utf-8'
29
30 """If cache and expire is enable, time before cache is expired.
31 This delay start first time value/setting is set in cache, even if
32 user access several time to value/setting
33 """
34 expires_time = 5
35 """List of default properties (you can add new one if needed).
36
37 For common properties and personalise properties, if a propery is set for
38 an Option and for the Config together, Setting raise a PropertiesOptionError
39
40 * Common properties:
41
42 hidden
43     option with this property can only get value in read only mode. This
44     option is not available in read write mode.
45
46 disabled
47     option with this property cannot be set/get
48
49 frozen
50     cannot set value for option with this properties if 'frozen' is set in
51     config
52
53 mandatory
54     should set value for option with this properties if 'mandatory' is set in
55     config
56
57
58 * Special property:
59
60 permissive
61     option with 'permissive' cannot raise PropertiesOptionError for properties
62     set in permissive
63     config with 'permissive', whole option in this config cannot raise
64     PropertiesOptionError for properties set in permissive
65
66 * Special Config properties:
67
68 cache
69     if set, enable cache settings and values
70
71 expire
72     if set, settings and values in cache expire after ``expires_time``
73
74 everything_frozen
75     whole option in config are frozen (even if option have not frozen
76     property)
77
78 empty
79     raise mandatory PropertiesOptionError if multi or master have empty value
80
81 validator
82     launch validator set by user in option (this property has no effect
83     for internal validator)
84
85 warnings
86     display warnings during validation
87 """
88 default_properties = ('cache', 'expire', 'validator', 'warnings')
89
90 """Config can be in two defaut mode:
91
92 read_only
93     you can get all variables not disabled but you cannot set any variables
94     if a value has a callback without any value, callback is launch and value
95     of this variable can change
96     you cannot access to mandatory variable without values
97
98 read_write
99     you can get all variables not disabled and not hidden
100     you can set all variables not frozen
101 """
102 ro_append = set(['frozen', 'disabled', 'validator', 'everything_frozen',
103                 'mandatory', 'empty'])
104 ro_remove = set(['permissive', 'hidden'])
105 rw_append = set(['frozen', 'disabled', 'validator', 'hidden'])
106 rw_remove = set(['permissive', 'everything_frozen', 'mandatory', 'empty'])
107
108
109 forbidden_set_properties = set(['force_store_value'])
110
111
112 log = getLogger('tiramisu')
113 #FIXME
114 #import logging
115 #logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
116 debug = False
117 static_set = frozenset()
118
119
120 # ____________________________________________________________
121 class _NameSpace(object):
122     """convenient class that emulates a module
123     and builds constants (that is, unique names)
124     when attribute is added, we cannot delete it
125     """
126
127     def __setattr__(self, name, value):
128         if name in self.__dict__:  # pragma: optional cover
129             raise ConstError(_("can't rebind {0}").format(name))
130         self.__dict__[name] = value
131
132     def __delattr__(self, name):  # pragma: optional cover
133         if name in self.__dict__:
134             raise ConstError(_("can't unbind {0}").format(name))
135         raise ValueError(name)
136
137
138 class GroupModule(_NameSpace):
139     "emulates a module to manage unique group (OptionDescription) names"
140     class GroupType(str):
141         """allowed normal group (OptionDescription) names
142         *normal* means : groups that are not master
143         """
144         pass
145
146     class DefaultGroupType(GroupType):
147         """groups that are default (typically 'default')"""
148         pass
149
150     class MasterGroupType(GroupType):
151         """allowed normal group (OptionDescription) names
152         *master* means : groups that have the 'master' attribute set
153         """
154         pass
155
156
157 class OwnerModule(_NameSpace):
158     """emulates a module to manage unique owner names.
159
160     owners are living in `Config._cfgimpl_value_owners`
161     """
162     class Owner(str):
163         """allowed owner names
164         """
165         pass
166
167     class DefaultOwner(Owner):
168         """groups that are default (typically 'default')"""
169         pass
170
171
172 class MultiTypeModule(_NameSpace):
173     "namespace for the master/slaves"
174     class MultiType(str):
175         pass
176
177     class DefaultMultiType(MultiType):
178         pass
179
180     class MasterMultiType(MultiType):
181         pass
182
183     class SlaveMultiType(MultiType):
184         pass
185
186
187 # ____________________________________________________________
188 def populate_groups():
189     """populates the available groups in the appropriate namespaces
190
191     groups.default
192         default group set when creating a new optiondescription
193
194     groups.master
195         master group is a special optiondescription, all suboptions should be
196         multi option and all values should have same length, to find master's
197         option, the optiondescription's name should be same than de master's
198         option
199
200     groups.family
201         example of group, no special behavior with this group's type
202     """
203     groups.default = groups.DefaultGroupType('default')
204     groups.master = groups.MasterGroupType('master')
205     groups.family = groups.GroupType('family')
206
207
208 def populate_owners():
209     """populates the available owners in the appropriate namespaces
210
211     default
212         is the config owner after init time
213
214     user
215         is the generic is the generic owner
216     """
217     setattr(owners, 'default', owners.DefaultOwner('default'))
218     setattr(owners, 'user', owners.Owner('user'))
219     setattr(owners, 'forced', owners.Owner('forced'))
220
221     def addowner(name):
222         """
223         :param name: the name of the new owner
224         """
225         setattr(owners, name, owners.Owner(name))
226     setattr(owners, 'addowner', addowner)
227
228 # ____________________________________________________________
229 # populate groups and owners with default attributes
230 groups = GroupModule()
231 populate_groups()
232 owners = OwnerModule()
233 populate_owners()
234
235
236 # ____________________________________________________________
237 class Undefined(object):
238     pass
239
240
241 undefined = Undefined()
242
243
244 # ____________________________________________________________
245 class Property(object):
246     "a property is responsible of the option's value access rules"
247     __slots__ = ('_setting', '_properties', '_opt', '_path')
248
249     def __init__(self, setting, prop, opt=None, path=None):
250         self._opt = opt
251         self._path = path
252         self._setting = setting
253         self._properties = prop
254
255     def append(self, propname):
256         """Appends a property named propname
257
258         :param propname: a predefined or user defined property name
259         :type propname: string
260         """
261         self._append(propname)
262
263     def _append(self, propname, save=True):
264         if self._opt is not None and self._opt.impl_getrequires() is not None \
265                 and propname in getattr(self._opt, '_calc_properties', static_set):  # pragma: optional cover
266             raise ValueError(_('cannot append {0} property for option {1}: '
267                                'this property is calculated').format(
268                                    propname, self._opt.impl_getname()))
269         if propname in forbidden_set_properties:
270             raise ConfigError(_('cannot add those properties: {0}').format(propname))
271         self._properties.add(propname)
272         if save:
273             self._setting._setproperties(self._properties, self._opt, self._path, force=True)
274
275     def remove(self, propname):
276         """Removes a property named propname
277
278         :param propname: a predefined or user defined property name
279         :type propname: string
280         """
281         if propname in self._properties:
282             self._properties.remove(propname)
283             self._setting._setproperties(self._properties, self._opt, self._path)
284
285     def extend(self, propnames):
286         """Extends properties to the existing properties
287
288         :param propnames: an iterable made of property names
289         :type propnames: iterable of string
290         """
291         for propname in propnames:
292             self._append(propname, save=False)
293         self._setting._setproperties(self._properties, self._opt, self._path)
294
295     def reset(self):
296         """resets the properties (does not **clear** the properties,
297         default properties are still present)
298         """
299         self._setting.reset(_path=self._path)
300
301     def __contains__(self, propname):
302         return propname in self._properties
303
304     def __repr__(self):
305         return str(list(self._properties))
306
307     def get(self):
308         return tuple(self._properties)
309
310
311 #____________________________________________________________
312 class Settings(object):
313     "``config.Config()``'s configuration options settings"
314     __slots__ = ('context', '_owner', '_p_', '_pp_', '__weakref__')
315
316     def __init__(self, context, properties, permissives):
317         """
318         initializer
319
320         :param context: the root config
321         :param storage: the storage type
322
323                         - dictionary -> in memory
324                         - sqlite3 -> persistent
325         """
326         # generic owner
327         self._owner = owners.user
328         self.context = weakref.ref(context)
329         self._p_ = properties
330         self._pp_ = permissives
331
332     def _getcontext(self):
333         """context could be None, we need to test it
334         context is None only if all reference to `Config` object is deleted
335         (for example we delete a `Config` and we manipulate a reference to
336         old `SubConfig`, `Values`, `Multi` or `Settings`)
337         """
338         context = self.context()
339         if context is None:  # pragma: optional cover
340             raise ConfigError(_('the context does not exist anymore'))
341         return context
342
343     #____________________________________________________________
344     # properties methods
345     def __contains__(self, propname):
346         "enables the pythonic 'in' syntaxic sugar"
347         return propname in self._getproperties(read_write=False)
348
349     def __repr__(self):
350         return str(list(self._getproperties(read_write=False)))
351
352     def __getitem__(self, opt):
353         path = opt.impl_getpath(self._getcontext())
354         return self.getproperties(opt, path)
355
356     def getproperties(self, opt, path, setting_properties=undefined):
357         return Property(self,
358                         self._getproperties(opt, path,
359                                             setting_properties=setting_properties),
360                         opt, path)
361
362     def __setitem__(self, opt, value):  # pragma: optional cover
363         raise ValueError(_('you should only append/remove properties'))
364
365     def reset(self, opt=None, _path=None, all_properties=False):
366         if all_properties and (_path or opt):  # pragma: optional cover
367             raise ValueError(_('opt and all_properties must not be set '
368                                'together in reset'))
369         if all_properties:
370             self._p_.reset_all_properties()
371         else:
372             if opt is not None and _path is None:
373                 _path = opt.impl_getpath(self._getcontext())
374             self._p_.delproperties(_path)
375         self._getcontext().cfgimpl_reset_cache(opt=opt, path=_path, only=('settings',))
376
377     def _getproperties(self, opt=None, path=None,
378                        setting_properties=undefined, read_write=True,
379                        apply_requires=True, index=None):
380         """
381         """
382         if opt is None:
383             ntime = int(time())
384             if self._p_.hascache(path, index):
385                 is_cached, props = self._p_.getcache(path, ntime, index)
386             else:
387                 is_cached = False
388             if not is_cached or 'cache' not in props:
389                 meta = self._getcontext().cfgimpl_get_meta()
390                 if meta is None:
391                     props = self._p_.getproperties(path, default_properties)
392                 else:
393                     props = meta.cfgimpl_get_settings()._getproperties()
394             if 'cache' in props:
395                 if 'expire' in props:
396                     ntime = ntime + expires_time
397                 else:
398                     ntime = None
399                 self._p_.setcache(path, props, ntime, index)
400         else:
401             if path is None:  # pragma: optional cover
402                 raise ValueError(_('if opt is not None, path should not be'
403                                    ' None in _getproperties'))
404             if setting_properties is undefined:
405                 setting_properties = self._getproperties(read_write=False)
406             is_cached = False
407
408             if apply_requires:
409                 if 'cache' in setting_properties and 'expire' in setting_properties:
410                     ntime = int(time())
411                 else:
412                     ntime = None
413                 if 'cache' in setting_properties and self._p_.hascache(path, index):
414                     is_cached, props = self._p_.getcache(path, ntime, index)
415             if not is_cached:
416                 props = self._p_.getproperties(path, opt.impl_getproperties())
417                 if opt.impl_is_multi() and not opt.impl_is_master_slaves('slave'):
418                     props.add('empty')
419                 if apply_requires:
420                     requires = self.apply_requires(opt, path, setting_properties, index, False)
421                     if requires != set([]):
422                         props = copy(props)
423                         props |= requires
424                     if 'cache' in setting_properties:
425                         if 'expire' in setting_properties:
426                             ntime = ntime + expires_time
427                         self._p_.setcache(path, props, ntime, index)
428         if read_write:
429             props = copy(props)
430         return props
431
432     def append(self, propname):
433         "puts property propname in the Config's properties attribute"
434         props = self._p_.getproperties(None, default_properties)
435         if propname not in props:
436             props.add(propname)
437             self._setproperties(props, None, None)
438
439     def remove(self, propname):
440         "deletes property propname in the Config's properties attribute"
441         props = self._p_.getproperties(None, default_properties)
442         if propname in props:
443             props.remove(propname)
444             self._setproperties(props, None, None)
445
446     def extend(self, propnames):
447         for propname in propnames:
448             self.append(propname)
449
450     def _setproperties(self, properties, opt, path, force=False):
451         """save properties for specified path
452         (never save properties if same has option properties)
453         """
454         if self._getcontext().cfgimpl_get_meta() is not None:
455             raise ConfigError(_('cannot change global property with metaconfig'))
456         if not force:
457             forbidden_properties = forbidden_set_properties & properties
458             if forbidden_properties:
459                 raise ConfigError(_('cannot add those properties: {0}').format(
460                     ' '.join(forbidden_properties)))
461         self._p_.setproperties(path, properties)
462         self._getcontext().cfgimpl_reset_cache(opt=opt, path=path)
463
464     def getpermissive(self, setting_properties, path=None):
465         if 'cache' in setting_properties and 'expire' in setting_properties:
466             ntime = int(time())
467         else:
468             ntime = None
469         if 'cache' in setting_properties and self._pp_.hascache(path, None):
470             is_cached, perm = self._pp_.getcache(path, ntime, None)
471         else:
472             is_cached = False
473         if not is_cached:
474             if path is not None:
475                 perm = self._pp_.getpermissive(path)
476             else:
477                 perm = self._pp_.getpermissive()
478             if 'cache' in setting_properties:
479                 if 'expire' in setting_properties:
480                     ntime = ntime + expires_time
481                 self._pp_.setcache(path, perm, ntime, None)
482         return perm
483
484     #____________________________________________________________
485     def validate_properties(self, opt_or_descr, is_descr, check_frozen, path,
486                             value=None, force_permissive=False,
487                             setting_properties=undefined,
488                             self_properties=undefined,
489                             index=None, debug=False):
490         """
491         validation upon the properties related to `opt_or_descr`
492
493         :param opt_or_descr: an option or an option description object
494         :param force_permissive: behaves as if the permissive property
495                                  was present
496         :param is_descr: we have to know if we are in an option description,
497                          just because the mandatory property
498                          doesn't exist here
499
500         :param check_frozen: in the validation process, an option is to be modified,
501                          the behavior can be different
502                          (typically with the `frozen` property)
503         """
504         # opt properties
505         if setting_properties is undefined:
506             setting_properties = self._getproperties(read_write=False)
507         if self_properties is not undefined:
508             properties = copy(self_properties)
509         else:
510             properties = self._getproperties(opt_or_descr, path,
511                                              setting_properties=setting_properties,
512                                              index=index)
513         # calc properties
514         properties &= setting_properties
515         if not is_descr:
516             #mandatory
517             if 'mandatory' in properties and \
518                     not self._getcontext().cfgimpl_get_values()._isempty(
519                         opt_or_descr, value, index=index):
520                 properties.remove('mandatory')
521             elif 'empty' in properties and \
522                     'empty' in setting_properties and \
523                     self._getcontext().cfgimpl_get_values()._isempty(
524                         opt_or_descr, value, force_allow_empty_list=True, index=index):
525                 properties.add('mandatory')
526             # should return 'frozen' only when tried to modify a value
527             if check_frozen and 'everything_frozen' in setting_properties:
528                 properties.add('frozen')
529             elif 'frozen' in properties and not check_frozen:
530                 properties.remove('frozen')
531             if 'empty' in properties:
532                 properties.remove('empty')
533
534         # remove permissive properties
535         if properties != frozenset():
536             # remove opt permissive
537             # permissive affect option's permission with or without permissive
538             # global property
539             properties -= self.getpermissive(setting_properties, path)
540             # remove global permissive if need
541             if force_permissive is True or 'permissive' in setting_properties:
542                 properties -= self.getpermissive(setting_properties)
543
544         # at this point an option should not remain in properties
545         if properties != frozenset():
546             props = list(properties)
547             datas = {'opt': opt_or_descr, 'path': path, 'setting_properties': setting_properties,
548                      'index': index, 'debug': True}
549             if is_descr:
550                 opt_type = 'optiondescription'
551             else:
552                 opt_type = 'option'
553             if 'frozen' in properties:
554                 return PropertiesOptionError(_('cannot change the value for '
555                                                'option "{0}" this option is'
556                                                ' frozen').format(
557                                                    opt_or_descr.impl_getname()),
558                                              props, self, datas, opt_type)
559             else:
560                 if len(props) == 1:
561                     prop_msg = _('property')
562                 else:
563                     prop_msg = _('properties')
564                 return PropertiesOptionError(_('cannot access to {0} "{1}" '
565                                                'because has {2} {3}'
566                                                '').format(opt_type,
567                                                           opt_or_descr.impl_get_display_name(),
568                                                           prop_msg,
569                                                           display_list(props)),
570                                                props,
571                                                self, datas, opt_type)
572
573     def setpermissive(self, permissive, opt=None, path=None):
574         """
575         enables us to put the permissives in the storage
576
577         :param path: the option's path
578         :param type: str
579         :param opt: if an option object is set, the path is extracted.
580                     it is better (faster) to set the path parameter
581                     instead of passing a :class:`tiramisu.option.Option()` object.
582         """
583         if opt is not None and path is None:
584             path = opt.impl_getpath(self._getcontext())
585         if not isinstance(permissive, tuple):  # pragma: optional cover
586             raise TypeError(_('permissive must be a tuple'))
587         self._pp_.setpermissive(path, permissive)
588         setting_properties = self._getproperties(read_write=False)
589         self._getcontext().cfgimpl_reset_cache(opt=opt, path=path, only=('values',))
590         if 'cache' in setting_properties:
591             if 'expire' in setting_properties:
592                 ntime = int(time()) + expires_time
593             else:
594                 ntime = None
595             self._pp_.setcache(path, set(permissive), ntime, None)
596
597     #____________________________________________________________
598     def setowner(self, owner):
599         ":param owner: sets the default value for owner at the Config level"
600         if not isinstance(owner, owners.Owner):  # pragma: optional cover
601             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
602         self._owner = owner
603         #FIXME qu'est ce qui se passe si pas de owner ??
604
605     def getowner(self):
606         return self._owner
607
608     #____________________________________________________________
609     def _read(self, remove, append):
610         props = self._p_.getproperties(None, default_properties)
611         modified = False
612         if remove & props != set([]):
613             props = props - remove
614             modified = True
615         if append & props != append:
616             props = props | append
617             modified = True
618         if modified:
619             self._setproperties(props, None, None)
620
621     def read_only(self):
622         "convenience method to freeze, hide and disable"
623         self._read(ro_remove, ro_append)
624
625     def read_write(self):
626         "convenience method to freeze, hide and disable"
627         self._read(rw_remove, rw_append)
628
629     def apply_requires(self, opt, path, setting_properties, index, debug):
630         """carries out the jit (just in time) requirements between options
631
632         a requirement is a tuple of this form that comes from the option's
633         requirements validation::
634
635             (option, expected, action, inverse, transitive, same_action)
636
637         let's have a look at all the tuple's items:
638
639         - **option** is the target option's
640
641         - **expected** is the target option's value that is going to trigger
642           an action
643
644         - **action** is the (property) action to be accomplished if the target
645           option happens to have the expected value
646
647         - if **inverse** is `True` and if the target option's value does not
648           apply, then the property action must be removed from the option's
649           properties list (wich means that the property is inverted)
650
651         - **transitive**: but what happens if the target option cannot be
652           accessed ? We don't kown the target option's value. Actually if some
653           property in the target option is not present in the permissive, the
654           target option's value cannot be accessed. In this case, the
655           **action** have to be applied to the option. (the **action** property
656           is then added to the option).
657
658         - **same_action**: actually, if **same_action** is `True`, the
659           transitivity is not accomplished. The transitivity is accomplished
660           only if the target option **has the same property** that the demanded
661           action. If the target option's value is not accessible because of
662           another reason, because of a property of another type, then an
663           exception :exc:`~error.RequirementError` is raised.
664
665         And at last, if no target option matches the expected values, the
666         action will not add to the option's properties list.
667
668         :param opt: the option on wich the requirement occurs
669         :type opt: `option.Option()`
670         :param path: the option's path in the config
671         :type path: str
672         """
673         current_requires = opt.impl_getrequires()
674
675         # filters the callbacks
676         if debug:
677             calc_properties = {}
678         else:
679             calc_properties = set()
680
681         if not current_requires:
682             return calc_properties
683
684         context = self._getcontext()
685         all_properties = None
686         for requires in current_requires:
687             for require in requires:
688                 exps, action, inverse, \
689                     transitive, same_action, operator = require
690                 breaked = False
691                 for exp in exps:
692                     option, expected = exp
693                     reqpath = option.impl_getpath(context)
694                     if reqpath == path or reqpath.startswith(path + '.'):  # pragma: optional cover
695                         raise RequirementError(_("malformed requirements "
696                                                  "imbrication detected for option:"
697                                                  " '{0}' with requirement on: "
698                                                  "'{1}'").format(path, reqpath))
699                     if option.impl_is_multi():
700                         if index is None:
701                             # multi is allowed only for slaves
702                             # so do not calculated requires if no index
703                             continue
704                         idx = index
705                     else:
706                         idx = None
707                     value = context.getattr(reqpath, force_permissive=True,
708                                             _setting_properties=setting_properties,
709                                             index=idx, returns_raise=True)
710                     if isinstance(value, Exception):
711                         if isinstance(value, PropertiesOptionError):
712                             if not transitive:
713                                 if all_properties is None:
714                                     all_properties = []
715                                     for requires in opt.impl_getrequires():
716                                         for require in requires:
717                                             all_properties.append(require[1])
718                                 if not set(value.proptype) - set(all_properties):
719                                     continue
720                             properties = value.proptype
721                             if same_action and action not in properties:  # pragma: optional cover
722                                 if len(properties) == 1:
723                                     prop_msg = _('property')
724                                 else:
725                                     prop_msg = _('properties')
726                                 raise RequirementError(_('cannot access to option "{0}" because '
727                                                          'required option "{1}" has {2} {3}'
728                                                          '').format(opt.impl_get_display_name(),
729                                                                     option.impl_get_display_name(),
730                                                                     prop_msg,
731                                                                     display_list(properties)))
732                             orig_value = value
733                             # transitive action, force expected
734                             value = expected[0]
735                             inverse = False
736                         else:  # pragma: no cover
737                             raise value
738                     else:
739                         orig_value = value
740                     if (not inverse and value in expected or
741                             inverse and value not in expected):
742                         if operator != 'and':
743                             if debug:
744                                 if isinstance(orig_value, PropertiesOptionError):
745                                     for msg in orig_value._settings.apply_requires(**orig_value._datas).values():
746                                         calc_properties.setdefault(action, []).extend(msg)
747                                 else:
748                                     if not inverse:
749                                         msg = _('the value of "{0}" is "{1}"')
750                                     else:
751                                         msg = _('the value of "{0}" is not "{1}"')
752                                     calc_properties.setdefault(action, []).append(
753                                         msg.format(option.impl_get_display_name(),
754                                                    display_list(expected, 'or')))
755                             else:
756                                 calc_properties.add(action)
757                                 breaked = True
758                                 break
759                     elif operator == 'and':
760                         break
761                 else:
762                     if operator == 'and':
763                         calc_properties.add(action)
764                     continue  # pragma: no cover
765                 if breaked:
766                     break
767         return calc_properties
768
769     def get_modified_properties(self):
770         return self._p_.get_modified_properties()
771
772     def get_modified_permissives(self):
773         return self._pp_.get_modified_permissives()