diff --git a/MANIFEST.in b/MANIFEST.in index 4aa67d50..b12dc293 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ include tox.ini include unittest.cfg include README.rst include license.txt -recursive-include nose2/tests/functional/support *.py *.txt *.cfg *.rst *.json *.egg .coveragerc +recursive-include nose2/tests/functional/support *.py *.txt *.toml *.cfg *.rst *.json *.egg .coveragerc recursive-include docs *.inc *.py *.rst Makefile global-exclude __pycache__ global-exclude *~ diff --git a/docs/changelog.rst b/docs/changelog.rst index 2d0bd045..6b33dbaa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,13 @@ Unreleased * Official support for ``python3.13`` betas. ``nose2`` now tests itself against Python 3.13. +* ``nose2`` now supports loading configuration data from the ``tool.nose2`` + table in ``pyproject.toml``. Thanks to :user:`qequ` for the PR! (:pr:`596`) + + On python 3.11+, ``tomllib`` is used to parse TOML data. On python 3.10 and + lower, ``tomli`` must be installed to enable TOML support. Simply + ``pip install tomli`` as necessary. + 0.14.2 (2024-05-07) ------------------- diff --git a/nose2/_toml.py b/nose2/_toml.py new file mode 100644 index 00000000..6577b223 --- /dev/null +++ b/nose2/_toml.py @@ -0,0 +1,21 @@ +TOML_ENABLED: bool = False + +try: + import tomllib as toml + + TOML_ENABLED = True +except ImportError: + try: + import tomli as toml + + TOML_ENABLED = False + except ImportError: + toml = None + TOML_ENABLED = False + + +def load_toml(file: str) -> dict: + if not TOML_ENABLED: + raise RuntimeError("toml library not found. Please install 'tomli'.") + with open(file, "rb") as fp: + return toml.load(fp) diff --git a/nose2/main.py b/nose2/main.py index 3d10d90d..08b3cedb 100644 --- a/nose2/main.py +++ b/nose2/main.py @@ -4,6 +4,7 @@ import unittest from nose2 import events, loader, plugins, runner, session, util +from nose2._toml import TOML_ENABLED log = logging.getLogger(__name__) __unittest = True @@ -242,6 +243,8 @@ def findConfigFiles(self, cfg_args): """Find available config files""" filenames = cfg_args.config[:] proj_opts = ("unittest.cfg", "nose2.cfg") + if TOML_ENABLED: + proj_opts += ("pyproject.toml",) for fn in proj_opts: if cfg_args.top_level_directory: fn = os.path.abspath(os.path.join(cfg_args.top_level_directory, fn)) diff --git a/nose2/session.py b/nose2/session.py index ed171ec3..bfc3e1d3 100644 --- a/nose2/session.py +++ b/nose2/session.py @@ -6,6 +6,7 @@ from configparser import ConfigParser from nose2 import config, events, util +from nose2._toml import load_toml log = logging.getLogger(__name__) __unittest = True @@ -118,7 +119,19 @@ def loadConfigFiles(self, *filenames): Loads all names files that exist into ``self.config``. """ - self.config.read(filenames) + for filename in filenames: + if filename.endswith(".cfg"): + self.config.read(filename) + elif filename.endswith("pyproject.toml"): + if not os.path.exists(filename): + continue + toml_config = load_toml(filename) + if not isinstance(toml_config.get("tool"), dict): + continue + tool_table = toml_config["tool"] + if not isinstance(tool_table.get("nose2"), dict): + continue + self.config.read_dict(tool_table["nose2"]) def loadPlugins(self, modules=None, exclude=None): """Load plugins. diff --git a/nose2/tests/functional/support/toml/a/pyproject.toml b/nose2/tests/functional/support/toml/a/pyproject.toml new file mode 100644 index 00000000..c93321f6 --- /dev/null +++ b/nose2/tests/functional/support/toml/a/pyproject.toml @@ -0,0 +1,5 @@ +[tool.nose2.a] +a = 1 + +[tool.nose2.unittest] +plugins = "plugin_a" diff --git a/nose2/tests/functional/support/toml/b/pyproject.toml b/nose2/tests/functional/support/toml/b/pyproject.toml new file mode 100644 index 00000000..2c23c117 --- /dev/null +++ b/nose2/tests/functional/support/toml/b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.nose2.b] +b = """ +4 +5 +""" diff --git a/nose2/tests/functional/test_session.py b/nose2/tests/functional/test_session.py index 3dea9601..9f7061a7 100644 --- a/nose2/tests/functional/test_session.py +++ b/nose2/tests/functional/test_session.py @@ -1,6 +1,8 @@ import sys +import unittest from nose2 import session +from nose2._toml import TOML_ENABLED from nose2.tests._common import FunctionalTestCase, support_file @@ -53,3 +55,55 @@ def test_session_config_cacheing(self): # rather than parsing config file again secondaccess = cache_sess.get("a") assert secondaccess.as_int("a") == 0 + + +class SessionTomlFunctionalTests(FunctionalTestCase): + def setUp(self): + if not TOML_ENABLED: + raise unittest.SkipTest("toml module not available") + self.s = session.Session() + self.s.loadConfigFiles( + support_file("toml", "a", "pyproject.toml"), + support_file("toml", "b", "pyproject.toml"), + ) + + def test_session_can_load_config_files(self): + assert self.s.config.has_section("a") + assert self.s.config.has_section("b") + + def test_session_holds_plugin_config(self): + plug_config = self.s.get("a") + assert plug_config + + def test_session_can_load_toml_plugins_from_modules(self): + self.s.loadPlugins() + assert self.s.plugins + plug = self.s.plugins[0] + self.assertEqual(plug.a, 1) + + def test_session_config_cacheing(self): + """Test caching of config sections works""" + + # Create new session (generic one likely already cached + # depending on test order) + cache_sess = session.Session() + cache_sess.loadConfigFiles(support_file("toml", "a", "pyproject.toml")) + + # First access to given section, should read from config file + firstaccess = cache_sess.get("a") + assert firstaccess.as_int("a") == 1 + # Hack cached Config object internals to make the stored value + # something different + cache_sess.configCache["a"]._mvd["a"] = "0" + newitems = [] + for item in cache_sess.configCache["a"]._items: + if item != ("a", "1"): + newitems.append(item) + else: + newitems.append(("a", "0")) + cache_sess.configCache["a"]._items = newitems + + # Second access to given section, confirm returns cached value + # rather than parsing config file again + secondaccess = cache_sess.get("a") + assert secondaccess.as_int("a") == 0 diff --git a/tox.ini b/tox.ini index 371acf02..89ceb7c5 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,9 @@ envlist=py{38,39,310,311,312,313}{,-nocov},pypy,docs,lint [testenv] passenv = CI extras = dev -deps = !nocov: coverage +deps = + !nocov: coverage + py{38,39,310}-toml: tomli setenv = PYTHONPATH={toxinidir} commands = nocov: nose2 -v --pretty-assert {posargs}