Skip to content
This repository has been archived by the owner on Feb 14, 2025. It is now read-only.

Commit

Permalink
Merge branch 'v10.5.0' into v2_main
Browse files Browse the repository at this point in the history
  • Loading branch information
boonhapus committed Jan 6, 2025
2 parents 0ea8b51 + c63d282 commit 0a8af6a
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 57 deletions.
13 changes: 0 additions & 13 deletions src/thoughtspot_tml/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@
Dumper = yaml.Dumper
Loader = yaml.SafeLoader

if sys.version_info < (3, 8):
from typing_extensions import get_origin, get_args
from typing_extensions import Literal, TypedDict
else:
# AVAILABLE IN PYTHON 3.8
from typing import get_origin, get_args
from typing import Literal, TypedDict

if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated

if sys.version_info < (3, 10):
# https://bugs.python.org/issue40564#msg377884
# :: zipfile.Path with several files prematurely closes zip
Expand Down
87 changes: 48 additions & 39 deletions src/thoughtspot_tml/_tml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@

from collections.abc import Collection
from dataclasses import asdict, dataclass, fields, is_dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, get_args, get_origin
import functools as ft
import json
import keyword
import pathlib
import re
import typing
import warnings

import yaml

from thoughtspot_tml import _scriptability, _yaml
from thoughtspot_tml._compat import Self, get_args, get_origin
from thoughtspot_tml._compat import Self
from thoughtspot_tml.exceptions import TMLDecodeError, TMLExtensionWarning

if TYPE_CHECKING:
from typing import Any, Dict

from typing import Any

RE_CAMEL_CASE = re.compile(r"[A-Z]?[a-z]+|[A-Z]{2,}(?=[A-Z][a-z]|\d|\W|$)|\d+")


def attempt_resolve_type(type_hint: Any) -> Any:
"""Resolves string type hints to actual types."""
if isinstance(type_hint, str):
return getattr(_scriptability, type_hint.replace("_scriptability.", ""), type_hint)
return type_hint


def recursive_complex_attrs_to_dataclasses(instance: Any) -> None:
"""
Convert all fields of type `dataclass` into an instance of the
Expand All @@ -35,65 +41,68 @@ def recursive_complex_attrs_to_dataclasses(instance: Any) -> None:
- list of basic types
- list of dataclass annotation
"""
RESOLVED_TYPEHINT_HAS_CHILDREN = ft.partial(lambda hint, expr: is_dataclass(hint) and isinstance(expr, dict))
cls = type(instance)

for field in fields(cls):
value = getattr(instance, field.name)

# ignore nulls, they get dropped to support optionality
if value is None:
continue

# it's a dataclass
if is_dataclass(field.type) and isinstance(value, dict):
new_value = field.type(**value)
recursive_complex_attrs_to_dataclasses(new_value)

# it's an _scriptability dataclass Annotation that needs resolution
elif isinstance(field.type, str) and isinstance(value, dict):
if field.type.startswith("_scriptability"):
_, _, field_type = field.type.partition(".")
else:
field_type = field.type
# TRY TO RESOLVE STRING REFERENCES FROM _scriptability.py
# eg. ActionObjectEDocProto , ConnectionEDocProto , etc ..
#
# NOTE: this falls back to the original type_hint when it can't be resolved.
field_type = attempt_resolve_type(field.type)

type_ = getattr(_scriptability, field_type)
new_value = type_(**value)
# RECURSE INTO RESOLVED _scripatability.py HINTS
if RESOLVED_TYPEHINT_HAS_CHILDREN(hint=field_type, expr=value):
new_value = field_type(**value)
recursive_complex_attrs_to_dataclasses(new_value)

# it's a List of basic types or ForwardRefs
elif get_origin(field.type) is list:
new_value = [] # type: ignore[assignment]
args = get_args(field.type)
field_type = args[0].__forward_value__ if isinstance(args[0], typing.ForwardRef) else args[0]
# list IS USED TO DENOTE THAT A TML OBJECT CAN CONTAIN MULTIPLE HOMOGENOUS
# CHILDREN SO WE TAKE JUST THE FIRST ELEMENT AND ATTEMPT TO RESOLVE IT.
elif get_origin(field_type) is list:
new_value = []
homo_type = next(iter(get_args(field_type)))
item_type = attempt_resolve_type(homo_type)

# OLD ... will keep this around JUST IN CASE.
#
# item_type = attempt_resolve_type(
# get_args(field_type)[0].__forward_value__
# if isinstance(get_args(field_type)[0], typing.ForwardRef)
# else get_args(field_type)[0]
# )

for item in value:
if is_dataclass(field_type) and isinstance(item, dict):
item = _sanitize_reserved_keyword_keys(item)
item = field_type(**item)

new_value.append(item) # type: ignore[attr-defined]

if is_dataclass(field_type):
# RECURSE INTO RESOLVED _scripatability.py HINTS
if RESOLVED_TYPEHINT_HAS_CHILDREN(hint=item_type, expr=item):
item = item_type(**_sanitize_reserved_keyword_keys(item))
recursive_complex_attrs_to_dataclasses(item)

# it's an empty mapping, python-betterproto doesn't support optional map_fields
elif get_origin(field.type) is dict and value == {}:
new_value.append(item)

# IF OUR VALUE IS EMPTY, WE'RE GOING TO DROP IT.
elif get_origin(field_type) is dict and not value:
new_value = None

else: # pragma: peephole optimizer
# SIMPLE TYPES DO NOT NEED RECURSION.
else:
continue

setattr(instance, field.name, new_value)


def _sanitize_reserved_keyword_keys(mapping: Dict[str, Any]) -> Dict[str, Any]:
def _sanitize_reserved_keyword_keys(mapping: dict[str, Any]) -> dict[str, Any]:
"""
Replace reserved keywords with a trailing sunder.
"""
return {(f"{k}_" if keyword.iskeyword(k) else k): v for k, v in mapping.items()}


def _recursive_remove_null(mapping: Dict[str, Any]) -> Dict[str, Any]:
def _recursive_remove_null(mapping: dict[str, Any]) -> dict[str, Any]:
"""
Drop all keys with null values, they're optional.
"""
Expand Down Expand Up @@ -144,13 +153,13 @@ def __post_init__(self):
recursive_complex_attrs_to_dataclasses(self)

@classmethod
def _loads(cls, tml_document: str) -> Dict[str, Any]:
def _loads(cls, tml_document: str) -> dict[str, Any]:
# DEV NOTE: @boonhapus
# DO NOT OVERRIDE THIS!!
# These exist to handle backwards compatible changes between TML versions.
return _yaml.load(tml_document)

def _to_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> dict[str, Any]:
# DEV NOTE: @boonhapus
# DO NOT OVERRIDE THIS!!
# These exist to handle backwards compatible changes between TML versions.
Expand Down Expand Up @@ -207,7 +216,7 @@ def load(cls, path: pathlib.Path) -> Self:

return instance

def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
"""
Serialize this object to native python data types.
"""
Expand Down
3 changes: 1 addition & 2 deletions src/thoughtspot_tml/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Type, Union
from typing import TYPE_CHECKING, Annotated, Literal, Type, TypedDict, Union

from thoughtspot_tml._compat import Annotated, Literal, TypedDict
from thoughtspot_tml.tml import Answer, Cohort, Liveboard, Model, SQLView, Table, View, Worksheet

if TYPE_CHECKING:
Expand Down
6 changes: 3 additions & 3 deletions src/thoughtspot_tml/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from __future__ import annotations

from dataclasses import fields, is_dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, get_args
import functools as ft
import json
import logging
import pathlib
import warnings

from thoughtspot_tml import _compat, _scriptability
from thoughtspot_tml import _scriptability
from thoughtspot_tml.exceptions import MissingGUIDMappedValueWarning, TMLError
from thoughtspot_tml.tml import Answer, Cohort, Connection, Liveboard, Model, Pinboard, SQLView, Table, View, Worksheet

Expand All @@ -24,7 +24,7 @@

def _recursive_scan(scriptability_object: Any, *, check: Optional[Callable[[Any], bool]] = None) -> List[Any]:
collect = []
is_container_type = lambda t: len(_compat.get_args(t)) > 0 # noqa: E731
is_container_type = lambda t: len(get_args(t)) > 0 # noqa: E731

for field in fields(scriptability_object):
child = getattr(scriptability_object, field.name)
Expand Down

0 comments on commit 0a8af6a

Please sign in to comment.