diff --git a/changelog/13743.feature.rst b/changelog/13743.feature.rst new file mode 100644 index 00000000000..b36782487f5 --- /dev/null +++ b/changelog/13743.feature.rst @@ -0,0 +1,37 @@ +Added support for native TOML configuration files. + +While pytest, since version 6, supports configuration in ``pyproject.toml`` files under ``[tool.pytest.ini_options]``, +it does so in an "INI compatibility mode", where all configuration values are treated as strings or list of strings. +Now, pytest supports the native TOML data model. + +In ``pyproject.toml``, the native TOML configuration is under the ``[tool.pytest]`` table. + +.. code-block:: toml + + # pyproject.toml + [tool.pytest] + minversion = "9.0" + addopts = ["-ra", "-q"] + testpaths = [ + "tests", + "integration", + ] + +The ``[tool.pytest.ini_options]`` table remains supported, but both tables cannot be used at the same time. + +If you prefer to use a separate configuration file, or don't use ``pyproject.toml``, you can use ``pytest.toml`` or ``.pytest.toml``: + +.. code-block:: toml + + # pytest.toml or .pytest.toml + [pytest] + minversion = "9.0" + addopts = ["-ra", "-q"] + testpaths = [ + "tests", + "integration", + ] + +The documentation now shows configuration snippets in both TOML and INI formats, in a tabbed interface. + +See :ref:`config file formats` for full details. diff --git a/changelog/13849.bugfix.rst b/changelog/13849.bugfix.rst new file mode 100644 index 00000000000..cdcc7b83591 --- /dev/null +++ b/changelog/13849.bugfix.rst @@ -0,0 +1,2 @@ +Hidden ``.pytest.ini`` files are now picked up as the config file even if empty. +This was an inconsistency with non-hidden ``pytest.ini``. diff --git a/doc/en/conf.py b/doc/en/conf.py index c89e14d07fa..81156493131 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -34,6 +34,7 @@ "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx_removed_in", + "sphinx_inline_tabs", "sphinxcontrib_trio", "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive "sphinx_issues", # implements `:issue:`, `:pr:` and other GH-related roles diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 790e1d89f29..65a05823517 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -845,20 +845,38 @@ that manipulate this type of file (for example, Jenkins, Azure Pipelines, etc.). Users are recommended to try the new ``xunit2`` format and see if their tooling that consumes the JUnit XML file supports it. -To use the new format, update your ``pytest.ini``: +To use the new format, update your configuration file: -.. code-block:: ini +.. tab:: toml - [pytest] - junit_family=xunit2 + .. code-block:: toml + + [pytest] + junit_family = "xunit2" + +.. tab:: ini + + .. code-block:: ini + + [pytest] + junit_family = xunit2 If you discover that your tooling does not support the new format, and want to keep using the legacy version, set the option to ``legacy`` instead: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + junit_family = "legacy" + +.. tab:: ini + + .. code-block:: ini - [pytest] - junit_family=legacy + [pytest] + junit_family = legacy By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading to pytest 6.0, where the default format will be ``xunit2``. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 071869c07b4..ed830b9fb2e 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -239,13 +239,21 @@ Registering markers Registering markers for your test suite is simple: -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - markers = - webtest: mark a test as a webtest. - slow: mark test as slow. + .. code-block:: toml + + [pytest] + markers = ["webtest: mark a test as a webtest.", "slow: mark test as slow."] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + markers = + webtest: mark a test as a webtest. + slow: mark test as slow. Multiple custom markers can be registered, by defining each one in its own line, as shown in above example. diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 9aada00345a..5ff3ae20247 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -88,13 +88,21 @@ Example: Changing directory recursion ----------------------------------------------------- -You can set the :confval:`norecursedirs` option in an ini-file, for example your ``pytest.ini`` in the project root directory: +You can set the :confval:`norecursedirs` option in a configuration file: -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - norecursedirs = .svn _build tmp* + .. code-block:: toml + + [pytest] + norecursedirs = [".svn", "_build", "tmp*"] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + norecursedirs = .svn _build tmp* This would tell ``pytest`` to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. @@ -108,14 +116,25 @@ the :confval:`python_files`, :confval:`python_classes` and :confval:`python_functions` in your :ref:`configuration file `. Here is an example: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml - # content of pytest.ini - # Example 1: have pytest look for "check" instead of "test" - [pytest] - python_files = check_*.py - python_classes = Check - python_functions = *_check + # Example 1: have pytest look for "check" instead of "test" + [pytest] + python_files = ["check_*.py"] + python_classes = ["Check"] + python_functions = ["*_check"] + +.. tab:: ini + + .. code-block:: ini + + # Example 1: have pytest look for "check" instead of "test" + [pytest] + python_files = check_*.py + python_classes = Check + python_functions = *_check This would make ``pytest`` look for tests in files that match the ``check_* .py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods @@ -152,12 +171,21 @@ The test collection would look like this: You can check for multiple glob patterns by adding a space between the patterns: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + # Example 2: have pytest look for files with "test" and "example" + [pytest] + python_files = ["test_*.py", "example_*.py"] - # Example 2: have pytest look for files with "test" and "example" - # content of pytest.ini - [pytest] - python_files = test_*.py example_*.py +.. tab:: ini + + .. code-block:: ini + + # Example 2: have pytest look for files with "test" and "example" + [pytest] + python_files = test_*.py example_*.py .. note:: @@ -178,14 +206,22 @@ example if you have unittest2 installed you can type: pytest --pyargs unittest2.test.test_skipping -q which would run the respective test module. Like with -other options, through an ini-file and the :confval:`addopts` option you +other options, through a configuration file and the :confval:`addopts` option you can make this change more permanently: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["--pyargs"] + +.. tab:: ini - # content of pytest.ini - [pytest] - addopts = --pyargs + .. code-block:: ini + + [pytest] + addopts = --pyargs Now a simple invocation of ``pytest NAME`` will check if NAME exists as an importable package/module and otherwise @@ -224,11 +260,19 @@ Customizing test collection You can easily instruct ``pytest`` to discover tests from every Python file: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + python_files = ["*.py"] + +.. tab:: ini + + .. code-block:: ini - # content of pytest.ini - [pytest] - python_files = *.py + [pytest] + python_files = *.py However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index c1a444bea18..7effb480480 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -11,11 +11,19 @@ every time you use ``pytest``. For example, if you always want to see detailed info on skipped and xfailed tests, as well as have terser "dot" progress output, you can write it into a configuration file: -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - addopts = -ra -q + .. code-block:: toml + + [pytest] + addopts = ["-ra", "-q"] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + addopts = -ra -q Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command @@ -29,7 +37,7 @@ Here's how the command-line is built in the presence of ``addopts`` or the envir .. code-block:: text - $PYTEST_ADDOPTS + $PYTEST_ADDOPTS So if the user executes in the command-line: diff --git a/doc/en/explanation/goodpractices.rst b/doc/en/explanation/goodpractices.rst index 51c0b960aed..83c6a5f4b56 100644 --- a/doc/en/explanation/goodpractices.rst +++ b/doc/en/explanation/goodpractices.rst @@ -94,14 +94,21 @@ This has the following benefits: For new projects, we recommend to use ``importlib`` :ref:`import mode ` (see which-import-mode_ for a detailed explanation). -To this end, add the following to your ``pyproject.toml``: +To this end, add the following to your configuration file: -.. code-block:: toml +.. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["--import-mode=importlib"] + +.. tab:: ini + + .. code-block:: ini - [tool.pytest.ini_options] - addopts = [ - "--import-mode=importlib", - ] + [pytest] + addopts = --import-mode=importlib .. _src-layout: @@ -126,12 +133,21 @@ which are better explained in this excellent `blog post`_ by Ionel Cristian Măr PYTHONPATH=src pytest or in a permanent manner by using the :confval:`pythonpath` configuration variable and adding the - following to your ``pyproject.toml``: + following to your configuration file: - .. code-block:: toml + .. tab:: toml + + .. code-block:: toml + + [pytest] + pythonpath = ["src"] + + .. tab:: ini + + .. code-block:: ini - [tool.pytest.ini_options] - pythonpath = "src" + [pytest] + pythonpath = src .. note:: diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst index a9bd894b6fd..8ed546bedf7 100644 --- a/doc/en/how-to/capture-warnings.rst +++ b/doc/en/how-to/capture-warnings.rst @@ -80,30 +80,32 @@ as an error: FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ... 1 failed in 0.12s -The same option can be set in the ``pytest.ini`` or ``pyproject.toml`` file using the -``filterwarnings`` ini option. For example, the configuration below will ignore all +The same option can be set in the configuration file using the +:confval:`filterwarnings` configuration option. For example, the configuration below will ignore all user warnings and specific deprecation warnings matching a regex, but will transform all other warnings into errors. -.. code-block:: ini +.. tab:: toml - # pytest.ini - [pytest] - filterwarnings = - error - ignore::UserWarning - ignore:function ham\(\) is deprecated:DeprecationWarning + .. code-block:: toml -.. code-block:: toml + [pytest] + filterwarnings = [ + "error", + "ignore::UserWarning", + # note the use of single quote below to denote "raw" strings in TOML + 'ignore:function ham\(\) is deprecated:DeprecationWarning', + ] + +.. tab:: ini - # pyproject.toml - [tool.pytest.ini_options] - filterwarnings = [ - "error", - "ignore::UserWarning", - # note the use of single quote below to denote "raw" strings in TOML - 'ignore:function ham\(\) is deprecated:DeprecationWarning', - ] + .. code-block:: ini + + [pytest] + filterwarnings = + error + ignore::UserWarning + ignore:function ham\(\) is deprecated:DeprecationWarning When a warning matches more than one option in the list, the action for the last matching option @@ -112,7 +114,7 @@ is performed. .. note:: - The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are + The ``-W`` flag and the :confval:`filterwarnings` configuration option use warning filters that are similar in structure, but each configuration option interprets its filter differently. For example, *message* in ``filterwarnings`` is a string containing a regular expression that the start of the warning message must match, @@ -169,7 +171,7 @@ You can specify multiple filters with separate decorators: Filters applied using a mark take precedence over filters passed on the command line or configured -by the :confval:`filterwarnings` ini option. +by the :confval:`filterwarnings` configuration option. You may apply a filter to all tests of a class by using the :ref:`filterwarnings ` mark as a class decorator or to all tests in a module by setting the :globalvar:`pytestmark` variable: @@ -202,7 +204,16 @@ warning summary entirely from the test run output. Disabling warning capture entirely ---------------------------------- -This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with: +This plugin is enabled by default but can be disabled entirely in your configuration file with: + +.. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["-p", "no:warnings"] + +.. tab:: ini .. code-block:: ini @@ -227,16 +238,27 @@ However, in the specific case where users capture any type of warnings in their no warning will be displayed at all. Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over -(such as third-party libraries), in which case you might use the warning filters options (ini or marks) to ignore +(such as third-party libraries), in which case you might use the warning filters options (configuration or marks) to ignore those warnings. For example: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + filterwarnings = [ + 'ignore:.*U.*mode is deprecated:DeprecationWarning', + ] + +.. tab:: ini + + .. code-block:: ini - [pytest] - filterwarnings = - ignore:.*U.*mode is deprecated:DeprecationWarning + [pytest] + filterwarnings = + ignore:.*U.*mode is deprecated:DeprecationWarning This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches diff --git a/doc/en/how-to/doctest.rst b/doc/en/how-to/doctest.rst index 0a778a8a246..5a11a475abd 100644 --- a/doc/en/how-to/doctest.rst +++ b/doc/en/how-to/doctest.rst @@ -68,13 +68,21 @@ and functions, including from test modules: ============================ 2 passed in 0.12s ============================= You can make these changes permanent in your project by -putting them into a pytest.ini file like this: +putting them into a configuration file like this: -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - addopts = --doctest-modules + .. code-block:: toml + + [pytest] + addopts = ["--doctest-modules"] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + addopts = --doctest-modules Encoding @@ -82,13 +90,21 @@ Encoding The default encoding is **UTF-8**, but you can specify the encoding that will be used for those doctest files using the -``doctest_encoding`` ini option: +:confval:`doctest_encoding` configuration option: + +.. tab:: toml + + .. code-block:: toml -.. code-block:: ini + [pytest] + doctest_encoding = "latin1" - # content of pytest.ini - [pytest] - doctest_encoding = latin1 +.. tab:: ini + + .. code-block:: ini + + [pytest] + doctest_encoding = latin1 .. _using doctest options: @@ -102,10 +118,19 @@ configuration file. For example, to make pytest ignore trailing whitespaces and ignore lengthy exception stack traces you can just write: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] + +.. tab:: ini + + .. code-block:: ini - [pytest] - doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL + [pytest] + doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL Alternatively, options can be enabled by an inline comment in the doc test itself: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 7f1a7610bb8..48b2653a0f6 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1736,13 +1736,21 @@ and you may specify fixture usage at the test module level using :globalvar:`pyt It is also possible to put fixtures required by all tests in your project -into an ini-file: +into a configuration file: -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - usefixtures = cleandir + .. code-block:: toml + + [pytest] + usefixtures = ["cleandir"] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + usefixtures = cleandir .. warning:: diff --git a/doc/en/how-to/logging.rst b/doc/en/how-to/logging.rst index 300e9f6e6c2..d6d87f03bbf 100644 --- a/doc/en/how-to/logging.rst +++ b/doc/en/how-to/logging.rst @@ -47,13 +47,23 @@ Shows failed tests like so: text going to stderr ==================== 2 failed in 0.02 seconds ===================== -These options can also be customized through ``pytest.ini`` file: +These options can also be customized through a configuration file: -.. code-block:: ini +.. tab:: toml - [pytest] - log_format = %(asctime)s %(levelname)s %(message)s - log_date_format = %Y-%m-%d %H:%M:%S + .. code-block:: toml + + [pytest] + log_format = "%(asctime)s %(levelname)s %(message)s" + log_date_format = "%Y-%m-%d %H:%M:%S" + +.. tab:: ini + + .. code-block:: ini + + [pytest] + log_format = %(asctime)s %(levelname)s %(message)s + log_date_format = %Y-%m-%d %H:%M:%S Specific loggers can be disabled via ``--log-disable={logger_name}``. This argument can be passed multiple times: @@ -198,12 +208,12 @@ Additionally, you can also specify ``--log-cli-format`` and ``--log-date-format`` if not provided, but are applied only to the console logging handler. -All of the CLI log options can also be set in the configuration INI file. The +All of the CLI log options can also be set in the configuration file. The option names are: -* ``log_cli_level`` -* ``log_cli_format`` -* ``log_cli_date_format`` +* :confval:`log_cli_level` +* :confval:`log_cli_format` +* :confval:`log_cli_date_format` If you need to record the whole test suite logging calls to a file, you can pass ``--log-file=/path/to/log/file``. This log file is opened in write mode by default which @@ -220,17 +230,17 @@ Additionally, you can also specify ``--log-file-format`` and ``--log-file-date-format`` which are equal to ``--log-format`` and ``--log-date-format`` but are applied to the log file logging handler. -All of the log file options can also be set in the configuration INI file. The +All of the log file options can also be set in the configuration file. The option names are: -* ``log_file`` -* ``log_file_mode`` -* ``log_file_level`` -* ``log_file_format`` -* ``log_file_date_format`` +* :confval:`log_file` +* :confval:`log_file_mode` +* :confval:`log_file_level` +* :confval:`log_file_format` +* :confval:`log_file_date_format` You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality -is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option. +is considered **experimental**. Note that ``set_log_path()`` respects the :confval:`log_file_mode` option. .. _log_colors: @@ -266,12 +276,21 @@ This feature was introduced as a drop-in replacement for the with each other. The backward compatibility API with ``pytest-capturelog`` has been dropped when this feature was introduced, so if for that reason you still need ``pytest-catchlog`` you can disable the internal feature by -adding to your ``pytest.ini``: +adding to your configuration file: + +.. tab:: toml + + .. code-block:: toml -.. code-block:: ini + [pytest] + addopts = ["-p", "no:logging"] - [pytest] - addopts=-p no:logging +.. tab:: ini + + .. code-block:: ini + + [pytest] + addopts = -p no:logging .. _log_changes_3_4: @@ -293,13 +312,23 @@ made in ``3.4`` after community feedback: * :ref:`Live Logs ` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option to work. -If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini`` +If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your configuration file: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + log_cli = true + log_level = "NOTSET" + +.. tab:: ini + + .. code-block:: ini - [pytest] - log_cli=true - log_level=NOTSET + [pytest] + log_cli = true + log_level = NOTSET More details about the discussion that lead to this changes can be read in :issue:`3013`. diff --git a/doc/en/how-to/mark.rst b/doc/en/how-to/mark.rst index 40ee14c36fd..575ce2f41c2 100644 --- a/doc/en/how-to/mark.rst +++ b/doc/en/how-to/mark.rst @@ -34,24 +34,26 @@ See :ref:`mark examples` for examples which also serve as documentation. Registering marks ----------------- -You can register custom marks in your ``pytest.ini`` file like this: +You can register custom marks in your configuration file like this: -.. code-block:: ini +.. tab:: toml - [pytest] - markers = - slow: marks tests as slow (deselect with '-m "not slow"') - serial + .. code-block:: toml -or in your ``pyproject.toml`` file like this: + [pytest] + markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "serial", + ] -.. code-block:: toml +.. tab:: ini - [tool.pytest.ini_options] - markers = [ - "slow: marks tests as slow (deselect with '-m \"not slow\"')", - "serial", - ] + .. code-block:: ini + + [pytest] + markers = + slow: marks tests as slow (deselect with '-m "not slow"') + serial Note that everything past the ``:`` after the mark name is an optional description. @@ -77,17 +79,30 @@ Raising errors on unknown marks Unregistered marks applied with the ``@pytest.mark.name_of_the_mark`` decorator will always emit a warning in order to avoid silently doing something surprising due to mistyped names. As described in the previous section, you can disable -the warning for custom marks by registering them in your ``pytest.ini`` file or +the warning for custom marks by registering them in your configuration file or using a custom ``pytest_configure`` hook. -When the :confval:`strict_markers` ini option is set, any unknown marks applied +When the :confval:`strict_markers` configuration option is set, any unknown marks applied with the ``@pytest.mark.name_of_the_mark`` decorator will trigger an error. You can enforce this validation in your project by setting :confval:`strict_markers` in your configuration: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["--strict-markers"] + markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "serial", + ] + +.. tab:: ini + + .. code-block:: ini - [pytest] - strict_markers = True - markers = - slow: marks tests as slow (deselect with '-m "not slow"') - serial + [pytest] + strict_markers = true + markers = + slow: marks tests as slow (deselect with '-m "not slow"') + serial diff --git a/doc/en/how-to/output.rst b/doc/en/how-to/output.rst index bc98b816484..e03f477b22d 100644 --- a/doc/en/how-to/output.rst +++ b/doc/en/how-to/output.rst @@ -558,13 +558,23 @@ Modifying truncation limits .. versionadded: 8.4 Default truncation limits are 8 lines or 640 characters, whichever comes first. -To set custom truncation limits you can use following ``pytest.ini`` file options: +To set custom truncation limits you can use the following configuration file options: -.. code-block:: ini +.. tab:: toml - [pytest] - truncation_limit_lines = 10 - truncation_limit_chars = 90 + .. code-block:: toml + + [pytest] + truncation_limit_lines = 10 + truncation_limit_chars = 90 + +.. tab:: ini + + .. code-block:: ini + + [pytest] + truncation_limit_lines = 10 + truncation_limit_chars = 90 That will cause pytest to truncate the assertions to 10 lines or 90 characters, whichever comes first. @@ -588,10 +598,19 @@ to create an XML file at ``path``. To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + junit_suite_name = "my_suite" - [pytest] - junit_suite_name = my_suite +.. tab:: ini + + .. code-block:: ini + + [pytest] + junit_suite_name = my_suite .. versionadded:: 4.0 @@ -602,10 +621,19 @@ should report total test execution times, including setup and teardown It is the default pytest behavior. To report just call durations instead, configure the ``junit_duration_report`` option like this: -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + junit_duration_report = "call" + +.. tab:: ini + + .. code-block:: ini - [pytest] - junit_duration_report = call + [pytest] + junit_duration_report = call .. _record_property example: diff --git a/doc/en/how-to/parametrize.rst b/doc/en/how-to/parametrize.rst index fe186146434..e06b85341a2 100644 --- a/doc/en/how-to/parametrize.rst +++ b/doc/en/how-to/parametrize.rst @@ -88,12 +88,21 @@ them in turn: for the parametrization because it has several downsides. If however you would like to use unicode strings in parametrization and see them in the terminal as is (non-escaped), use this option - in your ``pytest.ini``: + in your configuration file: - .. code-block:: ini + .. tab:: toml - [pytest] - disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True + .. code-block:: toml + + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true Keep in mind however that this might cause unwanted side effects and even bugs depending on the OS used and plugins currently installed, diff --git a/doc/en/how-to/plugins.rst b/doc/en/how-to/plugins.rst index b2cb1cda5c3..591c44dfa4d 100644 --- a/doc/en/how-to/plugins.rst +++ b/doc/en/how-to/plugins.rst @@ -120,12 +120,21 @@ This means that any subsequent try to activate/load the named plugin will not work. If you want to unconditionally disable a plugin for a project, you can add -this option to your ``pytest.ini`` file: +this option to your configuration file: -.. code-block:: ini +.. tab:: toml - [pytest] - addopts = -p no:NAME + .. code-block:: toml + + [pytest] + addopts = ["-p", "no:NAME"] + +.. tab:: ini + + .. code-block:: ini + + [pytest] + addopts = -p no:NAME Alternatively to disable it only in certain environments (for example in a CI server), you can set ``PYTEST_ADDOPTS`` environment variable to @@ -151,10 +160,19 @@ manually specify each plugin with ``-p`` or :envvar:`PYTEST_PLUGINS`, you can us pytest --disable-plugin-autoload -p NAME,NAME2 -.. code-block:: ini +.. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["--disable-plugin-autoload", "-p", "NAME", "-p", "NAME2"] + +.. tab:: ini + + .. code-block:: ini - [pytest] - addopts = + [pytest] + addopts = --disable-plugin-autoload -p NAME -p NAME2 diff --git a/doc/en/how-to/skipping.rst b/doc/en/how-to/skipping.rst index 10c45c23ed2..1887fbd53ef 100644 --- a/doc/en/how-to/skipping.rst +++ b/doc/en/how-to/skipping.rst @@ -333,10 +333,19 @@ This will make ``XPASS`` ("unexpectedly passing") results from this test to fail You can change the default value of the ``strict`` parameter using the ``strict_xfail`` ini option: -.. code-block:: ini +.. tab:: toml - [pytest] - strict_xfail=true + .. code-block:: toml + + [pytest] + xfail_strict = true + +.. tab:: ini + + .. code-block:: ini + + [pytest] + strict_xfail = true Ignoring xfail diff --git a/doc/en/how-to/writing_plugins.rst b/doc/en/how-to/writing_plugins.rst index 1bba9644649..d3ddf45c276 100644 --- a/doc/en/how-to/writing_plugins.rst +++ b/doc/en/how-to/writing_plugins.rst @@ -420,13 +420,21 @@ before running pytest on it. This way we can abstract the tested logic to separa which is especially useful for longer tests and/or longer ``conftest.py`` files. Note that for ``pytester.copy_example`` to work we need to set `pytester_example_dir` -in our ``pytest.ini`` to tell pytest where to look for example files. +in our configuration file to tell pytest where to look for example files. -.. code-block:: ini +.. tab:: toml - # content of pytest.ini - [pytest] - pytester_example_dir = . + .. code-block:: toml + + [pytest] + pytester_example_dir = "." + +.. tab:: ini + + .. code-block:: ini + + [pytest] + pytester_example_dir = . .. code-block:: python diff --git a/doc/en/reference/customize.rst b/doc/en/reference/customize.rst index 373223ec913..500e5519bdd 100644 --- a/doc/en/reference/customize.rst +++ b/doc/en/reference/customize.rst @@ -4,8 +4,7 @@ Configuration Command line options and configuration file settings ----------------------------------------------------------------- -You can get help on command line options and values in INI-style -configurations files by using the general help option: +You can get help on command line and configuration options by using the general help option: .. code-block:: bash @@ -24,51 +23,85 @@ by convention resides in the root directory of your repository. A quick example of the configuration files supported by pytest: +pytest.toml +~~~~~~~~~~~ + +.. versionadded:: 9.0 + +``pytest.toml`` files take precedence over other files, even when empty. + +Alternatively, the hidden version ``.pytest.toml`` can be used. + +.. tab:: toml + + .. code-block:: toml + + # pytest.toml or .pytest.toml + [pytest] + minversion = "9.0" + addopts = ["-ra", "-q"] + testpaths = [ + "tests", + "integration", + ] + pytest.ini ~~~~~~~~~~ -``pytest.ini`` files take precedence over other files, even when empty. +``pytest.ini`` files take precedence over other files (except ``pytest.toml`` and ``.pytest.toml``), even when empty. Alternatively, the hidden version ``.pytest.ini`` can be used. -.. code-block:: ini +.. tab:: ini - # pytest.ini or .pytest.ini - [pytest] - minversion = 6.0 - addopts = -ra -q - testpaths = - tests - integration + .. code-block:: ini + + # pytest.ini or .pytest.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration pyproject.toml ~~~~~~~~~~~~~~ .. versionadded:: 6.0 +.. versionchanged:: 9.0 + +``pyproject.toml`` files are supported for configuration. + +.. tab:: toml -``pyproject.toml`` are considered for configuration when they contain a ``tool.pytest.ini_options`` table. + Use ``[tool.pytest]`` to leverage native TOML types (supported since pytest 9.0): -.. code-block:: toml + .. code-block:: toml - # pyproject.toml - [tool.pytest.ini_options] - minversion = "6.0" - addopts = "-ra -q" - testpaths = [ - "tests", - "integration", - ] + # pyproject.toml + [tool.pytest] + minversion = "9.0" + addopts = ["-ra", "-q"] + testpaths = [ + "tests", + "integration", + ] -.. note:: +.. tab:: ini - One might wonder why ``[tool.pytest.ini_options]`` instead of ``[tool.pytest]`` as is the - case with other tools. + Use ``[tool.pytest.ini_options]`` for INI-style configuration (supported since pytest 6.0): - The reason is that the pytest team intends to fully utilize the rich TOML data format - for configuration in the future, reserving the ``[tool.pytest]`` table for that. - The ``ini_options`` table is being used, for now, as a bridge between the existing - ``.ini`` configuration system and the future configuration format. + .. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] tox.ini ~~~~~~~ @@ -76,15 +109,17 @@ tox.ini ``tox.ini`` files are the configuration files of the `tox `__ project, and can also be used to hold pytest configuration if they have a ``[pytest]`` section. -.. code-block:: ini +.. tab:: ini + + .. code-block:: ini - # tox.ini - [pytest] - minversion = 6.0 - addopts = -ra -q - testpaths = - tests - integration + # tox.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration setup.cfg @@ -93,15 +128,17 @@ setup.cfg ``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and :std:doc:`setuptools `, and can also be used to hold pytest configuration if they have a ``[tool:pytest]`` section. -.. code-block:: ini +.. tab:: ini + + .. code-block:: ini - # setup.cfg - [tool:pytest] - minversion = 6.0 - addopts = -ra -q - testpaths = - tests - integration + # setup.cfg + [tool:pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration .. warning:: @@ -138,7 +175,7 @@ influence how modules are imported. See :ref:`pythonpath` for more details. The ``--rootdir=path`` command-line option can be used to force a specific directory. Note that contrary to other command-line options, ``--rootdir`` cannot be used with -:confval:`addopts` inside ``pytest.ini`` because the ``rootdir`` is used to *find* ``pytest.ini`` +:confval:`addopts` inside a configuration file because the ``rootdir`` is used to *find* the configuration file already. Finding the ``rootdir`` @@ -152,14 +189,14 @@ Here is the algorithm which finds the rootdir from ``args``: recognised as paths that exist in the file system. If no such paths are found, the common ancestor directory is set to the current working directory. -- Look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` files in the ancestor +- Look for ``pytest.toml``, ``.pytest.toml``, ``pytest.ini``, ``.pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` files in the ancestor directory and upwards. If one is matched, it becomes the ``configfile`` and its directory becomes the ``rootdir``. - If no configuration file was found, look for ``setup.py`` upwards from the common ancestor directory to determine the ``rootdir``. -- If no ``setup.py`` was found, look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and +- If no ``setup.py`` was found, look for ``pytest.toml``, ``.pytest.toml``, ``pytest.ini``, ``.pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` in each of the specified ``args`` and upwards. If one is matched, it becomes the ``configfile`` and its directory becomes the ``rootdir``. @@ -172,13 +209,15 @@ directory and also starts determining the ``rootdir`` from there. Files will only be matched for configuration if: -* ``pytest.ini``: will always match and take precedence, even if empty. -* ``pyproject.toml``: contains a ``[tool.pytest.ini_options]`` table. +* ``pytest.toml``: will always match and take highest precedence, even if empty. +* ``pytest.ini``: will always match and take precedence (after ``pytest.toml`` and ``.pytest.toml``), even if empty. +* ``pyproject.toml``: contains a ``[tool.pytest]`` or ``[tool.pytest.ini_options]`` table. * ``tox.ini``: contains a ``[pytest]`` section. * ``setup.cfg``: contains a ``[tool:pytest]`` section. Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case -even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``). +even if it does not contain a ``[tool.pytest]`` table (since version ``9.0``) or a ``[tool.pytest.ini_options]`` +table (since version ``8.1``). The files are considered in the order above. Options from multiple ``configfiles`` candidates are never merged - the first match wins. @@ -213,11 +252,13 @@ check for configuration files as follows: .. code-block:: text - # first look for pytest.ini files + # first look for path/pytest.toml + path/pytest.toml path/pytest.ini - path/pyproject.toml # must contain a [tool.pytest.ini_options] table to match + path/pyproject.toml # must contain a [tool.pytest] table to match path/tox.ini # must contain [pytest] section to match path/setup.cfg # must contain [tool:pytest] section to match + pytest.toml pytest.ini ... # all the way up to the root diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index d5f8716d7b4..2bb720ada10 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1307,7 +1307,7 @@ To see each file format in details, see :ref:`config file formats`. Usage of ``setup.cfg`` is not recommended except for very simple use cases. ``.cfg`` files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track down problems. - When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your pytest configuration. + When possible, it is recommended to use the latter files, or ``pytest.toml`` or ``pyproject.toml``, to hold your pytest configuration. Configuration options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be passed multiple times. The expected format is ``name=value``. For example:: @@ -1318,13 +1318,21 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: addopts Add the specified ``OPTS`` to the set of command line arguments as if they - had been specified by the user. Example: if you have this ini file content: + had been specified by the user. Example: if you have this configuration file content: - .. code-block:: ini + .. tab:: toml - # content of pytest.ini - [pytest] - addopts = --maxfail=2 -rf # exit after 2 failures, report fail info + .. code-block:: toml + + [pytest] + addopts = ["--maxfail=2", "-rf"] # exit after 2 failures, report fail info + + .. tab:: ini + + .. code-block:: ini + + [pytest] + addopts = --maxfail=2 -rf # exit after 2 failures, report fail info issuing ``pytest test_hello.py`` actually means: @@ -1351,10 +1359,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Setting this to ``false`` will make pytest collect classes/functions from test files **only** if they are defined in that file (as opposed to imported there). - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + collect_imported_tests = false + + .. tab:: ini - [pytest] - collect_imported_tests = false + .. code-block:: ini + + [pytest] + collect_imported_tests = false Default: ``true`` @@ -1404,11 +1421,19 @@ passed multiple times. The expected format is ``name=value``. For example:: The default is ``progress``, but you can fallback to ``classic`` if you prefer or the new mode is causing unexpected problems: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + console_output_style = "classic" - # content of pytest.ini - [pytest] - console_output_style = classic + .. tab:: ini + + .. code-block:: ini + + [pytest] + console_output_style = classic .. confval:: disable_test_id_escaping_and_forfeit_all_rights_to_community_support @@ -1419,12 +1444,21 @@ passed multiple times. The expected format is ``name=value``. For example:: for the parametrization because it has several downsides. If however you would like to use unicode strings in parametrization and see them in the terminal as is (non-escaped), use this option - in your ``pytest.ini``: + in your configuration file: + + .. tab:: toml - .. code-block:: ini + .. code-block:: toml - [pytest] - disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true Keep in mind however that this might cause unwanted side effects and even bugs depending on the OS used and plugins currently installed, @@ -1458,11 +1492,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``xfail`` marks tests with an empty parameterset as xfail(run=False) * ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + empty_parameter_set_mark = "xfail" + + .. tab:: ini + + .. code-block:: ini - # content of pytest.ini - [pytest] - empty_parameter_set_mark = xfail + [pytest] + empty_parameter_set_mark = xfail .. note:: @@ -1470,17 +1512,45 @@ passed multiple times. The expected format is ``name=value``. For example:: as this is considered less error prone, see :issue:`3155` for more details. +.. confval:: enable_assertion_pass_hook + + Enables the :hook:`pytest_assertion_pass` hook. + Make sure to delete any previously generated ``.pyc`` cache files. + + .. tab:: toml + + .. code-block:: toml + + [pytest] + enable_assertion_pass_hook = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + enable_assertion_pass_hook = true + + .. confval:: faulthandler_timeout Dumps the tracebacks of all threads if a test takes longer than ``X`` seconds to run (including fixture setup and teardown). Implemented using the :func:`faulthandler.dump_traceback_later` function, so all caveats there apply. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + faulthandler_timeout = 5 + + .. tab:: ini + + .. code-block:: ini - # content of pytest.ini - [pytest] - faulthandler_timeout=5 + [pytest] + faulthandler_timeout = 5 For more information please refer to :ref:`faulthandler`. @@ -1494,12 +1564,21 @@ passed multiple times. The expected format is ``name=value``. For example:: This option is set to 'false' by default. - .. code-block:: ini + .. tab:: toml - # content of pytest.ini - [pytest] - faulthandler_timeout=5 - faulthandler_exit_on_timeout=true + .. code-block:: toml + + [pytest] + faulthandler_timeout = 5 + faulthandler_exit_on_timeout = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + faulthandler_timeout = 5 + faulthandler_exit_on_timeout = true For more information please refer to :ref:`faulthandler`. @@ -1511,13 +1590,21 @@ passed multiple times. The expected format is ``name=value``. For example:: warnings. By default all warnings emitted during the test session will be displayed in a summary at the end of the test session. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + filterwarnings = ["error", "ignore::DeprecationWarning"] - # content of pytest.ini - [pytest] - filterwarnings = - error - ignore::DeprecationWarning + .. tab:: ini + + .. code-block:: ini + + [pytest] + filterwarnings = + error + ignore::DeprecationWarning This tells pytest to ignore deprecation warnings and turn all other warnings into errors. For more information please refer to :ref:`warnings`. @@ -1532,10 +1619,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``total`` (the default): duration times reported include setup, call, and teardown times. * ``call``: duration times reported include only call times, excluding setup and teardown. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + junit_duration_report = "call" + + .. tab:: ini + + .. code-block:: ini - [pytest] - junit_duration_report = call + [pytest] + junit_duration_report = call .. confval:: junit_family @@ -1549,10 +1645,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. * ``xunit2``: produces `xunit 2.0 style output `__, which should be more compatible with latest Jenkins versions. **This is the default**. - .. code-block:: ini + .. tab:: toml - [pytest] - junit_family = xunit2 + .. code-block:: toml + + [pytest] + junit_family = "xunit2" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + junit_family = xunit2 .. confval:: junit_logging @@ -1570,10 +1675,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``all``: write captured ``logging``, ``stdout`` and ``stderr`` contents. * ``no`` (the default): no captured output is written. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + junit_logging = "system-out" - [pytest] - junit_logging = system-out + .. tab:: ini + + .. code-block:: ini + + [pytest] + junit_logging = system-out .. confval:: junit_log_passing_tests @@ -1583,20 +1697,38 @@ passed multiple times. The expected format is ``name=value``. For example:: If ``junit_logging != "no"``, configures if the captured output should be written to the JUnit XML file for **passing** tests. Default is ``True``. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + junit_log_passing_tests = false - [pytest] - junit_log_passing_tests = False + .. tab:: ini + + .. code-block:: ini + + [pytest] + junit_log_passing_tests = False .. confval:: junit_suite_name To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml - [pytest] - junit_suite_name = my_suite + [pytest] + junit_suite_name = "my_suite" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + junit_suite_name = my_suite .. confval:: log_auto_indent @@ -1611,10 +1743,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * False or "Off" or 0 - Do not auto-indent multiline log messages (the default behavior) * [positive integer] - auto-indent multiline log messages by [value] spaces - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_auto_indent = false + + .. tab:: ini - [pytest] - log_auto_indent = False + .. code-block:: ini + + [pytest] + log_auto_indent = false Supports passing kwarg ``extra={"auto_indent": [value]}`` to calls to ``logging.log()`` to specify auto-indentation behavior for @@ -1626,10 +1767,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Enable log display during test run (also known as :ref:`"live logging" `). The default is ``False``. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_cli = true + + .. tab:: ini - [pytest] - log_cli = True + .. code-block:: ini + + [pytest] + log_cli = true .. confval:: log_cli_date_format @@ -1637,10 +1787,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for live logging. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_cli_date_format = "%Y-%m-%d %H:%M:%S" - [pytest] - log_cli_date_format = %Y-%m-%d %H:%M:%S + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_cli_date_format = %Y-%m-%d %H:%M:%S For more information, see :ref:`live_logs`. @@ -1650,10 +1809,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:mod:`logging`-compatible string used to format live logging messages. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_cli_format = "%(asctime)s %(levelname)s %(message)s" + + .. tab:: ini + + .. code-block:: ini - [pytest] - log_cli_format = %(asctime)s %(levelname)s %(message)s + [pytest] + log_cli_format = %(asctime)s %(levelname)s %(message)s For more information, see :ref:`live_logs`. @@ -1665,10 +1833,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets the minimum log message level that should be captured for live logging. The integer value or the names of the levels can be used. - .. code-block:: ini + .. tab:: toml - [pytest] - log_cli_level = INFO + .. code-block:: toml + + [pytest] + log_cli_level = "INFO" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_cli_level = INFO For more information, see :ref:`live_logs`. @@ -1679,10 +1856,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for logging capture. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_date_format = "%Y-%m-%d %H:%M:%S" + + .. tab:: ini - [pytest] - log_date_format = %Y-%m-%d %H:%M:%S + .. code-block:: ini + + [pytest] + log_date_format = %Y-%m-%d %H:%M:%S For more information, see :ref:`logging`. @@ -1694,10 +1880,41 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a file name relative to the current working directory where log messages should be written to, in addition to the other logging facilities that are active. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_file = "logs/pytest-logs.txt" - [pytest] - log_file = logs/pytest-logs.txt + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_file = logs/pytest-logs.txt + + For more information, see :ref:`logging`. + + +.. confval:: log_file_mode + + Sets the mode that the logging file is opened with. + The options are ``"w"`` to recreate the file (the default) or ``"a"`` to append to the file. + + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_file_mode = "a" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_file_mode = a For more information, see :ref:`logging`. @@ -1708,10 +1925,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:func:`time.strftime`-compatible string that will be used when formatting dates for the logging file. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_file_date_format = "%Y-%m-%d %H:%M:%S" - [pytest] - log_file_date_format = %Y-%m-%d %H:%M:%S + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_file_date_format = %Y-%m-%d %H:%M:%S For more information, see :ref:`logging`. @@ -1721,10 +1947,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:mod:`logging`-compatible string used to format logging messages redirected to the logging file. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_file_format = "%(asctime)s %(levelname)s %(message)s" - [pytest] - log_file_format = %(asctime)s %(levelname)s %(message)s + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_file_format = %(asctime)s %(levelname)s %(message)s For more information, see :ref:`logging`. @@ -1735,10 +1970,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets the minimum log message level that should be captured for the logging file. The integer value or the names of the levels can be used. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml - [pytest] - log_file_level = INFO + [pytest] + log_file_level = "INFO" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + log_file_level = INFO For more information, see :ref:`logging`. @@ -1749,10 +1993,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets a :py:mod:`logging`-compatible string used to format captured logging messages. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_format = "%(asctime)s %(levelname)s %(message)s" + + .. tab:: ini - [pytest] - log_format = %(asctime)s %(levelname)s %(message)s + .. code-block:: ini + + [pytest] + log_format = %(asctime)s %(levelname)s %(message)s For more information, see :ref:`logging`. @@ -1764,10 +2017,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Sets the minimum log message level that should be captured for logging capture. The integer value or the names of the levels can be used. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + log_level = "INFO" + + .. tab:: ini - [pytest] - log_level = INFO + .. code-block:: ini + + [pytest] + log_level = INFO For more information, see :ref:`logging`. @@ -1778,27 +2040,45 @@ passed multiple times. The expected format is ``name=value``. For example:: only known markers - defined in code by core pytest or some plugin - are allowed. You can list additional markers in this setting to add them to the whitelist, - in which case you probably want to set :confval:`strict_markers` to ``True`` + in which case you probably want to set :confval:`strict_markers` to ``true`` to avoid future regressions: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + addopts = ["--strict-markers"] + markers = ["slow", "serial"] - [pytest] - strict_markers = True - markers = - slow - serial + .. tab:: ini + + .. code-block:: ini + + [pytest] + strict_markers = true + markers = + slow + serial .. confval:: minversion Specifies a minimal pytest version required for running tests. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + minversion = 3.0 # will fail if we run with pytest-2.8 + + .. tab:: ini + + .. code-block:: ini - # content of pytest.ini - [pytest] - minversion = 3.0 # will fail if we run with pytest-2.8 + [pytest] + minversion = 3.0 # will fail if we run with pytest-2.8 .. confval:: norecursedirs @@ -1818,10 +2098,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Setting a ``norecursedirs`` replaces the default. Here is an example of how to avoid certain directories: - .. code-block:: ini + .. tab:: toml - [pytest] - norecursedirs = .svn _build tmp* + .. code-block:: toml + + [pytest] + norecursedirs = [".svn", "_build", "tmp*"] + + .. tab:: ini + + .. code-block:: ini + + [pytest] + norecursedirs = .svn _build tmp* This would tell ``pytest`` to not look into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. @@ -1844,10 +2133,19 @@ passed multiple times. The expected format is ``name=value``. For example:: class prefixed with ``Test`` as a test collection. Here is an example of how to collect tests from classes that end in ``Suite``: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + python_classes = ["*Suite"] + + .. tab:: ini - [pytest] - python_classes = *Suite + .. code-block:: ini + + [pytest] + python_classes = *Suite Note that ``unittest.TestCase`` derived classes are always collected regardless of this option, as ``unittest``'s own collection framework is used @@ -1860,20 +2158,29 @@ passed multiple times. The expected format is ``name=value``. For example:: are considered as test modules. Search for multiple glob patterns by adding a space between patterns: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + python_files = ["test_*.py", "check_*.py", "example_*.py"] - [pytest] - python_files = test_*.py check_*.py example_*.py + .. tab:: ini - Or one per line: + .. code-block:: ini - .. code-block:: ini + [pytest] + python_files = test_*.py check_*.py example_*.py - [pytest] - python_files = - test_*.py - check_*.py - example_*.py + Or one per line: + + .. code-block:: ini + + [pytest] + python_files = + test_*.py + check_*.py + example_*.py By default, files matching ``test_*.py`` and ``*_test.py`` will be considered test modules. @@ -1887,10 +2194,19 @@ passed multiple times. The expected format is ``name=value``. For example:: function prefixed with ``test`` as a test. Here is an example of how to collect test functions and methods that end in ``_test``: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml - [pytest] - python_functions = *_test + [pytest] + python_functions = ["*_test"] + + .. tab:: ini + + .. code-block:: ini + + [pytest] + python_functions = *_test Note that this has no effect on methods that live on a ``unittest.TestCase`` derived class, as ``unittest``'s own collection framework is used @@ -1908,10 +2224,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Paths are relative to the :ref:`rootdir ` directory. Directories remain in path for the duration of the test session. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + pythonpath = ["src1", "src2"] + + .. tab:: ini - [pytest] - pythonpath = src1 src2 + .. code-block:: ini + + [pytest] + pythonpath = src1 src2 .. confval:: required_plugins @@ -1921,10 +2246,19 @@ passed multiple times. The expected format is ``name=value``. For example:: their name. Whitespace between different version specifiers is not allowed. If any one of the plugins is not found, emit an error. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + required_plugins = ["pytest-django>=3.0.0,<4.0.0", "pytest-html", "pytest-xdist>=1.0.0"] - [pytest] - required_plugins = pytest-django>=3.0.0,<4.0.0 pytest-html pytest-xdist>=1.0.0 + .. tab:: ini + + .. code-block:: ini + + [pytest] + required_plugins = pytest-django>=3.0.0,<4.0.0 pytest-html pytest-xdist>=1.0.0 .. confval:: testpaths @@ -1938,10 +2272,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Useful when all project tests are in a known location to speed up test collection and to avoid picking up undesired tests by accident. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml - [pytest] - testpaths = testing doc + [pytest] + testpaths = ["testing", "doc"] + + .. tab:: ini + + .. code-block:: ini + + [pytest] + testpaths = testing doc This configuration means that executing: @@ -1960,10 +2303,19 @@ passed multiple times. The expected format is ``name=value``. For example:: How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + tmp_path_retention_count = 3 + + .. tab:: ini + + .. code-block:: ini - [pytest] - tmp_path_retention_count = 3 + [pytest] + tmp_path_retention_count = 3 Default: ``3`` @@ -1979,10 +2331,19 @@ passed multiple times. The expected format is ``name=value``. For example:: * `failed`: retains directories only for tests with outcome `error` or `failed`. * `none`: directories are always removed after each test ends, regardless of the outcome. - .. code-block:: ini + .. tab:: toml - [pytest] - tmp_path_retention_policy = all + .. code-block:: toml + + [pytest] + tmp_path_retention_policy = "all" + + .. tab:: ini + + .. code-block:: ini + + [pytest] + tmp_path_retention_policy = all Default: ``all`` @@ -1993,10 +2354,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Setting value to ``0`` disables the character limit for truncation. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + truncation_limit_chars = 640 - [pytest] - truncation_limit_chars = 640 + .. tab:: ini + + .. code-block:: ini + + [pytest] + truncation_limit_chars = 640 pytest truncates the assert messages to a certain limit by default to prevent comparison with large data to overload the console output. @@ -2013,10 +2383,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Setting value to ``0`` disables the lines limit for truncation. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + truncation_limit_lines = 8 - [pytest] - truncation_limit_lines = 8 + .. tab:: ini + + .. code-block:: ini + + [pytest] + truncation_limit_lines = 8 pytest truncates the assert messages to a certain limit by default to prevent comparison with large data to overload the console output. @@ -2033,21 +2412,39 @@ passed multiple times. The expected format is ``name=value``. For example:: the ``@pytest.mark.usefixtures`` marker to all test functions. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml - [pytest] - usefixtures = - clean_db + [pytest] + usefixtures = ["clean_db"] + + .. tab:: ini + + .. code-block:: ini + + [pytest] + usefixtures = + clean_db .. confval:: verbosity_assertions Set a verbosity level specifically for assertion related output, overriding the application wide level. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + verbosity_assertions = 2 + + .. tab:: ini - [pytest] - verbosity_assertions = 2 + .. code-block:: ini + + [pytest] + verbosity_assertions = 2 Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of "auto" can be used to explicitly use the global verbosity level. @@ -2057,10 +2454,19 @@ passed multiple times. The expected format is ``name=value``. For example:: Set a verbosity level specifically for test case execution related output, overriding the application wide level. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + verbosity_test_cases = 2 + + .. tab:: ini - [pytest] - verbosity_test_cases = 2 + .. code-block:: ini + + [pytest] + verbosity_test_cases = 2 Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of "auto" can be used to explicitly use the global verbosity level. @@ -2068,7 +2474,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: strict - If set to ``True``, enables all strictness options: + If set to ``true``, enables all strictness options: * :confval:`strict_config` * :confval:`strict_markers` @@ -2084,25 +2490,42 @@ passed multiple times. The expected format is ``name=value``. For example:: We therefore only recommend using this option when using a locked version of pytest, or if you want to proactively adopt new strictness options as they are added. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + xfail_strict = true - [pytest] - strict = True + .. tab:: ini + + .. code-block:: ini + + [pytest] + strict = true .. versionadded:: 9.0 .. confval:: strict_xfail - If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the + If set to ``true``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the test suite. For more information, see :ref:`xfail strict tutorial`. + .. tab:: toml + + .. code-block:: toml - .. code-block:: ini + [pytest] + strict_xfail = true - [pytest] - strict_xfail = True + .. tab:: ini + + .. code-block:: ini + + [pytest] + strict_xfail = true You can also enable this option via the :confval:`strict` option. @@ -2113,39 +2536,65 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: strict_config - If set to ``True``, any warnings encountered while parsing the ``pytest`` section of the configuration file will raise errors. + If set to ``true``, any warnings encountered while parsing the ``pytest`` section of the configuration file will raise errors. + + .. tab:: toml - .. code-block:: ini + .. code-block:: toml - [pytest] - strict_config = True + [pytest] + strict_config = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + strict_config = true You can also enable this option via the :confval:`strict` option. .. confval:: strict_markers - If set to ``True``, markers not registered in the ``markers`` section of the configuration file will raise errors. + If set to ``true``, markers not registered in the ``markers`` section of the configuration file will raise errors. - .. code-block:: ini + .. tab:: toml - [pytest] - strict_markers = True + .. code-block:: toml - You can also enable this option via the :confval:`strict` option. + [pytest] + strict_markers = true + + .. tab:: ini + + .. code-block:: ini + [pytest] + strict_markers = true + + You can also enable this option via the :confval:`strict` option. .. confval:: strict_parametrization_ids - If set, pytest emits an error if it detects non-unique parameter set IDs. + If set to ``true``, pytest emits an error if it detects non-unique parameter set IDs. If not set (the default), pytest automatically handles this by adding `0`, `1`, ... to duplicate IDs, making them unique. - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [pytest] + strict_parametrization_ids = true + + .. tab:: ini + + .. code-block:: ini - [pytest] - strict_parametrization_ids = True + [pytest] + strict_parametrization_ids = true You can also enable this option via the :confval:`strict` option. diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index ddcb7efb99b..5483bb46063 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -8,3 +8,4 @@ sphinxcontrib-svg2pdfconverter furo sphinxcontrib-towncrier sphinx-issues +sphinx-inline-tabs diff --git a/pyproject.toml b/pyproject.toml index 09c3146d792..f57d7e8e85b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -353,9 +353,9 @@ ignore = "W009" indent = 4 max_supported_python = "3.14" -[tool.pytest.ini_options] +[tool.pytest] minversion = "2.0" -addopts = "-rfEX -p pytester" +addopts = [ "-rfEX", "-p", "pytester" ] python_files = [ "test_*.py", "*_test.py", diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e6d34ffc5c4..3cad7a7d5eb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs -"""Command line options, ini-file and conftest.py processing.""" +"""Command line options, config-file and conftest.py processing.""" from __future__ import annotations import argparse +import builtins import collections.abc from collections.abc import Callable from collections.abc import Generator @@ -52,6 +53,7 @@ from _pytest._code import filter_traceback from _pytest._code.code import TracebackStyle from _pytest._io import TerminalWriter +from _pytest.compat import assert_never from _pytest.config.argparsing import Argument from _pytest.config.argparsing import Parser import _pytest.deprecated @@ -993,7 +995,7 @@ class InvocationParams: .. note:: Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` - ini option are handled by pytest, not being included in the ``args`` attribute. + configuration option are handled by pytest, not being included in the ``args`` attribute. Plugins accessing ``InvocationParams`` must be aware of that. """ @@ -1451,8 +1453,8 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None: @hookimpl(wrapper=True) def pytest_collection(self) -> Generator[None, object, object]: - # Validate invalid ini keys after collection is done so we take in account - # options added by late-loading conftest files. + # Validate invalid configuration keys after collection is done so we + # take in account options added by late-loading conftest files. try: return (yield) finally: @@ -1588,7 +1590,7 @@ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: ) def addinivalue_line(self, name: str, line: str) -> None: - """Add a line to an ini-file option. The option must have been + """Add a line to a configuration option. The option must have been declared but might not yet be set in which case the line becomes the first line in its value.""" x = self.getini(name) @@ -1596,11 +1598,11 @@ def addinivalue_line(self, name: str, line: str) -> None: x.append(line) # modifies the cached list inline def getini(self, name: str) -> Any: - """Return configuration value from an :ref:`ini file `. + """Return configuration value the an :ref:`configuration file `. - If a configuration value is not defined in an - :ref:`ini file `, then the ``default`` value provided while - registering the configuration through + If a configuration value is not defined in a + :ref:`configuration file `, then the ``default`` value + provided while registering the configuration through :func:`parser.addini ` will be returned. Please note that you can even provide ``None`` as a valid default value. @@ -1629,8 +1631,9 @@ def getini(self, name: str) -> Any: try: return self._inicache[canonical_name] except KeyError: - self._inicache[canonical_name] = val = self._getini(canonical_name) - return val + pass + self._inicache[canonical_name] = val = self._getini(canonical_name) + return val # Meant for easy monkeypatching by legacypath plugin. # Can be inlined back (with no cover removed) once legacypath is gone. @@ -1666,22 +1669,44 @@ def _getini(self, name: str): # 2. Canonical name takes precedence over alias. selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0] value = selected.value + mode = selected.mode + + if mode == "ini": + # In ini mode, values are always str | list[str]. + assert isinstance(value, (str, list)) + return self._getini_ini(name, canonical_name, type, value, default) + elif mode == "toml": + return self._getini_toml(name, canonical_name, type, value, default) + else: + assert_never(mode) - # Coerce the values based on types. - # - # Note: some coercions are only required if we are reading from .ini files, because - # the file format doesn't contain type information, but when reading from toml we will - # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). - # For example: + def _getini_ini( + self, + name: str, + canonical_name: str, + type: str, + value: str | list[str], + default: Any, + ): + """Handle config values read in INI mode. + + In INI mode, values are stored as str or list[str] only, and coerced + from string based on the registered type. + """ + # Note: some coercions are only required if we are reading from .ini + # files, because the file format doesn't contain type information, but + # when reading from toml (in ini mode) we will get either str or list of + # str values (see load_config_dict_from_file). For example: # # ini: # a_line_list = "tests acceptance" - # in this case, we need to split the string to obtain a list of strings. # - # toml: + # in this case, we need to split the string to obtain a list of strings. + # + # toml (ini mode): # a_line_list = ["tests", "acceptance"] - # in this case, we already have a list ready to use. # + # in this case, we already have a list ready to use. if type == "paths": dp = ( self.inipath.parent @@ -1716,6 +1741,90 @@ def _getini(self, name: str): else: return self._getini_unknown_type(name, type, value) + def _getini_toml( + self, + name: str, + canonical_name: str, + type: str, + value: object, + default: Any, + ): + """Handle TOML config values with strict type validation and no coercion. + + In TOML mode, values already have native types from TOML parsing. + We validate types match expectations exactly, including list items. + """ + value_type = builtins.type(value).__name__ + if type == "paths": + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type 'paths', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) + return [dp / x for x in value] + elif type in {"args", "linelist"}: + # Expect a list of strings. + if not isinstance(value, list): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list for type '{type}', " + f"got {value_type}: {value!r}" + ) + for i, item in enumerate(value): + if not isinstance(item, str): + item_type = builtins.type(item).__name__ + raise TypeError( + f"{self.inipath}: config option '{name}' expects a list of strings, " + f"but item at index {i} is {item_type}: {item!r}" + ) + return list(value) + elif type == "bool": + # Expect a boolean. + if not isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a bool, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "int": + # Expect an integer (but not bool, which is a subclass of int). + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects an int, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "float": + # Expect a float or integer only. + if not isinstance(value, (float, int)) or isinstance(value, bool): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a float, " + f"got {value_type}: {value!r}" + ) + return value + elif type == "string": + # Expect a string. + if not isinstance(value, str): + raise TypeError( + f"{self.inipath}: config option '{name}' expects a string, " + f"got {value_type}: {value!r}" + ) + return value + else: + return self._getini_unknown_type(name, type, value) + def _getconftest_pathlist( self, name: str, path: pathlib.Path ) -> list[pathlib.Path] | None: @@ -1790,11 +1899,19 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int: Example: - .. code-block:: ini + .. tab:: toml + + .. code-block:: toml + + [tool.pytest] + verbosity_assertions = 2 + + .. tab:: ini + + .. code-block:: ini - # content of pytest.ini - [pytest] - verbosity_assertions = 2 + [pytest] + verbosity_assertions = 2 .. code-block:: console @@ -1828,7 +1945,7 @@ def _verbosity_ini_name(verbosity_type: str) -> str: def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: """Add a output verbosity configuration option for the given output type. - :param parser: Parser for command line arguments and ini-file values. + :param parser: Parser for command line arguments and config-file values. :param verbosity_type: Fine-grained verbosity category. :param help: Description of the output this type controls. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 67b89bbbb16..99835884848 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -30,7 +30,7 @@ def __repr__(self) -> str: @final class Parser: - """Parser for command line arguments and ini-file values. + """Parser for command line arguments and config-file values. :ivar extra_info: Dict of generic param -> value to display in case there's an error processing the command line arguments. @@ -183,12 +183,12 @@ def addini( *, aliases: Sequence[str] = (), ) -> None: - """Register an ini-file option. + """Register a configuration file option. :param name: - Name of the ini-variable. + Name of the configuration. :param type: - Type of the variable. Can be: + Type of the configuration. Can be: * ``string``: a string * ``bool``: a boolean @@ -203,19 +203,19 @@ def addini( The ``float`` and ``int`` types. - For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file. - In case the execution is happening without an ini-file defined, + For ``paths`` and ``pathlist`` types, they are considered relative to the config-file. + In case the execution is happening without a config-file defined, they will be considered relative to the current working directory (for example with ``--override-ini``). .. versionadded:: 7.0 The ``paths`` variable type. .. versionadded:: 8.1 - Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file. + Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file. Defaults to ``string`` if ``None`` or not passed. :param default: - Default value if no ini-file option exists but is queried. + Default value if no config-file option exists but is queried. :param aliases: Additional names by which this option can be referenced. Aliases resolve to the canonical name. @@ -223,7 +223,7 @@ def addini( .. versionadded:: 9.0 The ``aliases`` parameter. - The value of ini-variables can be retrieved via a call to + The value of configuration keys can be retrieved via a call to :py:func:`config.getini(name) `. """ assert type in ( @@ -246,7 +246,9 @@ def addini( for alias in aliases: if alias in self._inidict: - raise ValueError(f"alias {alias!r} conflicts with existing ini option") + raise ValueError( + f"alias {alias!r} conflicts with existing configuration option" + ) if (already := self._ini_aliases.get(alias)) is not None: raise ValueError(f"{alias!r} is already an alias of {already!r}") self._ini_aliases[alias] = name @@ -258,7 +260,7 @@ def get_ini_default_for_type( ], ) -> Any: """ - Used by addini to get the default value for a given ini-option type, when + Used by addini to get the default value for a given config option type, when default is not supplied. """ if type in ("paths", "pathlist", "args", "linelist"): @@ -553,7 +555,7 @@ class OverrideIniAction(argparse.Action): """Custom argparse action that makes a CLI flag equivalent to overriding an option, in addition to behaving like `store_true`. - This can simplify things since code only needs to inspect the ini option + This can simplify things since code only needs to inspect the config option and not consider the CLI flag. """ diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index db0b829a530..3c628a09c2d 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -21,19 +21,23 @@ @dataclass(frozen=True) class ConfigValue: - """Represents a configuration value with its origin. + """Represents a configuration value with its origin and parsing mode. This allows tracking whether a value came from a configuration file or from a CLI override (--override-ini), which is important for determining precedence when dealing with ini option aliases. + + The mode tracks the parsing mode/data model used for the value: + - "ini": from INI files or [tool.pytest.ini_options], where the only + supported value types are `str` or `list[str]`. + - "toml": from TOML files (not in INI mode), where native TOML types + are preserved. """ - # Even though TOML supports richer data types, all values are converted to - # str/list[str] during parsing to maintain compatibility with the rest of - # the configuration system. - value: str | list[str] + value: object _: KW_ONLY origin: Literal["file", "override"] + mode: Literal["ini", "toml"] ConfigDict: TypeAlias = dict[str, ConfigValue] @@ -64,11 +68,12 @@ def load_config_dict_from_file( if "pytest" in iniconfig: return { - k: ConfigValue(v, origin="file") for k, v in iniconfig["pytest"].items() + k: ConfigValue(v, origin="file", mode="ini") + for k, v in iniconfig["pytest"].items() } else: # "pytest.ini" files are always the source of configuration, even if empty. - if filepath.name == "pytest.ini": + if filepath.name in {"pytest.ini", ".pytest.ini"}: return {} # '.cfg' files are considered if they contain a "[tool:pytest]" section. @@ -77,7 +82,7 @@ def load_config_dict_from_file( if "tool:pytest" in iniconfig.sections: return { - k: ConfigValue(v, origin="file") + k: ConfigValue(v, origin="file", mode="ini") for k, v in iniconfig["tool:pytest"].items() } elif "pytest" in iniconfig.sections: @@ -85,7 +90,9 @@ def load_config_dict_from_file( # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) - # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. + # '.toml' files are considered if they contain a [tool.pytest] table (toml mode) + # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml, + # or [pytest] table (toml mode) for pytest.toml/.pytest.toml. elif filepath.suffix == ".toml": if sys.version_info >= (3, 11): import tomllib @@ -98,17 +105,52 @@ def load_config_dict_from_file( except tomllib.TOMLDecodeError as exc: raise UsageError(f"{filepath}: {exc}") from exc - result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) - if result is not None: - # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), - # however we need to convert all scalar values to str for compatibility with the rest - # of the configuration system, which expects strings only. - def make_scalar(v: object) -> str | list[str]: - return v if isinstance(v, list) else str(v) - - return { - k: ConfigValue(make_scalar(v), origin="file") for k, v in result.items() - } + # pytest.toml and .pytest.toml use [pytest] table directly. + if filepath.name in ("pytest.toml", ".pytest.toml"): + pytest_config = config.get("pytest", {}) + if pytest_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in pytest_config.items() + } + # "pytest.toml" files are always the source of configuration, even if empty. + return {} + + # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options]. + else: + tool_pytest = config.get("tool", {}).get("pytest", {}) + + # Check for toml mode config: [tool.pytest] with content outside of ini_options. + toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"} + # Check for ini mode config: [tool.pytest.ini_options]. + ini_config = tool_pytest.get("ini_options", None) + + if toml_config and ini_config: + raise UsageError( + f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and " + "[tool.pytest.ini_options] (string-based INI format) simultaneously. " + "Please use [tool.pytest] with native TOML types (recommended) " + "or [tool.pytest.ini_options] for backwards compatibility." + ) + + if toml_config: + # TOML mode - preserve native TOML types. + return { + k: ConfigValue(v, origin="file", mode="toml") + for k, v in toml_config.items() + } + + elif ini_config is not None: + # INI mode - TOML supports richer data types than INI files, but we need to + # convert all scalar values to str for compatibility with the INI system. + def make_scalar(v: object) -> str | list[str]: + return v if isinstance(v, list) else str(v) + + return { + k: ConfigValue(make_scalar(v), origin="file", mode="ini") + for k, v in ini_config.items() + } return None @@ -122,6 +164,8 @@ def locate_config( ignored-config-files is a list of config basenames found that contain pytest configuration but were ignored.""" config_names = [ + "pytest.toml", + ".pytest.toml", "pytest.ini", ".pytest.ini", "pyproject.toml", @@ -224,7 +268,7 @@ def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict: f"-o/--override-ini expects option=value style (got: {ini_config!r})." ) from e else: - overrides[key] = ConfigValue(user_ini_value, origin="override") + overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini") return overrides diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b3597615ba9..27846db13a4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1520,7 +1520,7 @@ class FixtureManager: relevant for a particular function. An initial list of fixtures is assembled like this: - - ini-defined usefixtures + - config-defined usefixtures - autouse-marked fixtures along the collection chain up from the function - usefixtures markers at module/class/function level - test function funcargs diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 32b807029fb..760f8269dd1 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -102,7 +102,7 @@ def pytest_addoption(parser: Parser) -> None: "--override-ini", dest="override_ini", action="append", - help='Override ini option with "option=value" style, ' + help='Override configuration option with "option=value" style, ' "e.g. `-o strict_xfail=True -o cache_dir=cache`.", ) @@ -176,8 +176,8 @@ def showhelp(config: Config) -> None: tw.write(config._parser.optparser.format_help()) tw.line() tw.line( - "[pytest] ini-options in the first " - "pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:" + "[pytest] configuration options in the first " + "pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:" ) tw.line() diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 12653ea11fe..c5bcc36ad4b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -98,13 +98,13 @@ def pytest_plugin_registered( @hookspec(historic=True) def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: - """Register argparse-style options and ini-style config values, + """Register argparse-style options and config-style config values, called once at the beginning of a test run. :param parser: To add command line options, call :py:func:`parser.addoption(...) `. - To add ini-file values call :py:func:`parser.addini(...) + To add config-file values call :py:func:`parser.addini(...) `. :param pluginmanager: @@ -119,7 +119,7 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None retrieve the value of a command line option. - :py:func:`config.getini(name) ` to retrieve - a value read from an ini-style file. + a value read from a configuration file. The config object is passed around on many internal objects via the ``.config`` attribute or can be retrieved as the ``pytestconfig`` fixture. @@ -998,13 +998,22 @@ def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None and the pytest introspected assertion information is available in the `expl` string. - This hook must be explicitly enabled by the ``enable_assertion_pass_hook`` - ini-file option: + This hook must be explicitly enabled by the :confval:`enable_assertion_pass_hook` + configuration option: - .. code-block:: ini + .. tab:: toml - [pytest] - enable_assertion_pass_hook=true + .. code-block:: toml + + [pytest] + enable_assertion_pass_hook = true + + .. tab:: ini + + .. code-block:: ini + + [pytest] + enable_assertion_pass_hook = true You need to **clean the .pyc** files in your project directory and interpreter libraries when enabling this option, as assertions will require to be re-written. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d5b67abdaee..1cd5f05dd7e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -837,6 +837,16 @@ def makeini(self, source: str) -> Path: """ return self.makefile(".ini", tox=source) + def maketoml(self, source: str) -> Path: + """Write a pytest.toml file. + + :param source: The contents. + :returns: The pytest.toml file. + + .. versionadded:: 9.0 + """ + return self.makefile(".toml", pytest=source) + def getinicfg(self, source: str) -> SectionWrapper: """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 67b1c0474f7..e63751877a4 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -383,7 +383,7 @@ def istestclass(self, obj: object, name: str) -> bool: def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: """Check if the given name matches the prefix or glob-pattern defined - in ini configuration.""" + in configuration.""" for option in self.config.getini(option_name): if name.startswith(option): return True diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index cf54788e246..4974532e888 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -589,7 +589,8 @@ def test_log_cli(request): ) def test_log_cli_auto_enable(pytester: Pytester, cli_args: str) -> None: """Check that live logs are enabled if --log-level or --log-cli-level is passed on the CLI. - It should not be auto enabled if the same configs are set on the INI file. + + It should not be auto enabled if the same configs are set on the configuration file. """ pytester.makepyfile( """ diff --git a/testing/test_collection.py b/testing/test_collection.py index cd8e13c8790..39753d80cac 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1353,7 +1353,7 @@ def test_collect_pyargs_with_testpaths( def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: - """The testpaths ini option should load conftests in those paths as 'initial' (#10987).""" + """The testpaths config option should load conftests in those paths as 'initial' (#10987).""" p = pytester.mkdir("some_path") p.joinpath("conftest.py").write_text( textwrap.dedent( diff --git a/testing/test_config.py b/testing/test_config.py index 01907132cd1..9df00d7a219 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -58,9 +58,9 @@ def test_getcfg_and_config( encoding="utf-8", ) _, _, cfg, _ = locate_config(Path.cwd(), [sub]) - assert cfg["name"] == ConfigValue("value", origin="file") + assert cfg["name"] == ConfigValue("value", origin="file", mode="ini") config = pytester.parseconfigure(str(sub)) - assert config.inicfg["name"] == ConfigValue("value", origin="file") + assert config.inicfg["name"] == ConfigValue("value", origin="file", mode="ini") def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None: p1 = pytester.makepyfile("def test(): pass") @@ -132,6 +132,20 @@ def test_ini_names(self, pytester: Pytester, name, section) -> None: config = pytester.parseconfig() assert config.getini("minversion") == "3.36" + @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) + def test_toml_config_names(self, pytester: Pytester, name: str) -> None: + pytester.path.joinpath(name).write_text( + textwrap.dedent( + """ + [pytest] + minversion = "3.36" + """ + ), + encoding="utf-8", + ) + config = pytester.parseconfig() + assert config.getini("minversion") == "3.36" + def test_pyproject_toml(self, pytester: Pytester) -> None: pyproject_toml = pytester.makepyprojecttoml( """ @@ -151,7 +165,7 @@ def test_empty_pyproject_toml(self, pytester: Pytester) -> None: def test_empty_pyproject_toml_found_many(self, pytester: Pytester) -> None: """ - In case we find multiple pyproject.toml files in our search, without a [tool.pytest.ini_options] + In case we find multiple pyproject.toml files in our search, without a [tool.pytest] table and without finding other candidates, the closest to where we started wins. """ pytester.makefile( @@ -165,9 +179,88 @@ def test_empty_pyproject_toml_found_many(self, pytester: Pytester) -> None: config = pytester.parseconfig(pytester.path / "foo/bar") assert config.inipath == pytester.path / "foo/bar/pyproject.toml" + def test_pytest_toml(self, pytester: Pytester) -> None: + pytest_toml = pytester.path.joinpath("pytest.toml") + pytest_toml = pytester.maketoml( + """ + [pytest] + minversion = "1.0" + """ + ) + config = pytester.parseconfig() + assert config.inipath == pytest_toml + assert config.getini("minversion") == "1.0" + + @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) + def test_empty_pytest_toml(self, pytester: Pytester, name: str) -> None: + """An empty pytest.toml is considered as config if no other option is found.""" + pytest_toml = pytester.path / name + pytest_toml.write_text("", encoding="utf-8") + config = pytester.parseconfig() + assert config.inipath == pytest_toml + + def test_pytest_toml_trumps_pyproject_toml(self, pytester: Pytester) -> None: + """A pytest.toml always takes precedence over a pyproject.toml file.""" + pytester.makepyprojecttoml( + """ + [tool.pytest] + minversion = "1.0" + """ + ) + pytest_toml = pytester.maketoml( + """ + [pytest] + minversion = "2.0" + """ + ) + config = pytester.parseconfig() + assert config.inipath == pytest_toml + assert config.getini("minversion") == "2.0" + + def test_pytest_toml_trumps_pytest_ini(self, pytester: Pytester) -> None: + """A pytest.toml always takes precedence over a pytest.ini file.""" + pytester.makeini( + """ + [pytest] + minversion = 1.0 + """, + ) + pytest_toml = pytester.maketoml( + """ + [pytest] + minversion = "2.0" + """, + ) + config = pytester.parseconfig() + assert config.inipath == pytest_toml + assert config.getini("minversion") == "2.0" + + def test_dot_pytest_toml_trumps_pytest_ini(self, pytester: Pytester) -> None: + """A .pytest.toml always takes precedence over a pytest.ini file.""" + pytester.makeini( + """ + [pytest] + minversion = 1.0 + """, + ) + pytest_toml = pytester.maketoml( + """ + [pytest] + minversion = "2.0" + """ + ) + config = pytester.parseconfig() + assert config.inipath == pytest_toml + assert config.getini("minversion") == "2.0" + def test_pytest_ini_trumps_pyproject_toml(self, pytester: Pytester) -> None: """A pytest.ini always take precedence over a pyproject.toml file.""" - pytester.makepyprojecttoml("[tool.pytest.ini_options]") + pytester.makepyprojecttoml( + """ + [tool.pytest] + minversion = "1.0" + """ + ) pytest_ini = pytester.makefile(".ini", pytest="") config = pytester.parseconfig() assert config.inipath == pytest_ini @@ -213,6 +306,17 @@ def test_toml_parse_error(self, pytester: Pytester) -> None: assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*") + def test_pytest_toml_parse_error(self, pytester: Pytester) -> None: + pytester.path.joinpath("pytest.toml").write_text( + """ + \\" + """, + encoding="utf-8", + ) + result = pytester.runpytest() + assert result.ret != 0 + result.stderr.fnmatch_lines("ERROR: *pytest.toml: Invalid statement*") + def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None: # If --confcutdir is not specified, and there is no configfile, default # to the rootpath. @@ -1043,7 +1147,7 @@ def pytest_addoption(parser): assert config.getini("old_name") == "hello" def test_addini_aliases_with_canonical_in_file(self, pytester: Pytester) -> None: - """Test that canonical name takes precedence over alias in ini file.""" + """Test that canonical name takes precedence over alias in configuration file.""" pytester.makeconftest( """ def pytest_addoption(parser): @@ -1160,7 +1264,7 @@ def pytest_addoption(parser): try: parser.addini("new_option", "second option", aliases=["existing"]) except ValueError as e: - assert "alias 'existing' conflicts with existing ini option" in str(e) + assert "alias 'existing' conflicts with existing configuration option" in str(e) else: assert False, "Should have raised ValueError" """ @@ -1330,7 +1434,9 @@ def test_inifilename(self, tmp_path: Path) -> None: # this indicates this is the file used for getting configuration values assert config.inipath == inipath - assert config.inicfg.get("name") == ConfigValue("value", origin="file") + assert config.inicfg.get("name") == ConfigValue( + "value", origin="file", mode="ini" + ) assert config.inicfg.get("should_not_be_set") is None @@ -1824,15 +1930,18 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: ) assert rootpath == tmp_path assert parsed_inipath == inipath - assert ini_config["x"] == ConfigValue("10", origin="file") + assert ini_config["x"] == ConfigValue("10", origin="file", mode="ini") - @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"]) - def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None: - inipath = tmp_path / "pytest.ini" + @pytest.mark.parametrize("pytest_ini", ["pytest.ini", ".pytest.ini"]) + @pytest.mark.parametrize("other", ["setup.cfg", "tox.ini"]) + def test_pytestini_overrides_empty_other( + self, tmp_path: Path, pytest_ini: str, other: str + ) -> None: + inipath = tmp_path / pytest_ini inipath.touch() a = tmp_path / "a" a.mkdir() - (a / name).touch() + (a / other).touch() rootpath, parsed_inipath, *_ = determine_setup( inifile=None, override_ini=None, @@ -1898,7 +2007,7 @@ def test_with_specific_inifile( ) assert rootpath == tmp_path assert inipath == p - assert ini_config["x"] == ConfigValue("10", origin="file") + assert ini_config["x"] == ConfigValue("10", origin="file", mode="ini") def test_explicit_config_file_sets_rootdir( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -2144,7 +2253,7 @@ def test_override_ini_usage_error_bad_style(self, pytester: Pytester) -> None: def test_override_ini_handled_asap( self, pytester: Pytester, with_ini: bool ) -> None: - """-o should be handled as soon as possible and always override what's in ini files (#2238)""" + """-o should be handled as soon as possible and always override what's in config files (#2238)""" if with_ini: pytester.makeini( """ @@ -2169,7 +2278,7 @@ def test_addopts_before_initini( config = _config_for_test config._preparse([], addopts=True) assert config.inicfg.get("cache_dir") == ConfigValue( - cache_dir, origin="override" + cache_dir, origin="override", mode="ini" ) def test_addopts_from_env_not_concatenated( @@ -2186,7 +2295,7 @@ def test_addopts_from_env_not_concatenated( ) def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None: - """`addopts` from ini should not take values from normal args (#4265).""" + """`addopts` from configuration should not take values from normal args (#4265).""" pytester.makeini( """ [pytest] @@ -2209,7 +2318,7 @@ def test_override_ini_does_not_contain_paths( config = _config_for_test config._preparse(["-o", "cache_dir=/cache", "/some/test/path"]) assert config.inicfg.get("cache_dir") == ConfigValue( - "/cache", origin="override" + "/cache", origin="override", mode="ini" ) def test_multiple_override_ini_options(self, pytester: Pytester) -> None: @@ -2724,3 +2833,169 @@ def test_level_matches_specified_override( config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) == TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL ) + + +class TestNativeTomlConfig: + """Test native TOML configuration parsing.""" + + def test_values(self, pytester: Pytester) -> None: + """Test that values are parsed as expected in TOML mode.""" + pytester.makepyprojecttoml( + """ + [tool.pytest] + test_bool = true + test_int = 5 + test_float = 30.5 + test_args = ["tests", "integration"] + test_paths = ["src", "lib"] + """ + ) + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("test_bool", "Test boolean config", type="bool", default=False) + parser.addini("test_int", "Test integer config", type="int", default=0) + parser.addini("test_float", "Test float config", type="float", default=0.0) + parser.addini("test_args", "Test args config", type="args") + parser.addini("test_paths", "Test paths config", type="paths") + """ + ) + config = pytester.parseconfig() + assert config.getini("test_bool") is True + assert config.getini("test_int") == 5 + assert config.getini("test_float") == 30.5 + assert config.getini("test_args") == ["tests", "integration"] + paths = config.getini("test_paths") + assert len(paths) == 2 + # Paths should be resolved relative to pyproject.toml location. + assert all(isinstance(p, Path) for p in paths) + + def test_override_with_list(self, pytester: Pytester) -> None: + """Test that -o overrides work with INI-style list syntax even when + config uses TOML mode.""" + pytester.makepyprojecttoml( + """ + [tool.pytest] + test_override_list = ["tests"] + """ + ) + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("test_override_list", "Test override list", type="args") + """ + ) + # -o uses INI mode, so uses space-separated syntax. + config = pytester.parseconfig("-o", "test_override_list=tests integration") + assert config.getini("test_override_list") == ["tests", "integration"] + + def test_conflict_between_native_and_ini_options(self, pytester: Pytester) -> None: + """Test that using both [tool.pytest] and [tool.pytest.ini_options] fails.""" + pytester.makepyprojecttoml( + """ + [tool.pytest] + test_conflict_1 = true + + [tool.pytest.ini_options] + test_conflict_2 = true + """, + ) + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("test_conflict_1", "Test conflict config 1", type="bool") + parser.addini("test_conflict_2", "Test conflict config 2", type="bool") + """ + ) + with pytest.raises(UsageError, match="Cannot use both"): + pytester.parseconfig() + + def test_type_errors(self, pytester: Pytester) -> None: + """Test all possible TypeError cases in getini.""" + pytester.maketoml( + """ + [pytest] + paths_not_list = "should_be_list" + paths_list_with_int = [1, 2] + + args_not_list = 123 + args_list_with_int = ["valid", 456] + + linelist_not_list = true + linelist_list_with_bool = ["valid", false] + + bool_not_bool = "true" + + int_not_int = "123" + int_is_bool = true + + float_not_float = "3.14" + float_is_bool = false + + string_not_string = 123 + """ + ) + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("paths_not_list", "test", type="paths") + parser.addini("paths_list_with_int", "test", type="paths") + parser.addini("args_not_list", "test", type="args") + parser.addini("args_list_with_int", "test", type="args") + parser.addini("linelist_not_list", "test", type="linelist") + parser.addini("linelist_list_with_bool", "test", type="linelist") + parser.addini("bool_not_bool", "test", type="bool") + parser.addini("int_not_int", "test", type="int") + parser.addini("int_is_bool", "test", type="int") + parser.addini("float_not_float", "test", type="float") + parser.addini("float_is_bool", "test", type="float") + parser.addini("string_not_string", "test", type="string") + """ + ) + config = pytester.parseconfig() + + with pytest.raises( + TypeError, match=r"expects a list for type 'paths'.*got str" + ): + config.getini("paths_not_list") + + with pytest.raises( + TypeError, match=r"expects a list of strings.*item at index 0 is int" + ): + config.getini("paths_list_with_int") + + with pytest.raises(TypeError, match=r"expects a list for type 'args'.*got int"): + config.getini("args_not_list") + + with pytest.raises( + TypeError, match=r"expects a list of strings.*item at index 1 is int" + ): + config.getini("args_list_with_int") + + with pytest.raises( + TypeError, match=r"expects a list for type 'linelist'.*got bool" + ): + config.getini("linelist_not_list") + + with pytest.raises( + TypeError, match=r"expects a list of strings.*item at index 1 is bool" + ): + config.getini("linelist_list_with_bool") + + with pytest.raises(TypeError, match=r"expects a bool.*got str"): + config.getini("bool_not_bool") + + with pytest.raises(TypeError, match=r"expects an int.*got str"): + config.getini("int_not_int") + + with pytest.raises(TypeError, match=r"expects an int.*got bool"): + config.getini("int_is_bool") + + with pytest.raises(TypeError, match=r"expects a float.*got str"): + config.getini("float_not_float") + + with pytest.raises(TypeError, match=r"expects a float.*got bool"): + config.getini("float_is_bool") + + with pytest.raises(TypeError, match=r"expects a string.*got int"): + config.getini("string_not_string") diff --git a/testing/test_conftest.py b/testing/test_conftest.py index bd083574ffc..4de61bceb90 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -654,7 +654,7 @@ def test_parsefactories_relative_node_ids( def test_search_conftest_up_to_inifile( pytester: Pytester, confcutdir: str, passed: int, error: int ) -> None: - """Test that conftest files are detected only up to an ini file, unless + """Test that conftest files are detected only up to a configuration file, unless an explicit --confcutdir option is given. """ root = pytester.path diff --git a/testing/test_doctest.py b/testing/test_doctest.py index e2ca1119e92..8b71dabbc77 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -907,7 +907,7 @@ class TestLiterals: def test_allow_unicode(self, pytester, config_mode): """Test that doctests which output unicode work in all python versions tested by pytest when the ALLOW_UNICODE option is used (either in - the ini file or by an inline comment). + the configuration file or by an inline comment). """ if config_mode == "ini": pytester.makeini( @@ -942,7 +942,7 @@ def foo(): def test_allow_bytes(self, pytester, config_mode): """Test that doctests which output bytes work in all python versions tested by pytest when the ALLOW_BYTES option is used (either in - the ini file or by an inline comment)(#1287). + the configuration file or by an inline comment)(#1287). """ if config_mode == "ini": pytester.makeini( diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index e8e70f9c6fb..aea7b1f9a4d 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -15,9 +15,10 @@ class TestLoadConfigDictFromFile: - def test_empty_pytest_ini(self, tmp_path: Path) -> None: + @pytest.mark.parametrize("filename", ["pytest.ini", ".pytest.ini"]) + def test_empty_pytest_ini(self, tmp_path: Path, filename: str) -> None: """pytest.ini files are always considered for configuration, even if empty""" - fn = tmp_path / "pytest.ini" + fn = tmp_path / filename fn.write_text("", encoding="utf-8") assert load_config_dict_from_file(fn) == {} @@ -25,13 +26,17 @@ def test_pytest_ini(self, tmp_path: Path) -> None: """[pytest] section in pytest.ini files is read correctly""" fn = tmp_path / "pytest.ini" fn.write_text("[pytest]\nx=1", encoding="utf-8") - assert load_config_dict_from_file(fn) == {"x": ConfigValue("1", origin="file")} + assert load_config_dict_from_file(fn) == { + "x": ConfigValue("1", origin="file", mode="ini") + } def test_custom_ini(self, tmp_path: Path) -> None: """[pytest] section in any .ini file is read correctly""" fn = tmp_path / "custom.ini" fn.write_text("[pytest]\nx=1", encoding="utf-8") - assert load_config_dict_from_file(fn) == {"x": ConfigValue("1", origin="file")} + assert load_config_dict_from_file(fn) == { + "x": ConfigValue("1", origin="file", mode="ini") + } def test_custom_ini_without_section(self, tmp_path: Path) -> None: """Custom .ini files without [pytest] section are not considered for configuration""" @@ -49,7 +54,9 @@ def test_valid_cfg_file(self, tmp_path: Path) -> None: """Custom .cfg files with [tool:pytest] section are read correctly""" fn = tmp_path / "custom.cfg" fn.write_text("[tool:pytest]\nx=1", encoding="utf-8") - assert load_config_dict_from_file(fn) == {"x": ConfigValue("1", origin="file")} + assert load_config_dict_from_file(fn) == { + "x": ConfigValue("1", origin="file", mode="ini") + } def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None: """.cfg files with [pytest] section are no longer supported and should fail to alert users""" @@ -66,7 +73,7 @@ def test_invalid_toml_file(self, tmp_path: Path) -> None: load_config_dict_from_file(fn) def test_custom_toml_file(self, tmp_path: Path) -> None: - """.toml files without [tool.pytest.ini_options] are not considered for configuration.""" + """.toml files without [tool.pytest] are not considered for configuration.""" fn = tmp_path / "myconfig.toml" fn.write_text( dedent( @@ -97,13 +104,70 @@ def test_valid_toml_file(self, tmp_path: Path) -> None: encoding="utf-8", ) assert load_config_dict_from_file(fn) == { - "x": ConfigValue("1", origin="file"), - "y": ConfigValue("20.0", origin="file"), - "values": ConfigValue(["tests", "integration"], origin="file"), - "name": ConfigValue("foo", origin="file"), - "heterogeneous_array": ConfigValue([1, "str"], origin="file"), # type: ignore[list-item] + "x": ConfigValue("1", origin="file", mode="ini"), + "y": ConfigValue("20.0", origin="file", mode="ini"), + "values": ConfigValue(["tests", "integration"], origin="file", mode="ini"), + "name": ConfigValue("foo", origin="file", mode="ini"), + "heterogeneous_array": ConfigValue([1, "str"], origin="file", mode="ini"), + } + + def test_native_toml_config(self, tmp_path: Path) -> None: + """[tool.pytest] sections with native types are parsed correctly without coercion.""" + fn = tmp_path / "pyproject.toml" + fn.write_text( + dedent( + """ + [tool.pytest] + minversion = "7.0" + xfail_strict = true + testpaths = ["tests", "integration"] + python_files = ["test_*.py", "*_test.py"] + verbosity_assertions = 2 + maxfail = 5 + timeout = 300.5 + """ + ), + encoding="utf-8", + ) + result = load_config_dict_from_file(fn) + assert result == { + "minversion": ConfigValue("7.0", origin="file", mode="toml"), + "xfail_strict": ConfigValue(True, origin="file", mode="toml"), + "testpaths": ConfigValue( + ["tests", "integration"], origin="file", mode="toml" + ), + "python_files": ConfigValue( + ["test_*.py", "*_test.py"], origin="file", mode="toml" + ), + "verbosity_assertions": ConfigValue(2, origin="file", mode="toml"), + "maxfail": ConfigValue(5, origin="file", mode="toml"), + "timeout": ConfigValue(300.5, origin="file", mode="toml"), } + def test_native_and_ini_conflict(self, tmp_path: Path) -> None: + """Using both [tool.pytest] and [tool.pytest.ini_options] should raise an error.""" + fn = tmp_path / "pyproject.toml" + fn.write_text( + dedent( + """ + [tool.pytest] + xfail_strict = true + + [tool.pytest.ini_options] + minversion = "7.0" + """ + ), + encoding="utf-8", + ) + with pytest.raises(UsageError, match="Cannot use both"): + load_config_dict_from_file(fn) + + def test_invalid_suffix(self, tmp_path: Path) -> None: + """A file with an unknown suffix is ignored.""" + fn = tmp_path / "pytest.config" + fn.write_text("", encoding="utf-8") + assert load_config_dict_from_file(fn) is None + class TestCommonAncestor: def test_has_ancestor(self, tmp_path: Path) -> None: diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 80936667835..d1f2255f30f 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -113,7 +113,7 @@ def test_session_scoped_unavailable_attributes(self, session_request): _ = session_request.fspath -@pytest.mark.parametrize("config_type", ["ini", "pyproject"]) +@pytest.mark.parametrize("config_type", ["ini", "toml"]) def test_addini_paths(pytester: pytest.Pytester, config_type: str) -> None: pytester.makeconftest( """ @@ -126,15 +126,15 @@ def pytest_addoption(parser): inipath = pytester.makeini( """ [pytest] - paths=hello world/sub.py - """ + paths = hello world/sub.py + """ ) - elif config_type == "pyproject": - inipath = pytester.makepyprojecttoml( + else: + inipath = pytester.maketoml( + """ + [pytest] + paths = ["hello", "world/sub.py"] """ - [tool.pytest.ini_options] - paths=["hello", "world/sub.py"] - """ ) config = pytester.parseconfig() values = config.getini("paths") diff --git a/testing/test_python_path.py b/testing/test_python_path.py index d12ef96115f..f75bcb6bb57 100644 --- a/testing/test_python_path.py +++ b/testing/test_python_path.py @@ -92,8 +92,8 @@ def test_module_not_found(pytester: Pytester, file_structure) -> None: result.stdout.fnmatch_lines([expected_error]) -def test_no_ini(pytester: Pytester, file_structure) -> None: - """If no ini file, test should error.""" +def test_no_config_file(pytester: Pytester, file_structure) -> None: + """If no configuration file, test should error.""" result = pytester.runpytest("test_foo.py") assert result.ret == pytest.ExitCode.INTERRUPTED result.assert_outcomes(errors=1) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index e6b77ae5546..cfc668fa6ad 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -940,7 +940,7 @@ def test_header(self, pytester: Pytester) -> None: pytester.path.joinpath("tests").mkdir() pytester.path.joinpath("gui").mkdir() - # no ini file + # no configuration file result = pytester.runpytest() result.stdout.fnmatch_lines(["rootdir: *test_header0"]) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 4800a916eac..d13ed72a2d4 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -382,7 +382,7 @@ def test_bar(): def test_option_precedence_cmdline_over_ini( pytester: Pytester, ignore_on_cmdline ) -> None: - """Filters defined in the command-line should take precedence over filters in ini files (#3946).""" + """Filters defined in the command-line should take precedence over filters in config files (#3946).""" pytester.makeini( """ [pytest]