diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01b5ce7..6eb2568 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,8 @@ repos: - id: pyupgrade # SEE pyproject.toml FOR py{min-support}-plus. args: [--py39-plus, --keep-runtime-typing] + # THIS FILE NEEDS TO STAY python27 COMPATIBLE "ABOVE" __main__ AND py{min-support} "BELOW" __main__. + exclude: ^examples/ - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.8.6 diff --git a/_generate/__main__.py b/_generate/__main__.py index 475e402..701c006 100644 --- a/_generate/__main__.py +++ b/_generate/__main__.py @@ -73,8 +73,10 @@ def _clean_edoc_proto() -> None: # REMOVE THE IMPORT STATEMENT SINCE WE ARE LOCALIZING OR STRIPPING THE PROTO text = re.sub(rf'^import "{preprocessor.import_name}";$', _const.VOID, text, flags=re.MULTILINE) - # STRIP OFF THE PACKAGE IDENTITY (and optional path separator) - text = re.sub(rf"(?<=\s){preprocessor.package}\.?", preprocessor.replace, text, flags=re.MULTILINE | re.DOTALL) + # STRIP OFF THE PACKAGE IDENTITY (and not following by an underscore, with an optional path separator) + # fmt: off + text = re.sub(rf"(?<=\s){preprocessor.package}(?!_)\.?", preprocessor.replace, text, flags=re.MULTILINE | re.DOTALL) # noqa: E501 + # fmt: on # DIVIDE THE edoc.proto INTO 3 PARTS, INJECT THE LOCAL PROTO, STICK IT BACK TOGETHER imports, package_info, edoc_contents = text.partition(SCRIPTABILITY_PACKAGE_INFO) diff --git a/_generate/_clean.py b/_generate/_clean.py index a88ebe7..a4b5cff 100644 --- a/_generate/_clean.py +++ b/_generate/_clean.py @@ -44,7 +44,7 @@ class ProtobufPreprocessor: ), ProtobufPreprocessor( import_name=r"common/common.proto", - package=r"common.(?!proto_validation)", + package=r"common(?!.proto_validation)", local=_proto_local.PROTO_COMMON, ), ProtobufPreprocessor( diff --git a/pyproject.toml b/pyproject.toml index af0a255..583fc97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,8 @@ strict_equality = true strict_concatenate = true exclude = ''' (?x)( - ^_scriptability.py$ # IGNORE AUTO-GENERATED FILES + _scriptability.py$ # IGNORE AUTO-GENERATED FILES + | _compat.py$ # IGNORE COMPAT FILES ) ''' @@ -116,7 +117,6 @@ target-version = "py39" line-length = 120 src = ["src/thoughtspot_tml"] exclude = [ - # PROJECT SPECIFIC IGNORES "__init__.py", # ignore __init__.py "__project__.py", # ignore project metadata diff --git a/src/thoughtspot_tml/__init__.py b/src/thoughtspot_tml/__init__.py index ef34724..fb0a64f 100644 --- a/src/thoughtspot_tml/__init__.py +++ b/src/thoughtspot_tml/__init__.py @@ -1,5 +1,7 @@ from thoughtspot_tml.__project__ import __version__ +from thoughtspot_tml._tml import TML + from thoughtspot_tml.tml import Connection from thoughtspot_tml.tml import Table, View, SQLView, Worksheet, Model from thoughtspot_tml.tml import Answer, Liveboard, Cohort @@ -14,6 +16,7 @@ __all__ = ( "__version__", + "TML", "Connection", "Table", "View", diff --git a/src/thoughtspot_tml/__project__.py b/src/thoughtspot_tml/__project__.py index 8a124bf..b19ee4b 100644 --- a/src/thoughtspot_tml/__project__.py +++ b/src/thoughtspot_tml/__project__.py @@ -1 +1 @@ -__version__ = "2.2.0" +__version__ = "2.2.1" diff --git a/src/thoughtspot_tml/_scriptability.py b/src/thoughtspot_tml/_scriptability.py index ba4456b..ce296c7 100644 --- a/src/thoughtspot_tml/_scriptability.py +++ b/src/thoughtspot_tml/_scriptability.py @@ -654,7 +654,7 @@ class WorksheetEDocProto(betterproto.Message): class WorksheetEDocProtoQueryProperties(betterproto.Message): is_bypass_rls: bool = betterproto.bool_field(1, optional=True) join_progressive: bool = betterproto.bool_field(2, optional=True) - config: "SageConfigProto" = betterproto.message_field(3, optional=True) + sage_config: "SageConfigProto" = betterproto.message_field(3, optional=True) @dataclass(eq=False, repr=False) @@ -1115,7 +1115,7 @@ class LogicalTableEDocProtoDbColumnProperties(betterproto.Message): @dataclass(eq=False, repr=False) class LogicalTableEDocProtoProperties(betterproto.Message): - config: "SageConfigProto" = betterproto.message_field(1, optional=True) + sage_config: "SageConfigProto" = betterproto.message_field(1, optional=True) @dataclass(eq=False, repr=False) diff --git a/src/thoughtspot_tml/_tml.py b/src/thoughtspot_tml/_tml.py index be032d6..93ff0f9 100644 --- a/src/thoughtspot_tml/_tml.py +++ b/src/thoughtspot_tml/_tml.py @@ -2,7 +2,7 @@ from collections.abc import Collection from dataclasses import asdict, dataclass, fields, is_dataclass -from typing import TYPE_CHECKING, get_args, get_origin +from typing import TYPE_CHECKING, ForwardRef, Optional, get_args, get_origin import functools as ft import json import keyword @@ -10,8 +10,6 @@ import re import warnings -import yaml - from thoughtspot_tml import _scriptability, _yaml from thoughtspot_tml._compat import Self from thoughtspot_tml.exceptions import TMLDecodeError, TMLExtensionWarning @@ -19,16 +17,35 @@ if TYPE_CHECKING: from typing import Any + from thoughtspot_tml.types import GUID + 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 IT'S A ForwardRef, RESOLVE IT. + # Further Reading: + # https://docs.python.org/3/library/typing.html#typing.ForwardRef + if isinstance(type_hint, ForwardRef): + return type_hint.__forward_value__ + + # IF IT'S A STRING, ATTEMPT TO LOOK IT UP IN _scriptability.py if isinstance(type_hint, str): return getattr(_scriptability, type_hint.replace("_scriptability.", ""), type_hint) return type_hint +def origin_or_fallback(type_hint: Any, *, default: Any) -> Any: + """ + Get the unsubscripted version of a type, with optional fallback. + + Further Reading: + https://docs.python.org/3/library/typing.html#typing.get_origin + """ + return get_origin(type_hint) or default + + def recursive_complex_attrs_to_dataclasses(instance: Any) -> None: """ Convert all fields of type `dataclass` into an instance of the @@ -56,6 +73,10 @@ def recursive_complex_attrs_to_dataclasses(instance: Any) -> None: # NOTE: this falls back to the original type_hint when it can't be resolved. field_type = attempt_resolve_type(field.type) + # ORIGIN TYPES ARE THE X in X[a, b, c] hints.. but does not include native types + # eg. typing.List[str] but NOT list[str] + origin_type = origin_or_fallback(field_type, default=field_type) + # RECURSE INTO RESOLVED _scripatability.py HINTS if RESOLVED_TYPEHINT_HAS_CHILDREN(hint=field_type, expr=value): new_value = field_type(**value) @@ -63,18 +84,11 @@ def recursive_complex_attrs_to_dataclasses(instance: Any) -> None: # 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 = [] + elif origin_type is list: 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] - # ) + new_value = [] for item in value: # RECURSE INTO RESOLVED _scripatability.py HINTS @@ -84,10 +98,16 @@ def recursive_complex_attrs_to_dataclasses(instance: Any) -> None: 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: + # IF OUR VALUE IS EMPTY, IT IS OPTIONAL AND SO WE'RE GOING TO DROP IT. + elif origin_type is dict and not value: new_value = None + # DEV NOTE: @boonhapus, 2025/01/08 + # Q. WHY NO (origin_type is dict and value) LIKE WE HAVE FOR LISTS? + # A. Currently the edoc spec does not maintain complex mapping types. If we + # need to support them, we'll need to add them at this priority (below + # empty dicts -- so we continue to support optionality). + # SIMPLE TYPES DO NOT NEED RECURSION. else: continue @@ -141,6 +161,8 @@ class TML: Base object for ThoughtSpot TML. """ + guid: Optional[GUID] + @property def tml_type_name(self) -> str: """Return the type name of the TML object.""" @@ -149,6 +171,11 @@ def tml_type_name(self) -> str: snakes = "_".join(camels) return snakes.lower() + @property + def name(self) -> str: + """This should be implemented in child classes.""" + raise NotImplementedError + def __post_init__(self): recursive_complex_attrs_to_dataclasses(self) @@ -180,14 +207,10 @@ def loads(cls, tml_document: str) -> Self: TMLDecodeError, when the document string cannot be parsed or receives extra data """ try: - document = cls._loads(tml_document) - except (yaml.scanner.ScannerError, yaml.parser.ParserError, yaml.reader.ReaderError) as e: - raise TMLDecodeError(cls, message=str(e), problem_mark=getattr(e, "problem_mark", None)) from None # type: ignore[arg-type] - - try: - instance = cls(**document) - except TypeError as e: - raise TMLDecodeError(cls, data=document, message=str(e)) from None # type: ignore[arg-type] + data = cls._loads(tml_document) + instance = cls(**data) + except Exception as e: + raise TMLDecodeError(cls, exc=e, document=tml_document) from None return instance @@ -211,7 +234,8 @@ def load(cls, path: pathlib.Path) -> Self: try: instance = cls.loads(path.read_text(encoding="utf-8")) except TMLDecodeError as e: - e.path = path + # INTERCEPT AND INJECT THE FILEPATH. + e.filepath = path raise e from None return instance diff --git a/src/thoughtspot_tml/_yaml.py b/src/thoughtspot_tml/_yaml.py index 04ce395..13ae1e6 100644 --- a/src/thoughtspot_tml/_yaml.py +++ b/src/thoughtspot_tml/_yaml.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any import re import yaml from thoughtspot_tml import _compat -NEARLY_INFINITY = 999999999 # This used to be math.inf, but C has no concept of infinity. ;) - # TML column ids typically take the form.. # # LOGICAL_TABLE_NAME_#::LOGICAL_COLUMN_NAME @@ -40,7 +38,7 @@ # fmt: on -def _double_quote_when_special_char(dumper: yaml.Dumper, data: str) -> yaml.ScalarNode: +def _double_quote_when_special_char(dumper: yaml.Dumper | yaml.CDumper, data: str) -> yaml.ScalarNode: """ Double quote the string when any condition is met. @@ -68,7 +66,7 @@ def _double_quote_when_special_char(dumper: yaml.Dumper, data: str) -> yaml.Scal yaml.Loader.yaml_implicit_resolvers.pop("=") -def load(document: str) -> Dict[str, Any]: +def load(document: str) -> dict[str, Any]: """ Load a TML object. """ @@ -80,7 +78,7 @@ def load(document: str) -> Dict[str, Any]: return yaml.load(document, Loader=yaml.SafeLoader) -def dump(document: Dict[str, Any]) -> str: +def dump(document: dict[str, Any]) -> str: """ Dump a TML object as YAML. @@ -94,6 +92,8 @@ def dump(document: Dict[str, Any]) -> str: We'll attempt to reproduce them in Python. """ + NEARLY_INFINITY = 999999999 # This used to be math.inf, but C has no concept of infinity. ;) + options = { "width": NEARLY_INFINITY, "default_flow_style": False, @@ -101,8 +101,8 @@ def dump(document: Dict[str, Any]) -> str: "allow_unicode": True, } try: - return yaml.dump(document, Dumper=_compat.Dumper, **options) + return yaml.dump(document, Dumper=_compat.Dumper, **options) # type: ignore[call-overload] # FALL BACK TO THE SLOWER PYTHON DUMPER IF WE CAN'T FULLY PARSE UNICODE except UnicodeEncodeError: - return yaml.dump(document, Dumper=yaml.SafeDumper, **options) + return yaml.dump(document, Dumper=yaml.SafeDumper, **options) # type: ignore[call-overload] diff --git a/src/thoughtspot_tml/exceptions.py b/src/thoughtspot_tml/exceptions.py index a478eb8..effc190 100644 --- a/src/thoughtspot_tml/exceptions.py +++ b/src/thoughtspot_tml/exceptions.py @@ -1,14 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional, Type -import dataclasses +from typing import TYPE_CHECKING, Optional + +from yaml import error if TYPE_CHECKING: from collections.abc import Iterable from pathlib import Path - from yaml import error - from thoughtspot_tml.types import GUID, TMLObject @@ -41,50 +40,40 @@ class TMLDecodeError(TMLError): Raised when a TML object cannot be instantiated from input data. """ - def __init__( - self, - tml_cls: Type[TMLObject], - *, - message: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - path: Optional[Path] = None, - problem_mark: Optional[error.Mark] = None, - ): # pragma: no cover + def __init__(self, tml_cls: type[TMLObject], *, exc: Exception, document: str, filepath: Optional[Path] = None): self.tml_cls = tml_cls - self.message = message - self.data = data - self.path = path - self.problem_mark = problem_mark + self.parent_exc = exc + self.document = document + self.filepath = filepath - def __str__(self) -> str: - lines = [] - class_name = self.tml_cls.__name__ + def with_filepath(self, filepath) -> TMLDecodeError: + """Add the file which generated the exception.""" + self.filepath = filepath + return self - if self.message is not None: - lines.append(self.message) + def __str__(self) -> str: + lines: list[str] = [] - if self.data is not None: - lines.append(f"supplied data does not produce a valid TML ({class_name}) document") - fields = {f.name for f in dataclasses.fields(self.tml_cls)} - data = set(self.data) + if isinstance(self.parent_exc, TypeError): + _, _, attribute = str(self.parent_exc).partition(" unexpected keyword argument ") + lines.append(f"Unrecognized attribute in the TML spec: {attribute}") - if data.difference(fields): - extra = ", ".join([f"'{arg}'" for arg in data.difference(fields)]) - lines.append(f"\ngot extra data: {extra}") + if self.filepath is not None: + lines.append("\n") + lines.append(f"File '{self.filepath}' may not be a valid {self.tml_cls.__name__} file") - if self.path is not None: - lines.append(f"'{self.path}' is not a valid TML ({class_name}) file") + if isinstance(self.parent_exc, error.MarkedYAMLError): + if mark := self.parent_exc.problem_mark: + lines.append("\n") + lines.append(f"Syntax error on line {mark.line + 1}, around column {mark.column + 1}") - if self.problem_mark is not None: - err_line = self.problem_mark.line + 1 - err_column = self.problem_mark.column + 1 - snippet = self.problem_mark.get_snippet() - lines.append(f"\nsyntax error on line {err_line}, around column {err_column}") + if snippet := mark.get_snippet(): + lines.append(snippet) - if snippet is not None: - lines.append(snippet) + if not lines: + lines.append(str(self.parent_exc)) - return "\n".join(lines) + return "\n".join(lines).strip() class TMLDisambiguationError(TMLError): diff --git a/src/thoughtspot_tml/spotapp.py b/src/thoughtspot_tml/spotapp.py index 9226f4d..c7befd8 100644 --- a/src/thoughtspot_tml/spotapp.py +++ b/src/thoughtspot_tml/spotapp.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Optional import json import pathlib import zipfile @@ -17,7 +17,7 @@ @dataclass class Manifest: - object: List[TMLDocInfo] + object: list[TMLDocInfo] @dataclass @@ -28,39 +28,39 @@ class SpotApp: This object is usually packaged together as a zip file. """ - tml: List[TMLObject] + tml: list[TMLObject] manifest: Optional[Manifest] = None @property - def tables(self) -> List[Table]: + def tables(self) -> list[Table]: return [tml for tml in self.tml if isinstance(tml, Table)] @property - def views(self) -> List[View]: + def views(self) -> list[View]: return [tml for tml in self.tml if isinstance(tml, View)] @property - def sql_views(self) -> List[SQLView]: + def sql_views(self) -> list[SQLView]: return [tml for tml in self.tml if isinstance(tml, SQLView)] @property - def worksheets(self) -> List[Worksheet]: + def worksheets(self) -> list[Worksheet]: return [tml for tml in self.tml if isinstance(tml, Worksheet)] @property - def answers(self) -> List[Answer]: + def answers(self) -> list[Answer]: return [tml for tml in self.tml if isinstance(tml, Answer)] @property - def liveboards(self) -> List[Liveboard]: + def liveboards(self) -> list[Liveboard]: return [tml for tml in self.tml if isinstance(tml, Liveboard)] @property - def model(self) -> List[Model]: + def model(self) -> list[Model]: return [tml for tml in self.tml if isinstance(tml, Model)] @property - def cohort(self) -> List[Cohort]: + def cohort(self) -> list[Cohort]: return [tml for tml in self.tml if isinstance(tml, Cohort)] @classmethod @@ -77,14 +77,14 @@ def from_api(cls, payload: EDocExportResponses) -> SpotApp: metadata/tml/export response data to parse """ info: SpotAppInfo = {"tml": [], "manifest": None} - manifest_data: Dict[str, List[TMLDocInfo]] = {"object": []} + manifest_data: dict[str, list[TMLDocInfo]] = {"object": []} for edoc_info in payload["object"]: tml_cls = determine_tml_type(info=edoc_info["info"]) document = json.loads(edoc_info["edoc"]) manifest_data["object"].append(edoc_info["info"]) tml = tml_cls(**document) - info["tml"].append(tml) # type: ignore[arg-type] + info["tml"].append(tml) info["manifest"] = Manifest(**manifest_data) return cls(**info) @@ -103,7 +103,7 @@ def read(cls, path: pathlib.Path) -> SpotApp: with zipfile.ZipFile(path, mode="r") as archive: for member in archive.infolist(): - path = ZipPath(archive, at=member.filename) + path = ZipPath(archive, at=member.filename) # type: ignore[assignment] if member.filename == "Manifest.yaml": document = _yaml.load(path.read_text()) @@ -112,7 +112,7 @@ def read(cls, path: pathlib.Path) -> SpotApp: tml_cls = determine_tml_type(path=pathlib.Path(member.filename)) tml = tml_cls.load(path) - info["tml"].append(tml) # type: ignore[arg-type] + info["tml"].append(tml) return cls(**info) diff --git a/src/thoughtspot_tml/tml.py b/src/thoughtspot_tml/tml.py index 1d7a366..e60f8e9 100644 --- a/src/thoughtspot_tml/tml.py +++ b/src/thoughtspot_tml/tml.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import asdict, dataclass -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any import copy import json import uuid @@ -35,7 +35,7 @@ def name(self) -> str: return self.connection.name @classmethod - def _loads(cls, tml_document: str) -> Dict[str, Any]: + def _loads(cls, tml_document: str) -> dict[str, Any]: # Handle backwards incompatible changes. document = _yaml.load(tml_document) @@ -246,7 +246,7 @@ def name(self) -> str: return self.model.name @classmethod - def _loads(cls, tml_document: str) -> Dict[str, Any]: + def _loads(cls, tml_document: str) -> dict[str, Any]: # DEV NOTE: @boonhapus, 2024/02/14 # The Worksheet V2 update include a python reserved word in the spec, which # python-betterproto automatically adds a trailing sunder to. This reverses it. @@ -255,7 +255,7 @@ def _loads(cls, tml_document: str) -> Dict[str, Any]: return _yaml.load(tml_document) - def _to_dict(self) -> Dict[str, Any]: + def _to_dict(self) -> dict[str, Any]: # DEV NOTE: @boonhapus, 2024/02/14 # The Worksheet V2 update include a python reserved word in the spec, which # python-betterproto automatically adds a trailing sunder to. This reverses it. diff --git a/src/thoughtspot_tml/types.py b/src/thoughtspot_tml/types.py index 0939f4a..a36614f 100644 --- a/src/thoughtspot_tml/types.py +++ b/src/thoughtspot_tml/types.py @@ -1,26 +1,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Annotated, Literal, Type, TypedDict, Union +from typing import TYPE_CHECKING, Annotated, Literal, TypedDict, Union -from thoughtspot_tml.tml import Answer, Cohort, Liveboard, Model, SQLView, Table, View, Worksheet +from thoughtspot_tml import TML, Answer, Cohort, Liveboard, Model, SQLView, Table, View, Worksheet if TYPE_CHECKING: - from typing import Any, Dict, List, Optional + from typing import Any, Optional from thoughtspot_tml.spotapp import Manifest # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reused Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TMLObject = Union[Table, View, SQLView, Worksheet, Answer, Liveboard, Cohort, Model] -TMLObjectType = Type[TMLObject] +TMLObject = Union[Table, View, SQLView, Worksheet, Answer, Liveboard, Cohort, Model, TML] +TMLObjectType = type[TMLObject] TMLType = Literal["table", "view", "sqlview", "worksheet", "answer", "liveboard", "pinboard", "cohort", "model"] TMLDocument = Annotated[str, "a TMLObject represented as a YAML 1.1 document"] GUID = Annotated[str, "A globally unique ID represented as a stringified UUID4"] class SpotAppInfo(TypedDict): - tml: List[TMLObject] + tml: list[TMLObject] manifest: Optional[Manifest] @@ -42,7 +42,7 @@ class TMLDocInfo(TypedDict): status: StatusCode type: str id: GUID - dependency: List[FileInfo] + dependency: list[FileInfo] class EDocExportResponse(TypedDict): @@ -51,7 +51,7 @@ class EDocExportResponse(TypedDict): class EDocExportResponses(TypedDict): - object: List[EDocExportResponse] + object: list[EDocExportResponse] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /connection/* Metadata Data Structure ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -75,22 +75,22 @@ class ExternalTable(TypedDict): description: str selected: bool linked: bool - columns: List[ExternalColumn] + columns: list[ExternalColumn] class ExternalSchema(TypedDict): name: Optional[str] - tables: List[ExternalTable] + tables: list[ExternalTable] class ExternalDatabase(TypedDict): name: Optional[str] isAutoCreated: bool - schemas: List[ExternalSchema] + schemas: list[ExternalSchema] class ConnectionMetadata(TypedDict): # for a full list of connection configurations # https://developers.thoughtspot.com/docs/?pageid=connections-api#connection-metadata - configuration: Dict[str, Any] - externalDatabases: List[ExternalDatabase] + configuration: dict[str, Any] + externalDatabases: list[ExternalDatabase] diff --git a/src/thoughtspot_tml/utils.py b/src/thoughtspot_tml/utils.py index d6b91e2..fd0a6fc 100644 --- a/src/thoughtspot_tml/utils.py +++ b/src/thoughtspot_tml/utils.py @@ -13,7 +13,8 @@ from thoughtspot_tml.tml import Answer, Cohort, Connection, Liveboard, Model, Pinboard, SQLView, Table, View, Worksheet if TYPE_CHECKING: - from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union + from collections.abc import Iterator + from typing import Any, Callable, Optional, Union from thoughtspot_tml.types import GUID, TMLDocInfo, TMLObject @@ -22,7 +23,7 @@ log = logging.getLogger(__name__) -def _recursive_scan(scriptability_object: Any, *, check: Optional[Callable[[Any], bool]] = None) -> List[Any]: +def _recursive_scan(scriptability_object: Any, *, check: Optional[Callable[[Any], bool]] = None) -> list[Any]: collect = [] is_container_type = lambda t: len(get_args(t)) > 0 # noqa: E731 @@ -48,7 +49,7 @@ def determine_tml_type( *, info: Optional[TMLDocInfo] = None, path: Optional[pathlib.Path] = None, -) -> Union[Type[Connection], Type[TMLObject]]: +) -> Union[type[Connection], type[TMLObject]]: """ Get the appropriate TML class based on input data. @@ -164,14 +165,14 @@ class EnvironmentGUIDMapper: def __init__(self, environment_transformer: Callable[[str], str] = str.upper): self.environment_transformer = environment_transformer - self._mapping: Dict[str, Dict[str, GUID]] = {} + self._mapping: dict[str, dict[str, GUID]] = {} - def __setitem__(self, guid: GUID, value: Tuple[str, GUID]) -> None: + def __setitem__(self, guid: GUID, value: tuple[str, GUID]) -> None: environment, guid_to_add = value environment = self.environment_transformer(environment) try: - envts: Dict[str, GUID] = self[guid] + envts: dict[str, GUID] = self[guid] except KeyError: new_key = guid_to_add envts = {environment: guid_to_add} @@ -180,11 +181,11 @@ def __setitem__(self, guid: GUID, value: Tuple[str, GUID]) -> None: self._mapping.pop(old_key) envts[environment] = guid_to_add - new_key = "__".join(envts.values()) # type: ignore[assignment] + new_key = "__".join(envts.values()) self._mapping.setdefault(new_key, {}).update(envts) - def __getitem__(self, guid: GUID) -> Dict[str, GUID]: + def __getitem__(self, guid: GUID) -> dict[str, GUID]: for guids_across_envts, envts in self._mapping.items(): if guid in guids_across_envts.split("__"): return envts @@ -204,7 +205,7 @@ def set(self, src_guid: GUID, *, environment: str, guid: GUID) -> None: """ self[src_guid] = (environment, guid) - def get(self, guid: GUID, *, default: Any = _UNDEFINED) -> Dict[str, GUID]: + def get(self, guid: GUID, *, default: Any = _UNDEFINED) -> dict[str, GUID]: """ Retrieve a GUID mapping. @@ -222,7 +223,7 @@ def get(self, guid: GUID, *, default: Any = _UNDEFINED) -> Dict[str, GUID]: return retval - def generate_mapping(self, from_environment: str, to_environment: str) -> Dict[GUID, GUID]: + def generate_mapping(self, from_environment: str, to_environment: str) -> dict[GUID, GUID]: """ Create a mapping of GUIDs between two environments. @@ -236,7 +237,7 @@ def generate_mapping(self, from_environment: str, to_environment: str) -> Dict[G """ from_environment = self.environment_transformer(from_environment) to_environment = self.environment_transformer(to_environment) - mapping: Dict[GUID, GUID] = {} + mapping: dict[GUID, GUID] = {} for envts in self._mapping.values(): envt_a = envts.get(from_environment, None) @@ -280,7 +281,7 @@ def read(cls, path: pathlib.Path, environment_transformer: Callable[[str], str] instance._mapping = data return instance - def save(self, path: pathlib.Path, *, info: Optional[Dict[str, Any]] = None) -> None: + def save(self, path: pathlib.Path, *, info: Optional[dict[str, Any]] = None) -> None: """ Save the guid mapping to file. @@ -299,7 +300,7 @@ def save(self, path: pathlib.Path, *, info: Optional[Dict[str, Any]] = None) -> with pathlib.Path(path).open(mode="w", encoding="UTF-8") as j: json.dump(data, j, indent=4) - def get_environment_guids(self, *, source: str, destination: str) -> Iterator[Tuple[GUID, GUID]]: + def get_environment_guids(self, *, source: str, destination: str) -> Iterator[tuple[GUID, GUID]]: """ Iterate through all guid pairs between source and destination. @@ -323,7 +324,7 @@ def __str__(self) -> str: def disambiguate( tml: TMLObject, *, - guid_mapping: Dict[str, GUID], + guid_mapping: dict[str, GUID], remap_object_guid: bool = True, delete_unmapped_guids: bool = False, ) -> TMLObject: @@ -352,7 +353,7 @@ def disambiguate( tml.guid = guid_mapping[tml.guid] elif delete_unmapped_guids: - tml.guid = None # type: ignore[assignment] + tml.guid = None IS_IDENTITY = ft.partial(lambda A: isinstance(A, (_scriptability.Identity, _scriptability.SchemaSchemaTable))) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 8c283d7..c40692f 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -13,7 +13,7 @@ def _(): with raises(TMLDecodeError) as exc: Answer.loads(tml_document="😅: INVALID") - assert "supplied data does not produce a valid TML (Answer) document" in str(exc.raised) + assert "Unrecognized attribute in the TML spec:" in str(exc.raised) @test("TMLDecodeError on invalid file input") @@ -23,5 +23,5 @@ def _(): with raises(TMLDecodeError) as exc: Answer.load(path=fp) - assert "is not a valid TML (Answer) file" in str(exc.raised) - assert "syntax error on line 2, around column 9" in str(exc.raised) + assert "may not be a valid Answer file" in str(exc.raised) + assert "Syntax error on line 2, around column 9" in str(exc.raised)