pytest icon indicating copy to clipboard operation
pytest copied to clipboard

`'store_true'` does not work correctly in parser.addoption

Open h-vetinari opened this issue 4 years ago • 15 comments

One more from https://github.com/conda-forge/staged-recipes/pull/16888, where (after patching in workarounds for #9300 & #9301), the test suite failed with

==================================== ERRORS ====================================
________________ ERROR at setup of test_serialization[cpu-gelu] ________________

self = <_pytest.config.Config object at 0x7fc1975d41c0>
name = 'skip_custom_ops', default = <NOTSET>, skip = False

    def getoption(self, name: str, default=notset, skip: bool = False):
        """Return command line option value.
    
        :param name: Name of the option.  You may also specify
            the literal ``--OPT`` option instead of the "dest" option name.
        :param default: Default value if no option of that name exists.
        :param skip: If True, raise pytest.skip if option does not exists
            or has a None value.
        """
        name = self._opt2dest.get(name, name)
        try:
>           val = getattr(self.option, name)
E           AttributeError: 'Namespace' object has no attribute 'skip_custom_ops'

../[...]/_pytest/config/__init__.py:1463: AttributeError

The above exception was the direct cause of the following exception:

request = <SubRequest 'set_global_variables' for <Function test_serialization[cpu-gelu]>>

    @pytest.fixture(scope="session", autouse=True)
    def set_global_variables(request):
>       if request.config.getoption("--skip-custom-ops"):
E       ValueError: no option named 'skip_custom_ops

So from that, it looks like something is going wrong with skip_custom_ops. Chasing around the upstream repo I find that this option is added here as follows:

def pytest_addoption(parser):
    parser.addoption(
        "--skip-custom-ops",
        action="store_true",
        help="When a custom op is being loaded in a test, skip this test.",
    )

The pytest docs for parser.addoption say that this works like argparse, where the linked docs say:

'store_true' and 'store_false' - These are special cases of 'store_const' used for storing the values True and False respectively. In addition, they create default values of False and True respectively.

What caught my eye in the stack trace was the default = <NOTSET>, which is clearly at odds with that documentation. To verify this hypothesis, I added the following patch

diff --git a/tensorflow_addons/utils/test_utils.py b/tensorflow_addons/utils/test_utils.py
index 9d1dec0..0efd707 100644
--- a/tensorflow_addons/utils/test_utils.py
+++ b/tensorflow_addons/utils/test_utils.py
@@ -134,7 +134,8 @@ def set_seeds():
 def pytest_addoption(parser):
     parser.addoption(
         "--skip-custom-ops",
-        action="store_true",
+        action="store",
+        default=True,
         help="When a custom op is being loaded in a test, skip this test.",
     )

and voilà, it works.

h-vetinari avatar Nov 12 '21 05:11 h-vetinari

I'll be happy to open a PR on this

yuvalshi0 avatar Nov 28 '21 08:11 yuvalshi0

@yuvalshi0 feel free to go ahead!

nicoddemus avatar Nov 28 '21 12:11 nicoddemus

@h-vetinari I've tried to reproduce the error, by writing a test. When this failed I've cloned the repo and wrote a simple test just to debug the parser, yet not error was presented, may you please add more info (python version, package version) thanks!

yuvalshi0 avatar Nov 28 '21 13:11 yuvalshi0

I think running the upstream repo is the wrong approach. The issue is about store_true not being respected, which is independent of a given package setup. The tensorflow-stack does a lot of unusual things, and may not be exposed due to various quirks around the build- & test-setup, but I'm definitely hitting this in https://github.com/conda-forge/staged-recipes/pull/16888. Still I reran this again by commenting out the patch from the OP, and indeed it keeps failing (here's a link to the respective CI run; look under "linux_64_centos7").

The versions in question are:

pytest:                  6.2.5-py39hf3d152e_1         conda-forge
python:                  3.9.7-hb7a2778_3_cpython     conda-forge
tensorflow-addons:       0.14.0-py39hfe13ede_0        local 

h-vetinari avatar Nov 29 '21 01:11 h-vetinari

PS. Thanks a lot for tackling this @yuvalshi0! :)

h-vetinari avatar Nov 29 '21 01:11 h-vetinari

@h-vetinari So I've tried to mimic the problem above, I wrote a test:

    def test_config_getoption_store(self, pytester: Pytester) -> None:
        pytester.makeconftest(
            """
            def pytest_addoption(parser):
                parser.addoption(
                    "--skip-custom-ops",
                    action="store_true",
                    help="When a custom op is being loaded in a test, skip this test.",
                )      
        """
        )
        config = pytester.parseconfig("-v")
        assert not config.getoption("--skip-custom-ops")

yet no luck in reproducing the issue, the test seems to pass:

============================================================================== test session starts ==============================================================================
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /.venv/bin/python3.9
cachedir: .pytest_cache
rootdir: pytest
plugins: typeguard-2.13.2
collected 1 item                                                                                                                                                                

testing/test_config.py::TestConfigAPI::test_config_getoption_store PASSED                                                                                                 [100%]

=============================================================================== 1 passed in 0.55s ===============================================================================

Do you have any idea of other quirks that might affect the environment?

yuvalshi0 avatar Nov 29 '21 08:11 yuvalshi0

Hey @yuvalshi0

Thanks for digging into this - no idea where this discrepancy is coming from, but you could try to download the artefact from here (assuming you're on linux, as the test session above seems to indicate), unzip it, install it as follows

conda create -n test_env -c "path/to/where/you/unpacked/the/artefact" -c conda-forge tensorflow-addons python=3.8

and then try to cd into a checkout of tensorflow-addons and run the tests.

h-vetinari avatar Nov 29 '21 08:11 h-vetinari

Still no luck unfortunately, under this conda environment the tests till passes:

# packages in environment at /root/anaconda3/envs/test_env:
#
# Name                    Version                   Build  Channel
_libgcc_mutex             0.1                 conda_forge    conda-forge
_openmp_mutex             4.5                       1_gnu    conda-forge
abseil-cpp                20210324.2           h9c3ff4c_0    conda-forge
absl-py                   0.15.0             pyhd8ed1ab_0    conda-forge
aiohttp                   3.8.1            py39h3811e60_0    conda-forge
aiosignal                 1.2.0              pyhd8ed1ab_0    conda-forge
appdirs                   1.4.4              pyh9f0ad1d_0    conda-forge
astunparse                1.6.3              pyhd8ed1ab_0    conda-forge
async-timeout             4.0.1              pyhd8ed1ab_0    conda-forge
attrs                     21.2.0             pyhd8ed1ab_0    conda-forge
blinker                   1.4                        py_1    conda-forge
blosc                     1.21.0               h9c3ff4c_0    conda-forge
brotli                    1.0.9                h7f98852_6    conda-forge
brotli-bin                1.0.9                h7f98852_6    conda-forge
brotlipy                  0.7.0           py39h3811e60_1003    conda-forge
brunsli                   0.1                  h9c3ff4c_0    conda-forge
bzip2                     1.0.8                h7f98852_4    conda-forge
c-ares                    1.18.1               h7f98852_0    conda-forge
c-blosc2                  2.0.4                h5f21a17_1    conda-forge
ca-certificates           2021.10.8            ha878542_0    conda-forge
cached-property           1.5.2                hd8ed1ab_1    conda-forge
cached_property           1.5.2              pyha770c72_1    conda-forge
cachetools                4.2.4              pyhd8ed1ab_0    conda-forge
certifi                   2021.10.8        py39hf3d152e_1    conda-forge
cffi                      1.15.0           py39h4bc2ebd_0    conda-forge
cfitsio                   4.0.0                h9a35b8e_0    conda-forge
charls                    2.2.0                h9c3ff4c_0    conda-forge
charset-normalizer        2.0.8              pyhd8ed1ab_0    conda-forge
click                     8.0.3            py39hf3d152e_1    conda-forge
cloudpickle               2.0.0              pyhd8ed1ab_0    conda-forge
colorama                  0.4.4              pyh9f0ad1d_0    conda-forge
cryptography              35.0.0           py39h95dcef6_2    conda-forge
cycler                    0.11.0             pyhd8ed1ab_0    conda-forge
cytoolz                   0.11.2           py39h3811e60_1    conda-forge
dask-core                 2021.11.2          pyhd8ed1ab_0    conda-forge
dataclasses               0.8                pyhc8e2a94_3    conda-forge
fonttools                 4.28.2           py39h3811e60_0    conda-forge
freetype                  2.10.4               h0708190_1    conda-forge
frozenlist                1.2.0            py39h3811e60_1    conda-forge
fsspec                    2021.11.1          pyhd8ed1ab_0    conda-forge
gast                      0.4.0              pyh9f0ad1d_0    conda-forge
giflib                    5.2.1                h36c2ea0_2    conda-forge
google-auth               1.35.0             pyh6c4a22f_0    conda-forge
google-auth-oauthlib      0.4.6              pyhd8ed1ab_0    conda-forge
google-pasta              0.2.0              pyh8c360ce_0    conda-forge
grpc-cpp                  1.40.0               h05f19cf_2    conda-forge
grpcio                    1.40.0           py39hff7568b_0    conda-forge
h5py                      3.1.0           nompi_py39h25020de_100    conda-forge
hdf5                      1.10.6          nompi_h6a2412b_1114    conda-forge
icu                       69.1                 h9c3ff4c_0    conda-forge
idna                      3.1                pyhd3deb0d_0    conda-forge
imagecodecs               2021.11.20       py39h571908b_1    conda-forge
imageio                   2.9.0                      py_0    conda-forge
importlib-metadata        4.8.2            py39hf3d152e_0    conda-forge
iniconfig                 1.1.1              pyh9f0ad1d_0    conda-forge
jbig                      2.1               h7f98852_2003    conda-forge
joblib                    1.1.0              pyhd8ed1ab_0    conda-forge
jpeg                      9d                   h36c2ea0_0    conda-forge
jxrlib                    1.1                  h7f98852_2    conda-forge
keras                     2.6.0              pyhd8ed1ab_1    conda-forge
keras-preprocessing       1.1.2              pyhd8ed1ab_0    conda-forge
kiwisolver                1.3.2            py39h1a9c180_1    conda-forge
krb5                      1.19.2               hcc1bbae_3    conda-forge
lcms2                     2.12                 hddcbb42_0    conda-forge
ld_impl_linux-64          2.36.1               hea4e1c9_2    conda-forge
lerc                      3.0                  h9c3ff4c_0    conda-forge
libaec                    1.0.6                h9c3ff4c_0    conda-forge
libblas                   3.9.0           12_linux64_openblas    conda-forge
libbrotlicommon           1.0.9                h7f98852_6    conda-forge
libbrotlidec              1.0.9                h7f98852_6    conda-forge
libbrotlienc              1.0.9                h7f98852_6    conda-forge
libcblas                  3.9.0           12_linux64_openblas    conda-forge
libcurl                   7.80.0               h2574ce0_0    conda-forge
libdeflate                1.8                  h7f98852_0    conda-forge
libedit                   3.1.20191231         he28a2e2_2    conda-forge
libev                     4.33                 h516909a_1    conda-forge
libffi                    3.4.2                h7f98852_5    conda-forge
libgcc-ng                 11.2.0              h1d223b6_11    conda-forge
libgfortran-ng            11.2.0              h69a702a_11    conda-forge
libgfortran5              11.2.0              h5c6108e_11    conda-forge
libgomp                   11.2.0              h1d223b6_11    conda-forge
liblapack                 3.9.0           12_linux64_openblas    conda-forge
libnghttp2                1.43.0               h812cca2_1    conda-forge
libopenblas               0.3.18          pthreads_h8fe5266_0    conda-forge
libpng                    1.6.37               h21135ba_2    conda-forge
libprotobuf               3.18.1               h780b84a_0    conda-forge
libssh2                   1.10.0               ha56f1ee_2    conda-forge
libstdcxx-ng              11.2.0              he4da1e4_11    conda-forge
libtensorflow_cc          2.6.2            cpu_hfd9b42a_0    conda-forge
libtiff                   4.3.0                h6f004c6_2    conda-forge
libwebp-base              1.2.1                h7f98852_0    conda-forge
libzlib                   1.2.11            h36c2ea0_1013    conda-forge
libzopfli                 1.0.3                h9c3ff4c_0    conda-forge
locket                    0.2.0                      py_2    conda-forge
lz4-c                     1.9.3                h9c3ff4c_1    conda-forge
markdown                  3.3.6              pyhd8ed1ab_0    conda-forge
matplotlib-base           3.5.0            py39h2fa2bec_0    conda-forge
more-itertools            8.12.0             pyhd8ed1ab_0    conda-forge
multidict                 5.2.0            py39h3811e60_1    conda-forge
munkres                   1.1.4              pyh9f0ad1d_0    conda-forge
ncurses                   6.2                  h58526e2_4    conda-forge
networkx                  2.6.3              pyhd8ed1ab_1    conda-forge
numpy                     1.19.5           py39hdbf815f_2    conda-forge
oauthlib                  3.1.1              pyhd8ed1ab_0    conda-forge
olefile                   0.46               pyh9f0ad1d_1    conda-forge
openjpeg                  2.4.0                hb52868f_1    conda-forge
openssl                   1.1.1l               h7f98852_0    conda-forge
opt_einsum                3.3.0              pyhd8ed1ab_1    conda-forge
packaging                 21.3               pyhd8ed1ab_0    conda-forge
pandas                    1.3.4            py39hde0f152_1    conda-forge
partd                     1.2.0              pyhd8ed1ab_0    conda-forge
pillow                    8.4.0            py39ha612740_0    conda-forge
pip                       21.3.1             pyhd8ed1ab_0    conda-forge
pluggy                    1.0.0            py39hf3d152e_2    conda-forge
pooch                     1.5.2              pyhd8ed1ab_0    conda-forge
protobuf                  3.18.1           py39he80948d_0    conda-forge
py                        1.11.0             pyh6c4a22f_0    conda-forge
pyasn1                    0.4.8                      py_0    conda-forge
pyasn1-modules            0.2.7                      py_0    conda-forge
pycparser                 2.21               pyhd8ed1ab_0    conda-forge
pyjwt                     2.3.0              pyhd8ed1ab_0    conda-forge
pyopenssl                 21.0.0             pyhd8ed1ab_0    conda-forge
pyparsing                 3.0.6              pyhd8ed1ab_0    conda-forge
pysocks                   1.7.1            py39hf3d152e_4    conda-forge
pytest                    6.2.5            py39hf3d152e_1    conda-forge
python                    3.9.7           hb7a2778_3_cpython    conda-forge
python-dateutil           2.8.2              pyhd8ed1ab_0    conda-forge
python-flatbuffers        1.12               pyhd8ed1ab_1    conda-forge
python_abi                3.9                      2_cp39    conda-forge
pytz                      2021.3             pyhd8ed1ab_0    conda-forge
pyu2f                     0.1.5              pyhd8ed1ab_0    conda-forge
pywavelets                1.2.0            py39hce5d2b2_0    conda-forge
pyyaml                    6.0              py39h3811e60_3    conda-forge
re2                       2021.09.01           h9c3ff4c_0    conda-forge
readline                  8.1                  h46c0cb4_0    conda-forge
requests                  2.26.0             pyhd8ed1ab_1    conda-forge
requests-oauthlib         1.3.0              pyh9f0ad1d_0    conda-forge
rsa                       4.8                pyhd8ed1ab_0    conda-forge
scikit-image              0.18.3           py39hde0f152_0    conda-forge
scikit-learn              1.0.1            py39h4dfa638_2    conda-forge
scipy                     1.7.3            py39hee8e79c_0    conda-forge
setuptools                59.2.0           py39hf3d152e_0    conda-forge
six                       1.15.0             pyh9f0ad1d_0    conda-forge
snappy                    1.1.8                he1b5a44_3    conda-forge
sqlite                    3.37.0               h9cd32fc_0    conda-forge
tensorboard               2.6.0              pyhd8ed1ab_1    conda-forge
tensorboard-data-server   0.6.0            py39h95dcef6_1    conda-forge
tensorboard-plugin-wit    1.8.0              pyh44b312d_0    conda-forge
tensorflow                2.6.2           cpu_py39h1b7c303_0    conda-forge
tensorflow-addons         0.14.0           py39hfe13ede_0    <unknown>
tensorflow-base           2.6.2           cpu_py39hbcb9a37_0    conda-forge
tensorflow-estimator      2.6.2           cpu_py39h1b7c303_0    conda-forge
termcolor                 1.1.0                      py_2    conda-forge
threadpoolctl             3.0.0              pyh8a188c0_0    conda-forge
tifffile                  2021.11.2          pyhd8ed1ab_0    conda-forge
tk                        8.6.11               h27826a3_1    conda-forge
toml                      0.10.2             pyhd8ed1ab_0    conda-forge
toolz                     0.11.2             pyhd8ed1ab_0    conda-forge
tqdm                      4.62.3             pyhd8ed1ab_0    conda-forge
typeguard                 2.13.0             pyhd8ed1ab_0    conda-forge
typing-extensions         3.7.4.3                       0    conda-forge
typing_extensions         3.7.4.3                    py_0    conda-forge
tzdata                    2021e                he74cb21_0    conda-forge
urllib3                   1.26.7             pyhd8ed1ab_0    conda-forge
werkzeug                  2.0.1              pyhd8ed1ab_0    conda-forge
wheel                     0.37.0             pyhd8ed1ab_1    conda-forge
wrapt                     1.12.1           py39h3811e60_3    conda-forge
xz                        5.2.5                h516909a_1    conda-forge
yaml                      0.2.5                h516909a_0    conda-forge
yarl                      1.7.2            py39h3811e60_1    conda-forge
zfp                       0.5.5                h9c3ff4c_8    conda-forge
zipp                      3.6.0              pyhd8ed1ab_0    conda-forge
zlib                      1.2.11            h36c2ea0_1013    conda-forge
zstd                      1.5.0                ha95c52a_0    conda-forge

yuvalshi0 avatar Nov 29 '21 10:11 yuvalshi0

Hi looking through this issue I was able to reproduce the error @h-vetinari ran into with the following python script:

from _pytest.config import get_config

cfg = get_config()

# This does not work
cfg._parser.addoption(
    "--deadbeef",
    action="store_true"
)
cfg.getoption("--deadbeef") # value error thrown here

while changing the arguments passed to addoptions to @h-vetinari workaround fixes things:

from _pytest.config import get_config

cfg = get_config()

# This works
cfg._parser.addoption(
    "--livebeef",
    action="store",
    default=True
)
cfg.getoption("--livebeef")

I'm wondering if the reason @yuvalshi0 wasn't able to reproduce this error is because pytester.parseconfig or pytester.makeconftest is patching the bug. I want to look further into this issue and work on a fix for it, just don't want to step on anyone's toes if they're already working on a fix.

kevinsantana11 avatar Dec 03 '21 05:12 kevinsantana11

Hi looking through this issue I was able to reproduce the error @h-vetinari ran into with the following python script:

from _pytest.config import get_config

cfg = get_config()

# This does not work
cfg._parser.addoption(
    "--deadbeef",
    action="store_true"
)
cfg.getoption("--deadbeef") # value error thrown here

while changing the arguments passed to addoptions to @h-vetinari workaround fixes things:

from _pytest.config import get_config

cfg = get_config()

# This works
cfg._parser.addoption(
    "--livebeef",
    action="store",
    default=True
)
cfg.getoption("--livebeef")

I'm wondering if the reason @yuvalshi0 wasn't able to reproduce this error is because pytester.parseconfig or pytester.makeconftest is patching the bug. I want to look further into this issue and work on a fix for it, just don't want to step on anyone's toes if they're already working on a fix.

Go ahead ;)

yuvalshi0 avatar Dec 03 '21 06:12 yuvalshi0

please note that pytest uses a internal api to compose the argument parser later

one should never use cfg._parser.addoption like the last snippet

RonnyPfannschmidt avatar Dec 03 '21 11:12 RonnyPfannschmidt

@RonnyPfannschmidt thanks for the feedback, will work on creating a proper test for reproducing the error instead of the hacky snippet I used. Creating a proper reproduction of the error should also help in finding/developing a fix.

kevinsantana11 avatar Dec 03 '21 20:12 kevinsantana11

So it looks like after doing a proper test, the test passes and the getoption method no longer throws a value error, my motivation for doing the tests this way was to try and mimic the way in which the tf_addons are being tested: File Structure:

test_like_tf_addons
-- __init__.py
-- conftest.py
-- test_it.py
-- utils.py

File Contents:

# conftest.py
import pytest

from .utils import pytest_addoption, some_fixture
# test_it.py
import pytest

def test_it_already():
    assert True
# utils.py
import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--dead-beef",
        action="store_true"
    )

@pytest.fixture(scope="session", autouse=True)
def some_fixture(request):
    val = request.config.getoption("--dead-beef")

I also made sure to test things with the same versions used in the failed build link and here are the results:

platform linux -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/ksantana/sandbox/oss/pytest-play-plugin/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/ksantana/sandbox/oss/pytest-play-plugin/test_like_tf_addons
plugins: typeguard-2.13.0
collected 1 item                                                                                                                                                                                                     

test_it.py::test_it_already PASSED 

I've also tested pulling down the tf_addons project, running the commands from here link, then running the tests and all the tests still seem to pass. Motivation here was to see if maybe it was an issue with the build process.

Still looking into this, just wanted to post an update of the things I've tried and see if someone has other ideas on what could be the problem.

kevinsantana11 avatar Dec 04 '21 17:12 kevinsantana11

@kevinsantana11 it may be helpfull to put the conftest into a subdirectory

i suspect the issue may be related to conftest discovery and initial conftest loading

for ensuring the rootdir, adding a pytest.ini or a setup.cfg may help

RonnyPfannschmidt avatar Dec 04 '21 18:12 RonnyPfannschmidt

@RonnyPfannschmidt it looks like you're recommendation was very helpful, thank you very much!. So after digging into the code these are my thoughts:

Issue occurs because the initial pass that collects the conftests does not look in directories that don't begin with test* as we see on _pytest.config.PytestPluginManager._try_load_conftest:511. Subsequently if the conftests isn't picked up in that stage the options it adds won't be parsed during the parse_setoption process. We see this become a problem in the tensorflow addons project because the conftest is embedded in the addons folder, running pytest from the root level dir will cause the value error we are seeing. In the previous situation the conftest is later picked up during the collection round right before the test round starts.

my question now is where should the patch live, should I fix this by extending _try_load_conftests to search sub directories that do not start with test*or parse the arguments picked up after the collection round.

kevinsantana11 avatar Dec 05 '21 03:12 kevinsantana11