forked from ChelseaKR/nearmiss
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
253 lines (237 loc) · 11.3 KB
/
Copy pathpyproject.toml
File metadata and controls
253 lines (237 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# nearmiss — project metadata and tooling configuration.
# Single source of truth for build, dependencies, and the lint/type/test gates.
[build-system]
requires = ["hatchling>=1.31.0"]
build-backend = "hatchling.build"
[project]
name = "nearmiss-safety"
version = "0.4.0"
description = "An open dataset and statistically honest analysis of road hazards and near misses for safe-streets advocacy."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
authors = [{ name = "Chelsea Kelly-Reif" }]
keywords = [
"road safety",
"near miss",
"cycling",
"pedestrian safety",
"open data",
"spatial statistics",
"getis-ord",
"exposure normalization",
"advocacy",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: GIS",
"Typing :: Typed",
]
# Core runtime dependency is intentionally minimal: only a JSON Schema validator.
# The geometry and statistics are pure, typed standard-library Python, so the
# analysis runs anywhere Python 3.11+ runs, with no native/geospatial build step.
# Versions are pinned and hashed in the generated lock file used for reproducible runs.
dependencies = [
"jsonschema>=4.26.0",
]
[project.optional-dependencies]
dev = [
"pytest>=9.1.1",
"pytest-cov>=7.1.0",
"ruff>=0.16.2",
"mypy>=2.3.0",
"pip-audit>=2.10.1",
# Property/metamorphic testing for the spatial-statistics core (FIX-14). A
# dev-only, audited dependency surface: it lives in `dev`, so the blocking
# `pip-audit --strict` security gate in `make verify` scans it like every
# other dev tool. Pure Python, no native build step.
"hypothesis>=6.165.2",
# i18n message-catalog tooling (INTERNATIONALIZATION-STANDARD §3/§4). Babel
# provides `pybabel extract` and the CLDR-backed `Locale.parse` used by the
# i18n gates. Pure Python (no PyICU/native ICU build) so the "runs anywhere
# Python runs, no native build step" property is preserved; `msgfmt` (G7) is
# a system gettext binary, like gitleaks, not a pip dependency.
"babel>=2.18.0",
# pip-audit pulls in CacheControl -> msgpack transitively; pin the fixed
# version so the blocking pip-audit gate cannot resolve to msgpack 1.2.0
# (GHSA-6v7p-g79w-8964, fixed in 1.2.1). Keeps the SCA gate deterministically
# green instead of relying on the resolver to pick the newest msgpack.
"msgpack>=1.2.1",
# pip-audit pulls in pip-api -> pip transitively; pin the fixed version so the
# blocking pip-audit gate cannot resolve to a vulnerable pip (PYSEC-2026-3721,
# fixed in 26.2). Same defect class and same fix as the msgpack pin above.
"pip>=26.2",
"pre-commit>=4.6.1",
# tools/check_lock_drift.py (CQ-09-equivalent gate for requirements-dev.lock,
# issue #189) parses version specifiers with this rather than hand-rolling
# PEP 440 comparison.
"packaging>=24.0",
]
# Advisory mutation testing (backlog #15). Kept in its OWN optional group, NOT in
# `dev`, so the `make verify` merge gate — and in particular the blocking
# `pip-audit --strict` security scan — audits exactly the same dependency surface
# it does today. mutmut pulls in a large transitive tree (libcst, textual, ...);
# isolating it here means a future CVE in one of those tools can never break the
# merge gate. Mutation testing is advisory and non-blocking (see `make mutation`).
# Install on demand: pip install -e ".[mutation]"
mutation = [
"mutmut>=3.7.0",
]
# The PEP 517 build front-end used ONLY by the tag-triggered release workflow
# (`.github/workflows/release.yml`, `make release-build`) to build the sdist +
# wheel via hatchling. Kept in its OWN optional group for the same reason as
# `mutation` above: it never touches the `make verify` / pip-audit-scanned
# surface that runs on every PR. Install on demand: pip install -e ".[release]"
release = [
"build>=1.5.1",
]
# EXP-12 "How to lie with heat maps" teaching module (docs/teaching/). Kept in
# its OWN optional group, NOT in `dev`, for the same reason as `mutation`: the
# `make verify` merge gate — and the blocking `pip-audit --strict` scan — audits
# exactly the dependency surface it does today. The Jupyter execution stack
# (nbconvert + a kernel) pulls a large transitive tree; isolating it here means a
# CVE in that tooling can never break the merge gate. The teaching notebooks
# themselves import ONLY `nearmiss.*` + `IPython.display` (no new analysis
# dependency — figures are hand-built SVG, exactly like src/nearmiss/figures.py).
# `make teach` installs this group on demand; a dedicated CI job runs it.
teaching = [
"nbconvert>=7.17.1",
"nbclient>=0.11.0",
"ipykernel>=7.3.0",
]
[project.urls]
Homepage = "https://nearmiss.chelseakr.com"
"Conflict Atlas" = "https://nearmiss.chelseakr.com/fars/national/"
Repository = "https://github.com/ChelseaKR/nearmiss"
Documentation = "https://github.com/ChelseaKR/nearmiss/tree/main/docs"
Changelog = "https://github.com/ChelseaKR/nearmiss/blob/main/CHANGELOG.md"
Issues = "https://github.com/ChelseaKR/nearmiss/issues"
Methodology = "https://github.com/ChelseaKR/nearmiss/blob/main/docs/METHODOLOGY.md"
[project.scripts]
nearmiss = "nearmiss.__main__:main"
[tool.hatch.build.targets.wheel]
# "src/honest_rates" (roadmap item EXP-08) is the standalone, dependency-free
# statistics core — no nearmiss import anywhere in it — that nearmiss itself
# consumes; see src/honest_rates/README.md.
packages = ["src/nearmiss", "src/honest_rates"]
[tool.hatch.build.targets.wheel.force-include]
"schema" = "nearmiss/schema"
# The JSON schemas are read at runtime (validation.find_report_schema), not just
# referenced by docs. They live at the repository root, outside src/, so without
# this the wheel ships code that cannot find its own contract: every release
# through 0.3.0 installed fine and then raised on the first report validation.
# ---------------------------------------------------------------------------
# Lint — ruff (style + a broad rule set). Part of `make verify`.
# ---------------------------------------------------------------------------
[tool.ruff]
target-version = "py311"
line-length = 100
src = ["src", "tests"]
# Notebooks are an exploratory space; don't hold them to import-order/format rules.
extend-exclude = ["notebooks"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "PTH", "RUF", "C90"]
# This project's user-facing strings intentionally use Unicode typography
# (en/em dashes, ★, ⚠) — so the "ambiguous character" rules are noise here.
ignore = ["RUF001", "RUF002", "RUF003"]
[tool.ruff.lint.mccabe]
max-complexity = 10
# ---------------------------------------------------------------------------
# Types — mypy strict. A typed, analyzable codebase. Part of `make verify`.
# ---------------------------------------------------------------------------
[tool.mypy]
python_version = "3.11"
strict = true
warn_unreachable = true
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
no_implicit_reexport = true
files = ["src", "tests"]
[[tool.mypy.overrides]]
# jsonschema ships limited inline types; treat its imports as untyped here.
module = ["jsonschema", "jsonschema.*"]
ignore_missing_imports = true
# ---------------------------------------------------------------------------
# Tests — pytest. Deterministic, against known-answer fixtures.
# ---------------------------------------------------------------------------
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = "-ra --strict-markers --strict-config --import-mode=importlib"
xfail_strict = true
markers = [
# Seeded Monte-Carlo interval-coverage simulation (METHODOLOGY §9.2). It runs
# thousands of Poisson draws, so it is markered `slow` and can be deselected
# with `-m 'not slow'` for a fast inner loop; the full gate still runs it.
"slow: marks a test as slow (deselect with '-m \"not slow\"')",
]
[tool.coverage.report]
# Merge-blocking branch-coverage floor (QUALITY-AND-METRICS). `make test`
# already passes the same floor explicitly on the command line; declaring it
# here makes the 90% floor hold for any coverage run that omits the flag.
# Measured 90.44% total (branch) on 2026-07-16, 1694 tests passing.
fail_under = 90
# ---------------------------------------------------------------------------
# Mutation testing — mutmut. ADVISORY ONLY (backlog #15), never a merge gate.
#
# Scoped deliberately to the correctness-critical spatial-statistics core, where
# a silent numerical bug (an off-by-one in neighbor weighting, a dropped sign,
# a `<=`/`<` slip in the distance band) would quietly corrupt a hotspot result
# without failing any coarse test:
# * honest_rates/hotspot.py — the Getis-Ord Gi* local hotspot z-score + FDR
# * honest_rates/rates.py — the Poisson (Byar) and Wilson confidence
# intervals, plus the quasi-Poisson dispersion
# (`pearson_dispersion`) and widening
# (`quasi_poisson_ci`) the exposure-normalized
# rate runs on
# * nearmiss/stats/maup.py — the MAUP rank-stability re-segmentation (RR-05): a
# dropped sign or a `<`/`<=` slip in the coarse-unit
# ranking or the survives/overlap logic would
# silently misreport robustness
# * nearmiss/network.py — the street-network adjacency graph + bounded
# Dijkstra that decide Gi*'s neighbor map (FIX-02)
#
# As of roadmap item EXP-08, the numerical rate/hotspot logic was extracted to
# the standalone src/honest_rates/ library; nearmiss's own stats/getis_ord.py
# and stats/rates.py are thin re-export shims with nothing left to mutate, so
# those mutated paths point at the extracted library. The MAUP and network
# modules remain nearmiss-domain code and are mutated in place.
#
# It reuses the existing pytest suite (the stats tests) as the kill oracle, so
# no separate mutation-only test harness is maintained. Run it with
# `make mutation`; see docs/MUTATION-TESTING.md for the baseline and workflow.
# ---------------------------------------------------------------------------
[tool.mutmut]
# Copy the whole package so imports resolve, but mutate ONLY the files below.
source_paths = ["src"]
only_mutate = [
"src/honest_rates/hotspot.py",
"src/honest_rates/rates.py",
"src/nearmiss/stats/maup.py",
"src/nearmiss/network.py",
]
# Kill oracle: the existing stats tests. test_hotspot.py exercises the Gi*
# computation end to end via the known-answer fixtures; test_fdr.py and
# test_rates.py cover the FDR and CI helpers directly; test_network.py covers
# the adjacency graph and bounded Dijkstra directly (including the barrier
# fixture). Scoping the selection keeps each mutant fast to check (advisory,
# bounded).
pytest_add_cli_args_test_selection = [
"tests/test_hotspot.py",
"tests/test_fdr.py",
"tests/test_rates.py",
"tests/test_stats_numerics.py",
"tests/test_maup.py",
"tests/test_overdispersion.py",
"tests/test_network.py",
"tests/test_getis_ord_differential.py",
]
# The bundle fixture loads config/davis-demo.toml (which points at the fixtures
# under tests/, already copied). mutmut runs from a mutants/ working copy, so the
# config dir must be copied in alongside it.
also_copy = ["config"]