From 8383d6959a231eb622c7ef0eb5a89850cf509061 Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Tue, 12 Jul 2022 10:32:25 -0700 Subject: [PATCH] Add pyproject.toml support (#120) * Add pyproject.toml support - [tool.ptr] Section support in pyproject.toml - Parses the toml into the same dict structure as `setup.cfg` + `setup.py` now - `pyproject.toml` is more preferred than `setup.cfg` + `setup.py` Test: - Add unittest for parsing pyproject.toml - See Integration test stay working as pyproject.toml would be preferred - @patch mock to keep test_get_test_modules go through the if statements for coverage Fixes #91 This does not change ptr to install using pyproject.toml yet ... just suport projects with ptr config in there. * Install ptr to the venv for actions as we now (unfortunately) have a depdency till 3.11 * Add a pyproject.toml section linking to example --- .github/workflows/ci.yml | 2 +- .github/workflows/ci_latest.yml | 2 +- .github/workflows/fuzz.yml | 2 +- README.md | 16 +++++++++++--- ptr.py | 39 +++++++++++++++++++++++++++++++-- ptr_tests.py | 28 +++++++++++++++++++++-- ptr_tests_fixtures.py | 15 +++++++++++++ pyproject.toml | 13 +++++++++++ setup.py | 2 +- 9 files changed, 108 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b2fbac..a05aa64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - name: Run Unitests via coverage run: | - python -m pip install coverage + python -m pip install . coverage coverage run ptr_tests.py -v - name: Show coverage diff --git a/.github/workflows/ci_latest.yml b/.github/workflows/ci_latest.yml index 9e3e259..6a6f868 100644 --- a/.github/workflows/ci_latest.yml +++ b/.github/workflows/ci_latest.yml @@ -27,7 +27,7 @@ jobs: - name: Run Unitests via coverage run: | - python -m pip install coverage + python -m pip install . coverage coverage run ptr_tests.py -v - name: Show coverage diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 3c0515e..e640ae8 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools - python -m pip install coverage hypothesis + python -m pip install . coverage hypothesis - name: Run fuzz tests run: | diff --git a/README.md b/README.md index d0cc036..1dcec1a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ Python Test Runner (ptr) was born to run tests in an opinionated way, within arb - `ptr` itself uses `ptr` to run its tests 👌🏼 - `ptr` is supported and tested on *Linux*, *MacOS* + *Windows* Operating Systems -By adding `ptr` configuration to your `setup.cfg` or `setup.py` you can have `ptr` perform the following, per test suite, in parallel: +By adding `ptr` configuration to your either of your `pyproject.toml`, `setup.cfg` or `setup.py` you can have `ptr` perform the following, +per test suite, in parallel: + - run your test suite - check and enforce coverage requirements (via [coverage](https://pypi.org/project/coverage/)), - format code (via [black](https://pypi.org/project/black/)) @@ -36,7 +38,7 @@ ptr I'm glad you ask. Under the covers `ptr` performs: - Recursively searches for `setup.(cfg|py)` files from `BASE_DIR` (defaults to your "current working directory" (CWD)) - [AST](https://docs.python.org/3/library/ast.html) parses out the config for each `setup.py` test requirements - - If a `setup.cfg` exists, load via configparser and prefer if a `[ptr]` section exists + - If a `pyproject.toml` or `setup.cfg` exists, load via configparser/tomli and prefer if a `[ptr]` section exists - Creates a [Python Virtual Environment](https://docs.python.org/3/tutorial/venv.html) (*OPTIONALLY* pointed at an internal PyPI mirror) - Runs `ATONCE` tests suites in parallel (i.e. per setup.(cfg|ptr)) - All steps will be run for each suite and ONLY *FAILED* runs will have output written to stdout @@ -129,11 +131,19 @@ ptr_params = { } ``` +### `pyproject.toml` + +This is per project in your repository and if exists is preferred over `setup.py` and `setup.cfg`. + +Please refer to [`pyproject.toml`](http://github.com/facebookincubator/ptr/blob/master/pyproject.toml) +for the options available + format. + ### `setup.cfg` This is per project in your repository and if exists is preferred over `setup.py`. -Please refer to [`setup.cfg.sample`](http://github.com/facebookincubator/ptr/blob/master/setup.cfg.sample) for the options available + format. +Please refer to [`setup.cfg.sample`](http://github.com/facebookincubator/ptr/blob/master/setup.cfg.sample) +for the options available + format. ### mypy Specifics diff --git a/ptr.py b/ptr.py index 8471b59..aa7fb00 100755 --- a/ptr.py +++ b/ptr.py @@ -27,6 +27,18 @@ from time import time from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, Union +# Support pyproject.toml +# In >= 3.11 we can remove this import dance +if sys.version_info >= (3, 11): # pragma: no cover + try: + import tomllib + except ImportError: + # Help users on older alphas + import tomli as tomllib +else: + # pyre-ignore: Undefined import [21] + import tomli as tomllib + LOG = logging.getLogger(__name__) MACOSX = system() == "Darwin" @@ -34,6 +46,7 @@ # Using 3.8 rather than 3.7 due to subprocess.exec in < 3.8 only support main loop # https://bugs.python.org/issue35621 PY_38_OR_GREATER = sys.version_info >= (3, 8) +PYPROJECT_TOML = "pyproject.toml" WINDOWS = system() == "Windows" # Windows needs to use a ProactorEventLoop for subprocesses # Need to use sys.platform for mypy to understand @@ -412,8 +425,11 @@ def _get_test_modules( test_modules: Dict[Path, Dict] = {} for setup_py in all_setup_pys: disabled_err_msg = f"Not running {setup_py} as ptr is disabled via config" - # If a setup.cfg exists lets prefer it, if there is a [ptr] section - ptr_params = parse_setup_cfg(setup_py) + # If a pyproject.toml or setup.cfg exists lets prefer them + # Only if there is a [ptr] section + ptr_params = parse_pyproject_toml(setup_py) + if not ptr_params: + ptr_params = parse_setup_cfg(setup_py) if not ptr_params: ptr_params = _parse_setup_params(setup_py) @@ -856,6 +872,25 @@ def find_setup_pys( return setup_pys +def parse_pyproject_toml( + setup_py: Path, tool_section: str = "tool", ptr_section: str = "ptr" +) -> Dict[str, Any]: + ptr_params: Dict[str, Any] = {} + pyproject_toml_path = setup_py.parent / PYPROJECT_TOML + if not pyproject_toml_path.exists(): + return ptr_params + + with pyproject_toml_path.open("rb") as f: + pyproject_toml = tomllib.load(f) + ptr_params = pyproject_toml.get(tool_section, {}).get(ptr_section, {}) + + if not ptr_params: + LOG.info(f"{pyproject_toml} does not have a tool.ptr section") + return ptr_params + + return ptr_params + + def parse_setup_cfg(setup_py: Path) -> Dict[str, Any]: req_cov_key_strip = "required_coverage_" ptr_params: Dict[str, Any] = {} diff --git a/ptr_tests.py b/ptr_tests.py index dded9a4..054f3fb 100644 --- a/ptr_tests.py +++ b/ptr_tests.py @@ -374,10 +374,17 @@ def test_get_site_packages_path_error(self) -> None: lib_path.mkdir() self.assertIsNone(ptr._get_site_packages_path(lib_path.parent)) + # Patch parsing except setup.py to keep coverage up + @patch("ptr.parse_setup_cfg") + @patch("ptr.parse_pyproject_toml") @patch("ptr.print") # noqa - def test_get_test_modules(self, mock_print: Mock) -> None: + def test_get_test_modules( + self, mock_print: Mock, mock_pyproject: Mock, mock_setup_cfg: Mock + ) -> None: + mock_pyproject.return_value = {} + mock_setup_cfg.return_value = {} base_path = Path(__file__).parent - stats = defaultdict(int) # type: Dict[str, int] + stats: Dict[str, int] = defaultdict(int) test_modules = ptr._get_test_modules(base_path, stats, True, True) self.assertEqual( test_modules[base_path / "setup.py"], @@ -425,6 +432,23 @@ def test_main(self, mock_args: Mock, mock_validate: Mock) -> None: with self.assertRaises(SystemExit): ptr.main() + def test_parse_pyproject_toml(self) -> None: + tmp_dir = Path(gettempdir()) + pyproject_toml = tmp_dir / ptr.PYPROJECT_TOML + setup_py = tmp_dir / "setup.py" + + with pyproject_toml.open("w", encoding=FILE_ENCODING) as pcp: + pcp.write(ptr_tests_fixtures.SAMPLE_PYPROJECT) + + # No pyproject.toml file exists + self.assertEqual(ptr.parse_pyproject_toml(setup_py.parent), {}) + # No ptr section + self.assertEqual(ptr.parse_pyproject_toml(setup_py, "not_a_tool"), {}) + # Everything works + self.assertEqual( + ptr.parse_pyproject_toml(setup_py), ptr_tests_fixtures.EXPECTED_TEST_PARAMS + ) + def test_parse_setup_cfg(self) -> None: tmp_dir = Path(gettempdir()) setup_cfg = tmp_dir / "setup.cfg" diff --git a/ptr_tests_fixtures.py b/ptr_tests_fixtures.py index 2640fb0..b67eda1 100644 --- a/ptr_tests_fixtures.py +++ b/ptr_tests_fixtures.py @@ -211,6 +211,21 @@ def run_until_complete(self, *args, **kwargs) -> int: ) """ +SAMPLE_PYPROJECT = """\ +[tool.ptr] +disabled = true +entry_point_module = "ptr" +test_suite = "ptr_tests" +test_suite_timeout = 120 +required_coverage = { 'ptr.py' = 84, TOTAL = 88 } +run_usort = true +run_black = true +run_mypy = true +run_flake8 = true +run_pylint = true +run_pyre = true +""" + # Disabled is set as we --run-disabled the run in CI SAMPLE_SETUP_CFG = """\ [ptr] diff --git a/pyproject.toml b/pyproject.toml index 00135a8..48bc58e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,16 @@ [build-system] requires = ["setuptools>=43.0.0"] build-backend = "setuptools.build_meta" + +[tool.ptr] +disabled = true +entry_point_module = "ptr" +test_suite = "ptr_tests" +test_suite_timeout = 120 +required_coverage = { 'ptr.py' = 84, TOTAL = 88 } +run_usort = true +run_black = true +run_mypy = true +run_flake8 = true +run_pylint = true +run_pyre = true diff --git a/setup.py b/setup.py index 2f0849b..bbdae33 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ def get_long_desc() -> str: "Programming Language :: Python :: 3.10", ], python_requires=">=3.7", - install_requires=None, + install_requires=["tomli>=1.1.0; python_full_version < '3.11.0a7'"], entry_points={"console_scripts": ["ptr = ptr:main"]}, test_suite=ptr_params["test_suite"], )