@@ -115,12 +115,35 @@ def show_diagnostics(self) -> bool:
115115 def show_diagnostics (self , value : bool ) -> None :
116116 self ._show_diagnostics = value
117117
118+ def color_for (self , file : Optional [IO [Any ]] = None , err : bool = False ) -> bool :
119+ """Resolve the color decision for a specific output destination.
120+
121+ Honors the explicit ``--color`` / ``--no-color`` choice first, then the
122+ ``FORCE_COLOR`` / ``NO_COLOR`` conventions (https://no-color.org/), and
123+ finally the TTY status of the relevant stream — which may be stdout,
124+ stderr, or an explicit ``file=``. This matters because stdout and
125+ stderr can be redirected independently: piping ``stdout`` into a file
126+ should not disable colour on warnings written to ``stderr`` if that
127+ is still attached to a terminal.
128+ """
129+ pref = self .config .colored_output
130+ if pref == ColoredOutput .NO :
131+ return False
132+ if pref == ColoredOutput .YES :
133+ return True
134+ if os .environ .get ("FORCE_COLOR" ):
135+ return True
136+ if os .environ .get ("NO_COLOR" ):
137+ return False
138+ stream = file if file is not None else (sys .stderr if err else sys .stdout )
139+ isatty = getattr (stream , "isatty" , None )
140+ return bool (isatty ()) if callable (isatty ) else False
141+
118142 @property
119143 def colored (self ) -> bool :
120- return self .config .colored_output in [
121- ColoredOutput .AUTO ,
122- ColoredOutput .YES ,
123- ]
144+ """Shortcut for the color decision on stdout — used for branching
145+ between rich and plain rendering paths that always target stdout."""
146+ return self .color_for ()
124147
125148 @property
126149 def has_rich (self ) -> bool :
@@ -139,12 +162,13 @@ def verbose(
139162 err : Optional [bool ] = True ,
140163 ) -> None :
141164 if self .config .verbose :
165+ err_resolved = err if err is not None else True
142166 click .secho (
143167 message () if callable (message ) else message ,
144168 file = file ,
145169 nl = nl if nl is not None else True ,
146- err = err if err is not None else True ,
147- color = self .colored ,
170+ err = err_resolved ,
171+ color = self .color_for ( file = file , err = err_resolved ) ,
148172 fg = "bright_black" ,
149173 )
150174
@@ -167,7 +191,7 @@ def progressbar(
167191 show_percent = show_percent ,
168192 show_pos = show_pos ,
169193 file = sys .stderr ,
170- color = self .colored ,
194+ color = self .color_for ( file = sys . stderr ) ,
171195 )
172196
173197 def warning (
@@ -177,12 +201,13 @@ def warning(
177201 nl : Optional [bool ] = True ,
178202 err : Optional [bool ] = True ,
179203 ) -> None :
204+ err_resolved = err if err is not None else True
180205 click .secho (
181206 f"[ { click .style ('WARN' , fg = 'yellow' )} ] { message () if callable (message ) else message } " ,
182207 file = file ,
183208 nl = nl if nl is not None else True ,
184- err = err if err is not None else True ,
185- color = self .colored ,
209+ err = err_resolved ,
210+ color = self .color_for ( file = file , err = err_resolved ) ,
186211 fg = "bright_yellow" ,
187212 )
188213
@@ -193,12 +218,13 @@ def error(
193218 nl : Optional [bool ] = True ,
194219 err : Optional [bool ] = True ,
195220 ) -> None :
221+ err_resolved = err if err is not None else True
196222 click .secho (
197223 f"[ { click .style ('ERROR' , fg = 'red' )} ] { message () if callable (message ) else message } " ,
198224 file = file ,
199225 nl = nl if nl is not None else True ,
200- err = err if err is not None else True ,
201- color = self .colored ,
226+ err = err_resolved ,
227+ color = self .color_for ( file = file , err = err_resolved ) ,
202228 )
203229
204230 def print_data (
@@ -267,7 +293,7 @@ def echo(
267293 message () if callable (message ) else message ,
268294 file = file ,
269295 nl = nl ,
270- color = self .colored ,
296+ color = self .color_for ( file = file , err = err ) ,
271297 err = err ,
272298 )
273299
0 commit comments