From e0241bad8bd430c6b14df2fac3fdfea7d443496a Mon Sep 17 00:00:00 2001 From: NixMelon Date: Tue, 22 Sep 2026 18:27:38 +0000 Subject: [PATCH 1/4] Add API for returning ASCII QR codes --- README.rst | 9 +++------ qrcode/main.py | 37 ++++++++++++++++++++++++------------- qrcode/tests/test_qrcode.py | 24 ++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 73722544..9d20af7d 100644 --- a/README.rst +++ b/README.rst @@ -242,18 +242,15 @@ and an embedded image: Examples ======== -Get the text content from `print_ascii`: +Get a QR code as an ASCII string: .. code:: python - import io import qrcode qr = qrcode.QRCode() qr.add_data("Some text") - f = io.StringIO() - qr.print_ascii(out=f) - f.seek(0) - print(f.read()) + ascii_qr = qr.get_ascii() + print(ascii_qr) The `add_data` method will append data to the current QR object. To add new data by replacing previous content in the same object, first use clear method: diff --git a/qrcode/main.py b/qrcode/main.py index 593c581b..9a4662ad 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -278,24 +278,19 @@ def print_tty(self, out=None): out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") out.flush() - def print_ascii(self, out=None, tty=False, invert=False): + def get_ascii(self, tty=False, invert=False): """ - Output the QR Code using ASCII characters. + Return the QR Code as a string using ASCII characters. :param tty: use fixed TTY color codes (forces invert=True) :param invert: invert the ASCII characters (solid <-> transparent) """ - if out is None: - out = sys.stdout - - if tty and not out.isatty(): - raise OSError("Not a tty") - if self.data_cache is None: self.make() modcount = self.modules_count codes = [bytes((code,)).decode("cp437") for code in (255, 223, 220, 219)] + output = [] if tty: invert = True if invert: @@ -311,14 +306,30 @@ def get_module(x, y) -> int: for r in range(-self.border, modcount + self.border, 2): if tty: if not invert or r < modcount + self.border - 1: - out.write("\x1b[48;5;232m") # Background black - out.write("\x1b[38;5;255m") # Foreground white + output.append("\x1b[48;5;232m") # Background black + output.append("\x1b[38;5;255m") # Foreground white for c in range(-self.border, modcount + self.border): pos = get_module(r, c) + (get_module(r + 1, c) << 1) - out.write(codes[pos]) + output.append(codes[pos]) if tty: - out.write("\x1b[0m") - out.write("\n") + output.append("\x1b[0m") + output.append("\n") + return "".join(output) + + def print_ascii(self, out=None, tty=False, invert=False): + """ + Output the QR Code using ASCII characters. + + :param tty: use fixed TTY color codes (forces invert=True) + :param invert: invert the ASCII characters (solid <-> transparent) + """ + if out is None: + out = sys.stdout + + if tty and not out.isatty(): + raise OSError("Not a tty") + + out.write(self.get_ascii(tty=tty, invert=invert)) out.flush() @overload diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index a98f8724..ae0484ec 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -267,6 +267,30 @@ def test_print_ascii(): assert printed[: len(expected)] == expected +@pytest.mark.parametrize( + ("border", "tty", "invert"), + [ + (0, False, False), + (4, False, False), + (4, False, True), + (4, True, False), + ], +) +def test_get_ascii_matches_print_ascii(border, tty, invert): + qr = qrcode.QRCode(border=border) + qr.add_data("Some data") + + output = io.StringIO() + output.isatty = lambda: True + qr.print_ascii(out=output, tty=tty, invert=invert) + + ascii_qr = qr.get_ascii(tty=tty, invert=invert) + + assert isinstance(ascii_qr, str) + assert ascii_qr + assert ascii_qr == output.getvalue() + + def test_print_tty_stdout(): qr = qrcode.QRCode() with mock.patch("sys.stdout") as fake_stdout: From 1db5305747ac2236b6d5f1b392da3b079361c419 Mon Sep 17 00:00:00 2001 From: NixMelon Date: Tue, 22 Sep 2026 18:27:38 +0000 Subject: [PATCH 2/4] Document get_ascii in changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 26e6b6aa..b6f58e1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -40,6 +40,7 @@ Change Log WIP 8.x ------- +- **Added** ``QRCode.get_ascii()`` to return ASCII QR codes as strings. - **Added** ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_) - **Added** ability to execute as a Python module: ``python -m qrcode --output qrcode.png "hello world"`` (stefansjs in `#400`_) - **Removed** the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_) From c9d320b3bbb08452d3ca6d888de353bba743f5c2 Mon Sep 17 00:00:00 2001 From: NixMelon Date: Wed, 23 Sep 2026 22:24:47 +0000 Subject: [PATCH 3/4] Strengthen get_ascii API tests --- qrcode/main.py | 2 +- qrcode/tests/test_qrcode.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/qrcode/main.py b/qrcode/main.py index 9a4662ad..5adc56f6 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -278,7 +278,7 @@ def print_tty(self, out=None): out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n") out.flush() - def get_ascii(self, tty=False, invert=False): + def get_ascii(self, tty: bool = False, invert: bool = False) -> str: """ Return the QR Code as a string using ASCII characters. diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index ae0484ec..5c0bef7c 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -291,6 +291,25 @@ def test_get_ascii_matches_print_ascii(border, tty, invert): assert ascii_qr == output.getvalue() +def test_get_ascii_tty_returns_terminal_escape_codes(): + qr = qrcode.QRCode(border=0) + qr.add_data("Some data") + + ascii_qr = qr.get_ascii(tty=True) + + assert ascii_qr.startswith("\x1b[48;5;232m\x1b[38;5;255m") + assert ascii_qr.endswith("\x1b[0m\n") + + +def test_get_ascii_is_stable_across_repeated_calls(): + qr = qrcode.QRCode() + qr.add_data("Some data") + + first_output = qr.get_ascii() + + assert qr.get_ascii() == first_output + + def test_print_tty_stdout(): qr = qrcode.QRCode() with mock.patch("sys.stdout") as fake_stdout: From 05d80e850d34f639781e97c1419d6aa4736591b2 Mon Sep 17 00:00:00 2001 From: NixMelon Date: Thu, 24 Sep 2026 21:33:29 +0000 Subject: [PATCH 4/4] Test get_ascii output dimensions --- qrcode/tests/test_qrcode.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 5c0bef7c..e92af193 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -310,6 +310,18 @@ def test_get_ascii_is_stable_across_repeated_calls(): assert qr.get_ascii() == first_output +@pytest.mark.parametrize("border", [0, 4]) +def test_get_ascii_dimensions(border): + qr = qrcode.QRCode(border=border) + qr.add_data("Some data") + + lines = qr.get_ascii().splitlines() + output_width = qr.modules_count + border * 2 + + assert len(lines) == (output_width + 1) // 2 + assert all(len(line) == output_width for line in lines) + + def test_print_tty_stdout(): qr = qrcode.QRCode() with mock.patch("sys.stdout") as fake_stdout: