13 from mgr_module
import CLICommand
17 T
= TypeVar("T", bound
="Parent")
21 def __init__(self
, name
, val
=None):
26 def to_simplified(self
) -> Dict
[str, Any
]:
28 "version": self
.version
,
34 class JSONer(Simpler
):
35 def to_json(self
) -> Dict
[str, Any
]:
36 d
= self
.to_simplified()
41 def from_json(cls
: Type
[T
], data
) -> T
:
42 o
= cls(data
.get("name", ""), data
.get("value"))
43 o
.version
= data
.get("version", 1) + 1
47 class YAMLer(Simpler
):
48 def to_yaml(self
) -> Dict
[str, Any
]:
49 d
= self
.to_simplified()
54 @pytest.mark
.parametrize(
55 "obj, compatible, json_val",
58 ({"name": "foobar"}, False, '{"name": "foobar"}'),
59 ([1, 2, 3], False, "[1, 2, 3]"),
60 (JSONer("bob"), False, '{"name": "bob", "value": {}, "version": 1}'),
64 '{"name": "betty", "value": 77, "version": 1}',
67 ({"name": "foobar"}, True, '{"name": "foobar"}'),
71 '{"_json": true, "name": "bob", "value": {}, "version": 1}',
75 def test_format_json(obj
: Any
, compatible
: bool, json_val
: str):
77 object_format
.ObjectFormatAdapter(
78 obj
, compatible
=compatible
, json_indent
=None
84 @pytest.mark
.parametrize(
85 "obj, compatible, yaml_val",
88 ({"name": "foobar"}, False, "name: foobar\n"),
90 {"stuff": [1, 88, 909, 32]},
92 "stuff:\n- 1\n- 88\n- 909\n- 32\n",
95 JSONer("zebulon", "999"),
97 "name: zebulon\nvalue: '999'\nversion: 1\n",
100 ({"name": "foobar"}, True, "name: foobar\n"),
102 YAMLer("thingy", "404"),
104 "_yaml: true\nname: thingy\nvalue: '404'\nversion: 1\n",
108 def test_format_yaml(obj
: Any
, compatible
: bool, yaml_val
: str):
110 object_format
.ObjectFormatAdapter(
111 obj
, compatible
=compatible
118 def __init__(self
, v
) -> None:
121 def mgr_return_value(self
) -> int:
125 @pytest.mark
.parametrize(
129 ({"fish": "sticks"}, 0),
135 def test_return_value(obj
: Any
, ret
: int):
136 rva
= object_format
.ReturnValueAdapter(obj
)
137 # a ReturnValueAdapter instance meets the ReturnValueProvider protocol.
138 assert object_format
._is
_return
_value
_provider
(rva
)
139 assert rva
.mgr_return_value() == ret
142 def test_valid_formats():
143 ofa
= object_format
.ObjectFormatAdapter({"fred": "wilma"})
144 vf
= ofa
.valid_formats()
151 def test_error_response_exceptions():
152 err
= object_format
.ErrorResponseBase()
153 with pytest
.raises(NotImplementedError):
154 err
.format_response()
156 err
= object_format
.UnsupportedFormat("cheese")
157 assert err
.format_response() == (-22, "", "Unsupported format: cheese")
159 err
= object_format
.UnknownFormat("chocolate")
160 assert err
.format_response() == (-22, "", "Unknown format name: chocolate")
163 @pytest.mark
.parametrize(
164 "value, format, result",
166 ({}, None, (0, "{}", "")),
167 ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
168 ({"blat": True}, "yaml", (0, "blat: true\n", "")),
169 ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
170 ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
172 JSONer("hoop", "303"),
174 (0, "name: hoop\nvalue: '303'\nversion: 1\n", ""),
178 def test_responder_decorator_default(
179 value
: Any
, format
: Optional
[str], result
: Tuple
[int, str, str]
181 @object_format.Responder()
182 def orf_value(format
: Optional
[str] = None):
185 assert orf_value(format
=format
) == result
188 class PhonyMultiYAMLFormatAdapter(object_format
.ObjectFormatAdapter
):
189 """This adapter puts a yaml document/directive separator line
190 before all output. It doesn't actully support multiple documents.
192 def format_yaml(self
):
193 yml
= super().format_yaml()
194 return "---\n{}".format(yml
)
197 @pytest.mark
.parametrize(
198 "value, format, result",
200 ({}, None, (0, "{}", "")),
201 ({"blat": True}, "json", (0, '{\n "blat": true\n}', "")),
202 ({"blat": True}, "yaml", (0, "---\nblat: true\n", "")),
203 ({"blat": True}, "toml", (-22, "", "Unknown format name: toml")),
204 ({"blat": True}, "xml", (-22, "", "Unsupported format: xml")),
206 JSONer("hoop", "303"),
208 (0, "---\nname: hoop\nvalue: '303'\nversion: 1\n", ""),
212 def test_responder_decorator_custom(
213 value
: Any
, format
: Optional
[str], result
: Tuple
[int, str, str]
215 @object_format.Responder(PhonyMultiYAMLFormatAdapter
)
216 def orf_value(format
: Optional
[str] = None):
219 assert orf_value(format
=format
) == result
222 class FancyDemoAdapter(PhonyMultiYAMLFormatAdapter
):
223 """This adapter demonstrates adding formatting for other formats
224 like xml and plain text.
226 def format_xml(self
) -> str:
227 name
= self
.obj
.get("name")
228 size
= self
.obj
.get("size")
229 return f
'<object name="{name}" size="{size}" />'
231 def format_plain(self
) -> str:
232 name
= self
.obj
.get("name")
233 size
= self
.obj
.get("size")
234 es
= 'es' if size
!= 1 else ''
235 return f
"{size} box{es} of {name}"
239 """Class to stand in for a mgr module, used to test CLICommand integration."""
241 @CLICommand("alpha one", perm
="rw")
242 @object_format.Responder()
243 def alpha_one(self
, name
: str = "default") -> Dict
[str, str]:
250 @CLICommand("beta two", perm
="r")
251 @object_format.Responder()
253 self
, name
: str = "default", format
: Optional
[str] = None
261 @CLICommand("gamma three", perm
="rw")
262 @object_format.Responder(FancyDemoAdapter
)
263 def gamma_three(self
, size
: int = 0) -> Dict
[str, Any
]:
264 return {"name": "funnystuff", "size": size
}
266 @CLICommand("z_err", perm
="rw")
267 @object_format.ErrorResponseHandler()
268 def z_err(self
, name
: str = "default") -> Tuple
[int, str, str]:
270 raise object_format
.ErrorResponse(f
"{name} bad")
273 @CLICommand("empty one", perm
="rw")
274 @object_format.EmptyResponder()
275 def empty_one(self
, name
: str = "default", retval
: Optional
[int] = None) -> None:
276 # in real code, this would be making some sort of state change
277 # but we need to handle erors still
281 raise object_format
.ErrorResponse(name
, return_value
=retval
)
284 @CLICommand("empty bad", perm
="rw")
285 @object_format.EmptyResponder()
286 def empty_bad(self
, name
: str = "default") -> int:
287 # in real code, this would be making some sort of state change
291 @pytest.mark
.parametrize(
292 "prefix, can_format, args, response",
297 {"name": "moonbase"},
300 '{\n "alpha": "one",\n "name": "moonbase",\n "weight": 300\n}',
308 {"name": "moonbase2", "format": "yaml"},
311 "alpha: one\nname: moonbase2\nweight: 300\n",
319 {"name": "moonbase2", "format": "chocolate"},
323 "Unknown format name: chocolate",
333 '{\n "beta": "two",\n "name": "blocker",\n "weight": 72\n}',
341 {"name": "test", "format": "yaml"},
344 "beta: two\nname: test\nweight: 72\n",
352 {"name": "test", "format": "plain"},
356 "Unsupported format: plain",
366 '{\n "name": "funnystuff",\n "size": 0\n}',
374 {"size": 1, "format": "json"},
377 '{\n "name": "funnystuff",\n "size": 1\n}',
385 {"size": 1, "format": "plain"},
388 "1 box of funnystuff",
396 {"size": 2, "format": "plain"},
399 "2 boxes of funnystuff",
407 {"size": 2, "format": "xml"},
410 '<object name="funnystuff" size="2" />',
418 {"size": 2, "format": "toml"},
422 "Unknown format name: toml",
451 {"name": "zucchini"},
469 # Ensure setting return_value to zero even on an exception is honored
473 {"name": "pow", "retval": 0},
482 def test_cli_with_decorators(prefix
, can_format
, args
, response
):
484 cmd
= CLICommand
.COMMANDS
[prefix
]
485 assert cmd
.call(dd
, args
, None) == response
486 # slighly hacky way to check that the CLI "knows" about a --format option
487 # checking the extra_args feature of the Decorators that provide them (Responder)
489 assert 'name=format,' in cmd
.args
492 def test_error_response():
493 e1
= object_format
.ErrorResponse("nope")
494 assert e1
.format_response() == (-22, "", "nope")
495 assert e1
.return_value
== -22
496 assert e1
.errno
== 22
497 assert "ErrorResponse" in repr(e1
)
498 assert "nope" in repr(e1
)
499 assert e1
.mgr_return_value() == -22
502 open("/this/is_/extremely_/unlikely/_to/exist.txt")
503 except Exception as e
:
504 e2
= object_format
.ErrorResponse
.wrap(e
)
505 r
= e2
.format_response()
506 assert r
[0] == -errno
.ENOENT
508 assert "No such file or directory" in r
[2]
509 assert "ErrorResponse" in repr(e2
)
510 assert "No such file or directory" in repr(e2
)
511 assert r
[0] == e2
.mgr_return_value()
513 e3
= object_format
.ErrorResponse
.wrap(RuntimeError("blat"))
514 r
= e3
.format_response()
515 assert r
[0] == -errno
.EINVAL
517 assert "blat" in r
[2]
518 assert r
[0] == e3
.mgr_return_value()
520 # A custom exception type with an errno property
522 class MyCoolException(Exception):
523 def __init__(self
, err_msg
: str, errno
: int = 0) -> None:
524 super().__init
__(errno
, err_msg
)
526 self
.err_msg
= err_msg
528 def __str__(self
) -> str:
531 e4
= object_format
.ErrorResponse
.wrap(MyCoolException("beep", -17))
532 r
= e4
.format_response()
535 assert r
[2] == "beep"
536 assert e4
.mgr_return_value() == -17
538 e5
= object_format
.ErrorResponse
.wrap(MyCoolException("ok, fine", 0))
539 r
= e5
.format_response()
542 assert r
[2] == "ok, fine"
544 e5
= object_format
.ErrorResponse
.wrap(MyCoolException("no can do", 8))
545 r
= e5
.format_response()
548 assert r
[2] == "no can do"
550 # A custom exception type that inherits from ErrorResponseBase
552 class MyErrorResponse(object_format
.ErrorResponseBase
):
553 def __init__(self
, err_msg
: str, return_value
: int):
554 super().__init
__(self
, err_msg
)
556 self
.return_value
= return_value
558 def format_response(self
):
559 return self
.return_value
, "", self
.msg
562 e6
= object_format
.ErrorResponse
.wrap(MyErrorResponse("yeah, sure", 0))
563 r
= e6
.format_response()
566 assert r
[2] == "yeah, sure"
567 assert isinstance(e5
, object_format
.ErrorResponseBase
)
568 assert isinstance(e6
, MyErrorResponse
)
570 e7
= object_format
.ErrorResponse
.wrap(MyErrorResponse("no can do", -8))
571 r
= e7
.format_response()
574 assert r
[2] == "no can do"
575 assert isinstance(e7
, object_format
.ErrorResponseBase
)
576 assert isinstance(e7
, MyErrorResponse
)
579 def test_empty_responder_return_check():
581 with pytest
.raises(ValueError):
582 CLICommand
.COMMANDS
["empty bad"].call(dd
, {}, None)