diff --git a/README.md b/README.md index 036e5de..7714d3d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,11 @@ Quick Start ----------- ```bash -pip3 install green # To upgrade: "pip3 install --upgrade green" +pip3 install green +# To upgrade: +pip3 install --upgrade green +# To add pyproject.toml support in python < 3.11: +pip3 install 'green[toml]' ``` Now run green... @@ -99,9 +103,10 @@ in the resolution chain overwriting earlier settings (last setting wins). 1) `$HOME/.green` 2) A config file specified by the environment variable `$GREEN_CONFIG` 3) `setup.cfg` in the current working directory of test run -4) `.green` in the current working directory of the test run -5) A config file specified by the command-line argument `--config FILE` -6) [Command-line arguments](https://github.com/CleanCut/green/blob/main/cli-options.txt) +4) `pyproject.toml` in the current working directory of test run +5) `.green` in the current working directory of the test run +6) A config file specified by the command-line argument `--config FILE` +7) [Command-line arguments](https://github.com/CleanCut/green/blob/main/cli-options.txt) Any arguments specified in more than one place will be overwritten by the value of the LAST place the setting is seen. So, for example, if a setting @@ -463,9 +468,9 @@ To run the unittests, we would change to the parent directory of the project $ green proj .... - + Ran 4 tests in 0.125s using 8 processes - + OK (passes=4) Okay, so that's the classic short-form output for unit tests. Green really @@ -475,7 +480,7 @@ shines when you start getting more verbose: $ green -vvv proj Green 4.1.0, Coverage 7.4.1, Python 3.12.2 - + test_foo TestAnswer . answer() returns 42 @@ -483,9 +488,9 @@ shines when you start getting more verbose: TestSchool . test_age . test_food - + Ran 4 tests in 0.123s using 8 processes - + OK (passes=4) Notes: @@ -624,5 +629,5 @@ Wait! What about the other test runners? - **the ones I missed** -- Er, haven't heard of them yet either. I'd love to hear **your** feedback regarding Green. Like it? Hate it? Have -some awesome suggestions? Whatever the case, go +some awesome suggestions? Whatever the case, go [open a discussion](https://github.com/CleanCut/green/discussions) diff --git a/green/config.py b/green/config.py index b32538e..fb964fc 100644 --- a/green/config.py +++ b/green/config.py @@ -21,6 +21,16 @@ import coverage # pragma: no cover +try: + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + + supports_tomllib = True +except ImportError: + supports_tomllib = False + coverage_version = f"Coverage {coverage.__version__}" # pragma: no cover # Used for debugging output in cmdline, since we can't do debug output here. @@ -628,7 +638,10 @@ def getConfig( # pragma: no cover cwd = pathlib.Path.cwd() # Medium priority - for cfg_file in ("setup.cfg", ".green"): + config_files = ["pyproject.toml", "setup.cfg", ".green"] + if not supports_tomllib: + config_files.remove("pyproject.toml") + for cfg_file in config_files + ["setup.cfg", ".green"]: config_path = cwd / cfg_file if config_path.is_file(): filepaths.append(config_path) @@ -647,6 +660,9 @@ def getConfig( # pragma: no cover # only if they use setup.cfg if config_path.name == "setup.cfg": parser.read(config_path) + elif config_path.name == "pyproject.toml": + data = tomllib.load(config_path.open("rb"))["tool"] + parser.read_dict(data, source="green") else: parser.read_file(ConfigFile(config_path)) diff --git a/green/test/test_config.py b/green/test/test_config.py index 09f268f..e1c5a05 100644 --- a/green/test/test_config.py +++ b/green/test/test_config.py @@ -2,12 +2,13 @@ import configparser import copy -import pathlib -from io import StringIO import os +import pathlib import shutil +import sys import tempfile import unittest +from io import StringIO from typing import Sequence from green import config @@ -133,6 +134,17 @@ def setUp(self): f"verbose = {self.setup_verbose}", ], ) + self.pyproject_filename = cwd_dir / "pyproject.toml" + self.pyproject_failfast = True + self.pyproject_verbose = 2 + self._write_file( + self.pyproject_filename, + [ + "[tool.green]", + f"verbose = {self.pyproject_verbose}", + f"failfast = {str(self.pyproject_failfast).lower()}", + ], + ) class TestConfig(ConfigBase): @@ -140,14 +152,15 @@ class TestConfig(ConfigBase): All variations of config file parsing works as expected. """ - def test_cmd_env_nodef_nosetup(self): + def test_cmd_env_nodef_nosetup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG is set, $HOME/.green does not - exist, setup.cfg does not exist + exist, setup.cfg does not exist, pyproject.toml does not exist Result: load --config """ self.default_filename.unlink(missing_ok=True) self.setup_filename.unlink(missing_ok=True) + self.pyproject_filename.unlink(missing_ok=True) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -168,14 +181,15 @@ def test_cmd_env_nodef_nosetup(self): configparser.NoOptionError, cfg.getboolean, "green", "verbose" ) - def test_cmd_noenv_def_nosetup(self): + def test_cmd_noenv_def_nosetup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG unset, $HOME/.green exists, - setup.cfg does not exist + setup.cfg does not exist, pypproject.toml does not exist Result: load --config """ os.unlink(self.env_filename) os.remove(self.setup_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig(self.cmd_filename) self.assertEqual(["green"], cfg.sections()) @@ -192,15 +206,16 @@ def test_cmd_noenv_def_nosetup(self): configparser.NoOptionError, cfg.getboolean, "green", "verbose" ) - def test_cmd_noenv_nodef_nosetup(self): + def test_cmd_noenv_nodef_nosetup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG unset, $HOME/.green does not - exist, setup.cfg does not exist + exist, setup.cfg does not exist, pyproject.toml does not exist Result: load --config """ os.unlink(self.env_filename) os.unlink(self.default_filename) os.remove(self.setup_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig(self.cmd_filename) self.assertEqual(["green"], cfg.sections()) @@ -227,6 +242,7 @@ def test_nocmd_env_cwd(self): os.chdir(self.tmpd) # setUp is already set to restore us to our pre-testing cwd os.unlink(self.cmd_filename) os.remove(self.setup_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -247,14 +263,15 @@ def test_nocmd_env_cwd(self): configparser.NoOptionError, cfg.getint, "green", "verbose" ) - def test_nocmd_env_def_nosetup(self): + def test_nocmd_env_def_nosetup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG is set, $HOME/.green exists, - setup.cfg does not exist + setup.cfg does not exist, pyproject.toml does not exist Result: load $GREEN_CONFIG """ self.cmd_filename.unlink(missing_ok=True) self.setup_filename.unlink(missing_ok=True) + self.pyproject_filename.unlink(missing_ok=True) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -273,15 +290,16 @@ def test_nocmd_env_def_nosetup(self): configparser.NoOptionError, cfg.getboolean, "green", "verbose" ) - def test_nocmd_env_nodef_nosetup(self): + def test_nocmd_env_nodef_nosetup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG is set, $HOME/.green does not - exist, setup.cfg does not exist + exist, setup.cfg does not exist, pyproject.toml does not exist Result: load $GREEN_CONFIG """ self.cmd_filename.unlink(missing_ok=True) self.default_filename.unlink(missing_ok=True) self.setup_filename.unlink(missing_ok=True) + self.pyproject_filename.unlink(missing_ok=True) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -302,15 +320,16 @@ def test_nocmd_env_nodef_nosetup(self): configparser.NoOptionError, cfg.getboolean, "green", "verbose" ) - def test_nocmd_noenv_def_nosetup(self): + def test_nocmd_noenv_def_nosetup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG unset, $HOME/.green exists, - setup.cfg does not exist + setup.cfg does not exist, pyproject.toml does not exist Result: load $HOME/.green """ os.unlink(self.cmd_filename) os.unlink(self.env_filename) os.remove(self.setup_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig() self.assertEqual(["green"], cfg.sections()) @@ -329,16 +348,17 @@ def test_nocmd_noenv_def_nosetup(self): configparser.NoOptionError, cfg.getboolean, "green", "verbose" ) - def test_nocmd_noenv_nodef_nosetup(self): + def test_nocmd_noenv_nodef_nosetup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG unset, no $HOME/.green, - setup.cfg does not exist + setup.cfg does not exist, pyproject.toml does not exist Result: empty config """ os.unlink(self.default_filename) os.unlink(self.env_filename) os.unlink(self.cmd_filename) os.remove(self.setup_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig() self.assertEqual([], cfg.sections()) @@ -355,13 +375,14 @@ def test_nocmd_noenv_nodef_nosetup(self): self.assertRaises(configparser.NoSectionError, cfg.get, "green", "version") self.assertRaises(configparser.NoSectionError, cfg.get, "green", "verbose") - def test_cmd_env_nodef_setup(self): + def test_cmd_env_nodef_setup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG is set, $HOME/.green does not - exist, setup.cfg exists + exist, setup.cfg exists, pyproject.toml does not exist Result: load --config """ os.unlink(self.default_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -381,13 +402,14 @@ def test_cmd_env_nodef_setup(self): configparser.NoOptionError, cfg.getboolean, "green", "version" ) - def test_cmd_noenv_def_setup(self): + def test_cmd_noenv_def_setup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG unset, $HOME/.green exists, - setup.cfg exists + setup.cfg exists, pyproject.toml does not exist Result: load --config """ os.unlink(self.env_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig(self.cmd_filename) self.assertEqual(["green"], cfg.sections()) @@ -403,7 +425,7 @@ def test_cmd_noenv_def_setup(self): self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) - def test_cmd_noenv_nodef_setup(self): + def test_cmd_noenv_nodef_setup_nopyproject(self): """ Setup: --config on cmd, $GREEN_CONFIG unset, $HOME/.green does not exist, setup.cfg exists @@ -411,6 +433,7 @@ def test_cmd_noenv_nodef_setup(self): """ os.unlink(self.env_filename) os.unlink(self.default_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig(self.cmd_filename) self.assertEqual(["green"], cfg.sections()) @@ -428,13 +451,14 @@ def test_cmd_noenv_nodef_setup(self): self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) - def test_nocmd_env_def_setup(self): + def test_nocmd_env_def_setup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG is set, $HOME/.green exists, - setup.cfg exists + setup.cfg exists, pyproject.toml does not exist Result: load $GREEN_CONFIG """ os.unlink(self.cmd_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -452,14 +476,15 @@ def test_nocmd_env_def_setup(self): self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) - def test_nocmd_env_nodef_setup(self): + def test_nocmd_env_nodef_setup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG is set, $HOME/.green does not - exist, setup.cfg exists + exist, setup.cfg exists, pyproject.toml does not exist Result: load $GREEN_CONFIG """ os.unlink(self.cmd_filename) os.unlink(self.default_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment( GREEN_CONFIG=str(self.env_filename), HOME=str(self.tmpd) ): @@ -479,14 +504,15 @@ def test_nocmd_env_nodef_setup(self): self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) - def test_nocmd_noenv_def_setup(self): + def test_nocmd_noenv_def_setup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG unset, $HOME/.green exists, - setup.cfg exists + setup.cfg exists, pyproject.toml does not exist Result: load $HOME/.green """ os.unlink(self.cmd_filename) os.unlink(self.env_filename) + os.unlink(self.pyproject_filename) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig() self.assertEqual(["green"], cfg.sections()) @@ -504,15 +530,16 @@ def test_nocmd_noenv_def_setup(self): self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) - def test_nocmd_noenv_nodef_setup(self): + def test_nocmd_noenv_nodef_setup_nopyproject(self): """ Setup: no --config option, $GREEN_CONFIG unset, no $HOME/.green, - setup.cfg exists + setup.cfg exists, pyproject.toml does not exist Result: empty config """ self.default_filename.unlink(missing_ok=True) self.env_filename.unlink(missing_ok=True) self.cmd_filename.unlink(missing_ok=True) + self.pyproject_filename.unlink(missing_ok=True) with ModifiedEnvironment(GREEN_CONFIG=None, HOME=str(self.tmpd)): cfg = config.getConfig() self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) @@ -529,6 +556,146 @@ def test_nocmd_noenv_nodef_setup(self): ) self.assertRaises(configparser.NoOptionError, cfg.get, "green", "version") + def test_nocmd_noenv_nodef_nosetup_pyproject(self): + """ + Setup: no --config option, $GREEN_CONFIG is unset, $HOME/.green does not exist, setup.cfg does not exist, pyproject.toml exists + Result: load pyproject.toml + """ + os.unlink(self.cmd_filename) + os.unlink(self.default_filename) + os.unlink(self.setup_filename) + with ModifiedEnvironment( + HOME=str(self.tmpd), + ): + cfg = config.getConfig() + self.assertEqual(["green"], cfg.sections()) + if sys.version_info.minor >= 11: + self.assertEqual( + self.pyproject_failfast, cfg.getboolean("green", "failfast") + ) + self.assertEqual(self.pyproject_verbose, cfg.getint("green", "verbose")) + else: + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "failfast" + ) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "verbose" + ) + + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "omit-patterns" + ) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "run-coverage" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "logging") + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "no-skip-report" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "version") + + def test_cmd_noenv_nodef_nosetup_pyproject(self): + """ + Setup: --config option, $GREEN_CONFIG is unset, $HOME/.green does not exist, setup.cfg does not exist, pyproject.toml exists + Result: load --config + """ + os.unlink(self.default_filename) + os.unlink(self.setup_filename) + with ModifiedEnvironment( + HOME=str(self.tmpd), + ): + cfg = config.getConfig(self.cmd_filename) + self.assertEqual(["green"], cfg.sections()) + self.assertEqual(str(self.cmd_filename), cfg.get("green", "omit-patterns")) + self.assertEqual(self.cmd_logging, cfg.getboolean("green", "logging")) + self.assertEqual( + self.cmd_run_coverage, cfg.getboolean("green", "run-coverage") + ) + self.assertEqual(self.pyproject_failfast, cfg.getboolean("green", "failfast")) + self.assertEqual(self.pyproject_verbose, cfg.getint("green", "verbose")) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "no-skip-report" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "version") + + def test_nocmd_noenv_nodef_setup_pyproject(self): + """ + Setup: no --config option, $GREEN_CONFIG is unset, $HOME/.green does not exist, setup.cfg exists, pyproject.toml exists + Result: load setup.cfg + """ + os.unlink(self.default_filename) + os.unlink(self.cmd_filename) + with ModifiedEnvironment(HOME=str(self.tmpd)): + cfg = config.getConfig() + self.assertEqual(["green"], cfg.sections()) + self.assertEqual(self.setup_failfast, cfg.getboolean("green", "failfast")) + self.assertEqual(self.setup_verbose, cfg.getint("green", "verbose")) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "omit-patterns" + ) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "run-coverage" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "logging") + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "no-skip-report" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "version") + + def test_nocmd_noenv_def_nosetup_pyproject(self): + """ + Setup: no --config option, $GREEN_CONFIG is unset, $HOME/.green exists, setup.cfg does not exist, pyproject exists + Result: load $HOME/.green + """ + os.unlink(self.env_filename) + os.unlink(self.setup_filename) + os.unlink(self.cmd_filename) + with ModifiedEnvironment(HOME=str(self.tmpd)): + cfg = config.getConfig() + self.assertEqual(["green"], cfg.sections()) + self.assertEqual(self.default_failfast, cfg.getboolean("green", "failfast")) + self.assertEqual(self.default_logging, cfg.getboolean("green", "logging")) + self.assertEqual( + self.default_termcolor, cfg.getboolean("green", "termcolor") + ) + self.assertEqual(self.default_version, cfg.getboolean("green", "version")) + self.assertEqual( + str(self.default_filename), cfg.get("green", "omit-patterns") + ) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "run-coverage" + ) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "no-skip-report" + ) + + def test_nocmd_env_nodef_nosetup_pyproject(self): + """ + Setup: no --config option, $GREEN_CONFIG is set, $HOME/.green does not exist, setup.cfg does not exist, pyproject.toml exists + Result: load $GREEN_CONFIG + """ + os.unlink(self.cmd_filename) + os.unlink(self.setup_filename) + os.unlink(self.default_filename) + with ModifiedEnvironment( + HOME=str(self.tmpd), GREEN_CONFIG=str(self.env_filename) + ): + cfg = config.getConfig() + self.assertEqual(["green"], cfg.sections()) + self.assertEqual(self.env_logging, cfg.getboolean("green", "logging")) + self.assertEqual( + self.env_no_skip_report, cfg.getboolean("green", "no-skip-report") + ) + self.assertEqual(str(self.env_filename), cfg.get("green", "omit-patterns")) + self.assertRaises( + configparser.NoOptionError, cfg.get, "green", "run-coverage" + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "termcolor") + self.assertEqual( + self.pyproject_failfast, cfg.getboolean("green", "failfast") + ) + self.assertRaises(configparser.NoOptionError, cfg.get, "green", "version") + class TestMergeConfig(ConfigBase): """ diff --git a/setup.cfg b/setup.cfg index c68d4cd..1335127 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,9 @@ packages = find: [options.extras_require] dev = file:requirements-dev.txt +# For the version marker syntax to work, the value needs to be on a dangling line +toml = + tomli>=1.10; python_version<'3.11' [options.package_data] green = VERSION, shell_completion.sh