Skip to content

1.4.0 Many bugfixes#27

Merged
BarnabasG merged 5 commits into
mainfrom
fix/audit-preexisting-issues
Jul 8, 2026
Merged

1.4.0 Many bugfixes#27
BarnabasG merged 5 commits into
mainfrom
fix/audit-preexisting-issues

Conversation

@BarnabasG

Copy link
Copy Markdown
Owner

Many bugfixes and refactors

BarnabasG added 5 commits July 8, 2026 21:01
Framework adapters:
- Flask: record calls with query strings (previously dropped); record the
  actual matched rule for multi-decorated views (return_rule=True); record
  followed trailing-slash redirects; exclude static routes by endpoint name
  (covers custom static_url_path and blueprint statics)
- FastAPI: discover plain Starlette routes and mounted Starlette sub-apps
  (docs routes stay excluded); record post-redirect path so slash-redirected
  calls count against the real route
- Django: convert re_path regexes to matchable <name> templates (previously
  never matched); count only implemented handler methods for CBVs (was
  inflating every view to 6 methods incl. TRACE)
- Detection: isinstance-based for Flask/FastAPI so app subclasses work;
  Django fallback no longer substring-matches unrelated modules

Plugin / xdist:
- Register DeferXdistPlugin only when --api-cov-report is active (was
  registered on every xdist run and crashed on any worker crash)
- Tolerate crashed workers with no workeroutput
- Merge discovered endpoints from all workers (was first-worker-wins)
- CoverageWrapper: delegate context-manager protocol and repr (with-blocks
  on wrapped clients no longer TypeError)

Report / config:
- Path parameters match a single segment ([^/]+) so nested-path calls no
  longer mark parent routes covered; path converters still match greedily
- fail_under now fails when discovery finds nothing, and is vacuous (not a
  spurious failure) when every endpoint is excluded
- Implement the documented group-methods-by-endpoint option (was a no-op)
- --api-cov-fail-under=0 no longer discarded as falsy
- parse_openapi_spec tolerates empty/non-mapping specs instead of crashing
  fixture setup; cli show-conftest no longer mangles paths containing .py

Packaging / CI / docs:
- pytest>=7.0 (pytest.Parser/Config are imported at collection time and do
  not exist before 7.0); add classifiers, keywords, py.typed
- publish workflow: tag only after successful publish, run non-mutating
  make pipeline-ci (auto-fixing lint could publish code matching no commit),
  pass token via UV_PUBLISH_TOKEN instead of a temp file
- CI: re-enable format/lint as non-mutating checks
- README: JSON example uses real method-prefixed keys; PUBLISH.md matches
  the actual Makefile targets and workflow order
Each fix was validated with a dedicated repro check run before (failing)
and after (passing) the change; all ten now pass.

Django route templates (frameworks.py):
- Regex shorthand (\d, \w), bare character classes and bare dots outside
  groups become placeholders instead of garbage literals; quantifiers are
  consumed; a trailing '/?' is dropped
- Groups whose body can span '/' (bare dot, '/', \S) get a <path:...>
  converter so multi-segment values like (?P<rest>.*) still match after
  the single-segment matching change
- Dispatch-only CBVs (no verb handlers) keep the default method set
  instead of vanishing from discovery

Recording and discovery:
- FastAPI: the requested path is always credited (a redirecting route
  keeps its own coverage) and a followed redirect also credits the final
  route
- Flask: static-rule exclusion additionally requires the rule to end in
  /<path:filename>, so user routes named '*.static' are kept
- Django detection: isinstance check against BaseHandler so handler
  subclasses in user modules are detected; optional-framework classes are
  resolved via sys.modules with failed imports memoised (no repeated
  finder scans, and robust to pytester's sys.modules snapshotting)

Gate semantics (report.py):
- All-endpoints-excluded now fails closed when fail_under > 0 (an
  over-broad exclusion pattern can no longer silently disable the gate)
  and passes when fail_under == 0
- Empty discovery only hard-fails for fail_under > 0; an explicit 0 is
  trivially satisfied
- Method-scoped exclusion patterns are applied before method grouping so
  they keep working under --api-cov-group-methods-by-endpoint

Tests: 12 new regression tests covering every finding; updated the two
tests whose expectations changed (all-excluded gate, redirect recording).
plugin.py:
- Extract _tracked_client_flow: create_coverage_fixture and coverage_client
  shared ~45 duplicated lines of client-resolution flow that had already
  drifted; both now delegate to one generator (coverage_client additionally
  gains create_coverage_fixture's catch around tracked-client construction)
- Share DEFAULT_CLIENT_FIXTURE_NAMES with config.py instead of a second
  hardcoded tuple; collapse the doubled --api-cov-report getoption in
  pytest_configure; 'except (FixtureLookupError, Exception)' -> Exception;
  bare partition() replaces three 'if "?" in url' conditionals; xdist
  endpoint merge uses dict.fromkeys for ordered dedup
- Parse the OpenAPI spec once per session: a spec yielding zero endpoints
  was re-read and re-parsed on every test (new
  SessionData.openapi_discovery_attempted flag)

report.py:
- Split categorise_endpoints into _partition_excluded + _match_covered
  (categorise_endpoints stays as their composition), collapsing the
  quadruplicated exclusion/negation match loops into one _matches_any
  helper and turning generate's grouping branch into a linear pipeline
  with no sentinel arguments
- _split_endpoint helper replaces four inline METHOD/path split idioms;
  group_endpoints_by_path uses dict.fromkeys; _compile_exclusion_patterns
  returns tuples instead of Optionals; footer now derives from the header
  string (was 2 chars wider via magic arithmetic); endpoint_to_regex cache
  unbounded so >512-endpoint sessions stop thrashing

frameworks.py:
- Merge the APIRoute and plain-Route discovery branches; _SKIPPED_METHODS
  constant replaces four ('HEAD', 'OPTIONS') literals
- Django template scanner: quantifier matching via one compiled regex,
  _skip_class helper replaces duplicated character-class handling, and an
  itertools.count param namer removes the param_count tuple-threading
- Bind the recorder once after the None guard in all three tracking
  clients, deleting dead 'if recorder is not None' checks and a type-ignore
- Inline single-caller _detect_by_isinstance into _detect_framework; drop
  the duplicate None check in is_supported_framework

openapi.py: logger.exception implies exc_info; drop the redundant argument.
Replaces the hand-rolled character scanner in _django_route_to_template
(and its _consume_quantifier/_skip_class/_group_placeholder helpers) with
a walk of the stdlib regex parse tree (re._parser on 3.11+, sre_parse on
3.10). Every rule now reads off a node type instead of character
arithmetic, and multi-segment detection inspects what the pattern can
actually match rather than grepping its source text.

Behavioral deltas, all covered by new tests:
- (?P<a>[^/]+) is now correctly single-segment (the old text heuristic saw
  the '/' inside the negated class and emitted <path:a>)
- top-level alternation (legacy|new) degrades to a matchable placeholder
  instead of passing through as an unmatchable literal
- bounded repeats (a{2,4}) become placeholders instead of leaking regex
  syntax into the template
- unparseable input passes through verbatim, so a future change to the
  private parser API degrades to unconverted routes rather than crashing
  (discovery already guards adapter failures)

Known trade-off, accepted deliberately: re._parser is a private stdlib
API. It has been stable for two decades, a large ecosystem (hypothesis,
sre_yield, exrex) walks the same tree, CI covers 3.10-3.14, and the
verbatim fallback bounds the failure mode. Verified on 3.12 and on a
fresh 3.10 environment (sre_parse branch).
@BarnabasG BarnabasG merged commit 876e431 into main Jul 8, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant