context_test.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import functools
  2. import pytest
  3. from unittest.mock import Mock, patch
  4. from pydantic import BaseModel, Field
  5. from pydantic_core import ValidationError
  6. import cipy
  7. from cipy.context import _Stub, Results
  8. from cipy.common import Inputs, Outputs
  9. def test_required_is_secretly_none() -> None:
  10. class Model(BaseModel):
  11. inputs: Inputs = cipy.required()
  12. model = Model()
  13. assert model.inputs is None
  14. def test_required_will_fail_if_reconstructed() -> None:
  15. class Model(BaseModel):
  16. inputs: Inputs = cipy.required()
  17. model = Model()
  18. with pytest.raises(ValidationError):
  19. model = Model(**vars(model))
  20. def test_fabricate_empty_inputs() -> None:
  21. class Model(BaseModel):
  22. inputs: Inputs = cipy.required()
  23. model = Model()
  24. context = cipy.Context()
  25. assert context.fabricate(model, "inputs") is not None
  26. assert model.inputs is not None
  27. def test_fabricate_empty_outputs() -> None:
  28. class Model(BaseModel):
  29. outputs: Outputs = cipy.required()
  30. model = Model()
  31. context = cipy.Context()
  32. assert context.fabricate(model, "outputs") is not None
  33. assert model.outputs is not None
  34. def test_fabricate_data() -> None:
  35. class Model(BaseModel):
  36. class _Inputs(Inputs):
  37. foo: int = 0
  38. bar: str = ""
  39. inputs: _Inputs = cipy.required()
  40. model = Model()
  41. context = cipy.Context()
  42. assert model.inputs is None
  43. assert context.fabricate(model, "inputs") is not None
  44. assert model.inputs.foo == 0
  45. assert model.inputs.bar == ""
  46. def test_fabricate_can_require_values() -> None:
  47. class Model(BaseModel):
  48. class _Inputs(Inputs):
  49. foo: int = cipy.required()
  50. bar: str = ""
  51. inputs: _Inputs = cipy.required()
  52. model = Model()
  53. context = cipy.Context()
  54. assert model.inputs is None
  55. with pytest.raises(ValidationError):
  56. context.fabricate(model, "inputs")
  57. def test_fabricate_can_use_prior_values() -> None:
  58. class Model(BaseModel):
  59. class _Inputs(Inputs):
  60. foo: int
  61. bar: str = ""
  62. inputs: _Inputs = cipy.required()
  63. model = Model(inputs=Model._Inputs(foo=1))
  64. context = cipy.Context()
  65. assert context.fabricate(model, "inputs") is not None
  66. assert model.inputs.foo == 1
  67. def test_fabricate_can_provide_extras() -> None:
  68. class Model(BaseModel):
  69. class _Inputs(Inputs):
  70. foo: int
  71. bar: str = ""
  72. inputs: _Inputs = cipy.required()
  73. model = Model()
  74. context = cipy.Context()
  75. assert context.fabricate(model, "inputs", {"foo": 1}) is not None
  76. assert model.inputs.foo == 1
  77. def test_fabricate_can_use_reference() -> None:
  78. class Model(BaseModel):
  79. class _Inputs(Inputs):
  80. foo: int = cipy.context("example.foo")
  81. bar: str = ""
  82. inputs: _Inputs = cipy.required()
  83. model = Model()
  84. context = cipy.Context(example={"foo": 5})
  85. assert context.fabricate(model, "inputs") is not None
  86. assert model.inputs.foo == 5
  87. def test_fabricate_reference_env(monkeypatch: pytest.MonkeyPatch) -> None:
  88. class Model(BaseModel):
  89. class _Inputs(Inputs):
  90. foo: int = cipy.context("env.FOO")
  91. bar: str = ""
  92. inputs: _Inputs = cipy.required()
  93. model = Model()
  94. context = cipy.Context()
  95. monkeypatch.setenv("FOO", "5")
  96. assert context.fabricate(model, "inputs") is not None
  97. assert model.inputs.foo == 5
  98. def test_fabricate_throws_on_missing_item() -> None:
  99. class Model(BaseModel):
  100. class _Inputs(Inputs):
  101. foo: int = cipy.context("example.foo")
  102. bar: str = ""
  103. inputs: _Inputs = cipy.required()
  104. model = Model()
  105. context = cipy.Context(example={})
  106. with pytest.raises(AttributeError) as ex:
  107. context.fabricate(model, "inputs")
  108. assert "not found" in str(ex)
  109. def test_fabricate_throws_on_null_parent() -> None:
  110. class Model(BaseModel):
  111. class _Inputs(Inputs):
  112. foo: int = cipy.context("example.foo")
  113. bar: str = ""
  114. inputs: _Inputs = cipy.required()
  115. model = Model()
  116. context = cipy.Context(example=None)
  117. with pytest.raises(AttributeError) as ex:
  118. context.fabricate(model, "inputs")
  119. assert "NULL" in str(ex)
  120. def test_fabricate_stub() -> None:
  121. class Model(BaseModel, arbitrary_types_allowed=True):
  122. class _Inputs(Inputs):
  123. foo: int = Field(default=_Stub()) # type: ignore[assignment]
  124. bar: str = ""
  125. inputs: _Inputs = cipy.required()
  126. logger: Mock = Mock()
  127. model = Model()
  128. context = cipy.Context()
  129. assert context.fabricate(model, "inputs") is not None
  130. assert model.inputs.foo == 0
  131. def test_fabricate_stub_union_with_first_type() -> None:
  132. class Model(BaseModel, arbitrary_types_allowed=True):
  133. class _Inputs(Inputs):
  134. foo: int | str = Field(default=_Stub()) # type: ignore[assignment]
  135. bar: str = ""
  136. inputs: _Inputs = cipy.required()
  137. logger: Mock = Mock()
  138. model = Model()
  139. context = cipy.Context()
  140. assert context.fabricate(model, "inputs") is not None
  141. assert model.inputs.foo == 0
  142. def test_fabricate_stub_produces_logs() -> None:
  143. class Model(BaseModel, arbitrary_types_allowed=True):
  144. class _Inputs(Inputs):
  145. foo: int = Field(default=_Stub()) # type: ignore[assignment]
  146. bar: str = ""
  147. inputs: _Inputs = cipy.required()
  148. logger: Mock = Mock()
  149. model = Model()
  150. context = cipy.Context()
  151. assert context.fabricate(model, "inputs") is not None
  152. model.logger.warning.assert_called_once()
  153. model.logger.debug.assert_called_once()
  154. def test_context_extend_does_not_pollute() -> None:
  155. context = cipy.Context()
  156. with context.extend(var=1) as ex:
  157. assert ex.var == 1
  158. assert not hasattr(context, "var")
  159. def test_results_is_constructed_empty() -> None:
  160. results = Results()
  161. with pytest.raises(AttributeError):
  162. results["foo"]
  163. def test_results_cannot_insert_empty_key() -> None:
  164. results = Results()
  165. results[""] = Results.Item()
  166. assert "" not in results
  167. with pytest.raises(AttributeError):
  168. results[""]
  169. def test_results_can_insert_and_access_by_key_or_attr() -> None:
  170. results = Results()
  171. results["foo"] = Results.Item()
  172. assert "foo" in results
  173. assert hasattr(results, "foo")
  174. assert results.foo is results["foo"]
  175. def test_fabricate_with_results_can_return_stub() -> None:
  176. class Model(BaseModel, arbitrary_types_allowed=True):
  177. class _Inputs(Inputs):
  178. foo: int = cipy.context("steps.foo.outputs.value")
  179. inputs: _Inputs = cipy.required()
  180. logger: Mock = Mock()
  181. model = Model()
  182. context = cipy.Context(steps=Results())
  183. context.steps["foo"] = Results.Item()
  184. call = functools.partial(cipy.Context.__call__, context)
  185. with patch.object(cipy.Context, "__call__", wraps=call) as mock:
  186. assert context.fabricate(model, "inputs") is not None
  187. mock.assert_called_once()