1# 2# Copyright (c), 2016-2021 , SISSA (International School for Advanced Studies). 3# All rights reserved. 4# This file is distributed under the terms of the MIT License. 5# See the file 'LICENSE' in the root directory of the present 6# distribution, or http://opensource.org/licenses/MIT. 7# 8# @author Davide Brunato <brunato@sissa.it> 9# 10from typing import Any, Optional, Tuple, Union 11 12from ..exceptions import XMLSchemaValueError 13from ..aliases import ElementType, ModelParticleType 14 15 16class ParticleMixin: 17 """ 18 Mixin for objects related to XSD Particle Schema Components: 19 20 https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#p 21 https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/structures.html#t 22 23 :ivar min_occurs: the minOccurs property of the XSD particle. Defaults to 1. 24 :ivar max_occurs: the maxOccurs property of the XSD particle. Defaults to 1, \ 25 a `None` value means 'unbounded'. 26 """ 27 name: Any 28 maps: Any 29 30 min_occurs: int = 1 31 max_occurs: Optional[int] = 1 32 33 def __init__(self, min_occurs: int = 1, max_occurs: Optional[int] = 1) -> None: 34 self.min_occurs = min_occurs 35 self.max_occurs = max_occurs 36 37 @property 38 def occurs(self) -> Tuple[int, Optional[int]]: 39 return self.min_occurs, self.max_occurs 40 41 @property 42 def effective_min_occurs(self) -> int: 43 """ 44 A property calculated from minOccurs, that is equal to minOccurs 45 for elements and may vary for content model groups, in dependance 46 of group model and structure. 47 """ 48 return self.min_occurs 49 50 @property 51 def effective_max_occurs(self) -> Optional[int]: 52 """ 53 A property calculated from maxOccurs, that is equal to maxOccurs 54 for elements and may vary for content model groups, in dependance 55 of group model and structure. Used for checking restrictions of 56 xs:choice model groups in XSD 1.1. 57 """ 58 return self.max_occurs 59 60 def is_emptiable(self) -> bool: 61 """ 62 Tests if max_occurs == 0. A zero-length model group is considered emptiable. 63 For model groups the test outcome depends also on nested particles. 64 """ 65 return self.min_occurs == 0 66 67 def is_empty(self) -> bool: 68 """ 69 Tests if max_occurs == 0. A zero-length model group is considered empty. 70 """ 71 return self.max_occurs == 0 72 73 def is_single(self) -> bool: 74 """ 75 Tests if the particle has max_occurs == 1. For elements the test 76 outcome depends also on parent group. For model groups the test 77 outcome depends also on nested model groups. 78 """ 79 return self.max_occurs == 1 80 81 def is_multiple(self) -> bool: 82 """Tests the particle can have multiple occurrences.""" 83 return not self.is_empty() and not self.is_single() 84 85 def is_ambiguous(self) -> bool: 86 """Tests if min_occurs != max_occurs.""" 87 return self.min_occurs != self.max_occurs 88 89 def is_univocal(self) -> bool: 90 """Tests if min_occurs == max_occurs.""" 91 return self.min_occurs == self.max_occurs 92 93 def is_missing(self, occurs: int) -> bool: 94 """Tests if provided occurrences are under the minimum.""" 95 return not self.is_emptiable() if occurs == 0 else self.min_occurs > occurs 96 97 def is_over(self, occurs: int) -> bool: 98 """Tests if provided occurrences are over the maximum.""" 99 return self.max_occurs is not None and self.max_occurs <= occurs 100 101 def has_occurs_restriction(self, other: Union[ModelParticleType, 'OccursCalculator']) -> bool: 102 if self.min_occurs < other.min_occurs: 103 return False 104 elif self.max_occurs == 0: 105 return True 106 elif other.max_occurs is None: 107 return True 108 elif self.max_occurs is None: 109 return False 110 else: 111 return self.max_occurs <= other.max_occurs 112 113 def parse_error(self, message: Any) -> None: 114 raise XMLSchemaValueError(message) 115 116 def _parse_particle(self, elem: ElementType) -> None: 117 if 'minOccurs' in elem.attrib: 118 try: 119 min_occurs = int(elem.attrib['minOccurs']) 120 except (TypeError, ValueError): 121 self.parse_error("minOccurs value is not an integer value") 122 else: 123 if min_occurs < 0: 124 self.parse_error("minOccurs value must be a non negative integer") 125 else: 126 self.min_occurs = min_occurs 127 128 max_occurs = elem.get('maxOccurs') 129 if max_occurs is None: 130 if self.min_occurs > 1: 131 self.parse_error("minOccurs must be lesser or equal than maxOccurs") 132 elif max_occurs == 'unbounded': 133 self.max_occurs = None 134 else: 135 try: 136 self.max_occurs = int(max_occurs) 137 except ValueError: 138 self.parse_error("maxOccurs value must be a non negative integer or 'unbounded'") 139 else: 140 if self.min_occurs > self.max_occurs: 141 self.parse_error("maxOccurs must be 'unbounded' or greater than minOccurs") 142 self.max_occurs = None 143 144 145class OccursCalculator: 146 """ 147 An helper class for adding and multiplying min/max occurrences of XSD particles. 148 """ 149 min_occurs: int 150 max_occurs: Optional[int] 151 152 def __init__(self) -> None: 153 self.min_occurs = self.max_occurs = 0 154 155 def __repr__(self) -> str: 156 return '%s(%r, %r)' % (self.__class__.__name__, self.min_occurs, self.max_occurs) 157 158 def __add__(self, other: ParticleMixin) -> 'OccursCalculator': 159 self.min_occurs += other.min_occurs 160 if self.max_occurs is not None: 161 if other.max_occurs is None: 162 self.max_occurs = None 163 else: 164 self.max_occurs += other.max_occurs 165 return self 166 167 def __mul__(self, other: ParticleMixin) -> 'OccursCalculator': 168 self.min_occurs *= other.min_occurs 169 if self.max_occurs is None: 170 if other.max_occurs == 0: 171 self.max_occurs = 0 172 elif other.max_occurs is None: 173 if self.max_occurs != 0: 174 self.max_occurs = None 175 else: 176 self.max_occurs *= other.max_occurs 177 return self 178 179 def reset(self) -> None: 180 self.min_occurs = self.max_occurs = 0 181