From 4c65f113a1478be30ed36f4f3ae584cd2696303e Mon Sep 17 00:00:00 2001 From: hdoupe Date: Tue, 5 Nov 2019 21:19:09 -0500 Subject: [PATCH 01/12] Add inplace keyword for calculations on calculators that are passed to response --- behresp/behavior.py | 33 ++++++++++++++++++++++----------- behresp/tests/test_behavior.py | 26 ++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/behresp/behavior.py b/behresp/behavior.py index aa82028..97169f5 100644 --- a/behresp/behavior.py +++ b/behresp/behavior.py @@ -10,20 +10,22 @@ import taxcalc as tc -def response(calc_1, calc_2, elasticities, dump=False): +def response(calc_1, calc_2, elasticities, dump=False, inplace=False): """ Implements TaxBrain "Partial Equilibrium Simulation" dynamic analysis returning results as a tuple of Pandas DataFrame objects (df1, df2) where: - df1 is extracted from a baseline-policy calc_1 copy, and - df2 is extracted from a reform-policy calc_2 copy that incorporates the + df1 is extracted from a baseline-policy calc_1, and + df2 is extracted from a reform-policy calc_2 that incorporates the behavioral responses given by the nature of the baseline-to-reform change in policy and elasticities in the specified behavior dictionary. - Note: this function internally modifies a copy of calc_2 records to account + Note: By default, this function internally modifies a copy of calc_2 records to account for behavioral responses that arise from the policy reform that involves moving from calc1 policy to calc2 policy. Neither calc_1 nor calc_2 need to have had calc_all() executed before calling the response function. - And neither calc_1 nor calc_2 are affected by this response function. + By default, neither calc_1 nor calc_2 are affected by this response + function. To perform in-place calculations that affect calc_1 and calc_2, + set inplace equal to True. The elasticities argument is a dictionary containing the assumed response elasticities. Omitting an elasticity key:value pair in the dictionary @@ -93,8 +95,12 @@ def response(calc_1, calc_2, elasticities, dump=False): # pylint: disable=too-many-locals,too-many-statements,too-many-branches # Check function argument types and elasticity values - calc1 = copy.deepcopy(calc_1) - calc2 = copy.deepcopy(calc_2) + if inplace: + calc1 = calc_1 + calc2 = calc_2 + else: + calc1 = copy.deepcopy(calc_1) + calc2 = copy.deepcopy(calc_2) assert isinstance(calc1, tc.Calculator) assert isinstance(calc2, tc.Calculator) assert isinstance(elasticities, dict) @@ -220,10 +226,14 @@ def _mtr12(calc__1, calc__2, mtr_of='e00200p', tax_type='combined'): df1['mtr_combined'] = wage_mtr1 * 100 else: df1 = calc1.dataframe(tc.DIST_VARIABLES) - del calc1 + if not inplace: + del calc1 # Add behavioral-response changes to income sources - calc2_behv = copy.deepcopy(calc2) - del calc2 + if inplace: + calc2_behv = calc2 + else: + calc2_behv = copy.deepcopy(calc2) + del calc2 if not zero_sub_and_inc: calc2_behv = _update_ordinary_income(si_chg, calc2_behv) calc2_behv = _update_cap_gain_income(ltcg_chg, calc2_behv) @@ -237,7 +247,8 @@ def _mtr12(calc__1, calc__2, mtr_of='e00200p', tax_type='combined'): df2['mtr_combined'] = wage_mtr2 * 100 else: df2 = calc2_behv.dataframe(tc.DIST_VARIABLES) - del calc2_behv + if not inplace: + del calc2_behv # Return the two dataframes return (df1, df2) diff --git a/behresp/tests/test_behavior.py b/behresp/tests/test_behavior.py index c606ee9..bee166c 100644 --- a/behresp/tests/test_behavior.py +++ b/behresp/tests/test_behavior.py @@ -13,7 +13,8 @@ from behresp import response, quantity_response, labor_response -def test_default_response_function(cps_subsample): +@pytest.mark.parametrize("inplace", [False, True]) +def test_default_response_function(cps_subsample, inplace): """ Test that default behavior parameters produce static results. """ @@ -37,10 +38,31 @@ def test_default_response_function(cps_subsample): calc2s.calc_all() df2s = calc2s.dataframe(['iitax', 's006']) itax2s = round((df2s['iitax'] * df2s['s006']).sum() * 1e-9, 3) + + # Keep track of some of the variables that will be modifed + # in response if inplace is True. + df_before_1 = calc1.dataframe(tc.DIST_VARIABLES) + df_before_2 = calc2d.dataframe(tc.DIST_VARIABLES) + # ... calculate aggregate inctax using zero response elasticities - _, df2d = response(calc1, calc2d, elasticities={}, dump=True) + _, df2d = response(calc1, calc2d, elasticities={}, dump=True, inplace=inplace) itax2d = round((df2d['iitax'] * df2d['s006']).sum() * 1e-9, 3) assert np.allclose(itax2d, itax2s) + + # Grab the same variables after. + df_after_1 = calc1.dataframe(tc.DIST_VARIABLES) + df_after_2 = calc2d.dataframe(tc.DIST_VARIABLES) + + # If inplace is True, the variables should have been modified. + # If inplace is False, response only modified a copy of calc_1 + # and calc_2. + if inplace: + assert not df_before_1.equals(df_after_1) + assert not df_before_2.equals(df_after_2) + else: + assert df_before_1.equals(df_after_1) + assert df_before_2.equals(df_after_2) + # ... clean up del calc1 del calc2s From 25f0ed06b6241e10d25c1ea998b4f36fa68960ae Mon Sep 17 00:00:00 2001 From: hdoupe Date: Tue, 5 Nov 2019 21:46:45 -0500 Subject: [PATCH 02/12] Include correct version of notebook --- behresp mem bench.ipynb | 107 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 behresp mem bench.ipynb diff --git a/behresp mem bench.ipynb b/behresp mem bench.ipynb new file mode 100644 index 0000000..a792dc7 --- /dev/null +++ b/behresp mem bench.ipynb @@ -0,0 +1,107 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import taxcalc as tc\n", + "import behresp\n", + "\n", + "%load_ext memory_profiler\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def test_func(inplace):\n", + " rec = tc.Records.cps_constructor()\n", + " refyear = 2020\n", + " assert refyear >= 2018\n", + " reform = {'II_em': {refyear: 1500}}\n", + " # ... construct pre-reform calculator\n", + " pol = tc.Policy()\n", + " calc1 = tc.Calculator(records=rec, policy=pol)\n", + " calc1.advance_to_year(refyear)\n", + " # ... construct two post-reform calculators\n", + " pol.implement_reform(reform)\n", + " calc2d = tc.Calculator(records=rec, policy=pol) # for default behavior\n", + " calc2d.advance_to_year(refyear)\n", + "\n", + " # Keep track of some of the variables that will be modifed\n", + " # in response if inplace is True.\n", + " df_before_1 = calc1.dataframe(tc.DIST_VARIABLES)\n", + " df_before_2 = calc2d.dataframe(tc.DIST_VARIABLES)\n", + "\n", + " behresp.response(calc1, calc2d, elasticities={\"inc\": -0.2}, dump=True, inplace=inplace)\n", + "\n", + " # Grab the same variables after.\n", + " df_after_1 = calc1.dataframe(tc.DIST_VARIABLES)\n", + " df_after_2 = calc2d.dataframe(tc.DIST_VARIABLES)\n", + "\n", + " # If inplace is True, the variables should have been modified.\n", + " # If inplace is False, response only modified a copy of calc_1\n", + " # and calc_2.\n", + " if inplace:\n", + " assert not df_before_1.equals(df_after_1)\n", + " assert not df_before_2.equals(df_after_2)\n", + " else:\n", + " assert df_before_1.equals(df_after_1)\n", + " assert df_before_2.equals(df_after_2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# get a line by line break down of memory usage and changes\n", + "# %mprun -f behresp.response test_func(inplace=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "peak memory: 5980.18 MiB, increment: 5805.55 MiB\n" + ] + } + ], + "source": [ + "# obtain high-level stats on memory usage\n", + "%memit test_func(inplace=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (behresp-dev)", + "language": "python", + "name": "behresp-dev" + }, + "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.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 0128e622c6a9c9236754f62e68ab173bf1fe0fc3 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 13:30:00 -0400 Subject: [PATCH 03/12] remove notebook benchmarking memory use --- behresp mem bench.ipynb | 107 ---------------------------------------- 1 file changed, 107 deletions(-) delete mode 100644 behresp mem bench.ipynb diff --git a/behresp mem bench.ipynb b/behresp mem bench.ipynb deleted file mode 100644 index a792dc7..0000000 --- a/behresp mem bench.ipynb +++ /dev/null @@ -1,107 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import taxcalc as tc\n", - "import behresp\n", - "\n", - "%load_ext memory_profiler\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def test_func(inplace):\n", - " rec = tc.Records.cps_constructor()\n", - " refyear = 2020\n", - " assert refyear >= 2018\n", - " reform = {'II_em': {refyear: 1500}}\n", - " # ... construct pre-reform calculator\n", - " pol = tc.Policy()\n", - " calc1 = tc.Calculator(records=rec, policy=pol)\n", - " calc1.advance_to_year(refyear)\n", - " # ... construct two post-reform calculators\n", - " pol.implement_reform(reform)\n", - " calc2d = tc.Calculator(records=rec, policy=pol) # for default behavior\n", - " calc2d.advance_to_year(refyear)\n", - "\n", - " # Keep track of some of the variables that will be modifed\n", - " # in response if inplace is True.\n", - " df_before_1 = calc1.dataframe(tc.DIST_VARIABLES)\n", - " df_before_2 = calc2d.dataframe(tc.DIST_VARIABLES)\n", - "\n", - " behresp.response(calc1, calc2d, elasticities={\"inc\": -0.2}, dump=True, inplace=inplace)\n", - "\n", - " # Grab the same variables after.\n", - " df_after_1 = calc1.dataframe(tc.DIST_VARIABLES)\n", - " df_after_2 = calc2d.dataframe(tc.DIST_VARIABLES)\n", - "\n", - " # If inplace is True, the variables should have been modified.\n", - " # If inplace is False, response only modified a copy of calc_1\n", - " # and calc_2.\n", - " if inplace:\n", - " assert not df_before_1.equals(df_after_1)\n", - " assert not df_before_2.equals(df_after_2)\n", - " else:\n", - " assert df_before_1.equals(df_after_1)\n", - " assert df_before_2.equals(df_after_2)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# get a line by line break down of memory usage and changes\n", - "# %mprun -f behresp.response test_func(inplace=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "peak memory: 5980.18 MiB, increment: 5805.55 MiB\n" - ] - } - ], - "source": [ - "# obtain high-level stats on memory usage\n", - "%memit test_func(inplace=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python (behresp-dev)", - "language": "python", - "name": "behresp-dev" - }, - "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.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 23d575290aee8cf2a3eaf4605da38d0a335f68f5 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 14:03:36 -0400 Subject: [PATCH 04/12] remove 3.6 adn add 3.9 to test matrix --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 194c1ed..ec62fb5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ dist: xenial language: python python: - - "3.6" - "3.7" - "3.8" + - "3.9" install: # Install conda @@ -15,7 +15,7 @@ install: - conda create -n behresp-dev python=$TRAVIS_PYTHON_VERSION; - source activate behresp-dev - conda env update -f environment.yml - - pip install pytest-pycodestyle + - pip install pytest-pycodestyle - pip install pyyaml - pip install coverage - pip install codecov From a86a1ca999eb89067728366bd29aadd184f60da0 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 14:21:03 -0400 Subject: [PATCH 05/12] add GH action for testing --- .github/workflows/build_and_test.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/build_and_test.yml diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..1e7e657 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,44 @@ +name: Build Package and Test Source Code [Python 3.7, 3.8, 3.9] + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Setup Miniconda using Python ${{ matrix.python-version }} + uses: conda-incubator/setup-miniconda@v2 + with: + activate-environment: behresp-dev + environment-file: environment.yml + python-version: ${{ matrix.python-version }} + auto-activate-base: false + + - name: Build + shell: bash -l {0} + run: | + pip install -e . + pip install pytest-cov + pip install pytest-pycodestyle + - name: Test + shell: bash -l {0} + working-directory: ./ + run: | + pytest -m 'not requires_pufcsv and not pre_release and not local' --pycodestyle --cov=./ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true \ No newline at end of file From 5bf3b6a18f0f205fe72f480ffff60baecb2ca94c Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 14:38:00 -0400 Subject: [PATCH 06/12] fix pycodestyle errors --- behresp/behavior.py | 15 ++++++++------- behresp/tests/test_behavior.py | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/behresp/behavior.py b/behresp/behavior.py index 97169f5..4d900b1 100644 --- a/behresp/behavior.py +++ b/behresp/behavior.py @@ -19,13 +19,14 @@ def response(calc_1, calc_2, elasticities, dump=False, inplace=False): behavioral responses given by the nature of the baseline-to-reform change in policy and elasticities in the specified behavior dictionary. - Note: By default, this function internally modifies a copy of calc_2 records to account - for behavioral responses that arise from the policy reform that involves - moving from calc1 policy to calc2 policy. Neither calc_1 nor calc_2 need - to have had calc_all() executed before calling the response function. - By default, neither calc_1 nor calc_2 are affected by this response - function. To perform in-place calculations that affect calc_1 and calc_2, - set inplace equal to True. + Note: By default, this function internally modifies a copy of calc_2 + records to account for behavioral responses that arise from the + policy reform that involves moving from calc1 policy to calc2 + policy. Neither calc_1 nor calc_2 need to have had calc_all() + executed before calling the response function. By default, neither + calc_1 nor calc_2 are affected by this response function. To + perform in-place calculations that affect calc_1 and calc_2, set + inplace equal to True. The elasticities argument is a dictionary containing the assumed response elasticities. Omitting an elasticity key:value pair in the dictionary diff --git a/behresp/tests/test_behavior.py b/behresp/tests/test_behavior.py index c0f7053..bfe1a0a 100644 --- a/behresp/tests/test_behavior.py +++ b/behresp/tests/test_behavior.py @@ -45,7 +45,8 @@ def test_default_response_function(cps_subsample, inplace): df_before_2 = calc2d.dataframe(tc.DIST_VARIABLES) # ... calculate aggregate inctax using zero response elasticities - _, df2d = response(calc1, calc2d, elasticities={}, dump=True, inplace=inplace) + _, df2d = response(calc1, calc2d, elasticities={}, dump=True, + inplace=inplace) itax2d = round((df2d['iitax'] * df2d['s006']).sum() * 1e-9, 3) assert np.allclose(itax2d, itax2s) From 59a54a743f589a13b5f8e1ba46905dfdc4b2941e Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 15:35:32 -0400 Subject: [PATCH 07/12] update test output for taxcalc 3.1 --- behresp/tests/test_behavior.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/behresp/tests/test_behavior.py b/behresp/tests/test_behavior.py index bfe1a0a..254dfd7 100644 --- a/behresp/tests/test_behavior.py +++ b/behresp/tests/test_behavior.py @@ -99,9 +99,9 @@ def test_nondefault_response_function(be_inc, cps_subsample): del df1 del df2 if be_inc == 0.0: - assert np.allclose([itax1, itax2], [1355.556, 1304.984]) + assert np.allclose([itax1, itax2], [1354.7, 1304.166]) elif be_inc == -0.1: - assert np.allclose([itax1, itax2], [1355.556, 1303.898]) + assert np.allclose([itax1, itax2], [1354.7, 1303.08]) def test_alternative_behavior_parameters(cps_subsample): @@ -130,7 +130,7 @@ def test_alternative_behavior_parameters(cps_subsample): itax2 = round((df2['iitax'] * df2['s006']).sum() * 1e-9, 3) del df1 del df2 - assert np.allclose([itax1, itax2], [1355.556, 1302.09]) + assert np.allclose([itax1, itax2], [1354.7, 1301.281]) def test_quantity_response(): From 040d893d0f2a775416a7e3df07ac037eedcb0d55 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 18:56:45 -0400 Subject: [PATCH 08/12] update env to specify taxcalc 3.1 or greater and remove PSL channel --- environment.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index c1470d9..26c2686 100644 --- a/environment.yml +++ b/environment.yml @@ -1,8 +1,7 @@ name: behresp-dev channels: -- PSLmodels - conda-forge dependencies: - python -- "taxcalc>=3.0.0" +- "taxcalc>=3.1.0" - pytest From 2553ec07ef09ac26cb65045c8a64b52885ccd710 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 19:04:06 -0400 Subject: [PATCH 09/12] update meta.yaml to be consistent with environment.yml --- conda.recipe/meta.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 5ab9da4..30b8b1b 100755 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -5,12 +5,12 @@ package: requirements: build: - python - - "taxcalc>=3.0.0" + - "taxcalc>=3.1.0" - pytest run: - python - - "taxcalc>=3.0.0" + - "taxcalc>=3.1.0" - pytest test: From ad4873c0abcc48bada4041de8c57e0f64f036d2e Mon Sep 17 00:00:00 2001 From: "Henry \"Hank\" Doupe" Date: Thu, 8 Apr 2021 14:14:19 -0400 Subject: [PATCH 10/12] Add workflow config file through Actions tab Added this file directly from this tab: https://github.com/PSLmodels/Behavioral-Responses/actions Related: https://github.com/PSLmodels/Behavioral-Responses/pull/81 --- .github/workflows/build_and_test.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/build_and_test.yml diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..f537855 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,44 @@ +name: Build Package and Test Source Code [Python 3.7, 3.8, 3.9] + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Setup Miniconda using Python ${{ matrix.python-version }} + uses: conda-incubator/setup-miniconda@v2 + with: + activate-environment: behresp-dev + environment-file: environment.yml + python-version: ${{ matrix.python-version }} + auto-activate-base: false + + - name: Build + shell: bash -l {0} + run: | + pip install -e . + pip install pytest-cov + pip install pytest-pycodestyle + - name: Test + shell: bash -l {0} + working-directory: ./ + run: | + pytest -m 'not requires_pufcsv and not pre_release and not local' --pycodestyle --cov=./ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true From 06b8170d5186bf6f00c8f9efede22f861a2e1ee3 Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 18:56:45 -0400 Subject: [PATCH 11/12] update env to specify taxcalc 3.1 or greater and remove PSL channel --- environment.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index c1470d9..26c2686 100644 --- a/environment.yml +++ b/environment.yml @@ -1,8 +1,7 @@ name: behresp-dev channels: -- PSLmodels - conda-forge dependencies: - python -- "taxcalc>=3.0.0" +- "taxcalc>=3.1.0" - pytest From 780e9acc17505bd42bac69082c457b050221464f Mon Sep 17 00:00:00 2001 From: jdebacker Date: Mon, 5 Apr 2021 19:04:06 -0400 Subject: [PATCH 12/12] update meta.yaml to be consistent with environment.yml --- conda.recipe/meta.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 5ab9da4..30b8b1b 100755 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -5,12 +5,12 @@ package: requirements: build: - python - - "taxcalc>=3.0.0" + - "taxcalc>=3.1.0" - pytest run: - python - - "taxcalc>=3.0.0" + - "taxcalc>=3.1.0" - pytest test: