Bug report
gettext.c2py() compiles a C plural-form expression into an equivalent Python function. In C the unary ! operator binds tighter than every binary operator, so !n + 1 means (!n) + 1. But c2py emits a bare not prefix (Lib/gettext.py, in _parse) and then appends the binary operators onto the whole string. Because Python's not binds looser than arithmetic/comparison operators, !n + 1 is compiled to not n + 1, i.e. not (n + 1) — a different expression.
Reproduction
import gettext
gettext.c2py('!n + 1')(0) # -> 0 ; C says (!0)+1 == 2
gettext.c2py('!n + 1')(1) # -> 0 ; C says (!1)+1 == 1
gettext.c2py('!n < 3')(0) # -> 0 ; C: (!0) < 3 == 1 (!n is 0/1, always < 3)
gettext.c2py('!n*2')(0) # -> 1 ; C: (!0)*2 == 2
The generated source for '!n + 1' is int(not n + 1) (i.e. int(not (n + 1))) instead of int((not n) + 1).
Only two forms happen to work today: a bare !x with nothing after it (which is what test_negation covers, e.g. !!!n), and !(...) with no trailing binary operator.
Impact
c2py is public API and is also used by GNUTranslations._parse to compile the plural= rule from a catalog's Plural-Forms header, so a valid .mo whose plural rule applies ! before a binary operator selects the wrong plural form from ngettext().
Why it's not caught
test_gettext.test_negation only exercises !!!n (no following binary operator), which is exactly the case that works, so the precedence bug is not pinned by any test. The neighbouring cases (chained comparisons, /→//) are handled deliberately; unary ! was overlooked.
Fix
Negate the operand as a self-contained parenthesised unit ((not <operand>)) before the binary-operator loop, so !n + 1 compiles to (not n) + 1. Double negation still normalises to 0/1 (!!n → (not (not n))). I have a small patch + tests and will open a PR referencing this issue.
Reproduced on main (3.16.0a0). Found with AI assistance; I've verified and understand the behaviour and the fix.
Linked PRs
Bug report
gettext.c2py()compiles a C plural-form expression into an equivalent Python function. In C the unary!operator binds tighter than every binary operator, so!n + 1means(!n) + 1. Butc2pyemits a barenotprefix (Lib/gettext.py, in_parse) and then appends the binary operators onto the whole string. Because Python'snotbinds looser than arithmetic/comparison operators,!n + 1is compiled tonot n + 1, i.e.not (n + 1)— a different expression.Reproduction
The generated source for
'!n + 1'isint(not n + 1)(i.e.int(not (n + 1))) instead ofint((not n) + 1).Only two forms happen to work today: a bare
!xwith nothing after it (which is whattest_negationcovers, e.g.!!!n), and!(...)with no trailing binary operator.Impact
c2pyis public API and is also used byGNUTranslations._parseto compile theplural=rule from a catalog'sPlural-Formsheader, so a valid.mowhose plural rule applies!before a binary operator selects the wrong plural form fromngettext().Why it's not caught
test_gettext.test_negationonly exercises!!!n(no following binary operator), which is exactly the case that works, so the precedence bug is not pinned by any test. The neighbouring cases (chained comparisons,/→//) are handled deliberately; unary!was overlooked.Fix
Negate the operand as a self-contained parenthesised unit (
(not <operand>)) before the binary-operator loop, so!n + 1compiles to(not n) + 1. Double negation still normalises to 0/1 (!!n→(not (not n))). I have a small patch + tests and will open a PR referencing this issue.Reproduced on
main(3.16.0a0). Found with AI assistance; I've verified and understand the behaviour and the fix.Linked PRs
!before a binary operator #157453