Skip to content

Commit

Permalink
Mark unsafe TypedDicts in ServicePackageParser
Browse files Browse the repository at this point in the history
  • Loading branch information
vemel committed Oct 28, 2024
1 parent e27e4e8 commit 541a113
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 9 deletions.
1 change: 1 addition & 0 deletions mypy_boto3_builder/generators/base_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ def _process_service(
templates_path: Path,
) -> ServicePackage:
service_package = self._parse_service_package(service_name, version, package_data)
ServicePackageParser.mark_unsafe_typed_dicts(service_package)

self.logger.debug(f"Writing {service_name.boto3_name}")
self.package_writer.write_service_package(
Expand Down
26 changes: 26 additions & 0 deletions mypy_boto3_builder/parsers/service_package_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from mypy_boto3_builder.structures.waiter import Waiter
from mypy_boto3_builder.type_annotations.type_def_sortable import TypeDefSortable
from mypy_boto3_builder.type_maps.typed_dicts import CloudwatchEventTypeDef
from mypy_boto3_builder.utils.strings import RESERVED_NAMES, is_reserved
from mypy_boto3_builder.utils.type_checks import is_typed_dict
from mypy_boto3_builder.utils.type_def_sorter import TypeDefSorter


Expand Down Expand Up @@ -85,6 +87,30 @@ def parse(self) -> ServicePackage:

return result

@staticmethod
def mark_unsafe_typed_dicts(service_package: ServicePackage) -> None:
"""
Mark TypedDicts that can't be rendered as classes safely.
TypedDict cannot be rendered as class if its name or any attribute is a reserver word,
or if any argument is names as another TypeDef.
"""
unsafe_keys = {
*RESERVED_NAMES,
*(type_def.name for type_def in service_package.type_defs),
*(literal.name for literal in service_package.literals),
}
for type_def in service_package.type_defs:
if not is_typed_dict(type_def):
continue
type_def.is_safe_as_class = True
if is_reserved(type_def.name):
type_def.is_safe_as_class = False
continue
if any(attribute.name in unsafe_keys for attribute in type_def.children):
type_def.is_safe_as_class = False
continue

def _parse_service_package(self) -> ServicePackage:
client = parse_client(self.session, self.service_name, self.shape_parser)
service_resource_parser = ServiceResourceParser(
Expand Down
11 changes: 2 additions & 9 deletions mypy_boto3_builder/type_annotations/type_typed_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from mypy_boto3_builder.type_annotations.type_parent import TypeParent
from mypy_boto3_builder.type_annotations.type_subscript import TypeSubscript
from mypy_boto3_builder.utils.jinja2 import render_jinja2_template
from mypy_boto3_builder.utils.strings import is_reserved


class TypedDictAttribute:
Expand Down Expand Up @@ -109,6 +108,7 @@ def __init__(
self.children = list(children)
self.docstring = docstring
self._stringify = stringify
self.is_safe_as_class = True

def is_stringified(self) -> bool:
"""
Expand Down Expand Up @@ -146,20 +146,13 @@ def render(self) -> str:

return self.name

def _is_safe_as_class(self) -> bool:
"""
Whether type annotation can be safely rendered as a class.
"""
names = (self.name, *(child.name for child in self.children))
return not any(is_reserved(name) for name in names)

def render_definition(self) -> str:
"""
Render type annotation definition.
"""
template = (
Path("common/typed_dict_class.py.jinja2")
if self._is_safe_as_class()
if self.is_safe_as_class
else Path("common/typed_dict.py.jinja2")
)
return render_jinja2_template(template, {"type_def": self})
Expand Down
2 changes: 2 additions & 0 deletions tests/type_annotations/test_type_typed_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def test_render_definition(self) -> None:
TypedDictAttribute("Type", Type.str, False),
],
)
typed_dict.is_safe_as_class = False
assert (
typed_dict.render_definition()
== 'MyDict = TypedDict("MyDict", {"required": str, "Type": NotRequired[str], })'
Expand Down Expand Up @@ -175,6 +176,7 @@ def test_replace_self_references(self) -> None:
TypedDictAttribute("required", Type.str, True),
],
)
typed_dict.is_safe_as_class = False
typed_dict.add_attribute("self_one", typed_dict, True)
typed_dict.add_attribute("Type", typed_dict, False)
assert typed_dict.replace_self_references(Type.DictStrAny)
Expand Down

0 comments on commit 541a113

Please sign in to comment.