Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading config from pyproject.toml #606

Merged
merged 6 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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