Skip to content
This repository has been archived by the owner on Dec 4, 2023. It is now read-only.

Commit

Permalink
Add pyproject.toml support (#120)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cooperlees authored Jul 12, 2022
1 parent 4bc3fca commit 8383d69
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci_latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/))
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
39 changes: 37 additions & 2 deletions ptr.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,26 @@
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"
# To make main use asyncio.run and unittests to test approrpiately for older cpython
# 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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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] = {}
Expand Down
28 changes: 26 additions & 2 deletions ptr_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions ptr_tests_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)

0 comments on commit 8383d69

Please sign in to comment.