-
-
Notifications
You must be signed in to change notification settings - Fork 834
✨ Support bytes in Options and Arguments #1190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
doubledare704
wants to merge
22
commits into
fastapi:master
Choose a base branch
from
doubledare704:feature/support-bytes-type
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
bfb093a
Remove debug_process.py file
doubledare704 87bcd4b
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] 73bd728
Fix linting issues and add type annotations
doubledare704 2766fe7
Fix mypy type error in BytesParamType.convert
doubledare704 38145f5
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] c0ce2ce
make full coverage of tests
doubledare704 8a52f94
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] de03a32
avoid assert false
doubledare704 dc98576
avoid coverage in 1 line of tests
doubledare704 3f94475
Merge branch 'master' into feature/support-bytes-type
doubledare704 32520a7
Merge branch 'master' into feature/support-bytes-type
doubledare704 8eb5fcc
Merge branch 'fastapi:master' into feature/support-bytes-type
doubledare704 963b0d1
feat(bytes): support encoding/errors for bytes params\n\n- Add encodi…
doubledare704 b5111fc
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 a59129f
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 2e8b8ec
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 996a5d7
fix(tests): correct stderr assertion in bytes encoding test
doubledare704 77c2846
set no cover for uncoverable line
doubledare704 14b60f3
update tutorial files to use Typer()
svlandeg e64aa57
Merge branch 'master' into feature/support-bytes-type
svlandeg 375bcf1
test(tutorial): add tests for bytes parameter types
doubledare704 d86b0bd
docs(tutorial): fix range syntax for bytes tutorial example code
doubledare704 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import base64 | ||
| import binascii | ||
|
|
||
| import typer | ||
|
|
||
| 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) from e | ||
|
|
||
|
|
||
| @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) from e | ||
|
|
||
|
|
||
| @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) 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) from e | ||
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import base64 | ||
|
|
||
| import typer | ||
|
|
||
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import base64 | ||
| import binascii | ||
|
|
||
| import typer | ||
| 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!") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| 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 | ||
|
|
||
|
|
||
| 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.""" | ||
| import click | ||
| from typer.main import BytesParamType | ||
|
|
||
| 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) | ||
| raise AssertionError( | ||
| "Should have raised click.BadParameter" | ||
| ) # pragma: no cover | ||
| except click.BadParameter: | ||
| pass # Test passes if we get here | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| test_bytes_type() | ||
| test_bytes_option() | ||
| test_bytes_argument() | ||
| print("All tests passed!") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a quick comment, if we do decide to progress with this PR, we should add the correct tests for the tutorial files in
test_tutorial/test_parameter_types/test_bytes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just added tests for tutorials. Please check.