1import decimal 
    2import re 
    3 
    4from .exceptions import JsonSchemaDefinitionException 
    5from .generator import CodeGenerator, enforce_list 
    6 
    7 
    8JSON_TYPE_TO_PYTHON_TYPE = { 
    9    'null': 'NoneType', 
    10    'boolean': 'bool', 
    11    'number': 'int, float, Decimal', 
    12    'integer': 'int', 
    13    'string': 'str', 
    14    'array': 'list, tuple', 
    15    'object': 'dict', 
    16} 
    17 
    18DOLLAR_FINDER = re.compile(r"(?<!\\)\$")  # Finds any un-escaped $ (including inside []-sets) 
    19 
    20 
    21# pylint: disable=too-many-instance-attributes,too-many-public-methods 
    22class CodeGeneratorDraft04(CodeGenerator): 
    23    # pylint: disable=line-too-long 
    24    # I was thinking about using ipaddress module instead of regexps for example, but it's big 
    25    # difference in performance. With a module I got this difference: over 100 ms with a module 
    26    # vs. 9 ms with a regex! Other modules are also ineffective or not available in standard 
    27    # library. Some regexps are not 100% precise but good enough, fast and without dependencies. 
    28    FORMAT_REGEXS = { 
    29        'date-time': r'^\d{4}-[01]\d-[0-3]\d(t|T)[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:[+-][0-2]\d:[0-5]\d|[+-][0-2]\d[0-5]\d|z|Z)\Z', 
    30        'email': r'^[^@]+@[^@]+\.[^@]+\Z', 
    31        'hostname': r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9])\Z', 
    32        'ipv4': r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\Z', 
    33        'ipv6': r'^(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)\Z', 
    34        'uri': r'^\w+:(\/?\/?)[^\s]+\Z', 
    35    } 
    36 
    37    def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True): 
    38        super().__init__(definition, resolver) 
    39        self._custom_formats = formats 
    40        self._use_formats = use_formats 
    41        self._use_default = use_default 
    42        self._json_keywords_to_function.update(( 
    43            ('type', self.generate_type), 
    44            ('enum', self.generate_enum), 
    45            ('allOf', self.generate_all_of), 
    46            ('anyOf', self.generate_any_of), 
    47            ('oneOf', self.generate_one_of), 
    48            ('not', self.generate_not), 
    49            ('minLength', self.generate_min_length), 
    50            ('maxLength', self.generate_max_length), 
    51            ('pattern', self.generate_pattern), 
    52            ('format', self.generate_format), 
    53            ('minimum', self.generate_minimum), 
    54            ('maximum', self.generate_maximum), 
    55            ('multipleOf', self.generate_multiple_of), 
    56            ('minItems', self.generate_min_items), 
    57            ('maxItems', self.generate_max_items), 
    58            ('uniqueItems', self.generate_unique_items), 
    59            ('items', self.generate_items), 
    60            ('minProperties', self.generate_min_properties), 
    61            ('maxProperties', self.generate_max_properties), 
    62            ('required', self.generate_required), 
    63            # Check dependencies before properties generates default values. 
    64            ('dependencies', self.generate_dependencies), 
    65            ('properties', self.generate_properties), 
    66            ('patternProperties', self.generate_pattern_properties), 
    67            ('additionalProperties', self.generate_additional_properties), 
    68        )) 
    69        self._any_or_one_of_count = 0 
    70 
    71    @property 
    72    def global_state(self): 
    73        res = super().global_state 
    74        res['custom_formats'] = self._custom_formats 
    75        return res 
    76 
    77    def generate_type(self): 
    78        """ 
    79        Validation of type. Can be one type or list of types. 
    80 
    81        .. code-block:: python 
    82 
    83            {'type': 'string'} 
    84            {'type': ['string', 'number']} 
    85        """ 
    86        types = enforce_list(self._definition['type']) 
    87        try: 
    88            python_types = ', '.join(JSON_TYPE_TO_PYTHON_TYPE[t] for t in types) 
    89        except KeyError as exc: 
    90            raise JsonSchemaDefinitionException('Unknown type: {}'.format(exc)) 
    91 
    92        extra = '' 
    93        if ('number' in types or 'integer' in types) and 'boolean' not in types: 
    94            extra = ' or isinstance({variable}, bool)'.format(variable=self._variable) 
    95 
    96        with self.l('if not isinstance({variable}, ({})){}:', python_types, extra): 
    97            self.exc('{name} must be {}', ' or '.join(types), rule='type') 
    98 
    99    def generate_enum(self): 
    100        """ 
    101        Means that only value specified in the enum is valid. 
    102 
    103        .. code-block:: python 
    104 
    105            { 
    106                'enum': ['a', 'b'], 
    107            } 
    108        """ 
    109        enum = self._definition['enum'] 
    110        if not isinstance(enum, (list, tuple)): 
    111            raise JsonSchemaDefinitionException('enum must be an array') 
    112        with self.l('if {variable} not in {enum}:'): 
    113            self.exc('{name} must be one of {}', self.e(enum), rule='enum') 
    114 
    115    def generate_all_of(self): 
    116        """ 
    117        Means that value have to be valid by all of those definitions. It's like put it in 
    118        one big definition. 
    119 
    120        .. code-block:: python 
    121 
    122            { 
    123                'allOf': [ 
    124                    {'type': 'number'}, 
    125                    {'minimum': 5}, 
    126                ], 
    127            } 
    128 
    129        Valid values for this definition are 5, 6, 7, ... but not 4 or 'abc' for example. 
    130        """ 
    131        for definition_item in self._definition['allOf']: 
    132            self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 
    133 
    134    def generate_any_of(self): 
    135        """ 
    136        Means that value have to be valid by any of those definitions. It can also be valid 
    137        by all of them. 
    138 
    139        .. code-block:: python 
    140 
    141            { 
    142                'anyOf': [ 
    143                    {'type': 'number', 'minimum': 10}, 
    144                    {'type': 'number', 'maximum': 5}, 
    145                ], 
    146            } 
    147 
    148        Valid values for this definition are 3, 4, 5, 10, 11, ... but not 8 for example. 
    149        """ 
    150        self._any_or_one_of_count += 1 
    151        count = self._any_or_one_of_count 
    152        self.l('{variable}_any_of_count{count} = 0', count=count) 
    153        for definition_item in self._definition['anyOf']: 
    154            # When we know it's passing (at least once), we do not need to do another expensive try-except. 
    155            with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False): 
    156                with self.l('try:', optimize=False): 
    157                    self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 
    158                    self.l('{variable}_any_of_count{count} += 1', count=count) 
    159                self.l('except JsonSchemaValueException: pass') 
    160 
    161        with self.l('if not {variable}_any_of_count{count}:', count=count, optimize=False): 
    162            self.exc('{name} cannot be validated by any definition', rule='anyOf') 
    163 
    164    def generate_one_of(self): 
    165        """ 
    166        Means that value have to be valid by only one of those definitions. It can't be valid 
    167        by two or more of them. 
    168 
    169        .. code-block:: python 
    170 
    171            { 
    172                'oneOf': [ 
    173                    {'type': 'number', 'multipleOf': 3}, 
    174                    {'type': 'number', 'multipleOf': 5}, 
    175                ], 
    176            } 
    177 
    178        Valid values for this definition are 3, 5, 6, ... but not 15 for example. 
    179        """ 
    180        self._any_or_one_of_count += 1 
    181        count = self._any_or_one_of_count 
    182        self.l('{variable}_one_of_count{count} = 0', count=count) 
    183        for definition_item in self._definition['oneOf']: 
    184            # When we know it's failing (one of means exactly once), we do not need to do another expensive try-except. 
    185            with self.l('if {variable}_one_of_count{count} < 2:', count=count, optimize=False): 
    186                with self.l('try:', optimize=False): 
    187                    self.generate_func_code_block(definition_item, self._variable, self._variable_name, clear_variables=True) 
    188                    self.l('{variable}_one_of_count{count} += 1', count=count) 
    189                self.l('except JsonSchemaValueException: pass') 
    190 
    191        with self.l('if {variable}_one_of_count{count} != 1:', count=count): 
    192            dynamic = '" (" + str({variable}_one_of_count{}) + " matches found)"' 
    193            self.exc('{name} must be valid exactly by one definition', count, append_to_msg=dynamic, rule='oneOf') 
    194 
    195    def generate_not(self): 
    196        """ 
    197        Means that value have not to be valid by this definition. 
    198 
    199        .. code-block:: python 
    200 
    201            {'not': {'type': 'null'}} 
    202 
    203        Valid values for this definition are 'hello', 42, {} ... but not None. 
    204 
    205        Since draft 06 definition can be boolean. False means nothing, True 
    206        means everything is invalid. 
    207        """ 
    208        not_definition = self._definition['not'] 
    209        if not_definition is True: 
    210            self.exc('{name} must not be there', rule='not') 
    211        elif not_definition is False: 
    212            return 
    213        elif not not_definition: 
    214            with self.l('if {}:', self._variable): 
    215                self.exc('{name} must NOT match a disallowed definition', rule='not') 
    216        else: 
    217            with self.l('try:', optimize=False): 
    218                self.generate_func_code_block(not_definition, self._variable, self._variable_name) 
    219            self.l('except JsonSchemaValueException: pass') 
    220            with self.l('else:'): 
    221                self.exc('{name} must NOT match a disallowed definition', rule='not') 
    222 
    223    def generate_min_length(self): 
    224        with self.l('if isinstance({variable}, str):'): 
    225            self.create_variable_with_length() 
    226            if not isinstance(self._definition['minLength'], int): 
    227                raise JsonSchemaDefinitionException('minLength must be a number') 
    228            with self.l('if {variable}_len < {minLength}:'): 
    229                self.exc('{name} must be longer than or equal to {minLength} characters', rule='minLength') 
    230 
    231    def generate_max_length(self): 
    232        with self.l('if isinstance({variable}, str):'): 
    233            self.create_variable_with_length() 
    234            if not isinstance(self._definition['maxLength'], int): 
    235                raise JsonSchemaDefinitionException('maxLength must be a number') 
    236            with self.l('if {variable}_len > {maxLength}:'): 
    237                self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength') 
    238 
    239    def generate_pattern(self): 
    240        with self.l('if isinstance({variable}, str):'): 
    241            pattern = self._definition['pattern'] 
    242            safe_pattern = pattern.replace('\\', '\\\\').replace('"', '\\"') 
    243            end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern) 
    244            self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern) 
    245            with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)): 
    246                self.exc('{name} must match pattern {}', safe_pattern, rule='pattern') 
    247 
    248    def generate_format(self): 
    249        """ 
    250        Means that value have to be in specified format. For example date, email or other. 
    251 
    252        .. code-block:: python 
    253 
    254            {'format': 'email'} 
    255 
    256        Valid value for this definition is user@example.com but not @username 
    257        """ 
    258        if not self._use_formats: 
    259            return 
    260        with self.l('if isinstance({variable}, str):'): 
    261            format_ = self._definition['format'] 
    262            # Checking custom formats - user is allowed to override default formats. 
    263            if format_ in self._custom_formats: 
    264                custom_format = self._custom_formats[format_] 
    265                if isinstance(custom_format, str): 
    266                    self._generate_format(format_, format_ + '_re_pattern', custom_format) 
    267                else: 
    268                    with self.l('if not custom_formats["{}"]({variable}):', format_): 
    269                        self.exc('{name} must be {}', format_, rule='format') 
    270            elif format_ in self.FORMAT_REGEXS: 
    271                format_regex = self.FORMAT_REGEXS[format_] 
    272                self._generate_format(format_, format_ + '_re_pattern', format_regex) 
    273            # Format regex is used only in meta schemas. 
    274            elif format_ == 'regex': 
    275                with self.l('try:', optimize=False): 
    276                    self.l('re.compile({variable})') 
    277                with self.l('except Exception:'): 
    278                    self.exc('{name} must be a valid regex', rule='format') 
    279            else: 
    280                raise JsonSchemaDefinitionException('Unknown format: {}'.format(format_)) 
    281 
    282 
    283    def _generate_format(self, format_name, regexp_name, regexp): 
    284        if self._definition['format'] == format_name: 
    285            if not regexp_name in self._compile_regexps: 
    286                self._compile_regexps[regexp_name] = re.compile(regexp) 
    287            with self.l('if not REGEX_PATTERNS["{}"].match({variable}):', regexp_name): 
    288                self.exc('{name} must be {}', format_name, rule='format') 
    289 
    290    def generate_minimum(self): 
    291        with self.l('if isinstance({variable}, (int, float, Decimal)):'): 
    292            if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)): 
    293                raise JsonSchemaDefinitionException('minimum must be a number') 
    294            if self._definition.get('exclusiveMinimum', False): 
    295                with self.l('if {variable} <= {minimum}:'): 
    296                    self.exc('{name} must be bigger than {minimum}', rule='minimum') 
    297            else: 
    298                with self.l('if {variable} < {minimum}:'): 
    299                    self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum') 
    300 
    301    def generate_maximum(self): 
    302        with self.l('if isinstance({variable}, (int, float, Decimal)):'): 
    303            if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)): 
    304                raise JsonSchemaDefinitionException('maximum must be a number') 
    305            if self._definition.get('exclusiveMaximum', False): 
    306                with self.l('if {variable} >= {maximum}:'): 
    307                    self.exc('{name} must be smaller than {maximum}', rule='maximum') 
    308            else: 
    309                with self.l('if {variable} > {maximum}:'): 
    310                    self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum') 
    311 
    312    def generate_multiple_of(self): 
    313        with self.l('if isinstance({variable}, (int, float, Decimal)):'): 
    314            if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)): 
    315                raise JsonSchemaDefinitionException('multipleOf must be a number') 
    316            # For proper multiplication check of floats we need to use decimals, 
    317            # because for example 19.01 / 0.01 = 1901.0000000000002. 
    318            if isinstance(self._definition['multipleOf'], float): 
    319                self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))') 
    320            else: 
    321                self.l('quotient = {variable} / {multipleOf}') 
    322            with self.l('if int(quotient) != quotient:'): 
    323                self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') 
    324 
    325    def generate_min_items(self): 
    326        self.create_variable_is_list() 
    327        with self.l('if {variable}_is_list:'): 
    328            if not isinstance(self._definition['minItems'], int): 
    329                raise JsonSchemaDefinitionException('minItems must be a number') 
    330            self.create_variable_with_length() 
    331            with self.l('if {variable}_len < {minItems}:'): 
    332                self.exc('{name} must contain at least {minItems} items', rule='minItems') 
    333 
    334    def generate_max_items(self): 
    335        self.create_variable_is_list() 
    336        with self.l('if {variable}_is_list:'): 
    337            if not isinstance(self._definition['maxItems'], int): 
    338                raise JsonSchemaDefinitionException('maxItems must be a number') 
    339            self.create_variable_with_length() 
    340            with self.l('if {variable}_len > {maxItems}:'): 
    341                self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems') 
    342 
    343    def generate_unique_items(self): 
    344        """ 
    345        With Python 3.4 module ``timeit`` recommended this solutions: 
    346 
    347        .. code-block:: python 
    348 
    349            >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000) 
    350            0.5839540958404541 
    351            >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000) 
    352            0.7094449996948242 
    353            >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000) 
    354            2.0819358825683594 
    355            >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000) 
    356            2.1439831256866455 
    357        """ 
    358        unique_definition = self._definition['uniqueItems'] 
    359        if not unique_definition: 
    360            return 
    361 
    362        self.create_variable_is_list() 
    363        with self.l('if {variable}_is_list:'): 
    364            self.l( 
    365                'def fn(var): ' 
    366                'return frozenset(dict((k, fn(v)) ' 
    367                'for k, v in var.items()).items()) ' 
    368                'if hasattr(var, "items") else tuple(fn(v) ' 
    369                'for v in var) ' 
    370                'if isinstance(var, (dict, list)) else str(var) ' 
    371                'if isinstance(var, bool) else var') 
    372            self.create_variable_with_length() 
    373            with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'): 
    374                self.exc('{name} must contain unique items', rule='uniqueItems') 
    375 
    376    def generate_items(self): 
    377        """ 
    378        Means array is valid only when all items are valid by this definition. 
    379 
    380        .. code-block:: python 
    381 
    382            { 
    383                'items': [ 
    384                    {'type': 'integer'}, 
    385                    {'type': 'string'}, 
    386                ], 
    387            } 
    388 
    389        Valid arrays are those with integers or strings, nothing else. 
    390 
    391        Since draft 06 definition can be also boolean. True means nothing, False 
    392        means everything is invalid. 
    393        """ 
    394        items_definition = self._definition['items'] 
    395        if items_definition is True: 
    396            return 
    397 
    398        self.create_variable_is_list() 
    399        with self.l('if {variable}_is_list:'): 
    400            self.create_variable_with_length() 
    401            if items_definition is False: 
    402                with self.l('if {variable}:'): 
    403                    self.exc('{name} must not be there', rule='items') 
    404            elif isinstance(items_definition, list): 
    405                for idx, item_definition in enumerate(items_definition): 
    406                    with self.l('if {variable}_len > {}:', idx): 
    407                        self.l('{variable}__{0} = {variable}[{0}]', idx) 
    408                        self.generate_func_code_block( 
    409                            item_definition, 
    410                            '{}__{}'.format(self._variable, idx), 
    411                            '{}[{}]'.format(self._variable_name, idx), 
    412                        ) 
    413                    if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition: 
    414                        self.l('else: {variable}.append({})', repr(item_definition['default'])) 
    415 
    416                if 'additionalItems' in self._definition: 
    417                    if self._definition['additionalItems'] is False: 
    418                        with self.l('if {variable}_len > {}:', len(items_definition)): 
    419                            self.exc('{name} must contain only specified items', rule='items') 
    420                    else: 
    421                        with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)): 
    422                            count = self.generate_func_code_block( 
    423                                self._definition['additionalItems'], 
    424                                '{}_item'.format(self._variable), 
    425                                '{}[{{{}_x}}]'.format(self._variable_name, self._variable), 
    426                            ) 
    427                            if count == 0: 
    428                                self.l('pass') 
    429            else: 
    430                if items_definition: 
    431                    with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'): 
    432                        count = self.generate_func_code_block( 
    433                            items_definition, 
    434                            '{}_item'.format(self._variable), 
    435                            '{}[{{{}_x}}]'.format(self._variable_name, self._variable), 
    436                        ) 
    437                        if count == 0: 
    438                            self.l('pass') 
    439 
    440    def generate_min_properties(self): 
    441        self.create_variable_is_dict() 
    442        with self.l('if {variable}_is_dict:'): 
    443            if not isinstance(self._definition['minProperties'], int): 
    444                raise JsonSchemaDefinitionException('minProperties must be a number') 
    445            self.create_variable_with_length() 
    446            with self.l('if {variable}_len < {minProperties}:'): 
    447                self.exc('{name} must contain at least {minProperties} properties', rule='minProperties') 
    448 
    449    def generate_max_properties(self): 
    450        self.create_variable_is_dict() 
    451        with self.l('if {variable}_is_dict:'): 
    452            if not isinstance(self._definition['maxProperties'], int): 
    453                raise JsonSchemaDefinitionException('maxProperties must be a number') 
    454            self.create_variable_with_length() 
    455            with self.l('if {variable}_len > {maxProperties}:'): 
    456                self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties') 
    457 
    458    def generate_required(self): 
    459        self.create_variable_is_dict() 
    460        with self.l('if {variable}_is_dict:'): 
    461            if not isinstance(self._definition['required'], (list, tuple)): 
    462                raise JsonSchemaDefinitionException('required must be an array') 
    463            if len(self._definition['required']) != len(set(self._definition['required'])): 
    464                raise JsonSchemaDefinitionException('required must contain unique elements') 
    465            if not self._definition.get('additionalProperties', True): 
    466                not_possible = [ 
    467                    prop 
    468                    for prop in self._definition['required'] 
    469                    if 
    470                        prop not in self._definition.get('properties', {}) 
    471                        and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {})) 
    472                ] 
    473                if not_possible: 
    474                    raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible)) 
    475            self.l('{variable}__missing_keys = set({required}) - {variable}.keys()') 
    476            with self.l('if {variable}__missing_keys:'): 
    477                dynamic = 'str(sorted({variable}__missing_keys)) + " properties"' 
    478                self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic) 
    479 
    480    def generate_properties(self): 
    481        """ 
    482        Means object with defined keys. 
    483 
    484        .. code-block:: python 
    485 
    486            { 
    487                'properties': { 
    488                    'key': {'type': 'number'}, 
    489                }, 
    490            } 
    491 
    492        Valid object is containing key called 'key' and value any number. 
    493        """ 
    494        self.create_variable_is_dict() 
    495        with self.l('if {variable}_is_dict:'): 
    496            self.create_variable_keys() 
    497            for key, prop_definition in self._definition['properties'].items(): 
    498                key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key) 
    499                if not isinstance(prop_definition, (dict, bool)): 
    500                    raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name)) 
    501                with self.l('if "{}" in {variable}_keys:', self.e(key)): 
    502                    self.l('{variable}_keys.remove("{}")', self.e(key)) 
    503                    self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key)) 
    504                    self.generate_func_code_block( 
    505                        prop_definition, 
    506                        '{}__{}'.format(self._variable, key_name), 
    507                        '{}.{}'.format(self._variable_name, self.e(key)), 
    508                        clear_variables=True, 
    509                    ) 
    510                if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition: 
    511                    self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default'])) 
    512 
    513    def generate_pattern_properties(self): 
    514        """ 
    515        Means object with defined keys as patterns. 
    516 
    517        .. code-block:: python 
    518 
    519            { 
    520                'patternProperties': { 
    521                    '^x': {'type': 'number'}, 
    522                }, 
    523            } 
    524 
    525        Valid object is containing key starting with a 'x' and value any number. 
    526        """ 
    527        self.create_variable_is_dict() 
    528        with self.l('if {variable}_is_dict:'): 
    529            self.create_variable_keys() 
    530            for pattern, definition in self._definition['patternProperties'].items(): 
    531                self._compile_regexps[pattern] = re.compile(pattern) 
    532            with self.l('for {variable}_key, {variable}_val in {variable}.items():'): 
    533                for pattern, definition in self._definition['patternProperties'].items(): 
    534                    with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)): 
    535                        with self.l('if {variable}_key in {variable}_keys:'): 
    536                            self.l('{variable}_keys.remove({variable}_key)') 
    537                        self.generate_func_code_block( 
    538                            definition, 
    539                            '{}_val'.format(self._variable), 
    540                            '{}.{{{}_key}}'.format(self._variable_name, self._variable), 
    541                            clear_variables=True, 
    542                        ) 
    543 
    544    def generate_additional_properties(self): 
    545        """ 
    546        Means object with keys with values defined by definition. 
    547 
    548        .. code-block:: python 
    549 
    550            { 
    551                'properties': { 
    552                    'key': {'type': 'number'}, 
    553                } 
    554                'additionalProperties': {'type': 'string'}, 
    555            } 
    556 
    557        Valid object is containing key called 'key' and it's value any number and 
    558        any other key with any string. 
    559        """ 
    560        self.create_variable_is_dict() 
    561        with self.l('if {variable}_is_dict:'): 
    562            self.create_variable_keys() 
    563            add_prop_definition = self._definition["additionalProperties"] 
    564            if add_prop_definition is True or add_prop_definition == {}: 
    565                return 
    566            if add_prop_definition: 
    567                properties_keys = list(self._definition.get("properties", {}).keys()) 
    568                with self.l('for {variable}_key in {variable}_keys:'): 
    569                    with self.l('if {variable}_key not in {}:', properties_keys): 
    570                        self.l('{variable}_value = {variable}.get({variable}_key)') 
    571                        self.generate_func_code_block( 
    572                            add_prop_definition, 
    573                            '{}_value'.format(self._variable), 
    574                            '{}.{{{}_key}}'.format(self._variable_name, self._variable), 
    575                        ) 
    576            else: 
    577                with self.l('if {variable}_keys:'): 
    578                    self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties') 
    579 
    580    def generate_dependencies(self): 
    581        """ 
    582        Means when object has property, it needs to have also other property. 
    583 
    584        .. code-block:: python 
    585 
    586            { 
    587                'dependencies': { 
    588                    'bar': ['foo'], 
    589                }, 
    590            } 
    591 
    592        Valid object is containing only foo, both bar and foo or none of them, but not 
    593        object with only bar. 
    594 
    595        Since draft 06 definition can be boolean or empty array. True and empty array 
    596        means nothing, False means that key cannot be there at all. 
    597        """ 
    598        self.create_variable_is_dict() 
    599        with self.l('if {variable}_is_dict:'): 
    600            is_empty = True 
    601            for key, values in self._definition["dependencies"].items(): 
    602                if values == [] or values is True: 
    603                    continue 
    604                is_empty = False 
    605                with self.l('if "{}" in {variable}:', self.e(key)): 
    606                    if values is False: 
    607                        self.exc('{} in {name} must not be there', key, rule='dependencies') 
    608                    elif isinstance(values, list): 
    609                        for value in values: 
    610                            with self.l('if "{}" not in {variable}:', self.e(value)): 
    611                                self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies') 
    612                    else: 
    613                        self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True) 
    614            if is_empty: 
    615                self.l('pass')