pyrefly icon indicating copy to clipboard operation
pyrefly copied to clipboard

Type Inference for `Popen.communicate()`

Open odysa opened this issue 6 months ago • 2 comments
trafficstars

Describe the Bug

import subprocess

p = subprocess.Popen(["ls"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()

stdout.decode("utf-8")  # Object of class `str` has no attribute `decode` [missing-attribute]
stderr.decode("utf-8")  # Object of class `str` has no attribute `decode` [missing-attribute]
assert isinstance(stdout, bytes)
assert not isinstance(stdout, str)

pyrefly infers stdout and stderr as str, but they are bytes unless text mode is enabled.

Per the Python doc

By default, all communication is in bytes, and therefore any "input" should be bytes, and the (stdout, stderr) will be bytes. If in text mode (indicated by self.text_mode), any "input" should be a string, and (stdout, stderr) will be strings decoded according to locale encoding, or by "encoding" if set. Text mode is triggered by setting any of text, encoding, errors or universal_newlines.

Inside the communicate method:

if stdout is not None:
    # it's bytes here
    stdout = b''.join(stdout)

if self.text_mode:
    if stdout is not None:
       # reassigned as str here
        stdout = self._translate_newlines(stdout,
                                          self.stdout.encoding,
                                          self.stdout.errors)
       .....

Expected

pyrefly infers the correct types of stdout and stderr

Sandbox Link

Sandbox

(Only applicable for extension issues) IDE Information

pyrefly 0.15.0

odysa avatar May 20 '25 18:05 odysa

A simple snippet:

def pp(flag: bool) -> str | int:
    if flag:
        return "STR"
    return 123

a = pp(True)
assert a.lower() == "str"

Result:

....: Object of class `int` has no attribute `lower` [missing-attribute]

Sandbox

odysa avatar May 20 '25 18:05 odysa

Just a note for whenever someone can take this up: we have a series of issues that are related to some combination of overload and __new__ resolution, it's not always immediately obvious which is responsible.

In this case there is no __new__ so I think the bug must be in overload resolution of __init__, which is heavily overloaded including on the self parameter, and the overload probably needs to correctly "pin" the type argument.

This may be connected to the need for a backtracking-like mechanism in overload resolution

stroxler avatar May 21 '25 15:05 stroxler