From 4999fea0744a418fd03369a546e3611b54d0d3df Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 26 Dec 2023 21:57:57 -0300 Subject: [PATCH 1/6] add toml dependency Signed-off-by: Alvaro Frias Garay --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3b4cd8ca..6744a99c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ classifiers = [ dynamic = [ "version", ] +dependencies = [ + "toml>=0.10.2", # Add other dependencies here +] optional-dependencies.coverage_plugin = [ "coverage", ] From 7227199f2a78fdda7eca612a32afb876e25f7997 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 26 Dec 2023 21:58:18 -0300 Subject: [PATCH 2/6] update session to handle toml files Signed-off-by: Alvaro Frias Garay --- nose2/session.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nose2/session.py b/nose2/session.py index ed171ec3..11bd6cad 100644 --- a/nose2/session.py +++ b/nose2/session.py @@ -5,6 +5,8 @@ # py2/py3 compatible load of SafeConfigParser/ConfigParser from configparser import ConfigParser +import toml + from nose2 import config, events, util log = logging.getLogger(__name__) @@ -118,7 +120,14 @@ 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(".toml"): + with open(filename) as f: + toml_config = toml.load(f) + if "tool" in toml_config and "nose2" in toml_config["tool"]: + self.config.read_dict(toml_config["tool"]["nose2"]) def loadPlugins(self, modules=None, exclude=None): """Load plugins. From 557536d5bde030a32c997120da364a5577181ff9 Mon Sep 17 00:00:00 2001 From: Alvaro Frias Garay Date: Tue, 26 Dec 2023 22:01:40 -0300 Subject: [PATCH 3/6] add tests Signed-off-by: Alvaro Frias Garay --- nose2/tests/functional/support/toml/a.toml | 5 +++ nose2/tests/functional/support/toml/b.toml | 5 +++ nose2/tests/functional/test_session.py | 51 ++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 nose2/tests/functional/support/toml/a.toml create mode 100644 nose2/tests/functional/support/toml/b.toml diff --git a/nose2/tests/functional/support/toml/a.toml b/nose2/tests/functional/support/toml/a.toml new file mode 100644 index 00000000..c93321f6 --- /dev/null +++ b/nose2/tests/functional/support/toml/a.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.toml b/nose2/tests/functional/support/toml/b.toml new file mode 100644 index 00000000..2c23c117 --- /dev/null +++ b/nose2/tests/functional/support/toml/b.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..651e884a 100644 --- a/nose2/tests/functional/test_session.py +++ b/nose2/tests/functional/test_session.py @@ -53,3 +53,54 @@ 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): + self.s = session.Session() + self.s.loadConfigFiles( + support_file("toml", "a.toml"), support_file("toml", "b.toml") + ) + sys.path.insert(0, support_file("lib")) + + 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.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 From baa763ad44e526d269e0b472b01490b0c90e687c Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 8 May 2024 14:26:28 -0500 Subject: [PATCH 4/6] Convert 'toml' usage to tomllib/tomli tomli was ported to stdlib tomllib, so it's preferable to use these implementations for clearer (and more standard/common style) compatibility across versions. In order to have the failthrough behavior be clear, with dispatch onto TOML support controlled via a bool, this is wrapped in a module which provides failover on import errors per the normal path to prefer tomllib where available. 'tomli' is not required by nose2, not even on lower python versions, to ensure that application testing which is sensitive to and dispatched over the presence/absence of a toml loader is supported. tox matrix config supports toml-enabled test invocation for the lower python versions. --- nose2/_toml.py | 21 +++++++++++++++++++ nose2/main.py | 3 +++ nose2/session.py | 18 +++++++++------- .../support/toml/{a.toml => a/pyproject.toml} | 0 .../support/toml/{b.toml => b/pyproject.toml} | 0 nose2/tests/functional/test_session.py | 11 ++++++---- pyproject.toml | 3 --- tox.ini | 4 +++- 8 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 nose2/_toml.py rename nose2/tests/functional/support/toml/{a.toml => a/pyproject.toml} (100%) rename nose2/tests/functional/support/toml/{b.toml => b/pyproject.toml} (100%) 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 11bd6cad..bfc3e1d3 100644 --- a/nose2/session.py +++ b/nose2/session.py @@ -5,9 +5,8 @@ # py2/py3 compatible load of SafeConfigParser/ConfigParser from configparser import ConfigParser -import toml - from nose2 import config, events, util +from nose2._toml import load_toml log = logging.getLogger(__name__) __unittest = True @@ -123,11 +122,16 @@ def loadConfigFiles(self, *filenames): for filename in filenames: if filename.endswith(".cfg"): self.config.read(filename) - elif filename.endswith(".toml"): - with open(filename) as f: - toml_config = toml.load(f) - if "tool" in toml_config and "nose2" in toml_config["tool"]: - self.config.read_dict(toml_config["tool"]["nose2"]) + 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.toml b/nose2/tests/functional/support/toml/a/pyproject.toml similarity index 100% rename from nose2/tests/functional/support/toml/a.toml rename to nose2/tests/functional/support/toml/a/pyproject.toml diff --git a/nose2/tests/functional/support/toml/b.toml b/nose2/tests/functional/support/toml/b/pyproject.toml similarity index 100% rename from nose2/tests/functional/support/toml/b.toml rename to nose2/tests/functional/support/toml/b/pyproject.toml diff --git a/nose2/tests/functional/test_session.py b/nose2/tests/functional/test_session.py index 651e884a..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 @@ -57,11 +59,13 @@ def test_session_config_cacheing(self): 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.toml"), support_file("toml", "b.toml") + support_file("toml", "a", "pyproject.toml"), + support_file("toml", "b", "pyproject.toml"), ) - sys.path.insert(0, support_file("lib")) def test_session_can_load_config_files(self): assert self.s.config.has_section("a") @@ -83,12 +87,11 @@ def test_session_config_cacheing(self): # Create new session (generic one likely already cached # depending on test order) cache_sess = session.Session() - cache_sess.loadConfigFiles(support_file("toml", "a.toml")) + 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" diff --git a/pyproject.toml b/pyproject.toml index 6744a99c..3b4cd8ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,6 @@ classifiers = [ dynamic = [ "version", ] -dependencies = [ - "toml>=0.10.2", # Add other dependencies here -] optional-dependencies.coverage_plugin = [ "coverage", ] 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} From 7ccb3d261113b4f7faa9f400c6116d3dce0ab222 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 8 May 2024 14:35:31 -0500 Subject: [PATCH 5/6] Update changelog for pyproject.toml support --- docs/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) 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) ------------------- From 3cb4d45b1bc51319e0faa964fa6251e669da83d1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 29 May 2024 14:52:19 -0500 Subject: [PATCH 6/6] Add TOML test data to packaged source This ensures that the tests work when run from the packaged source, as the pyproject.toml files used as test cases are included. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 *~