diff --git a/mypy.ini b/mypy.ini
index 6a468f0..a6279a9 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,5 +1,5 @@
 [mypy]
-python_version = 3.5
+python_version = 3.6
 check_untyped_defs = True
 disallow_incomplete_defs = True
 disallow_untyped_decorators = True
diff --git a/ptr.py b/ptr.py
index d60320e..1649640 100755
--- a/ptr.py
+++ b/ptr.py
@@ -232,6 +232,12 @@ def _get_test_modules(base_path: Path, stats: Dict[str, int]) -> Dict[Path, Dict
 
     test_modules = {}  # type: Dict[Path, Dict]
     for setup_py in all_setup_pys:
+        # If a setup.cfg exists lets prefer it, if there is a [ptr] section
+        ptr_params = parse_setup_cfg(setup_py)
+        if ptr_params:
+            test_modules[setup_py] = ptr_params
+            continue
+
         with setup_py.open("r") as sp:
             setup_tree = ast.parse(sp.read())
 
@@ -564,6 +570,15 @@ async def create_venv(
     return venv_path
 
 
+def find_py_files(py_files: Set[str], base_dir: Path) -> None:
+    dirs = [d for d in base_dir.iterdir() if d.is_dir()]
+    py_files.update(
+        {str(x) for x in base_dir.iterdir() if x.is_file() and x.suffix == ".py"}
+    )
+    for directory in dirs:
+        find_py_files(py_files, directory)
+
+
 def find_setup_pys(
     base_path: Path, exclude_patterns: Set[str], follow_symlinks: bool = False
 ) -> Set[Path]:
@@ -593,6 +608,37 @@ def find_setup_pys(
     return setup_pys
 
 
+def parse_setup_cfg(setup_py: Path) -> Dict[str, Any]:
+    req_cov_key_strip = "required_coverage_"
+    ptr_params = {}  # type: Dict[str, Any]
+    setup_cfg = setup_py.parent / "setup.cfg"
+    if not setup_cfg.exists():
+        return ptr_params
+
+    cp = ConfigParser()
+    # Have configparser maintain key case - T484 error = callable
+    cp.optionxform = str  # type: ignore
+    cp.read(setup_cfg)
+    if "ptr" not in cp:
+        LOG.info("{} does not have a ptr section")
+        return ptr_params
+
+    # Create a setup.py like ptr_params to return
+    ptr_params["required_coverage"] = {}
+    for key, value in cp["ptr"].items():
+        if key.startswith(req_cov_key_strip):
+            key = key.strip(req_cov_key_strip)
+            ptr_params["required_coverage"][key] = int(value)
+        elif key.startswith("run_"):
+            ptr_params[key] = cp.getboolean("ptr", key)
+        elif key == "test_suite_timeout":
+            ptr_params[key] = cp.getint("ptr", key)
+        else:
+            ptr_params[key] = value
+
+    return ptr_params
+
+
 def print_test_results(
     test_results: Sequence[test_result], stats: Optional[Dict[str, int]] = None
 ) -> Dict[str, int]:
@@ -640,15 +686,6 @@ def print_test_results(
     return stats
 
 
-def find_py_files(py_files: Set[str], base_dir: Path) -> None:
-    dirs = [d for d in base_dir.iterdir() if d.is_dir()]
-    py_files.update(
-        {str(x) for x in base_dir.iterdir() if x.is_file() and x.suffix == ".py"}
-    )
-    for directory in dirs:
-        find_py_files(py_files, directory)
-
-
 async def run_tests(
     atonce: int,
     mirror: str,
diff --git a/setup.cfg.sample b/setup.cfg.sample
index 0f1ea18..1940e25 100644
--- a/setup.cfg.sample
+++ b/setup.cfg.sample
@@ -1,8 +1,9 @@
 [ptr]
 entry_point_module = ptr
 test_suite = ptr_tests
-test_suite_timeout = 300
-# TODO: Make this more ini like? - Big JSON will get U G L Y
-required_coverage = {"ptr.py": 84, "TOTAL": 90}
+test_suite_timeout = 120
+# `required_coverage_` will be stripped for use
+required_coverage_ptr.py = 84
+required_coverage_TOTAL = 90
 run_black = true
 run_mypy = true
diff --git a/setup.py b/setup.py
index b61df98..d75cb3f 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ ptr_params = {
     "test_suite": "ptr_tests",
     "test_suite_timeout": 120,
     # Relative path from setup.py to module (e.g. ptr == ptr.py)
-    "required_coverage": {"ptr.py": 84, "TOTAL": 90},
+    "required_coverage": {"ptr.py": 85, "TOTAL": 91},
     # Run black or not
     "run_black": True,
     # Run mypy or not

Add `setup.cfg` support
Allow people to configure tests to be ran from `setup.cfg` as well as `setup.py`
diff --git a/ptr_tests.py b/ptr_tests.py
index 687c49f..7c954bf 100644
--- a/ptr_tests.py
+++ b/ptr_tests.py
@@ -241,6 +241,18 @@ class TestPtr(unittest.TestCase):
         with self.assertRaises(SystemExit):
             ptr.main()
 
+    def test_parse_setup_cfg(self) -> None:
+        tmp_dir = Path(gettempdir())
+        setup_cfg = tmp_dir / "setup.cfg"
+        setup_py = tmp_dir / "setup.py"
+
+        with setup_cfg.open("w") as scp:
+            scp.write(ptr_tests_fixtures.SAMPLE_SETUP_CFG)
+
+        self.assertEqual(
+            ptr.parse_setup_cfg(setup_py), ptr_tests_fixtures.EXPECTED_TEST_PARAMS
+        )
+
     @patch("ptr.print")  # noqa
     def test_print_test_results(self, mock_print: Mock) -> None:
         stats = ptr.print_test_results(ptr_tests_fixtures.EXPECTED_COVERAGE_RESULTS)
diff --git a/ptr_tests_fixtures.py b/ptr_tests_fixtures.py
index b5d1d62..4f08086 100644
--- a/ptr_tests_fixtures.py
+++ b/ptr_tests_fixtures.py
@@ -24,7 +24,7 @@ EXPECTED_TEST_PARAMS = {
     "entry_point_module": "ptr",
     "test_suite": "ptr_tests",
     "test_suite_timeout": 120,
-    "required_coverage": {"ptr.py": 84, "TOTAL": 90},
+    "required_coverage": {"ptr.py": 85, "TOTAL": 91},
     "run_black": True,
     "run_mypy": True,
 }
@@ -148,6 +148,17 @@ setup(
 )
 """
 
+SAMPLE_SETUP_CFG = """\
+[ptr]
+entry_point_module = ptr
+test_suite = ptr_tests
+test_suite_timeout = 120
+required_coverage_ptr.py = 85
+required_coverage_TOTAL = 91
+run_black = true
+run_mypy = true
+"""
+
 SAMPLE_SETUP_PY_PTR = {
     "test_suite": "coop.tests.base",
     "test_suite_timeout": 60,

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
black==25.1.0
click==8.1.8
coverage==7.8.0
exceptiongroup==1.2.2
execnet==2.1.1
iniconfig==2.1.0
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
-e git+https://github.com/facebookincubator/ptr.git@b95934e1b672870d0a29f5aa0d8715be8ec6de44#egg=ptr
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-xdist==3.6.1
tomli==2.2.1
typing_extensions==4.13.0

name: ptr
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - black==25.1.0
      - click==8.1.8
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - iniconfig==2.1.0
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - tomli==2.2.1
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/ptr

ptr_tests.py::TestPtr::test_get_test_modules ptr_tests.py::TestPtr::test_parse_setup_cfg
ptr_tests.py::TestPtr::test_write_stats_file_raise
ptr_tests.py::TestPtr::test_analyze_coverage ptr_tests.py::TestPtr::test_analyze_coverage_errors ptr_tests.py::TestPtr::test_async_main ptr_tests.py::TestPtr::test_config ptr_tests.py::TestPtr::test_create_venv ptr_tests.py::TestPtr::test_find_setup_py ptr_tests.py::TestPtr::test_find_setup_py_exclude_default ptr_tests.py::TestPtr::test_gen_output ptr_tests.py::TestPtr::test_generate_black_command ptr_tests.py::TestPtr::test_generate_install_cmd ptr_tests.py::TestPtr::test_generate_mypy_cmd ptr_tests.py::TestPtr::test_get_site_packages_path_error ptr_tests.py::TestPtr::test_handle_debug ptr_tests.py::TestPtr::test_main ptr_tests.py::TestPtr::test_print_test_results ptr_tests.py::TestPtr::test_process_reporter ptr_tests.py::TestPtr::test_set_build_env ptr_tests.py::TestPtr::test_set_pip_mirror ptr_tests.py::TestPtr::test_test_runner ptr_tests.py::TestPtr::test_test_steps_runner ptr_tests.py::TestPtr::test_validate_base_dir ptr_tests.py::TestPtr::test_validate_base_dir_fail ptr_tests.py::TestPtr::test_write_stats_file
setup.py mypy.ini setup.cfg.sample ptr.py
setup.py mypy.ini setup.cfg.sample ptr.py