From bfb093a95773bd44e954485de54ac88631ccd420 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 18:20:12 +0300 Subject: [PATCH 1/9] Remove debug_process.py file Add comprehensive tests for bytes type with base64 and hex encoding Add support for bytes type in Options and Arguments --- examples/bytes_encoding_example.py | 95 +++++++++++++++++++++++++++++ examples/bytes_type_example.py | 24 ++++++++ tests/test_bytes_encoding.py | 97 ++++++++++++++++++++++++++++++ tests/test_bytes_type.py | 58 ++++++++++++++++++ typer/main.py | 17 ++++++ 5 files changed, 291 insertions(+) create mode 100644 examples/bytes_encoding_example.py create mode 100644 examples/bytes_type_example.py create mode 100644 tests/test_bytes_encoding.py create mode 100644 tests/test_bytes_type.py diff --git a/examples/bytes_encoding_example.py b/examples/bytes_encoding_example.py new file mode 100644 index 0000000000..021c6bdfda --- /dev/null +++ b/examples/bytes_encoding_example.py @@ -0,0 +1,95 @@ +import typer +import base64 +import binascii + +app = typer.Typer() + + +@app.command() +def base64_encode(text: bytes): + """Encode text to base64.""" + encoded = base64.b64encode(text) + typer.echo(f"Original: {text!r}") + typer.echo(f"Base64 encoded: {encoded.decode()}") + + +@app.command() +def base64_decode(encoded: str): + """Decode base64 to bytes.""" + try: + decoded = base64.b64decode(encoded) + typer.echo(f"Base64 encoded: {encoded}") + typer.echo(f"Decoded: {decoded!r}") + typer.echo(f"As string: {decoded.decode(errors='replace')}") + except Exception as e: + typer.echo(f"Error decoding base64: {e}", err=True) + raise typer.Exit(code=1) + + +@app.command() +def hex_encode(data: bytes): + """Convert bytes to hex string.""" + hex_str = binascii.hexlify(data).decode() + typer.echo(f"Original: {data!r}") + typer.echo(f"Hex encoded: {hex_str}") + + +@app.command() +def hex_decode(hex_str: str): + """Convert hex string to bytes.""" + try: + data = binascii.unhexlify(hex_str) + typer.echo(f"Hex encoded: {hex_str}") + typer.echo(f"Decoded: {data!r}") + typer.echo(f"As string: {data.decode(errors='replace')}") + except Exception as e: + typer.echo(f"Error decoding hex: {e}", err=True) + raise typer.Exit(code=1) + + +@app.command() +def convert( + data: bytes = typer.Argument(..., help="Data to convert"), + from_format: str = typer.Option( + "raw", "--from", "-f", help="Source format: raw, base64, or hex" + ), + to_format: str = typer.Option( + "base64", "--to", "-t", help="Target format: raw, base64, or hex" + ), +): + """Convert between different encodings.""" + # First decode from source format to raw bytes + raw_bytes = data + if from_format == "base64": + try: + raw_bytes = base64.b64decode(data) + except Exception as e: + typer.echo(f"Error decoding base64: {e}", err=True) + raise typer.Exit(code=1) + elif from_format == "hex": + try: + raw_bytes = binascii.unhexlify(data) + except Exception as e: + typer.echo(f"Error decoding hex: {e}", err=True) + raise typer.Exit(code=1) + elif from_format != "raw": + typer.echo(f"Unknown source format: {from_format}", err=True) + raise typer.Exit(code=1) + + # Then encode to target format + if to_format == "raw": + typer.echo(f"Raw bytes: {raw_bytes!r}") + typer.echo(f"As string: {raw_bytes.decode(errors='replace')}") + elif to_format == "base64": + encoded = base64.b64encode(raw_bytes).decode() + typer.echo(f"Base64 encoded: {encoded}") + elif to_format == "hex": + encoded = binascii.hexlify(raw_bytes).decode() + typer.echo(f"Hex encoded: {encoded}") + else: + typer.echo(f"Unknown target format: {to_format}", err=True) + raise typer.Exit(code=1) + + +if __name__ == "__main__": + app() diff --git a/examples/bytes_type_example.py b/examples/bytes_type_example.py new file mode 100644 index 0000000000..a044229b9a --- /dev/null +++ b/examples/bytes_type_example.py @@ -0,0 +1,24 @@ +import typer +import base64 + +app = typer.Typer() + + +@app.command() +def encode(text: bytes): + """Encode text to base64.""" + encoded = base64.b64encode(text) + typer.echo(f"Original: {text!r}") + typer.echo(f"Encoded: {encoded.decode()}") + + +@app.command() +def decode(encoded: str): + """Decode base64 to bytes.""" + decoded = base64.b64decode(encoded) + typer.echo(f"Encoded: {encoded}") + typer.echo(f"Decoded: {decoded!r}") + + +if __name__ == "__main__": + app() diff --git a/tests/test_bytes_encoding.py b/tests/test_bytes_encoding.py new file mode 100644 index 0000000000..b6b1f71e9a --- /dev/null +++ b/tests/test_bytes_encoding.py @@ -0,0 +1,97 @@ +import typer +import base64 +import binascii +from typer.testing import CliRunner + +runner = CliRunner() + + +def test_base64_encode_decode(): + """Test base64 encoding and decoding with bytes type.""" + app = typer.Typer() + + @app.command() + def encode(text: bytes): + """Encode text to base64.""" + encoded = base64.b64encode(text) + typer.echo(encoded.decode()) + + @app.command() + def decode(encoded: str): + """Decode base64 to bytes.""" + decoded = base64.b64decode(encoded) + typer.echo(repr(decoded)) + + # Test encoding + result = runner.invoke(app, ["encode", "Hello, world!"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "SGVsbG8sIHdvcmxkIQ==" + + # Test decoding + result = runner.invoke(app, ["decode", "SGVsbG8sIHdvcmxkIQ=="]) + assert result.exit_code == 0 + assert result.stdout.strip() == repr(b'Hello, world!') + + +def test_hex_encode_decode(): + """Test hex encoding and decoding with bytes type.""" + app = typer.Typer() + + @app.command() + def to_hex(data: bytes): + """Convert bytes to hex string.""" + hex_str = binascii.hexlify(data).decode() + typer.echo(hex_str) + + @app.command() + def from_hex(hex_str: str): + """Convert hex string to bytes.""" + data = binascii.unhexlify(hex_str) + typer.echo(repr(data)) + + # Test to_hex + result = runner.invoke(app, ["to-hex", "ABC123"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "414243313233" # Hex for "ABC123" + + # Test from_hex + result = runner.invoke(app, ["from-hex", "414243313233"]) + assert result.exit_code == 0 + assert result.stdout.strip() == repr(b'ABC123') + + +def test_complex_bytes_operations(): + """Test more complex operations with bytes type.""" + app = typer.Typer() + + @app.command() + def main( + data: bytes = typer.Argument(..., help="Data to process"), + encoding: str = typer.Option("utf-8", help="Encoding to use for output"), + prefix: bytes = typer.Option(b"PREFIX:", help="Prefix to add to the data"), + ): + """Process bytes data with options.""" + result = prefix + data + typer.echo(result.decode(encoding)) + + # Test with default encoding + result = runner.invoke(app, ["Hello"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "PREFIX:Hello" + + # Test with custom encoding + result = runner.invoke(app, ["Hello", "--encoding", "ascii"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "PREFIX:Hello" + + # Test with custom prefix + result = runner.invoke(app, ["Hello", "--prefix", "CUSTOM:"]) + assert result.exit_code == 0 + assert result.stdout.strip() == "CUSTOM:Hello" + + +if __name__ == "__main__": + test_base64_encode_decode() + test_hex_encode_decode() + test_complex_bytes_operations() + print("All tests passed!") diff --git a/tests/test_bytes_type.py b/tests/test_bytes_type.py new file mode 100644 index 0000000000..9ddcdd34a6 --- /dev/null +++ b/tests/test_bytes_type.py @@ -0,0 +1,58 @@ +import typer +from typer.testing import CliRunner + +runner = CliRunner() + + +def test_bytes_type(): + """Test that bytes type works correctly.""" + app = typer.Typer() + + @app.command() + def main(name: bytes): + typer.echo(f"Bytes: {name!r}") + + result = runner.invoke(app, ["hello"]) + assert result.exit_code == 0 + assert "Bytes: b'hello'" in result.stdout + + +def test_bytes_option(): + """Test that bytes type works correctly as an option.""" + app = typer.Typer() + + @app.command() + def main(name: bytes = typer.Option(b"default")): + typer.echo(f"Bytes: {name!r}") + + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Bytes: b'default'" in result.stdout + + result = runner.invoke(app, ["--name", "custom"]) + assert result.exit_code == 0 + assert "Bytes: b'custom'" in result.stdout + + +def test_bytes_argument(): + """Test that bytes type works correctly as an argument.""" + app = typer.Typer() + + @app.command() + def main(name: bytes = typer.Argument(b"default")): + typer.echo(f"Bytes: {name!r}") + + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Bytes: b'default'" in result.stdout + + result = runner.invoke(app, ["custom"]) + assert result.exit_code == 0 + assert "Bytes: b'custom'" in result.stdout + + +if __name__ == "__main__": + test_bytes_type() + test_bytes_option() + test_bytes_argument() + print("All tests passed!") diff --git a/typer/main.py b/typer/main.py index 508d96617e..c60438ab8e 100644 --- a/typer/main.py +++ b/typer/main.py @@ -701,6 +701,21 @@ def wrapper(**kwargs: Any) -> Any: return wrapper +class BytesParamType(click.ParamType): + name = "bytes" + + def convert(self, value, param, ctx): + if isinstance(value, bytes): + return value + try: + return value.encode() + except (UnicodeDecodeError, AttributeError): + self.fail(f"{value!r} is not a valid string that can be encoded to bytes", param, ctx) + + +BYTES = BytesParamType() + + def get_click_type( *, annotation: Any, parameter_info: ParameterInfo ) -> click.ParamType: @@ -712,6 +727,8 @@ def get_click_type( elif annotation is str: return click.STRING + elif annotation is bytes: + return BYTES elif annotation is int: if parameter_info.min is not None or parameter_info.max is not None: min_ = None From 87bcd4b930e682d5d8e079eb6e4edaf9135eec30 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:36:45 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/bytes_encoding_example.py | 3 ++- examples/bytes_type_example.py | 3 ++- tests/test_bytes_encoding.py | 7 ++++--- typer/main.py | 6 +++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/bytes_encoding_example.py b/examples/bytes_encoding_example.py index 021c6bdfda..4c09350d44 100644 --- a/examples/bytes_encoding_example.py +++ b/examples/bytes_encoding_example.py @@ -1,7 +1,8 @@ -import typer import base64 import binascii +import typer + app = typer.Typer() diff --git a/examples/bytes_type_example.py b/examples/bytes_type_example.py index a044229b9a..0d23ed49dc 100644 --- a/examples/bytes_type_example.py +++ b/examples/bytes_type_example.py @@ -1,6 +1,7 @@ -import typer import base64 +import typer + app = typer.Typer() diff --git a/tests/test_bytes_encoding.py b/tests/test_bytes_encoding.py index b6b1f71e9a..4432e9157a 100644 --- a/tests/test_bytes_encoding.py +++ b/tests/test_bytes_encoding.py @@ -1,6 +1,7 @@ -import typer import base64 import binascii + +import typer from typer.testing import CliRunner runner = CliRunner() @@ -30,7 +31,7 @@ def decode(encoded: str): # Test decoding result = runner.invoke(app, ["decode", "SGVsbG8sIHdvcmxkIQ=="]) assert result.exit_code == 0 - assert result.stdout.strip() == repr(b'Hello, world!') + assert result.stdout.strip() == repr(b"Hello, world!") def test_hex_encode_decode(): @@ -57,7 +58,7 @@ def from_hex(hex_str: str): # Test from_hex result = runner.invoke(app, ["from-hex", "414243313233"]) assert result.exit_code == 0 - assert result.stdout.strip() == repr(b'ABC123') + assert result.stdout.strip() == repr(b"ABC123") def test_complex_bytes_operations(): diff --git a/typer/main.py b/typer/main.py index c60438ab8e..430cb0f46a 100644 --- a/typer/main.py +++ b/typer/main.py @@ -710,7 +710,11 @@ def convert(self, value, param, ctx): try: return value.encode() except (UnicodeDecodeError, AttributeError): - self.fail(f"{value!r} is not a valid string that can be encoded to bytes", param, ctx) + self.fail( + f"{value!r} is not a valid string that can be encoded to bytes", + param, + ctx, + ) BYTES = BytesParamType() From 73bd728e485b71ca89fe3c453252bd6cd32fdf99 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 18:42:30 +0300 Subject: [PATCH 3/9] Fix linting issues and add type annotations --- examples/bytes_encoding_example.py | 8 ++++---- typer/main.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/bytes_encoding_example.py b/examples/bytes_encoding_example.py index 4c09350d44..f40fce267c 100644 --- a/examples/bytes_encoding_example.py +++ b/examples/bytes_encoding_example.py @@ -24,7 +24,7 @@ def base64_decode(encoded: str): typer.echo(f"As string: {decoded.decode(errors='replace')}") except Exception as e: typer.echo(f"Error decoding base64: {e}", err=True) - raise typer.Exit(code=1) + raise typer.Exit(code=1) from e @app.command() @@ -45,7 +45,7 @@ def hex_decode(hex_str: str): typer.echo(f"As string: {data.decode(errors='replace')}") except Exception as e: typer.echo(f"Error decoding hex: {e}", err=True) - raise typer.Exit(code=1) + raise typer.Exit(code=1) from e @app.command() @@ -66,13 +66,13 @@ def convert( raw_bytes = base64.b64decode(data) except Exception as e: typer.echo(f"Error decoding base64: {e}", err=True) - raise typer.Exit(code=1) + raise typer.Exit(code=1) from e elif from_format == "hex": try: raw_bytes = binascii.unhexlify(data) except Exception as e: typer.echo(f"Error decoding hex: {e}", err=True) - raise typer.Exit(code=1) + raise typer.Exit(code=1) from e elif from_format != "raw": typer.echo(f"Unknown source format: {from_format}", err=True) raise typer.Exit(code=1) diff --git a/typer/main.py b/typer/main.py index 430cb0f46a..5246001b4d 100644 --- a/typer/main.py +++ b/typer/main.py @@ -704,7 +704,7 @@ def wrapper(**kwargs: Any) -> Any: class BytesParamType(click.ParamType): name = "bytes" - def convert(self, value, param, ctx): + def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes: if isinstance(value, bytes): return value try: From 2766fe7326e5d6dc1a951698691faa901c2e21bf Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 18:43:44 +0300 Subject: [PATCH 4/9] Fix mypy type error in BytesParamType.convert --- typer/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index 5246001b4d..ea3df423e9 100644 --- a/typer/main.py +++ b/typer/main.py @@ -708,7 +708,9 @@ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[cl if isinstance(value, bytes): return value try: - return value.encode() + if isinstance(value, str): + return value.encode() + return str(value).encode() except (UnicodeDecodeError, AttributeError): self.fail( f"{value!r} is not a valid string that can be encoded to bytes", From 38145f51705161da5b3995ce971c8006770767f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:45:05 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typer/main.py b/typer/main.py index ea3df423e9..60689bef49 100644 --- a/typer/main.py +++ b/typer/main.py @@ -704,7 +704,9 @@ def wrapper(**kwargs: Any) -> Any: class BytesParamType(click.ParamType): name = "bytes" - def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes: + def convert( + self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context] + ) -> bytes: if isinstance(value, bytes): return value try: From c0ce2ce88b7d6d68740b385ff1885d1e5bf8bb0a Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 18:55:50 +0300 Subject: [PATCH 6/9] make full coverage of tests --- tests/test_bytes_type.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_bytes_type.py b/tests/test_bytes_type.py index 9ddcdd34a6..1bf69f26b1 100644 --- a/tests/test_bytes_type.py +++ b/tests/test_bytes_type.py @@ -51,6 +51,44 @@ def main(name: bytes = typer.Argument(b"default")): assert "Bytes: b'custom'" in result.stdout +def test_bytes_non_string_input(): + """Test that bytes type works correctly with non-string input.""" + app = typer.Typer() + + @app.command() + def main(value: bytes): + typer.echo(f"Bytes: {value!r}") + + # Test with a number (will be converted to string then bytes) + result = runner.invoke(app, ["123"]) + assert result.exit_code == 0 + assert "Bytes: b'123'" in result.stdout + + +def test_bytes_conversion_error(): + """Test error handling when bytes conversion fails.""" + from typer.main import BytesParamType + import click + + bytes_type = BytesParamType() + + # Create a mock object that will raise UnicodeDecodeError when str() is called + class MockObj: + def __str__(self): + # This will trigger the UnicodeDecodeError in the except block + raise UnicodeDecodeError('utf-8', b'\x80abc', 0, 1, 'invalid start byte') + + # Create a mock context for testing + ctx = click.Context(click.Command("test")) + + # This should raise a click.BadParameter exception + try: + bytes_type.convert(MockObj(), None, ctx) + assert False, "Should have raised an exception" + except click.BadParameter: + assert True + + if __name__ == "__main__": test_bytes_type() test_bytes_option() From 8a52f9464f716150fc0c0de9c85b212651092e1e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:56:06 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_bytes_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bytes_type.py b/tests/test_bytes_type.py index 1bf69f26b1..e237fa09ae 100644 --- a/tests/test_bytes_type.py +++ b/tests/test_bytes_type.py @@ -67,8 +67,8 @@ def main(value: bytes): def test_bytes_conversion_error(): """Test error handling when bytes conversion fails.""" - from typer.main import BytesParamType import click + from typer.main import BytesParamType bytes_type = BytesParamType() @@ -76,7 +76,7 @@ def test_bytes_conversion_error(): class MockObj: def __str__(self): # This will trigger the UnicodeDecodeError in the except block - raise UnicodeDecodeError('utf-8', b'\x80abc', 0, 1, 'invalid start byte') + raise UnicodeDecodeError("utf-8", b"\x80abc", 0, 1, "invalid start byte") # Create a mock context for testing ctx = click.Context(click.Command("test")) From de03a327946f9e0c30ccd0989fdbac5d97b4fed9 Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 19:14:02 +0300 Subject: [PATCH 8/9] avoid assert false --- tests/test_bytes_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bytes_type.py b/tests/test_bytes_type.py index e237fa09ae..98ed1fc29d 100644 --- a/tests/test_bytes_type.py +++ b/tests/test_bytes_type.py @@ -84,9 +84,9 @@ def __str__(self): # This should raise a click.BadParameter exception try: bytes_type.convert(MockObj(), None, ctx) - assert False, "Should have raised an exception" + raise AssertionError("Should have raised an exception") except click.BadParameter: - assert True + pass # Test passes if we get here if __name__ == "__main__": From dc98576b2ff962c6c87c486df56734b8b1153ada Mon Sep 17 00:00:00 2001 From: doubledare704 Date: Thu, 3 Apr 2025 20:56:44 +0300 Subject: [PATCH 9/9] avoid coverage in 1 line of tests --- tests/test_bytes_type.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_bytes_type.py b/tests/test_bytes_type.py index 98ed1fc29d..51b74bd27c 100644 --- a/tests/test_bytes_type.py +++ b/tests/test_bytes_type.py @@ -84,7 +84,9 @@ def __str__(self): # This should raise a click.BadParameter exception try: bytes_type.convert(MockObj(), None, ctx) - raise AssertionError("Should have raised an exception") + raise AssertionError( + "Should have raised click.BadParameter" + ) # pragma: no cover except click.BadParameter: pass # Test passes if we get here