pythonpytest

Why isn't the `pytest_addoption` hook run with the configured `testpaths`? (usage error)


Summary:

I'm trying to set up a custom pytest option with the pytest_addoption feature.

But when trying to configure my project with a project.toml file while using the said custom option, I'm getting the following error:

$ pytest --foo foo
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --foo
  inifile: /home/vmonteco/Code/MREs/pytest__addoption__pyproject_toml/01_adding_pyproject.toml/pyproject.toml
  rootdir: /home/vmonteco/Code/MREs/pytest__addoption__pyproject_toml/01_adding_pyproject.toml

Why is this problem occurring despite the configured test path and how could I solve it?

Used versions are:


How to reproduce:

Step 1 - before organizing the project, it works:

I start with a very simple test in a single directory.

$ tree
.
├── conftest.py
└── test_foo.py

1 directory, 2 files
$ 
import pytest

def pytest_addoption(parser):
    parser.addoption("--foo", action="store")

@pytest.fixture
def my_val(request):
    return request.config.getoption("--foo")
def test_foo(my_val):
    assert my_val == "foo"
$ pytest --foo bar
=============================== test session starts ===============================
platform linux -- Python 3.10.13, pytest-8.1.1, pluggy-1.5.0
rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/00_simplest_case
collected 1 item                                                                  

test_foo.py F                                                               [100%]

==================================== FAILURES =====================================
____________________________________ test_foo _____________________________________

my_val = 'bar'

    def test_foo(my_val):
>       assert my_val == "foo"
E       AssertionError: assert 'bar' == 'foo'
E         
E         - foo
E         + bar

test_foo.py:2: AssertionError
============================= short test summary info =============================
FAILED test_foo.py::test_foo - AssertionError: assert 'bar' == 'foo'
================================ 1 failed in 0.01s ================================
$ 

Step 2 - When adding an empty pyproject.toml and reorganizing the project, it fails:

$ tree
.
├── my_project
│   └── my_tests
│       ├── conftest.py
│       ├── __init__.py
│       └── test_foo.py
└── pyproject.toml

3 directories, 4 files
$ pytest --foo bar
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --foo
  inifile: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/01_adding_pyproject_toml/pyproject.toml
  rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/01_adding_pyproject_toml

$ 

Notes:

However, there's a simple workaround that seems to work in this specific case:

Just manually passing the test path to my tests as command line argument seems enough to make things work correctly again:

$ pytest --foo bar my_project/my_tests
=============================== test session starts ===============================
platform linux -- Python 3.10.13, pytest-8.1.1, pluggy-1.5.0
rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/01_adding_pyproject_toml
configfile: pyproject.toml
collected 1 item                                                                  

my_project/my_tests/test_foo.py F                                           [100%]

==================================== FAILURES =====================================
____________________________________ test_foo _____________________________________

my_val = 'bar'

    def test_foo(my_val):
>       assert my_val == "foo"
E       AssertionError: assert 'bar' == 'foo'
E         
E         - foo
E         + bar

my_project/my_tests/test_foo.py:2: AssertionError
============================= short test summary info =============================
FAILED my_project/my_tests/test_foo.py::test_foo - AssertionError: assert 'bar' == 'foo'
================================ 1 failed in 0.02s ================================
$

But I'd like to avoid that and I'd rather have my project correctly configured.

Step 3 - Unsuccessfully trying to configure testpaths in the pyproject.toml.

The best explanation I found so far relies on the following points from the documentation:

So, if I understand well:

  1. my error seems to occurs because if I don't provide an explicit test path, the current directory is used. But in that case, my conftest.py is too deep into the arborescence to be used as an initial conftest.

  2. With my workaround, explicitly passing a deeper test path solves this by making the conftest "initial" again.

From this, it would seem appropriate to try to translate my command line argument path into a bit of configuration (testpaths) as shown in the relevant documentation.

But when trying to run my command again, I still get the same error:

$ cat pyproject.toml
[tool.pytest.ini_options]
testpaths = [
          "my_project/my_tests",
]
$ tree
.
├── my_project
│   └── my_tests
│       ├── conftest.py
│       ├── __init__.py
│       └── test_foo.py
└── pyproject.toml

3 directories, 4 files
$ pytest --foo bar                  
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --foo
  inifile: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/02_attempt_to_solve/pyproject.toml
  rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/02_attempt_to_solve

$ 

I also tried to use a different kind of configuration file:

$ cat pytest.ini  
[pytest]
testpaths = my_project/my_tests
$ pytest --foo bar
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --foo
  inifile: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/03_with_pytest_ini/pytest.ini
  rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/03_with_pytest_ini

$ 

But it still doesn't solve the problem despite the equivalent as command line argument seems to work. Why?


Addendum - raw output of new step 2 reproduction session:

Script started on 2024-08-06 03:23:13+02:00 [TERM="tmux-256color" TTY="/dev/pts/8" COLUMNS="126" LINES="69"]
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004httree[?1l>[?2004l
 
 
ktree\[01;34m.[0m
├── [01;34mmy_project[0m
│   └── [01;34mmy_tests[0m
│       ├── conftest.py
│       ├── __init__.py
│       └── test_foo.py
└── pyproject.toml
 
3 directories, 4 files
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hccat pyproject.toml[1m [0m[0m [?1l>[?2004l
 
 
kcat\[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hccat my_project[1m/[0m[0m/my_tests[1m/[0m[0m/conftest.py[1m [0m[0m [?1l>[?2004l
 
 
kcat\import pytest
 
def pytest_addoption(parser):
    parser.addoption("--foo", action="store")
 
@pytest.fixture
def my_val(request):
    return request.config.getoption("--foo")
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hcat my_project/my_tests/conftest.py           __init__.py[1m [0m[0m [?1l>[?2004l
 
 
kcat\[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hcat my_project/my_tests/__init__.py           test_foo.py[1m [0m[0m [?1l>[?2004l
 
 
kcat\def test_foo(my_val):
    assert my_val == "foo"
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hppython --version[?1l>[?2004l
 
 
kpython\Python 3.10.13
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hpython --version            est --version[?1l>[?2004l
 
 
kpytest\pytest 8.1.1
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;32m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004hppytest -fo  -foo bar[?1l>[?2004l
 
 
kpytest\[31mERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --foo
  inifile: /home/vmonteco/code/MREs/new_pytest_MRE/pyproject.toml
  rootdir: /home/vmonteco/code/MREs/new_pytest_MRE
[0m
[1m[7m%[27m[1m[0m                                                                                                                             
 
k..ew_pytest_MRE\]7;file://vmonteco-desktop/home/vmonteco/code/MREs/new_pytest_MRE\
[0m[27m[24m[J[01;31m➜  [36mnew_pytest_MRE[00m [K[?1h=[?2004h[?2004l
 
 
 

    Script done on 2024-08-06 03:24:49+02:00 [COMMAND_EXIT_CODE="4"]

link to pastebin


Solution

  • It looks like it indeed was an usage error (and also facepalm-worthy material):

    pytest --foo=bar rather than pytest --foo bar.

    $ pytest --foo bar
    ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
    pytest: error: unrecognized arguments: --foo
      inifile: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/02_attempt_to_solve/pyproject.toml
      rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/02_attempt_to_solve
    
    $ pytest --foo=bar
    =============================== test session starts ===============================
    platform linux -- Python 3.10.13, pytest-8.3.2, pluggy-1.5.0
    rootdir: /home/vmonteco/code/MREs/pytest__addoption__pyproject_toml/02_attempt_to_solve
    configfile: pyproject.toml
    testpaths: my_project/my_tests
    collected 1 item                                                                  
    
    my_project/my_tests/test_foo.py F                                           [100%]
    
    ==================================== FAILURES =====================================
    ____________________________________ test_foo _____________________________________
    
    my_val = 'bar'
    
        def test_foo(my_val):
    >       assert my_val == "foo"
    E       AssertionError: assert 'bar' == 'foo'
    E         
    E         - foo
    E         + bar
    
    my_project/my_tests/test_foo.py:2: AssertionError
    ============================= short test summary info =============================
    FAILED my_project/my_tests/test_foo.py::test_foo - AssertionError: assert 'bar' == 'foo'
    ================================ 1 failed in 0.02s ================================
    $ 
    

    Addendum: Here's on explanation on why pytest is designed like thas.