Skip to content

gh-155499: Fix curses border() and box() rejecting 0 mixed with str/bytes - #155521

Open
Lohitha0-0 wants to merge 2 commits into
python:mainfrom
Lohitha0-0:fix-border-box-zero-sentinel
Open

gh-155499: Fix curses border() and box() rejecting 0 mixed with str/bytes#155521
Lohitha0-0 wants to merge 2 commits into
python:mainfrom
Lohitha0-0:fix-border-box-zero-sentinel

Conversation

@Lohitha0-0

@Lohitha0-0 Lohitha0-0 commented Aug 10, 2026

Copy link
Copy Markdown

Fixes #155499

border() and box() were raising TypeError when you mix a one-character str/bytes arg with a plain 0, even though 0 is documented as meaning "use the default character" for that spot. This broke in GH-151758, which added a check to stop mixing wide-char types with int/bytes types, but didn't account for 0 being a valid sentinel value regardless of type.

For border(), the fix is small — just treat an int argument as fine when its value is 0, in addition to the existing wide-char case.

box() needed a bit more work. It was checking verch and horch together (t1 != 2 || t2 != 2), so it would already reject the call before it even got to check whether the individual value was 0. I restructured it to resolve each side's cchar_t pointer on its own, so 0 is accepted per-side instead of requiring both sides to match types.

Added a test (test_border_box_zero_sentinel) covering both functions with 0 mixed in, and also checking that a real type mismatch (like 1 mixed with a str) still correctly raises TypeError.

… str/bytes

border() and box() raise TypeError when a wide-character (ncursesw)
build receives a one-character str/bytes argument alongside an int 0,
even though 0 is documented as the sentinel meaning "use the default
character" (pythonGH-151758 introduced the stricter type-mixing check but
didn't account for this documented case).

border() now treats an int argument as acceptable when it is 0.
box() is restructured to select each side's cchar_t pointer
independently, since it previously required both verch and horch to
share the same type.

Adds Lib/test/test_curses.py coverage for both functions.
Comment thread Modules/_cursesmodule.c Outdated
else if (types[i] == 2) {
wch_p[i] = &wch[i];
}
else if (types[i] == 1 && ch[i] == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can merge that check with the check at L2708

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, merged both branches into a single objs[i] == NULL || (types[i] == 1 && ch[i] == 0) condition.

Comment thread Modules/_cursesmodule.c Outdated
return NULL;
const cchar_t *wch1_p, *wch2_p;

if (t1 == 2) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that we could have a claener if-else if-else and keep an if (cond) { raise } and later wch1p = c2 ? NULL : &wch1; and same for wch2p (namely we can simplify the checks and diff). Unfortunately it's too late now for me to think in terms of boolean arithmetic so I'll let you figure it out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reworked it to a single upfront check (t1_ok/t2_ok) plus a ternary for the pointer assignment, no more goto/label.

- border(): merge the NULL-default and int-0-default branches into
  a single condition, as suggested by @picnixz.
- box(): replace the goto-based per-side validation with a single
  upfront check plus ternary pointer assignment, avoiding the extra
  label.
@serhiy-storchaka

Copy link
Copy Markdown
Member

Thank you for the patch.

Only the integer 0 is documented as the request for the default character, but the patch gives b'\0' the same meaning — it is an ordinary byte character.

The sentinel is also not the only regression from GH-151758: a string character could be mixed with byte and integer characters, including the ACS_* constants, in 3.14, but not in main.

#155871 solves both: border() and box() now choose the drawing function by what the arguments need, so only a complexchar cannot be mixed with integer and byte characters, and a string character is narrowed for them.

One note for future patches: test_border_box_zero_sentinel asserts TypeError for mixing but is not guarded by @requires_wide_build — a narrow build has no mixing check, so it would fail there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

curses border() and box() no longer accept 0 with a str

3 participants