Skip to content

Commit b7fdcd6

Browse files
committed
encoding: drop public to_str, rename internal helper to path_to_str
Replace the C-boundary uses of to_str in options.py with decode_string, which decodes the char pointer directly (no manual ffi.string call) and handles NULL. Remove pygit2.to_str from the public API; it was an undocumented accidental public helper, like to_bytes. Rename the remaining internal helper to path_to_str, which better describes its only remaining job: normalizing a user-supplied path argument to str in init_repository. Add a TODO noting the decode/encode asymmetry: decode_string uses surrogateescape while encode_string defaults to strict, so a value read from libgit2 with bad bytes cannot be written back without raising. Update changelog. Assisted-by: Kimi K3
1 parent 74de0db commit b7fdcd6

4 files changed

Lines changed: 15 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ Breaking changes:
4141
- Remove deprecated `Remote.ls_remotes(...)`, use `Remote.list_heads(...)`
4242
instead
4343

44-
- Remove `pygit2.to_bytes`; it was an undocumented accidental public API
44+
- Remove `pygit2.to_bytes` and `pygit2.to_str`; they were undocumented
45+
accidental public APIs
4546

4647

4748
# 1.19.3 (2026-06-13)

pygit2/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,6 @@
366366
from .settings import Settings
367367
from .submodules import Submodule
368368
from .transaction import ReferenceTransaction
369-
from .utils import to_str
370369

371370
# Features
372371
features = enums.Feature(C.git_libgit2_features())
@@ -456,7 +455,7 @@ def init_repository(
456455
check_error(err)
457456

458457
# Ok
459-
return Repository(to_str(path))
458+
return Repository(utils.path_to_str(path))
460459

461460

462461
def clone_repository(
@@ -938,7 +937,6 @@ def filter_unregister(name: str) -> None:
938937
'transaction',
939938
'ReferenceTransaction',
940939
'utils',
941-
'to_str',
942940
# __init__ module defined symbols
943941
'features',
944942
'LIBGIT2_VER',

pygit2/options.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from .errors import check_error
3636
from .ffi import C, ffi
37-
from .utils import decode_fs_path, encode_fs_path, encode_string, to_str
37+
from .utils import decode_fs_path, decode_string, encode_fs_path, encode_string
3838

3939
if TYPE_CHECKING:
4040
from ._libgit2.ffi import NULL_TYPE, ArrayC, char, char_pointer
@@ -507,10 +507,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
507507
check_error(err)
508508

509509
try:
510-
if buf.ptr != ffi.NULL:
511-
result = to_str(ffi.string(buf.ptr))
512-
else:
513-
result = None
510+
result = decode_string(buf.ptr)
514511
finally:
515512
C.git_buf_dispose(buf)
516513

@@ -643,8 +640,9 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
643640
# Cast to the non-NULL type for type checking
644641
strings = cast('ArrayC[char_pointer]', strarray.strings)
645642
for i in range(strarray.count):
646-
if strings[i] != ffi.NULL:
647-
result.append(to_str(ffi.string(strings[i])))
643+
s = decode_string(strings[i])
644+
if s is not None:
645+
result.append(s)
648646
finally:
649647
# Must dispose of the strarray to free the memory
650648
C.git_strarray_dispose(strarray)
@@ -767,10 +765,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED)
767765
check_error(err)
768766

769767
try:
770-
if buf.ptr != ffi.NULL:
771-
result = to_str(ffi.string(buf.ptr))
772-
else:
773-
result = None
768+
result = decode_string(buf.ptr)
774769
finally:
775770
C.git_buf_dispose(buf)
776771

pygit2/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ def encode_fs_path(
8484
return os.fsencode(s) # type: ignore[arg-type]
8585

8686

87+
# TODO decode_string uses errors='surrogateescape', but encode_string defaults
88+
# to errors='strict', so a value read from libgit2 with bad bytes cannot be
89+
# written back without raising. Decide whether encode_string should default to
90+
# 'surrogateescape' too, and audit every caller to make sure that's safe
91+
# (this is a behavior change, not just a rename).
8792
@overload
8893
def encode_string(
8994
s: str | bytes | os.PathLike[str] | os.PathLike[bytes],
@@ -113,7 +118,7 @@ def encode_string(
113118
return s.encode(encoding, errors) # type: ignore[union-attr]
114119

115120

116-
def to_str(s: str | bytes | os.PathLike[str] | os.PathLike[bytes]) -> str:
121+
def path_to_str(s: str | bytes | os.PathLike[str] | os.PathLike[bytes]) -> str:
117122
if hasattr(s, '__fspath__'):
118123
s = os.fspath(s)
119124

0 commit comments

Comments
 (0)