• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aio-libs / multidict / 35851280448
87%

Build:
DEFAULT BRANCH: master
Ran 23 Sep 2026 10:53AM UTC
Jobs 1
Files 42
Run time 1min
Badge
Embed ▾
README BADGES
x

If you need to use a raster PNG badge, change the '.svg' to '.png' in the link

Markdown

Textile

RDoc

HTML

Rst

23 Sep 2026 10:52AM UTC coverage: 87.437% (+0.06%) from 87.376%
35851280448

push

github

web-flow
Compute the ASCII identity of a CIMultiDict key directly (#1527)

<!-- Thank you for your contribution! -->

## What do these changes do?

`_ci_key_to_identity()` called `str.lower()` on every plain `str` key,
so a
`CIMultiDict` paid an attribute lookup, a vectorcall, a string
allocation and
a from-scratch hash on every single lookup, insert and delete. For an
ASCII
string CPython's `str.lower()` is `PyUnicode_New(len, 127)` filled by
`Py_TOLOWER()` per byte, and it has no shortcut for a string that is
already
lowercase, so even `d["content-type"]` allocated and rehashed a copy.

An exact `str` that is ASCII is now handled directly. The key is scanned
for an
uppercase byte, eight at a time, and if it has none it already is its
own
identity, so it is reused as is: no copy, and `_unicode_hash()` finds
the key's
cached `ob_shash` instead of hashing a fresh string. A key that does
have
uppercase is lowered straight into a new string, still without the
method call.

The gate is deliberately `PyUnicode_CheckExact`. A `str` subclass may
override
`lower()`, and the identity of a subclass instance has to keep coming
from the
override; `tests/test_mutable_multidict.py` and
`tests/test_multidict.py`
already depend on that, and there is a new test that pins it. `istr`,
`str`
subclasses and non-ASCII keys all take the path they took before.

Everything below the `istr` check now sits behind a `noinline` call, and
`compiler.h` gains a `NOINLINE` macro alongside `COLD` and
`ALWAYS_INLINE` for
it. This is load-bearing, not cosmetic. `md_calc_identity()` is inlined
into
every `md_*()` that takes a key, so it carries `_ci_key_to_identity()`
with it,
and growing that function pushed `md_contains()` and `md_next()` past
the
inliner's budget at their own call sites. The cost landed on code the
fast path
cannot help: `keys().isdisjoint()` took 31% more instructions on a plain
`MultiDict`, whose keys never reach the case-insensitive branch at all.
Behind
the call, `_ci_key_... (continued)

765 of 1530 branches covered (50.0%)

Branch coverage included in aggregate %.

65 of 68 new or added lines in 1 file covered. (95.59%)

7218 of 7600 relevant lines covered (94.97%)

1.9 hits per line

Uncovered Changes

Lines Coverage ∆ File
3
93.26
0.08% tests/test_multidict.py
Jobs
ID Job ID Ran Files Coverage
1 MyPy - 35851280448.1 23 Sep 2026 10:53AM UTC 84
87.44
GitHub Action Run
Source Files on build 35851280448
  • Tree
  • List 42
  • Changed 3
  • Source Changed 1
  • Coverage Changed 3
Coverage ∆ File Lines Relevant Covered Missed Hits/Line Branch Hits Branch Misses
  • Back to Repo
  • Github Actions Build #35851280448
  • e50bed4e on github
  • Prev Build on master (#35846507942)
  • Next Build on master (#35853118928)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc