Quadratic comment split on a requirements file line (pip_requirements_parser.py:1058)
@Anurag-S1ngh ci sta già lavorando.
Dal 24/10/2026.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 72/100
Direzione di ricerca
Il pattern lento è COMMENT_RE in src/pip_requirements_parser.py vicino alla riga 1058, usato dal ciclo di suddivisione dei commenti vicino alla riga 1569. Leggi perché il gruppo di spazi bianchi viene scartato lì, e riproduci i tempi con una lunga riga di spazi bianchi tramite RequirementsFile.from_file(). Il lavoro è completato quando l'output della suddivisione per gli input esistenti resta invariato, una riga di spazi bianchi da 64 KB viene analizzata in millisecondi e la suite di test esistente non mostra nuovi errori.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Parsing a requirements file takes time quadratic in the length of any line that is mostly whitespace. Measured on 32.0.1 through RequirementsFile.from_file(), with one whitespace line in an otherwise ordinary three-line file:
| whitespace line | file size | before | after |
|---|---|---|---|
| 4 KB | 4,027 B | 0.1165 s | 0.0007 s |
| 16 KB | 16,027 B | 1.4871 s | 0.0011 s |
| 64 KB | 64,027 B | 20.5483 s | 0.0030 s |
| 128 KB | 128,027 B | - | 0.0057 s |
Two controls, both measured, and together they show the cost is the shape and not the size:
- 4,000 ordinary pinned lines with trailing comments - a 149,780-byte file, more than twice the size - costs 0.7479 s, against 20.5483 s for the 64,027-byte hostile one.
- The same 64 KB of whitespace followed by a
#costs 0.0008 s. The cost needs the match to fail.
Who this matters to. The package exists to parse requirements files without installing anything - the README stresses that it works offline - which is the SBOM and software-composition-analysis use: reading requirements files out of third-party or unknown codebases. Dependents include CycloneDX/cyclonedx-python and BentoML. So the payload is one long whitespace line in an otherwise ordinary file, and a small commit to a scanned repository costs minutes of scanner CPU. (The same shape exists in pip itself, at src/pip/_internal/req/req_file.py:41, still with \s+ on current master - but pip install -r on an untrusted requirements file already runs arbitrary code, so there is nothing to protect there. Mentioned only so you know it is not a fork-only divergence.)
Cause. src/pip_requirements_parser.py:1058:
COMMENT_RE = re.compile(r"(^|\s+)(#.*)$")
applied at :1569, once per line:
parts = [l.strip() for l in COMMENT_RE.split(line) if l.strip()]
split tries every offset, and at each one \s+ consumes the rest of the whitespace run before failing to find the #. O(n) work at O(n) start positions. The two COMMENT_RE.match() uses in join_lines() (:1539, :1540) are anchored and stay linear - measured at 2.5 ms on a 256 KB line - so only the split is affected.
Fix - one character, and no cap. The caller strips every part and drops the empty ones, so whatever the whitespace group captured is discarded. A single whitespace character therefore produces the same list as a run, and makes each start position cost O(1):
COMMENT_RE = re.compile(r"(^|\s+)(#.*)$")
+
+COMMENT_SPLIT_RE = re.compile(r"(^|\s)(#.*)$")
...
- parts = [l.strip() for l in COMMENT_RE.split(line) if l.strip()]
+ parts = [l.strip() for l in COMMENT_SPLIT_RE.split(line) if l.strip()]
COMMENT_RE itself is left alone, because the two match() uses in join_lines() do rely on the run: with a single \s they would stop recognising " # comment" as a comment line.
This has no divergence class. Over 311,161 inputs - every string of length 0 to 5 over # space tab \x0b \x0c \r a = . -, 200,000 random strings over the same alphabet, hand-written real requirement lines, and whitespace runs either side of 200 characters - the list the caller builds is identical in every case. Zero divergences, no cap to justify.
Two alternatives were tested and should not be used.
- Bounding the run (
(^|\s{1,200})) is also exactly equivalent on those inputs, but it is about 90 times slower than the single-character fix - 0.1253 s against 0.0014 s on a 64 KB run - and it introduces a cap that has to be justified. There is no reason to take that trade. - Narrowing the class (
(^|[ \t]{1,200})) changes behaviour and must not be used.\scovers\x0b,\x0cand\r, and[ \t]does not, so on the same 311,161 inputs it differs on 21,427 - every one of them because of those characters. The failure is not cosmetic:"a\x0b#b"currently splits into['a', '#b']and under that pattern stays['a\x0b#b'], so the comment is no longer separated and the comment text is carried into the requirement line.
Verification.
- Differential as described above, compared at the caller's output rather than at the regex.
- Project test suite on both trees: 8,246 passed, 20 failed, 5 skipped, 1 xfailed, the 20 failures identical by name. They are pre-existing error-message comparisons - the vendored
packagingin this environment words "Invalid requirement" differently from the checked-in expectations - and they fail on unmodified 32.0.1. - Control test: ten real lines split identically and a three-line file is parsed to the right requirement names, plus a timing assertion on a 16 KB whitespace line. On 32.0.1 the splitting assertions pass and the timing assertion fails at 1.3453 s; patched, both pass in 0.002 s.
patch -p1verified against the 32.0.1 sdist.
For context rather than as prior art: pypa/pip #3662 (2016, closed 2019) discusses this expression, but as a Jython maximum recursion depth exceeded error with no cost figure, and pip declined to change it on the grounds that "keeping existing behaviour is more important". Nobody in that thread identified the CPython behaviour, and the expression is unchanged in pip today.
I am happy to open a pull request with the patch if that is easier.
Found with AI assistance (Claude) and verified by hand.
Patch: full patch
--- a/src/pip_requirements_parser.py
+++ b/src/pip_requirements_parser.py
@@ -1057,6 +1057,15 @@
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
COMMENT_RE = re.compile(r"(^|\s+)(#.*)$")
+# Used only by split_comments(), which strips every part and drops the empty
+# ones, so what the whitespace group captures is discarded. A single whitespace
+# character therefore gives the same result as a run, and makes each start
+# position cost O(1): with "\s+" every offset in a run of whitespace starts an
+# attempt that consumes the rest of the line before failing to find the "#", so
+# splitting a line is quadratic in its length. COMMENT_RE itself is unchanged,
+# because the two match() uses in join_lines() rely on the run.
+COMMENT_SPLIT_RE = re.compile(r"(^|\s)(#.*)$")
+
SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
index_url,
extra_index_url,
@@ -1566,7 +1575,7 @@
Yield TextLine or Commentline
"""
for line_number, line in lines_enum:
- parts = [l.strip() for l in COMMENT_RE.split(line) if l.strip()]
+ parts = [l.strip() for l in COMMENT_SPLIT_RE.split(line) if l.strip()]
if len(parts) == 1:
part = parts[0]
- Lingua principale
- Python
- Stelle
- 20
- Fork
- 16
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di aboutcode-org/pip-requirements-parser
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 60/100
-
Tests failures with packaging 26.0Forse già presa @s-t-e-v-e-n-k l’ha presa 271 giorni fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 45/100
-
Doesn't support local paths to wheelsForse già presa @Anurag-S1ngh l’ha presa 1 giorno fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 48/100
aboutcode-org/pip-requirements-parser#26 · 2 commenti ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 48/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 45/100
Tutte le issue di aboutcode-org/pip-requirements-parser
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
NousResearch/hermes-agent#147104 ·
I maintainer di solito rispondono entro 1 giorno
-
[oblt-aw][autodoc] Document new required `shared-org-key` input for `obs-aw-dependency-review`Forse già presa @github-actions l’ha presa oggi. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
I maintainer di solito rispondono entro 1 giorno
-
bot-triaged module: inductor oncall: pt2 topic: fuzzer triaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
getchaintips leaves out the active tip once a header is staged on it, where Core always lists itAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
btclib-org/btclib-node#2158 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
patrick-hudson/put.gd-desktop#979 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno