Console.print(text) silently drops Text's own justify unless justify= is also passed to print()

#4216 · open · 5 comments

View on GitHub ↗

tritsystem

**AI disclosure (per AI_POLICY.md):** this was investigated and written with the assistance of Claude (Anthropic). I independently reproduced it against a fresh clone of `main` myself before filing — repro below, runnable standalone, no dependencies beyond Rich itself. ## Summary A `Text` object's own `.justify` attribute is silently dropped when the `Text` is passed directly as a top-level argument to `Console.print()`, unless `justify=` is *also* passed to `print()` itself — even though `Text.__rich_console__` documents/implements `self.justify` as taking priority over the console-level default. ## Reproduction ```python from rich.console import Console from rich.text import Text import io buf1 = io.StringIO() Console(file=buf1, width=20).print(Text("hi", justify="center")) print(repr(buf1.getvalue())) # -> 'hi\n' <- not centered, despite Text's own justify="center" buf2 = io.StringIO() Console(file=buf2, width=20).print(Text("hi", justify="center"), justify="center") print(repr(buf2.getvalue())) # -> ' hi \n' <- only works if justify is ALSO passed to print() ``` ## Root cause `Console._collect_renderables()` (`rich/console.py:1550-1554`, inside `check_text()`) funnels every `Text` object through: ```python sep_text = Text(sep, justify=justify, end=end) # justify = print()'s own kwarg, default None append(sep_text.join(text)) ``` `Text.join()` (`rich/text.py:788`) builds the merged result via `self.blank_copy()`, where `self` is the *separator* `sep_text` — so the joined result inherits the **separator's** `justify`/`overflow`/`no_wrap`, silently clobbering whatever the joined-in `Text`'s own `.justify` was set to. The same `Text` object centers correctly when wrapped in `Panel(...)` or `Group(...)`, because those containers call `console.render()` on the inner `Text` directly, bypassing this join path — so the bug is specific to passing a `Text` as a direct top-level argument to `Console.print()`. ## Checked `gh search issues --repo Textualize/rich` for "justify" (30+ related hits reviewed) — found related-but-distinct issues (#460 `console.log` missing a `justify` kwarg entirely, fixed 2020; #3948/#4021 `Table` title justify with `soft_wrap`) but nothing matching this specific "`Text.justify` silently dropped by `print()`" defect. Appears unreported. Happy to put together a PR (something like: preserve the joined item's own `justify`/`overflow`/`no_wrap` when it's a single `Text` and `print()`'s own `justify` wasn't explicitly passed) if this is confirmed as worth fixing.

Comments

Vansh-Sharmaa

Hi @willmcgugan / maintainers, I've investigated this and created a fix with accompanying tests on branch `fix-console-print-text-justify`: https://github.com/Vansh-Sharmaa/rich/tree/fix-console-print-text-justify ### Root Cause In `Console._collect_renderables()` (`rich/console.py`), when `text` contains a single `Text` object, `sep_text = Text(sep, justify=justify, end=end)` constructs a separator with `justify=None` (if `print()` wasn't passed an explicit justify). Then `sep_text.join(text)` builds the result from `sep_text.blank_copy()`, resetting the `Text`'s own `justify`, `overflow`, and `no_wrap` to `None`. ### Proposed Fix When `len(text) == 1`, initialize `sep_text` preserving `first_text.justify`, `first_text.overflow`, `first_text.no_wrap`, `first_text.tab_size`, and `first_text.style` (while allowing any explicit `justify` or other overrides passed to `print()` to take precedence). PR interaction limits are currently enabled on the repository for non-collaborators, but the patch and unit tests are ready here: https://github.com/Vansh-Sharmaa/rich/commit/d6dbb0968c07d18056c998f5f96522dd4ce2569e Would you like me to open a PR if permissions allow, or would you prefer to pull the branch directly? Thanks!

tritsystem

Thanks for digging into this, @Vansh-Sharmaa — this matches what I found when filing the issue, and I ran your branch to confirm. **Root cause check:** confirmed. `sep_text = Text(sep, justify=justify, end=end)` builds the separator with `justify=None` whenever `print()` isn't given an explicit `justify=`, and `Text.join()`'s use of `sep_text.blank_copy()` then stamps that (plus `overflow`/`no_wrap`) onto the joined result — clobbering the original `Text`'s own values. Your fix's extension to also preserve `overflow`, `no_wrap`, `tab_size`, and `style` is correct and more complete than my original report, which only called out `justify` explicitly — the same `blank_copy()` mechanism breaks those too. **Independently verified, not just read:** - Reproduced on current `main` (285d9e9) and on the released PyPI package (rich 15.0.0): `console.print(Text("hi", justify="center"))` at width 20 prints `'hi\n'`, not centered. - Checked out `fix-console-print-text-justify` (d6dbb096) in the same environment: same call now prints `' hi \n'` — fixed. - Ran the full `tests/test_console.py` suite against the fix branch: 96 passed, 7 skipped (pre-existing, unrelated), no regressions. Your three new tests (`test_print_text_justify`, `test_print_text_justify_override`, `test_print_text_overflow_and_no_wrap`) all pass, including the override case confirming an explicit `print(..., justify=...)` still correctly takes precedence over the `Text`'s own value. - The scope limit (only touching the `len(text) == 1` path, leaving multi-`Text` joins on the old behavior) looks like the right conservative call — that path isn't part of what's reported here, and the existing multi-text join tests still pass unchanged. Setup: Windows, Python 3.12.10, editable install from a fresh clone at each ref. Happy to share the exact repro/test commands if useful. (Disclosure: my original report and this verification pass were done with AI-assisted tooling — Claude — reviewed and run by me before posting.)

Vansh-Sharmaa

Thanks so much for taking the time to test and verify the branch, @tritsystem! Really appreciate the detailed confirmation and regression checks across Python 3.12. cc @willmcgugan — since repository interaction limits currently prevent non-collaborators from submitting pull requests directly, would you prefer to pull/cherry-pick commit `d6dbb0968c07d18056c998f5f96522dd4ce2569e` directly, or grant temporary contributor access so I can submit the PR via the standard review flow? Branch: https://github.com/Vansh-Sharmaa/rich/tree/fix-console-print-text-justify Commit: https://github.com/Vansh-Sharmaa/rich/commit/d6dbb0968c07d18056c998f5f96522dd4ce2569e Thanks!

ShamikOfficial

Took a look at this — agree with the diagnosis. `_collect_renderables` builds the join separator with `print()`'s `justify` (default `None`), and `Text.join` → `blank_copy()` then overwrites the `Text`'s own justify/overflow/no_wrap. I'll open a small PR that preserves those attrs for the single-`Text` case, with `print(justify=...)` still winning when set.

ShamikOfficial

Opened a fix branch (PR create is blocked for non-collaborators on this repo): https://github.com/ShamikOfficial/rich/tree/fix-text-justify-print Compare: https://github.com/Textualize/rich/compare/main...ShamikOfficial:rich:fix-text-justify-print?expand=1 Single-`Text` path keeps justify/overflow/no_wrap/tab_size; `print(justify=...)` still overrides. `tests/test_console.py` green locally (96 passed, 7 skipped).