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?
I start with a very simple test in a single directory.
$ tree
.
├── conftest.py
└── test_foo.py
1 directory, 2 files
$
conftest.py
:import pytest
def pytest_addoption(parser):
parser.addoption("--foo", action="store")
@pytest.fixture
def my_val(request):
return request.config.getoption("--foo")
test_foo.py
: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 ================================
$
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
$
__init__.py
file.pyproject.toml
seems to be recognized despite not having the required [tool.pytest.ini_options]
table, thus apparently contradicting the documentation.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.
testpaths
in the pyproject.toml
.The best explanation I found so far relies on the following points from the documentation:
The necessity to put pytest_addoption in an "initial" conftest.py
:
This hook is only called for initial conftests.
What is actually an "initial conftest.py
":
Initial conftest are for each test path the files whose path match <test_path>/conftest.py
or <test_test>/test*/conftest.py
.
- by loading all “initial “conftest.py files:
- determine the test paths: specified on the command line, otherwise in testpaths if defined and running from the rootdir, otherwise the current dir
- for each test path, load conftest.py and test*/conftest.py relative to the directory part of the test path, if exist. Before a conftest.py file is loaded, load conftest.py files in all of its parent directories. After a conftest.py file is loaded, recursively load all plugins specified in its pytest_plugins variable if present.
So, if I understand well:
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.
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?
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"]
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.