From dea6675430d2e7cf4b2669aeaa7efcaf7ad64a74 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Sat, 18 Jul 2026 20:34:10 -0300 Subject: [PATCH] Accept bytes input directly (#43) --- README.rst | 2 +- tests/test_parser.py | 17 +++++++++++++++++ unidiff/patch.py | 17 ++++++++++------- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 604b24f..72a3284 100644 --- a/README.rst +++ b/README.rst @@ -98,7 +98,7 @@ To instantiate :code:`PatchSet` from a local file, you can use: >>> patch , , ]> -Notice the (optional) :code:`encoding` parameter. If not specified, unicode input will be expected. Or alternatively: +Notice the (optional) :code:`encoding` parameter. If not specified, unicode input will be expected (or, when the data is :code:`bytes`, it is decoded using :code:`encoding`, defaulting to UTF-8). Or alternatively: .. code-block:: python diff --git a/tests/test_parser.py b/tests/test_parser.py index cf7598d..b4cf3e8 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -206,6 +206,23 @@ def test_patchset_string_input(self): self.assertEqual(ps1, ps2) + def test_patchset_bytes_input(self): + # issue #43: accept bytes directly so callers need not pre-decode; + # with no encoding given, bytes default to UTF-8 + utf8_file = os.path.join(self.samples_dir, 'samples/sample3.diff') + with open(utf8_file, 'rb') as diff_file: + diff_bytes = diff_file.read() + + ps_default = PatchSet(diff_bytes) + ps_explicit = PatchSet(diff_bytes, encoding='utf-8') + with open(utf8_file, 'rb') as diff_file: + ps_ref = PatchSet(diff_file, encoding='utf-8') + + self.assertEqual(ps_default, ps_ref) + self.assertEqual(ps_explicit, ps_ref) + # from_string also accepts bytes without an explicit encoding + self.assertEqual(PatchSet.from_string(diff_bytes), ps_ref) + def test_parse_malformed_diff(self): """Parse malformed file.""" with open(self.sample_bad_file) as diff_file: diff --git a/unidiff/patch.py b/unidiff/patch.py index c3aa171..68489cb 100644 --- a/unidiff/patch.py +++ b/unidiff/patch.py @@ -428,14 +428,17 @@ def is_symlink(self) -> bool: class PatchSet(list[PatchedFile]): """A list of PatchedFiles.""" - def __init__(self, f: Union[StringIO, str, Iterable[str]], + def __init__(self, f: Union[StringIO, str, bytes, Iterable[str]], encoding: Optional[str] = None, metadata_only: bool = False) -> None: super(PatchSet, self).__init__() - # convert string inputs to StringIO objects - if isinstance(f, str): + # convert str/bytes inputs to StringIO objects (bytes are decoded, + # defaulting to UTF-8 when no encoding is given) + if isinstance(f, (str, bytes)): f = self._convert_string(f, encoding) + # the data has already been decoded into text + encoding = None # make sure we pass an iterator object to parse data = iter(f) @@ -620,10 +623,10 @@ def from_filename(cls, filename: str, encoding: str = DEFAULT_ENCODING, @staticmethod def _convert_string(data: Union[str, bytes], encoding: Optional[str] = None, errors: str = 'strict') -> StringIO: - if encoding is not None: - # if encoding is given, assume bytes and decode - data = str(data, encoding=encoding, errors=errors) # type: ignore[arg-type] - return StringIO(data) # type: ignore[arg-type] + if isinstance(data, bytes): + # decode bytes input, defaulting to UTF-8 when no encoding is given + data = data.decode(encoding or DEFAULT_ENCODING, errors) + return StringIO(data) @classmethod def from_string(cls, data: Union[str, bytes], encoding: Optional[str] = None,