Skip to content

Commit

Permalink
Merge pull request #606 from nose-devs/support-pyproject.toml
Browse files Browse the repository at this point in the history
Support loading config from pyproject.toml
  • Loading branch information
sirosen authored May 29, 2024
2 parents 74d5f9a + 3cb4d45 commit 10b3188
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 3 deletions.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 *~
Expand Down
7 changes: 7 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
-------------------

Expand Down
21 changes: 21 additions & 0 deletions nose2/_toml.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions nose2/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
15 changes: 14 additions & 1 deletion nose2/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions nose2/tests/functional/support/toml/a/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.nose2.a]
a = 1

[tool.nose2.unittest]
plugins = "plugin_a"
5 changes: 5 additions & 0 deletions nose2/tests/functional/support/toml/b/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.nose2.b]
b = """
4
5
"""
54 changes: 54 additions & 0 deletions nose2/tests/functional/test_session.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit 10b3188

Please sign in to comment.