コード例 #1
0
def test_enum_override_variations(no_warnings):
    enum_override_variations = ['language_list', 'LanguageEnum']
    if DJANGO_VERSION > '3':
        enum_override_variations += ['LanguageChoices', 'LanguageChoices.choices']

    for variation in enum_override_variations:
        with mock.patch(
            'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES',
            {'LanguageEnum': f'tests.test_postprocessing.{variation}'}
        ):
            assert list_hash(['en']) in load_enum_name_overrides()
コード例 #2
0
ファイル: hooks.py プロジェクト: ToGoBananas/drf-spectacular
def postprocess_schema_enums(result, generator, **kwargs):
    """
    simple replacement of Enum/Choices that globally share the same name and have
    the same choices. Aids client generation to not generate a separate enum for
    every occurrence. only takes effect when replacement is guaranteed to be correct.
    """
    def iter_prop_containers(schema, component_name=None):
        if not component_name:
            for component_name, schema in schema.items():
                if spectacular_settings.COMPONENT_SPLIT_PATCH:
                    component_name = re.sub('^Patched(.+)', r'\1',
                                            component_name)
                if spectacular_settings.COMPONENT_SPLIT_REQUEST:
                    component_name = re.sub('(.+)Request$', r'\1',
                                            component_name)
                yield from iter_prop_containers(schema, component_name)
        elif isinstance(schema, list):
            for item in schema:
                yield from iter_prop_containers(item, component_name)
        elif isinstance(schema, dict):
            if schema.get('properties'):
                yield component_name, schema['properties']
            yield from iter_prop_containers(schema.get('oneOf', []),
                                            component_name)
            yield from iter_prop_containers(schema.get('allOf', []),
                                            component_name)
            yield from iter_prop_containers(schema.get('anyOf', []),
                                            component_name)

    def create_enum_component(name, schema):
        component = ResolvedComponent(
            name=name,
            type=ResolvedComponent.SCHEMA,
            schema=schema,
            object=name,
        )
        generator.registry.register_on_missing(component)
        return component

    schemas = result.get('components', {}).get('schemas', {})

    overrides = load_enum_name_overrides()

    prop_hash_mapping = defaultdict(set)
    hash_name_mapping = defaultdict(set)
    # collect all enums, their names and choice sets
    for component_name, props in iter_prop_containers(schemas):
        for prop_name, prop_schema in props.items():
            if prop_schema.get('type') == 'array':
                prop_schema = prop_schema.get('items', {})
            if 'enum' not in prop_schema:
                continue
            # remove blank/null entry for hashing. will be reconstructed in the last step
            prop_enum_cleaned_hash = list_hash(
                [i for i in prop_schema['enum'] if i not in ['', None]])
            prop_hash_mapping[prop_name].add(prop_enum_cleaned_hash)
            hash_name_mapping[prop_enum_cleaned_hash].add(
                (component_name, prop_name))

    # traverse all enum properties and generate a name for the choice set. naming collisions
    # are resolved and a warning is emitted. giving a choice set multiple names is technically
    # correct but potentially unwanted. also emit a warning there to make the user aware.
    enum_name_mapping = {}
    for prop_name, prop_hash_set in prop_hash_mapping.items():
        for prop_hash in prop_hash_set:
            if prop_hash in overrides:
                enum_name = overrides[prop_hash]
            elif len(prop_hash_set) == 1:
                # prop_name has been used exclusively for one choice set (best case)
                enum_name = f'{camelize(prop_name)}Enum'
            elif len(hash_name_mapping[prop_hash]) == 1:
                # prop_name has multiple choice sets, but each one limited to one component only
                component_name, _ = next(iter(hash_name_mapping[prop_hash]))
                enum_name = f'{camelize(component_name)}{camelize(prop_name)}Enum'
            else:
                enum_name = f'{camelize(prop_name)}{prop_hash[:3].capitalize()}Enum'
                warn(
                    f'enum naming encountered a non-optimally resolvable collision for fields '
                    f'named "{prop_name}". The same name has been used for multiple choice sets '
                    f'in multiple components. The collision was resolved with "{enum_name}". '
                    f'add an entry to ENUM_NAME_OVERRIDES to fix the naming.')
            if enum_name_mapping.get(prop_hash, enum_name) != enum_name:
                warn(
                    f'encountered multiple names for the same choice set ({enum_name}). This '
                    f'may be unwanted even though the generated schema is technically correct. '
                    f'Add an entry to ENUM_NAME_OVERRIDES to fix the naming.')
                del enum_name_mapping[prop_hash]
            else:
                enum_name_mapping[prop_hash] = enum_name
            enum_name_mapping[(prop_hash, prop_name)] = enum_name

    # replace all enum occurrences with a enum schema component. cut out the
    # enum, replace it with a reference and add a corresponding component.
    for _, props in iter_prop_containers(schemas):
        for prop_name, prop_schema in props.items():
            is_array = prop_schema.get('type') == 'array'
            if is_array:
                prop_schema = prop_schema.get('items', {})

            if 'enum' not in prop_schema:
                continue

            prop_enum_original_list = prop_schema['enum']
            prop_schema['enum'] = [
                i for i in prop_schema['enum'] if i not in ['', None]
            ]
            prop_hash = list_hash(prop_schema['enum'])
            # when choice sets are reused under multiple names, the generated name cannot be
            # resolved from the hash alone. fall back to prop_name and hash for resolution.
            enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[
                prop_hash, prop_name]

            # split property into remaining property and enum component parts
            enum_schema = {
                k: v
                for k, v in prop_schema.items() if k in ['type', 'enum']
            }
            prop_schema = {
                k: v
                for k, v in prop_schema.items() if k not in ['type', 'enum']
            }

            components = [create_enum_component(enum_name, schema=enum_schema)]
            if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE:
                if '' in prop_enum_original_list or None in prop_enum_original_list:
                    components.append(
                        create_enum_component('BlankEnum',
                                              schema={'enum': ['']}))
                # if None in prop_enum_original_list:
                #     components.append(create_enum_component('NullEnum', schema={'enum': [None]}))

            if len(components) == 1:
                prop_schema.update(components[0].ref)
            else:
                prop_schema.update({'oneOf': [c.ref for c in components]})

            if is_array:
                props[prop_name]['items'] = safe_ref(prop_schema)
            else:
                props[prop_name] = safe_ref(prop_schema)

    # sort again with additional components
    result['components'] = generator.registry.build(
        spectacular_settings.APPEND_COMPONENTS)
    return result
コード例 #3
0
def postprocess_schema_enums(result, generator, **kwargs):
    """
    simple replacement of Enum/Choices that globally share the same name and have
    the same choices. Aids client generation to not generate a separate enum for
    every occurrence. only takes effect when replacement is guaranteed to be correct.
    """
    def iter_prop_containers(schema):
        if isinstance(schema, list):
            for item in schema:
                yield from iter_prop_containers(item)
        elif isinstance(schema, dict):
            if schema.get('properties'):
                yield schema['properties']
            yield from iter_prop_containers(schema.get('oneOf', []))
            yield from iter_prop_containers(schema.get('allOf', []))

    def create_enum_component(name, schema):
        component = ResolvedComponent(
            name=name,
            type=ResolvedComponent.SCHEMA,
            schema=schema,
            object=name,
        )
        generator.registry.register_on_missing(component)
        return component

    schemas = result.get('components', {}).get('schemas', {})

    overrides = load_enum_name_overrides()

    hash_mapping = defaultdict(set)
    # collect all enums, their names and choice sets
    for props in iter_prop_containers(list(schemas.values())):
        for prop_name, prop_schema in props.items():
            if 'enum' not in prop_schema:
                continue
            # remove blank/null entry for hashing. will be reconstructed in the last step
            prop_enum_cleaned_list = [i for i in prop_schema['enum'] if i]
            hash_mapping[prop_name].add(list_hash(prop_enum_cleaned_list))

    # traverse all enum properties and generate a name for the choice set. naming collisions
    # are resolved and a warning is emitted. giving a choice set multiple names is technically
    # correct but potentially unwanted. also emit a warning there to make the user aware.
    enum_name_mapping = {}
    for prop_name, prop_hash_set in hash_mapping.items():
        for prop_hash in prop_hash_set:
            if prop_hash in overrides:
                enum_name = overrides[prop_hash]
            elif len(prop_hash_set) == 1:
                enum_name = f'{inflection.camelize(prop_name)}Enum'
            else:
                enum_name = f'{inflection.camelize(prop_name)}{prop_hash[:3].capitalize()}Enum'
                warn(
                    f'automatic enum naming encountered a collision for field "{prop_name}". the '
                    f'same name has been used for multiple choice sets. the collision was resolved '
                    f'with {enum_name}. add an entry to ENUM_NAME_OVERRIDES to fix the naming.'
                )
            if enum_name_mapping.get(prop_hash, enum_name) != enum_name:
                warn(
                    f'encountered multiple names for the same choice set ({enum_name}). this '
                    f'may be unwanted even though the generated schema is technically correct. '
                    f'add an entry to ENUM_NAME_OVERRIDES to fix the naming.')
                del enum_name_mapping[prop_hash]
            else:
                enum_name_mapping[prop_hash] = enum_name
            enum_name_mapping[(prop_hash, prop_name)] = enum_name

    # replace all enum occurrences with a enum schema component. cut out the
    # enum, replace it with a reference and add a corresponding component.
    for props in iter_prop_containers(list(schemas.values())):
        for prop_name, prop_schema in props.items():
            if 'enum' not in prop_schema:
                continue

            prop_enum_original_list = prop_schema['enum']
            prop_schema['enum'] = [i for i in prop_schema['enum'] if i]
            prop_hash = list_hash(prop_schema['enum'])
            # when choice sets are reused under multiple names, the generated name cannot be
            # resolved from the hash alone. fall back to prop_name and hash for resolution.
            enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[
                prop_hash, prop_name]

            # split property into remaining property and enum component parts
            enum_schema = {
                k: v
                for k, v in prop_schema.items() if k in ['type', 'enum']
            }
            prop_schema = {
                k: v
                for k, v in prop_schema.items() if k not in ['type', 'enum']
            }

            components = [create_enum_component(enum_name, schema=enum_schema)]
            if '' in prop_enum_original_list:
                components.append(
                    create_enum_component('BlankEnum', schema={'enum': ['']}))
            if None in prop_enum_original_list:
                components.append(
                    create_enum_component('NullEnum', schema={'enum': [None]}))

            if len(components) == 1:
                prop_schema.update(components[0].ref)
            else:
                prop_schema.update({'oneOf': [c.ref for c in components]})

            props[prop_name] = safe_ref(prop_schema)

    # sort again with additional components
    result['components'] = generator.registry.build(
        spectacular_settings.APPEND_COMPONENTS)
    return result