Skip to content

Commit

Permalink
Merge pull request #1051 from sbrunner/fix
Browse files Browse the repository at this point in the history
Prevent type name collisions with random number generator
  • Loading branch information
sbrunner authored Jun 18, 2024
2 parents de96df9 + ec4a847 commit dd8402a
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 27 deletions.
24 changes: 20 additions & 4 deletions jsonschema_gentypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import keyword
import random
import re
import textwrap
import unicodedata
Expand Down Expand Up @@ -375,6 +376,10 @@ def imports(self, python_version: tuple[int, ...]) -> list[tuple[str, str]]:
return [(self.workaround_package, self._name)]
return [(self.package, self._name)]

def __repr__(self) -> str:
"""Get the representation of the object."""
return f"NativeType({self.package!r}.{self._name!r})"


class CombinedType(Type):
"""
Expand Down Expand Up @@ -479,6 +484,7 @@ def __init__(self, name: str, values: list[Union[int, float, bool, str, None]],
assert len(values) > 0
super().__init__(name)
self.values = values
self.value_names = {value: get_name({"title": f"{name} {value}"}, upper=True) for value in values}
self.descriptions = descriptions
self.sub_type: Type = CombinedType(NativeType("Union"), [LiteralType(value) for value in values])

Expand All @@ -500,7 +506,7 @@ def definition(self, line_length: Optional[int] = None) -> list[str]:
elif comments:
result += ['"""', *comments, '"""']
for value in self.values:
name = get_name({"title": f"{self._name} {value}"}, upper=True)
name = self.value_names[value]
formatted_value = f'"{value}"' if isinstance(value, str) else str(value)
result.append(f"{name}: {LiteralType(value).name()} = {formatted_value}")
name = self.descriptions[0] if self.descriptions else self._name
Expand Down Expand Up @@ -688,6 +694,7 @@ def get_name(
proposed_name: Optional[str] = None,
upper: bool = False,
get_name_properties: Optional[str] = None,
postfix: Optional[str] = None,
) -> str:
"""
Get the name for an element.
Expand All @@ -704,21 +711,30 @@ def get_name(
name = normalize(name)

prefix = "" if has_title else "_"
rand = str(random.randint(0, 9999)) if name != "Root" else "" # nosec
if upper:
# Upper case
name = name.upper()
# Remove spaces
return prefix + "".join(["_" if char.isspace() else char for char in name])
output = prefix + "".join(["_" if char.isspace() else char for char in name])
elif get_name_properties == "UpperFirst":
# Change just the first letter to upper case
name = name[0].upper() + name[1:]
# Remove spaces
return prefix + "".join([char for char in name if not char.isspace()])
output = prefix + "".join([char for char in name if not char.isspace()])
else:
# Title case
name = name.title()
# Remove spaces
return prefix + "".join([char for char in name if not char.isspace()])
output = prefix + "".join([char for char in name if not char.isspace()])
if postfix:
output += postfix
if not get_name.__dict__.get("names"):
get_name.__dict__["names"] = set()
elif output in get_name.__dict__["names"]:
output += rand
get_name.__dict__["names"].add(output)
return output


def get_description(
Expand Down
12 changes: 9 additions & 3 deletions jsonschema_gentypes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,13 @@ def get_type(
description.append("")
description += additional_description
if description:
if not isinstance(the_type, NamedType):
if auto_alias:
if auto_alias:
alias = True
if isinstance(the_type, NamedType):
alias = False
elif isinstance(the_type, CombinedType):
alias = not isinstance(the_type.base, NamedType)
if alias:
the_type = TypeAlias(
self.get_name(schema_meta_data, proposed_name), the_type, description
)
Expand Down Expand Up @@ -205,8 +210,9 @@ def get_name(
],
proposed_name: Optional[str] = None,
upper: bool = False,
postfix: Optional[str] = None,
) -> str:
return get_name(schema, proposed_name, upper, self.get_name_properties)
return get_name(schema, proposed_name, upper, self.get_name_properties, postfix=postfix)

def resolve_ref(
self,
Expand Down
7 changes: 5 additions & 2 deletions jsonschema_gentypes/api_draft_04.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def object(
)

std_dict = None
name = self.get_name(schema_meta_data, proposed_name)

schema.setdefault("used", set()).add("additionalProperties") # type: ignore[typeddict-item]
additional_properties = cast(
Union[jsonschema_draft_04.JSONSchemaD4, jsonschema_draft_2020_12_applicator.JSONSchemaD2020],
Expand Down Expand Up @@ -150,8 +150,11 @@ def object(
for prop, sub_schema in properties.items()
}

name = self.get_name(
schema_meta_data, proposed_name, postfix="Typed" if std_dict is not None else ""
)
type_: Type = TypedDictType(
name if std_dict is None else name + "Typed",
name,
struct,
get_description(schema_meta_data) if std_dict is None else [],
required=required,
Expand Down
4 changes: 1 addition & 3 deletions tests/get_name_properties.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from typing import TypedDict

from typing_extensions import Required
from typing import Required, TypedDict


class ResponseType(TypedDict, total=False):
Expand Down
4 changes: 2 additions & 2 deletions tests/openapi3.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ class OgcapiCollectionsCollectionidGetQuery(TypedDict, total=False):


OgcapiCollectionsCollectionidGetQueryF = Union[Literal["json"], Literal["html"]]
OGCAPICOLLECTIONSCOLLECTIONIDGETQUERYF_JSON: Literal["json"] = "json"
_OGCAPICOLLECTIONSCOLLECTIONIDGETQUERYF_JSON: Literal["json"] = "json"
"""The values for the 'OgcapiCollectionsCollectionidGetQueryF' enum"""
OGCAPICOLLECTIONSCOLLECTIONIDGETQUERYF_HTML: Literal["html"] = "html"
_OGCAPICOLLECTIONSCOLLECTIONIDGETQUERYF_HTML: Literal["html"] = "html"
"""The values for the 'OgcapiCollectionsCollectionidGetQueryF' enum"""


Expand Down
42 changes: 29 additions & 13 deletions tests/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@


def get_types(schema) -> Type:
jsonschema_gentypes.get_name.__dict__.setdefault("names", set()).clear()
resolver = jsonschema_gentypes.resolver.RefResolver("https://example.com/fake", schema)
api = jsonschema_gentypes.api_draft_07.APIv7(resolver)
return api.get_type(schema, "Base")


def get_types_2019_09(schema) -> Type:
jsonschema_gentypes.get_name.__dict__.setdefault("names", set()).clear()
resolver = jsonschema_gentypes.resolver.RefResolver("https://example.com/fake", schema)
api = jsonschema_gentypes.api_draft_2019_09.APIv201909(resolver)
return api.get_type(schema, "Base")


def get_types_2020_12(schema) -> Type:
jsonschema_gentypes.get_name.__dict__.setdefault("names", set()).clear()
resolver = jsonschema_gentypes.resolver.RefResolver("https://example.com/fake", schema)
api = jsonschema_gentypes.api_draft_2020_12.APIv202012(resolver)
return api.get_type(schema, "Base")
Expand Down Expand Up @@ -459,7 +462,7 @@ class TestBasicTypes(TypedDict, total=False):
)


def test_enum():
def test_dict_enum():
type_ = get_types(
{
"type": "object",
Expand All @@ -472,26 +475,37 @@ def test_enum():
)
assert (
"\n".join([d.rstrip() for d in type_.definition(None)])
== f'''
== '''
"""
test basic types.
"""
TestBasicTypes = TypedDict('TestBasicTypes', {
'enum': "_TestBasicTypesEnum",
}, total=False) """
class TestBasicTypes(TypedDict, total=False):
""" test basic types. """
enum: Required["Properties"]
"""
properties.
Required property
"""
'''
)

assert len(type_.depends_on()) == 2
enum_type = type_.depends_on()[1]
assert len(enum_type.depends_on()) == 2
enum_type = enum_type.depends_on()[1]
assert (
"\n".join([d.rstrip() for d in type_.depends_on()[1].definition(None)])
"\n".join([d.rstrip() for d in enum_type.definition(None)])
== '''
class _TestBasicTypesEnum(Enum):
RED = "red"
AMBER = "amber"
GREEN = "green"'''
Properties = Union[Literal['red'], Literal['amber'], Literal['green']]
""" properties. """
PROPERTIES_RED: Literal['red'] = "red"
"""The values for the 'properties' enum"""
PROPERTIES_AMBER: Literal['amber'] = "amber"
"""The values for the 'properties' enum"""
PROPERTIES_GREEN: Literal['green'] = "green"
"""The values for the 'properties' enum"""
'''
)


Expand Down Expand Up @@ -971,10 +985,12 @@ def test_linesplit() -> None:
],
)
def test_name(config, title, expected):
jsonschema_gentypes.get_name.__dict__.setdefault("names", set()).clear()
assert jsonschema_gentypes.get_name(config, title) == expected


def test_name_upper():
jsonschema_gentypes.get_name.__dict__.setdefault("names", set()).clear()
assert jsonschema_gentypes.get_name({"title": "test"}, upper=True) == "TEST"


Expand Down

0 comments on commit dd8402a

Please sign in to comment.