separate baseoption and option
[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 not opt.impl_is_optiondescription() and opt.impl_is_multi() and \
418                         not opt.impl_is_master_slaves('slave'):
419                     props.add('empty')
420                 if apply_requires:
421                     requires = self.apply_requires(opt, path, setting_properties, index, False)
422                     if requires != set([]):
423                         props = copy(props)
424                         props |= requires
425                     if 'cache' in setting_properties:
426                         if 'expire' in setting_properties:
427                             ntime = ntime + expires_time
428                         self._p_.setcache(path, props, ntime, index)
429         if read_write:
430             props = copy(props)
431         return props
432
433     def append(self, propname):
434         "puts property propname in the Config's properties attribute"
435         props = self._p_.getproperties(None, default_properties)
436         if propname not in props:
437             props.add(propname)
438             self._setproperties(props, None, None)
439
440     def remove(self, propname):
441         "deletes property propname in the Config's properties attribute"
442         props = self._p_.getproperties(None, default_properties)
443         if propname in props:
444             props.remove(propname)
445             self._setproperties(props, None, None)
446
447     def extend(self, propnames):
448         for propname in propnames:
449             self.append(propname)
450
451     def _setproperties(self, properties, opt, path, force=False):
452         """save properties for specified path
453         (never save properties if same has option properties)
454         """
455         if self._getcontext().cfgimpl_get_meta() is not None:
456             raise ConfigError(_('cannot change global property with metaconfig'))
457         if not force:
458             forbidden_properties = forbidden_set_properties & properties
459             if forbidden_properties:
460                 raise ConfigError(_('cannot add those properties: {0}').format(
461                     ' '.join(forbidden_properties)))
462         self._p_.setproperties(path, properties)
463         self._getcontext().cfgimpl_reset_cache(opt=opt, path=path)
464
465     def getpermissive(self, setting_properties, path=None):
466         if 'cache' in setting_properties and 'expire' in setting_properties:
467             ntime = int(time())
468         else:
469             ntime = None
470         if 'cache' in setting_properties and self._pp_.hascache(path, None):
471             is_cached, perm = self._pp_.getcache(path, ntime, None)
472         else:
473             is_cached = False
474         if not is_cached:
475             if path is not None:
476                 perm = self._pp_.getpermissive(path)
477             else:
478                 perm = self._pp_.getpermissive()
479             if 'cache' in setting_properties:
480                 if 'expire' in setting_properties:
481                     ntime = ntime + expires_time
482                 self._pp_.setcache(path, perm, ntime, None)
483         return perm
484
485     #____________________________________________________________
486     def validate_properties(self, opt_or_descr, is_descr, check_frozen, path,
487                             value=None, force_permissive=False,
488                             setting_properties=undefined,
489                             self_properties=undefined,
490                             index=None, debug=False):
491         """
492         validation upon the properties related to `opt_or_descr`
493
494         :param opt_or_descr: an option or an option description object
495         :param force_permissive: behaves as if the permissive property
496                                  was present
497         :param is_descr: we have to know if we are in an option description,
498                          just because the mandatory property
499                          doesn't exist here
500
501         :param check_frozen: in the validation process, an option is to be modified,
502                          the behavior can be different
503                          (typically with the `frozen` property)
504         """
505         # opt properties
506         if setting_properties is undefined:
507             setting_properties = self._getproperties(read_write=False)
508         if self_properties is not undefined:
509             properties = copy(self_properties)
510         else:
511             properties = self._getproperties(opt_or_descr, path,
512                                              setting_properties=setting_properties,
513                                              index=index)
514         # calc properties
515         properties &= setting_properties
516         if not is_descr:
517             #mandatory
518             if 'mandatory' in properties and \
519                     not self._getcontext().cfgimpl_get_values()._isempty(
520                         opt_or_descr, value, index=index):
521                 properties.remove('mandatory')
522             elif 'empty' in properties and \
523                     'empty' in setting_properties and \
524                     self._getcontext().cfgimpl_get_values()._isempty(
525                         opt_or_descr, value, force_allow_empty_list=True, index=index):
526                 properties.add('mandatory')
527             # should return 'frozen' only when tried to modify a value
528             if check_frozen and 'everything_frozen' in setting_properties:
529                 properties.add('frozen')
530             elif 'frozen' in properties and not check_frozen:
531                 properties.remove('frozen')
532             if 'empty' in properties:
533                 properties.remove('empty')
534
535         # remove permissive properties
536         if properties != frozenset():
537             # remove opt permissive
538             # permissive affect option's permission with or without permissive
539             # global property
540             properties -= self.getpermissive(setting_properties, path)
541             # remove global permissive if need
542             if force_permissive is True or 'permissive' in setting_properties:
543                 properties -= self.getpermissive(setting_properties)
544
545         # at this point an option should not remain in properties
546         if properties != frozenset():
547             props = list(properties)
548             datas = {'opt': opt_or_descr, 'path': path, 'setting_properties': setting_properties,
549                      'index': index, 'debug': True}
550             if is_descr:
551                 opt_type = 'optiondescription'
552             else:
553                 opt_type = 'option'
554             if 'frozen' in properties:
555                 return PropertiesOptionError(_('cannot change the value for '
556                                                'option "{0}" this option is'
557                                                ' frozen').format(
558                                                    opt_or_descr.impl_getname()),
559                                              props, self, datas, opt_type)
560             else:
561                 if len(props) == 1:
562                     prop_msg = _('property')
563                 else:
564                     prop_msg = _('properties')
565                 return PropertiesOptionError(_('cannot access to {0} "{1}" '
566                                                'because has {2} {3}'
567                                                '').format(opt_type,
568                                                           opt_or_descr.impl_get_display_name(),
569                                                           prop_msg,
570                                                           display_list(props)),
571                                                props,
572                                                self, datas, opt_type)
573
574     def setpermissive(self, permissive, opt=None, path=None):
575         """
576         enables us to put the permissives in the storage
577
578         :param path: the option's path
579         :param type: str
580         :param opt: if an option object is set, the path is extracted.
581                     it is better (faster) to set the path parameter
582                     instead of passing a :class:`tiramisu.option.Option()` object.
583         """
584         if opt is not None and path is None:
585             path = opt.impl_getpath(self._getcontext())
586         if not isinstance(permissive, tuple):  # pragma: optional cover
587             raise TypeError(_('permissive must be a tuple'))
588         self._pp_.setpermissive(path, permissive)
589         setting_properties = self._getproperties(read_write=False)
590         self._getcontext().cfgimpl_reset_cache(opt=opt, path=path, only=('values',))
591         if 'cache' in setting_properties:
592             if 'expire' in setting_properties:
593                 ntime = int(time()) + expires_time
594             else:
595                 ntime = None
596             self._pp_.setcache(path, set(permissive), ntime, None)
597
598     #____________________________________________________________
599     def setowner(self, owner):
600         ":param owner: sets the default value for owner at the Config level"
601         if not isinstance(owner, owners.Owner):  # pragma: optional cover
602             raise TypeError(_("invalid generic owner {0}").format(str(owner)))
603         self._owner = owner
604         #FIXME qu'est ce qui se passe si pas de owner ??
605
606     def getowner(self):
607         return self._owner
608
609     #____________________________________________________________
610     def _read(self, remove, append):
611         props = self._p_.getproperties(None, default_properties)
612         modified = False
613         if remove & props != set([]):
614             props = props - remove
615             modified = True
616         if append & props != append:
617             props = props | append
618             modified = True
619         if modified:
620             self._setproperties(props, None, None)
621
622     def read_only(self):
623         "convenience method to freeze, hide and disable"
624         self._read(ro_remove, ro_append)
625
626     def read_write(self):
627         "convenience method to freeze, hide and disable"
628         self._read(rw_remove, rw_append)
629
630     def apply_requires(self, opt, path, setting_properties, index, debug):
631         """carries out the jit (just in time) requirements between options
632
633         a requirement is a tuple of this form that comes from the option's
634         requirements validation::
635
636             (option, expected, action, inverse, transitive, same_action)
637
638         let's have a look at all the tuple's items:
639
640         - **option** is the target option's
641
642         - **expected** is the target option's value that is going to trigger
643           an action
644
645         - **action** is the (property) action to be accomplished if the target
646           option happens to have the expected value
647
648         - if **inverse** is `True` and if the target option's value does not
649           apply, then the property action must be removed from the option's
650           properties list (wich means that the property is inverted)
651
652         - **transitive**: but what happens if the target option cannot be
653           accessed ? We don't kown the target option's value. Actually if some
654           property in the target option is not present in the permissive, the
655           target option's value cannot be accessed. In this case, the
656           **action** have to be applied to the option. (the **action** property
657           is then added to the option).
658
659         - **same_action**: actually, if **same_action** is `True`, the
660           transitivity is not accomplished. The transitivity is accomplished
661           only if the target option **has the same property** that the demanded
662           action. If the target option's value is not accessible because of
663           another reason, because of a property of another type, then an
664           exception :exc:`~error.RequirementError` is raised.
665
666         And at last, if no target option matches the expected values, the
667         action will not add to the option's properties list.
668
669         :param opt: the option on wich the requirement occurs
670         :type opt: `option.Option()`
671         :param path: the option's path in the config
672         :type path: str
673         """
674         current_requires = opt.impl_getrequires()
675
676         # filters the callbacks
677         if debug:
678             calc_properties = {}
679         else:
680             calc_properties = set()
681
682         if not current_requires:
683             return calc_properties
684
685         context = self._getcontext()
686         all_properties = None
687         for requires in current_requires:
688             for require in requires:
689                 exps, action, inverse, \
690                     transitive, same_action, operator = require
691                 breaked = False
692                 for exp in exps:
693                     option, expected = exp
694                     reqpath = option.impl_getpath(context)
695                     if reqpath == path or reqpath.startswith(path + '.'):  # pragma: optional cover
696                         raise RequirementError(_("malformed requirements "
697                                                  "imbrication detected for option:"
698                                                  " '{0}' with requirement on: "
699                                                  "'{1}'").format(path, reqpath))
700                     if option.impl_is_multi():
701                         if index is None:
702                             # multi is allowed only for slaves
703                             # so do not calculated requires if no index
704                             continue
705                         idx = index
706                     else:
707                         idx = None
708                     value = context.getattr(reqpath, force_permissive=True,
709                                             _setting_properties=setting_properties,
710                                             index=idx, returns_raise=True)
711                     if isinstance(value, Exception):
712                         if isinstance(value, PropertiesOptionError):
713                             if not transitive:
714                                 if all_properties is None:
715                                     all_properties = []
716                                     for requires in opt.impl_getrequires():
717                                         for require in requires:
718                                             all_properties.append(require[1])
719                                 if not set(value.proptype) - set(all_properties):
720                                     continue
721                             properties = value.proptype
722                             if same_action and action not in properties:  # pragma: optional cover
723                                 if len(properties) == 1:
724                                     prop_msg = _('property')
725                                 else:
726                                     prop_msg = _('properties')
727                                 raise RequirementError(_('cannot access to option "{0}" because '
728                                                          'required option "{1}" has {2} {3}'
729                                                          '').format(opt.impl_get_display_name(),
730                                                                     option.impl_get_display_name(),
731                                                                     prop_msg,
732                                                                     display_list(properties)))
733                             orig_value = value
734                             # transitive action, force expected
735                             value = expected[0]
736                             inverse = False
737                         else:  # pragma: no cover
738                             raise value
739                     else:
740                         orig_value = value
741                     if (not inverse and value in expected or
742                             inverse and value not in expected):
743                         if operator != 'and':
744                             if debug:
745                                 if isinstance(orig_value, PropertiesOptionError):
746                                     for msg in orig_value._settings.apply_requires(**orig_value._datas).values():
747                                         calc_properties.setdefault(action, []).extend(msg)
748                                 else:
749                                     if not inverse:
750                                         msg = _('the value of "{0}" is "{1}"')
751                                     else:
752                                         msg = _('the value of "{0}" is not "{1}"')
753                                     calc_properties.setdefault(action, []).append(
754                                         msg.format(option.impl_get_display_name(),
755                                                    display_list(expected, 'or')))
756                             else:
757                                 calc_properties.add(action)
758                                 breaked = True
759                                 break
760                     elif operator == 'and':
761                         break
762                 else:
763                     if operator == 'and':
764                         calc_properties.add(action)
765                     continue  # pragma: no cover
766                 if breaked:
767                     break
768         return calc_properties
769
770     def get_modified_properties(self):
771         return self._p_.get_modified_properties()
772
773     def get_modified_permissives(self):
774         return self._pp_.get_modified_permissives()