pyproject.toml Tooling — Black, isort, mypy, pytest, CoveragePYPROJECT.TOML TOOLING — Data Flow
pyproject.toml
│
├─── [tool.black] ── line-length 100, target py311 (pre-commit)
├─── [tool.isort] ── profile=black, multi_line_output=3
├─── [tool.mypy] ── follows imports, ignores missing, warn_unused
├─── [tool.pytest.ini_options] ── asyncio mode, --cov=app, --cov-report term+html
├─── [tool.coverage.run] ── omit tests/, migrations/
└─── [tool.coverage.report] ── exclude pragma, Protocol lines
│
▼
developers: pre-commit hooks (black, isort, flake8)
CI: pytest, mypy, coverage gate
pyproject.toml is the single source of truth for Python build metadata AND developer-tool configuration. PEP 621 governs the [project] block; PEP 518 governs [build-system]. Tool-specific blocks ([tool.black], [tool.pytest.ini_options], etc.) live in the same file so a git clone reproduces lint, type-check, test, and coverage behaviour with zero auxiliary config.
The FCA Support Agent’s pyproject.toml lays out the contracts the team relies on:
tests/*asyncio, integrates coverage by default, and writes both terminal missing-line and HTML reportspragma: no cover, __repr__, abstract methods, type-checking branches)# pyproject.toml
[build-system]
requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "fca-multi-agent-support"
version = "0.1.0"
description = "FCA-compliant multi-agent AI support system for UK financial services"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [{name = "David Sandeep", email = "..."}]
keywords = ["ai", "multi-agent", "fca", "compliance", "support"]
classifiers = [
"Development Status :: 3 - Alpha",
"Framework :: FastAPI",
]
# pyproject.toml
[tool.black]
line-length = 100
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''/(\.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv
| build | dist)/'''
[tool.isort]
profile = "black" # harmonise with Black's import grouping
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
# pyproject.toml
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false # onboarding-friendly
disallow_incomplete_defs = false
check_untyped_defs = true # CHEC类型 for untyped fns
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
follow_imports = "normal"
ignore_missing_imports = true # tolerate third-party stubs
[[tool.mypy.overrides]]
module = ["tests.*"]
ignore_errors = true # tests get a pass on type errors
# pyproject.toml
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = """
-v
--strict-markers
--strict-config
--cov=app
--cov-report=term-missing
--cov-report=html
"""
markers = [
"asyncio: marks tests as async (deselect with '-m \"not asyncio\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
[tool.coverage.run]
source = ["app"]
omit = ["*/tests/*", "*/migrations/*", "*/__init__.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
line-length = 100 — Industry compromise: wider than Black’s default 88 to fit domain-rich identifier names (agent_coordinator_url_sync), tighter than 120 so log lines stay readable.profile = "black" for isort — Forces isort to import-group EXACTLY like Black — no more “isort moved my import” battles. Without it, you waste minutes per file on merge conflicts.warn_return_any = true, disallow_untyped_defs = false — Pragmatic tension: STRICT detection of unsafe returns, but allow untyped defs so junior engineers aren’t blocked. Tightens over time as the codebase matures.ignore_errors = true for tests.* — Tests emphasize behaviour over types; type-checking them adds friction without proportional value. Exemption is conventional.--strict-markers & --strict-config — Errors out on an undeclared @pytest.mark.something instead of silently passing. Critical for catching typos in CI.--cov=app + --cov-report=htm — Coverage runs as part of every pytest invocation; the HTML report goes into htmlcov/ for PR diff inspection. Coverage as a side effect, not a separate command.[tool.coverage.report].exclude_lines — Standard exclusions: dunder methods, abstract methods, type-checking blocks. Reduces noise so coverage delta gates signal real regressions.setup.cfg, .flake8, mypy.ini, and pyproject.toml. Migrate ALL tool config into pyproject.toml first; legacy files take precedence silently.disallow_untyped_defs = true on day one — Even senior teams hit friction on bulky class hierarchies (__init__, __repr__, Pydantic models). Don’t enforce until codebase is typed end-to-end.disallow_untyped_defs = false to true incrementally?A: Add per-file overrides. Start with disallow_untyped_defs = true for app/services/ (highest value, easiest to type), set disallow_untyped_defs = false globally. Use an [[tool.mypy.overrides]] block to expand the strict module list over time. Track the count of untyped defs in mypy --strict-style reports and gate on it via CI: --warn-unused-ignores + a custom script that fails PRs introducing new untyped defs in protected modules.
pytest works locally but the project’s CI marks it. Why?A: Three likely answers. (1) They didn’t run --cov because they excluded it locally; CI runs it as part of addopts → coverage < threshold fails. (2) They wrote @pytest.mark.slow without declaring slow in markers = [...]; --strict-markers fails. (3) They used a Python version > target-version = ['py311']. Standard fix: provide a Makefile / tox.ini target that mirrors CI: make ci → install + lint + type-check + test in one shot.
A: Investigate before celebrating. (1) Run diff-cover against the PR branch — which LINES dropped? If they’re in untested admin routes you don’t care about, it’s not a real regression. (2) Coverage-percent alone misleads: removing 50 LOC of easily-covered code can drop % faster than adding complex logic. Switch to coverage report --fail-under=80 --skip-covered. (3) Add tests proportional to the new feature’s risk surface, not its LOC.
pyproject.toml)pyproject.toml is the modern single source of truth for Python project metadata, dependencies, and tool configuration. It replaces setup.py, setup.cfg, and per-tool config files.
[build-system][build-system]
requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
Tells pip install . which build system to use. setuptools is the default.
[project][project]
name = "fca-multi-agent-support"
version = "0.1.0"
description = "FCA-compliant multi-agent AI support system for UK financial services"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [{name = "David Sandeep", email = "davidsandeep1996@gmail.com"}]
classifiers = [
"Programming Language :: Python :: 3.11",
"Framework :: FastAPI",
]
PEP 621 metadata — what’s published to PyPI if you ever pip install fca-multi-agent-support. The requires-python = ">=3.11" makes pip reject incompatible Python versions early.
[tool.black][tool.black]
line-length = 100
target-version = ['py311']
extend-exclude = '''/(...)/'''
Black is the formatter. line-length = 100 (a touch wider than Black’s default 88 for readability). target-version = ['py311'] lets Black use Python 3.11 syntax.
[tool.isort][tool.isort]
profile = "black"
line_length = 100
multi_line_output = 3
include_trailing_comma = true
profile = "black" makes isort agree with Black’s style — no more fights between the two formatters. multi_line_output = 3 (vertical hanging indent) is the most readable.
[tool.pytest.ini_options][tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = """
-v --strict-markers --strict-config
--cov=app --cov-report=term-missing --cov-report=html
"""
markers = [
"asyncio: marks tests as async",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
--strict-markers is critical — typos like @pytest.mark.intgeration fail instead of running silently. --cov=app measures coverage of the app directory.
[tool.coverage.run] and [tool.coverage.report][tool.coverage.run]
source = ["app"]
omit = ["*/tests/*", "*/migrations/*", "*/__init__.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
source = ["app"] — only measure packages within app/.exclude_lines — tells coverage to skip abstract methods, debug __main__ blocks, type-checking imports.[tool.mypy][tool.mypy]
python_version = "3.11"
warn_return_any = true
disallow_incomplete_defs = false
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any — flags any function returning Any (workaround for libraries without type hints).no_implicit_optional — explicit x: int | None style preferred.[[tool.mypy.overrides]] module = ["tests.*"] — loosens type-checking for tests.Forgetting markers = [...] with new marker names — pytest rejects unknown markers when --strict-markers is on.
Conflicting Black and isort configs — both want to format imports. profile = "black" is the harmoniser.
Setting disallow_untyped_defs = true on FastAPI starts an uprising because FastAPI uses Pydantic internals without type hints. The team’s compromise is disallow_untyped_defs = false; check_untyped_defs = true.
pyproject.toml over multiple config files?A: One place to look. With per-tool config files you’d have setup.cfg, .black, .isort.cfg, pytest.ini, .mypy.ini, .flake8 — 6+ files of config. pyproject.toml consolidates into a single TOML that’s PEP-standard.
setuptools-scm[toml]?A: Auto-generates the version from the git tag. git tag v1.2.3 → project reports 1.2.3 without manual editing.
python = ">=3.11" specifically?A: Project uses StrEnum, tomllib, and Self — features that need 3.11+. Without the constraint, pip installs on 3.9 fail at first import.
black --check.line-length = 88 in pyproject.toml but black 24.x default is 88, not 100.black==24.x with line-length = 100.None | str regression in production.disallow_untyped_defs = false in mypy config.omit = ["*/__init__.py"] covers boilerplate; data files in data/ were accidentally included.