Skip to content

Instantly share code, notes, and snippets.

@dwt
Last active February 23, 2019 15:14
Show Gist options
  • Save dwt/cf97661734c315731fdc647319ae7d03 to your computer and use it in GitHub Desktop.
Save dwt/cf97661734c315731fdc647319ae7d03 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Hosted copy:\n",
"* https://tinyurl.com/pyexpect-ipynb\n",
"* https://gist.github.com/dwt/cf97661734c315731fdc647319ae7d03\n",
"\n",
"Project:\n",
"* https://pypi.org/project/pyexpect/\n",
"* https://bitbucket.org/dwt/pyexpect"
]
},
{
"cell_type": "markdown",
"metadata": {
"toc-hr-collapsed": false
},
"source": [
"# Friendly python assertions in the expect style\n",
"\n",
"Hello, my name is Martin Häcker (<spamfaenger@gmx.de> https://github.com/dwt/).\n",
"\n",
"I like Singing, Juggling, Bouldering, playing Go, Paragliding and ... unit testing.\n",
"\n",
"## Unit testing == The art of teaching your code to tell you when you fail.\n",
"\n",
"... but, what does that mean in the practice of writing tests?\n",
"\n",
"* Clear definition of actual and expected value\n",
"* Error messages that take advantage of this\n",
"* and map clearly back to the code you write\n",
"* Expressive assertions/ matchers"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"inputHidden": false
},
"outputs": [],
"source": [
"import unittest\n",
"\n",
"def run_test(a_test_class):\n",
" suite = unittest.TestLoader().loadTestsFromTestCase(a_test_class)\n",
" runner = unittest.TextTestRunner(verbosity=0)\n",
" runner.run(suite)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import ipytest\n",
"import ipytest.magics\n",
"\n",
"ipytest.config.rewrite_asserts = True\n",
"\n",
"__file__ = \"pyexpect.ipynb\""
]
},
{
"cell_type": "markdown",
"metadata": {
"inputHidden": false,
"outputHidden": false
},
"source": [
"## Expected, Actual?"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"======================================================================\n",
"FAIL: test_equals (__main__.ExpectedActualConfusionTest)\n",
"----------------------------------------------------------------------\n",
"Traceback (most recent call last):\n",
" File \"<ipython-input-4-a19c8d7db4a9>\", line 6, in test_equals\n",
" self.assertEqual(sorted(unsorted), [1,2,3,5])\n",
"AssertionError: Lists differ: [1, 2, 3, 5, 8] != [1, 2, 3, 5]\n",
"\n",
"First list contains 1 additional elements.\n",
"First extra element 4:\n",
"8\n",
"\n",
"- [1, 2, 3, 5, 8]\n",
"? ---\n",
"\n",
"+ [1, 2, 3, 5]\n",
"\n",
"======================================================================\n",
"FAIL: test_equals_reversed (__main__.ExpectedActualConfusionTest)\n",
"----------------------------------------------------------------------\n",
"Traceback (most recent call last):\n",
" File \"<ipython-input-4-a19c8d7db4a9>\", line 10, in test_equals_reversed\n",
" self.assertEqual([1,2,3,5], sorted(unsorted))\n",
"AssertionError: Lists differ: [1, 2, 3, 5] != [1, 2, 3, 5, 8]\n",
"\n",
"Second list contains 1 additional elements.\n",
"First extra element 4:\n",
"8\n",
"\n",
"- [1, 2, 3, 5]\n",
"+ [1, 2, 3, 5, 8]\n",
"? +++\n",
"\n",
"\n",
"----------------------------------------------------------------------\n",
"Ran 2 tests in 0.001s\n",
"\n",
"FAILED (failures=2)\n"
]
}
],
"source": [
"unsorted = [3,2,1,5,8]\n",
"\n",
"class ExpectedActualConfusionTest(unittest.TestCase):\n",
" \n",
" def test_equals(self):\n",
" self.assertEqual(sorted(unsorted), [1,2,3,5])\n",
" # actual, expected?\n",
"\n",
" def test_equals_reversed(self):\n",
" self.assertEqual([1,2,3,5], sorted(unsorted))\n",
" # expected, actual?\n",
"\n",
"run_test(ExpectedActualConfusionTest)"
]
},
{
"cell_type": "markdown",
"metadata": {
"inputHidden": false,
"outputHidden": false
},
"source": [
"## Error messages"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false,
"inputHidden": false,
"outputHidden": false
},
"outputs": [
{
"ename": "AssertionError",
"evalue": "assert 3 == 4",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mfoo\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbar\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mfoo\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mbar\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m: assert 3 == 4"
]
}
],
"source": [
"foo, bar = 3, 4\n",
"\n",
"assert foo == bar"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false,
"inputHidden": false,
"outputHidden": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"======================================================================\n",
"FAIL: test_equals (__main__.ErrorMessageTest)\n",
"----------------------------------------------------------------------\n",
"Traceback (most recent call last):\n",
" File \"<ipython-input-9-e3450c60df18>\", line 2, in test_equals\n",
" def test_equals(self): self.assertEqual(1,2)\n",
"AssertionError: 1 != 2\n",
"\n",
"======================================================================\n",
"FAIL: test_in (__main__.ErrorMessageTest)\n",
"----------------------------------------------------------------------\n",
"Traceback (most recent call last):\n",
" File \"<ipython-input-9-e3450c60df18>\", line 3, in test_in\n",
" def test_in(self): self.assertIn('ab', 'bbb')\n",
"AssertionError: 'ab' not found in 'bbb'\n",
"\n",
"======================================================================\n",
"FAIL: test_is_not (__main__.ErrorMessageTest)\n",
"----------------------------------------------------------------------\n",
"Traceback (most recent call last):\n",
" File \"<ipython-input-9-e3450c60df18>\", line 4, in test_is_not\n",
" def test_is_not(self): self.assertIsNot(1, 1)\n",
"AssertionError: unexpectedly identical: 1\n",
"\n",
"----------------------------------------------------------------------\n",
"Ran 3 tests in 0.000s\n",
"\n",
"FAILED (failures=3)\n"
]
}
],
"source": [
"class ErrorMessageTest(unittest.TestCase):\n",
" def test_equals(self): self.assertEqual(1,2)\n",
" def test_in(self): self.assertIn('ab', 'bbb')\n",
" def test_is_not(self): self.assertIsNot(1, 1)\n",
"\n",
"run_test(ErrorMessageTest)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================================================== test session starts ====================================================================\n",
"platform darwin -- Python 3.7.2, pytest-4.2.0, py-1.7.0, pluggy-0.8.1\n",
"rootdir: /Users/dwt/Code/Projekte/pyexpect, inifile:\n",
"collected 1 item\n",
"\n",
"pyexpect.py F [100%]\n",
"\n",
"========================================================================= FAILURES ==========================================================================\n",
"__________________________________________________________________ test_bad_error_message ___________________________________________________________________\n",
"\n",
" def test_bad_error_message():\n",
"> assert isinstance(obj, int)\n",
"E assert False\n",
"E + where False = isinstance(3, int)\n",
"\n",
"<ipython-input-4-623ff194f1da>:5: AssertionError\n",
"================================================================= 1 failed in 0.04 seconds ==================================================================\n"
]
}
],
"source": [
"%%run_pytest[clean]\n",
"\n",
"import numpy as np\n",
"obj = np.int8(3)\n",
"def test_bad_error_message():\n",
" assert isinstance(obj, int)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from pyexpect import expect"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================================================== test session starts ====================================================================\n",
"platform darwin -- Python 3.7.2, pytest-4.2.0, py-1.7.0, pluggy-0.8.1\n",
"rootdir: /Users/dwt/Code/Projekte/pyexpect, inifile:\n",
"collected 2 items\n",
"\n",
"pyexpect.py FF [100%]\n",
"\n",
"========================================================================= FAILURES ==========================================================================\n",
"________________________________________________________________________ test_equals ________________________________________________________________________\n",
"\n",
" def test_equals():\n",
"> expect(foo).to_equal(bar) # many variant spellings, see source\n",
"E AssertionError: Expect 3 to equal 4\n",
"\n",
"<ipython-input-6-3fb134c91d73>:5: AssertionError\n",
"___________________________________________________________________ test_equals_shorthand ___________________________________________________________________\n",
"\n",
" def test_equals_shorthand():\n",
"> expect(foo) == bar # if you like that better\n",
"E AssertionError: Expect 3 to equal 4\n",
"\n",
"<ipython-input-6-3fb134c91d73>:8: AssertionError\n",
"================================================================= 2 failed in 0.06 seconds ==================================================================\n"
]
}
],
"source": [
"%%run_pytest[clean]\n",
"\n",
"foo, bar = 3, 4\n",
"\n",
"def test_equals():\n",
" expect(foo).to_equal(bar) # many variant spellings, see source\n",
"\n",
"def test_equals_shorthand():\n",
" expect(foo) == bar # if you like that better"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Concise Matchers"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect 1 to be True",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobject\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_trueish\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_true\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect 1 to be True"
]
}
],
"source": [
"expect(23).to_equal(23)\n",
"expect(object()).is_trueish()\n",
"expect(1).is_true()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect 23 not to equal 23",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnot_to_equal\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# not_* variant always autogenerated\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect 23 not to equal 23"
]
}
],
"source": [
"expect(23).not_to_equal(23) # not_* variant always autogenerated"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect {'id': 23, 'name': 'fnord', 'email': 'fnord@fnord.fnord'}\n\nto contain dict {'id': 23, 'name': 'fnord', 'email': 'fnord@example.com'}",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0msomething\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0memail\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord@fnord.fnord'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msomething\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhas_subdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0memail\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord@example.com'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect {'id': 23, 'name': 'fnord', 'email': 'fnord@fnord.fnord'}\n\nto contain dict {'id': 23, 'name': 'fnord', 'email': 'fnord@example.com'}"
]
}
],
"source": [
"something = dict(id=23, name='fnord', email='fnord@fnord.fnord')\n",
"expect(something).has_subdict(id=23, name='fnord', email='fnord@example.com')"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect <__main__.Foo object at 0x11bf496d8> to have attributes {'id': 12, 'name': 'fnord', 'email': 'fnord@example.com'}, \n\tbut has {'id': 12, 'name': 'fnord', 'email': 'fnord@fnord.fnord'}",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'fnord'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0memail\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'fnord@fnord.fnord'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mFoo\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhas_attributes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m12\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0memail\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'fnord@example.com'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect <__main__.Foo object at 0x11bf496d8> to have attributes {'id': 12, 'name': 'fnord', 'email': 'fnord@example.com'}, \n\tbut has {'id': 12, 'name': 'fnord', 'email': 'fnord@fnord.fnord'}"
]
}
],
"source": [
"class Foo:\n",
" id = 12\n",
" name = 'fnord'\n",
" email = 'fnord@fnord.fnord'\n",
"expect(Foo()).has_attributes(id=12, name='fnord', email='fnord@example.com')"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect [1, 2, 3] to contain sequence (1, 3)",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhas_sub_sequence\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect [1, 2, 3] to contain sequence (1, 3)"
]
}
],
"source": [
"expect([1,2,3]).has_sub_sequence(1,3)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect <function a_function at 0x11bf581e0> to raise AssertionError with message matching:\n\tr'wh[ia]chever'\nbut it raised:\n\tAssertionError('whatever\\nassert False')",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0ma_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'whatever'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma_function\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_raise\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mAssertionError\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mr'wh[ia]chever'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect <function a_function at 0x11bf581e0> to raise AssertionError with message matching:\n\tr'wh[ia]chever'\nbut it raised:\n\tAssertionError('whatever\\nassert False')"
]
}
],
"source": [
"def a_function():\n",
" assert False, 'whatever'\n",
"expect(a_function).to_raise(AssertionError, r'wh[ia]chever')"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect [23, 42] to have length 3, but found length 2",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m23\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m42\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhas_len\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect [23, 42] to have length 3, but found length 2"
]
}
],
"source": [
"expect([23, 42]).has_len(3)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect 3.14136 to be close to 3.3 with max delta 0.1",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3.14136\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose_to\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3.3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmax_delta\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m.1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect 3.14136 to be close to 3.3 with max delta 0.1"
]
}
],
"source": [
"expect(3.14136).close_to(3.3, max_delta=.1)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect 3.14136 to be between 3.2 and 3.3",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3.14136\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbetween\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m3.2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3.3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect 3.14136 to be between 3.2 and 3.3"
]
}
],
"source": [
"expect(3.14136).between(3.2, 3.3)"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"ename": "AssertionError",
"evalue": "Expect 'Martin Häcker' to be matched by regex r'Nitram'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Code/Projekte/pyexpect/docs/talk/pyexpect.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mexpect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Martin Häcker'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatches\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mr'Nitram'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m~/Code/Projekte/pyexpect/pyexpect/internals.py\u001b[0m in \u001b[0;36mpyexpect_internals_hidden_in_backtraces\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;31m# use `with expect.disabled_backtrace_cleaning():` to show full backtrace\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpyexpect_internals_hidden_in_backtraces\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mAssertionError\u001b[0m: Expect 'Martin Häcker' to be matched by regex r'Nitram'"
]
}
],
"source": [
"expect('Martin Häcker').matches(r'Nitram')"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
"expect(3) < 4\n",
"expect(3) == 3\n",
"expect(5) >= 3\n",
"# ..."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Takeaway\n",
"\n",
"* much readable\n",
"* concisely writeable (aliasses)\n",
"* single namespace for all assertions (completion)\n",
"* can use everywhere (jupyter notebooks!)\n",
"* And much more -> read the source!\n",
"\n",
"And most important: Use `pyexpect`\n",
"\n",
"https://pypi.org/project/pyexpect/"
]
}
],
"metadata": {
"kernel_info": {
"name": "ki"
},
"kernelspec": {
"display_name": "KI (Python 3)",
"language": "python",
"name": "ki"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
},
"nteract": {
"version": "0.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment