|
4 | 4 | """ |
5 | 5 | PyTest configuration and fixtures for pyatlan_v9 unit tests. |
6 | 6 | These fixtures provide test utilities for msgspec-based models. |
7 | | -
|
8 | | -This module also applies compatibility patches that allow v9 msgspec models |
9 | | -to work seamlessly with the legacy Pydantic-based client layer. The patches |
10 | | -MUST run at module load time (before any client classes are imported) so that |
11 | | -Pydantic's cached validators pick up the patched behaviour. |
12 | 7 | """ |
13 | 8 |
|
14 | | -# --------------------------------------------------------------------------- |
15 | | -# v9 ↔ legacy compatibility patches (executed at import time) |
16 | | -# --------------------------------------------------------------------------- |
17 | | - |
18 | | -import msgspec |
19 | | -from pydantic.v1.json import ENCODERS_BY_TYPE |
20 | | -from pydantic.v1.main import ModelMetaclass |
21 | | - |
22 | | -from pyatlan.model.assets.core.asset import Asset as _LegacyAsset |
23 | | -from pyatlan_v9.model.assets import Asset as _V9Asset |
24 | | - |
25 | | -# ---------- Patch 1: Pydantic isinstance --------------------------------- |
26 | | -# Pydantic's ModelMetaclass (which extends ABCMeta) overrides |
27 | | -# ``__instancecheck__`` and short-circuits with a |
28 | | -# ``hasattr(instance, '__post_root_validators__')`` guard that always rejects |
29 | | -# msgspec Structs. We relax that guard so that a v9 model whose *class name* |
30 | | -# appears in the MRO of the checked type is accepted. |
31 | | -_original_instancecheck = ModelMetaclass.__instancecheck__ |
32 | | - |
33 | | - |
34 | | -def _v9_instancecheck(self, instance): |
35 | | - """Accept v9 msgspec models where MRO names match.""" |
36 | | - if _original_instancecheck(self, instance): |
37 | | - return True |
38 | | - if isinstance(instance, msgspec.Struct): |
39 | | - v9_mro_names = {cls.__name__ for cls in type(instance).__mro__} |
40 | | - if self.__name__ in v9_mro_names: |
41 | | - return True |
42 | | - return False |
43 | | - |
44 | | - |
45 | | -ModelMetaclass.__instancecheck__ = _v9_instancecheck |
46 | | - |
47 | | -# ---------- Patch 2: Pydantic JSON encoder -------------------------------- |
48 | | -# Register ``msgspec.Struct`` so that Pydantic's json-serialisation path |
49 | | -# (used by BulkRequest.json() and AtlanRequest.json()) can serialise v9 |
50 | | -# models without raising ``TypeError: not JSON serializable``. |
51 | | -ENCODERS_BY_TYPE[msgspec.Struct] = lambda o: msgspec.to_builtins(o) |
52 | | - |
53 | | -# ---------- Patch 3: Asset._convert_to_real_type_ ------------------------- |
54 | | -# The legacy @validate_arguments decorator calls |
55 | | -# ``Asset._convert_to_real_type_(data)`` when validating Union[Asset, …] |
56 | | -# parameters. This patch makes it accept v9 Struct instances as-is. |
57 | | -_original_convert = _LegacyAsset._convert_to_real_type_.__func__ |
58 | | - |
59 | | - |
60 | | -@classmethod # type: ignore[misc] |
61 | | -def _convert_to_real_type_v9_compat(cls, data): |
62 | | - """Accept v9 msgspec models in legacy Pydantic validation.""" |
63 | | - if isinstance(data, _V9Asset): |
64 | | - return data |
65 | | - return _original_convert(cls, data) |
66 | | - |
67 | | - |
68 | | -_LegacyAsset._convert_to_real_type_ = _convert_to_real_type_v9_compat # type: ignore[assignment] |
69 | | - |
70 | | -# ---------- Patch 4: BulkRequest.process_attributes ----------------------- |
71 | | -# The legacy BulkRequest validator tries to access Pydantic-specific |
72 | | -# attributes (``remove_relationship_attributes``, ``attributes.__fields_set__``, |
73 | | -# ``.dict()``) on every entity. v9 Struct models don't have those. |
74 | | -# We monkey-patch the validator's *code object* so that any reference captured |
75 | | -# in Pydantic's lambda closures automatically gets the updated behaviour. |
76 | | -from pyatlan.model import core as _core_module # noqa: E402 |
77 | | - |
78 | | -_core_module.msgspec = msgspec # inject into module globals |
79 | | - |
80 | | -from pyatlan.model.core import BulkRequest # noqa: E402 |
81 | | - |
82 | | -_original_process_func = BulkRequest.process_attributes.__func__ |
83 | | - |
84 | | - |
85 | | -def _new_process_attributes(cls, asset): |
86 | | - """BulkRequest validator that skips v9 models.""" |
87 | | - if isinstance(asset, msgspec.Struct): |
88 | | - return asset |
89 | | - # --- original legacy logic (inlined) --- |
90 | | - from pyatlan.model.assets import Asset # noqa: F811 |
91 | | - |
92 | | - if not isinstance(asset, Asset): |
93 | | - return asset |
94 | | - |
95 | | - exclude_attributes = set() |
96 | | - asset.remove_relationship_attributes = {} |
97 | | - asset.append_relationship_attributes = {} |
98 | | - for attribute in asset.attributes.__fields_set__: |
99 | | - exclude_attributes.update(cls.process_relationship_attributes(asset, attribute)) |
100 | | - exclude_relationship_attributes = { |
101 | | - key: True |
102 | | - for key in [ |
103 | | - "remove_relationship_attributes", |
104 | | - "append_relationship_attributes", |
105 | | - ] |
106 | | - if not getattr(asset, key) |
107 | | - } |
108 | | - if exclude_attributes: |
109 | | - exclude_relationship_attributes = { |
110 | | - **{"attributes": exclude_attributes}, |
111 | | - **exclude_relationship_attributes, |
112 | | - } |
113 | | - return asset.__class__( |
114 | | - **asset.dict( |
115 | | - by_alias=True, |
116 | | - exclude_unset=True, |
117 | | - exclude=exclude_relationship_attributes, |
118 | | - ) |
119 | | - ) |
120 | | - |
121 | | - |
122 | | -_original_process_func.__code__ = _new_process_attributes.__code__ |
123 | | - |
124 | | -# --------------------------------------------------------------------------- |
125 | | -# NOW import the client (triggers AssetClient import with @validate_arguments) |
126 | | -# --------------------------------------------------------------------------- |
127 | | -from json import load # noqa: E402 |
128 | | -from pathlib import Path # noqa: E402 |
129 | | -from unittest.mock import patch # noqa: E402 |
130 | | - |
131 | | -import pytest # noqa: E402 |
132 | | - |
133 | | -from pyatlan.client.atlan import AtlanClient # noqa: E402 |
134 | | -from pyatlan_v9.model.serde import Serde, get_serde # noqa: E402 |
| 9 | +from json import load |
| 10 | +from pathlib import Path |
| 11 | +from unittest.mock import patch |
| 12 | + |
| 13 | +import pytest |
| 14 | + |
| 15 | +from pyatlan.client.atlan import AtlanClient |
| 16 | +from pyatlan_v9.model.serde import Serde, get_serde |
135 | 17 |
|
136 | 18 | # Use the same test data directory as the original tests |
137 | 19 | TEST_DATA_DIR = Path(__file__).parent.parent.parent / "tests" / "unit" / "data" |
|
0 commit comments