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

Add pyproject.toml support #120

Merged
merged 3 commits into from
Jul 12, 2022
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 .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"],
)