Skip to content

more typing #1389

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

Merged
merged 9 commits into from
Jul 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pygit2/_libgit2/ffi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ class GitCommitC:
pass

class GitConfigC:
# incomplete
pass

class GitConfigEntryC:
# incomplete
name: char_pointer
level: int

class GitDescribeFormatOptionsC:
version: int
abbreviated_size: int
Expand Down Expand Up @@ -229,6 +235,10 @@ def new(a: Literal['git_commit **']) -> _Pointer[GitCommitC]: ...
@overload
def new(a: Literal['git_config *']) -> GitConfigC: ...
@overload
def new(a: Literal['git_config **']) -> _Pointer[GitConfigC]: ...
@overload
def new(a: Literal['git_config_entry **']) -> _Pointer[GitConfigEntryC]: ...
@overload
def new(a: Literal['git_describe_format_options *']) -> GitDescribeFormatOptionsC: ...
@overload
def new(a: Literal['git_describe_options *']) -> GitDescribeOptionsC: ...
Expand Down
35 changes: 29 additions & 6 deletions pygit2/_pygit2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from typing import (
Iterator,
Literal,
Optional,
Sequence,
Type,
TypedDict,
TypeVar,
Expand All @@ -26,6 +27,7 @@ from ._libgit2.ffi import (
)
from .blame import Blame
from .callbacks import CheckoutCallbacks
from .config import Config
from .enums import (
ApplyLocation,
AttrCheck,
Expand All @@ -45,6 +47,7 @@ from .enums import (
Option,
ReferenceFilter,
ReferenceType,
RepositoryState,
ResetMode,
SortMode,
)
Expand Down Expand Up @@ -386,20 +389,23 @@ class Blob(Object):
) -> Patch: ...
def diff_to_buffer(
self,
buffer: Optional[bytes] = None,
buffer: Optional[bytes | str] = None,
flag: DiffOption = DiffOption.NORMAL,
old_as_path: str = ...,
buffer_as_path: str = ...,
) -> Patch: ...
def _write_to_queue(
self,
queue: Queue,
closed: Event,
queue: Queue[bytes],
ready: Event,
done: Event,
chunk_size: int = DEFAULT_BUFFER_SIZE,
as_path: Optional[str] = None,
flags: BlobFilter = BlobFilter.CHECK_FOR_BINARY,
commit_id: Optional[Oid] = None,
) -> None: ...
def __buffer__(self, flags: int) -> memoryview: ...
def __release_buffer__(self, buffer: memoryview) -> None: ...

class Branch(Reference):
branch_name: str
Expand Down Expand Up @@ -736,6 +742,16 @@ class Repository:
def _from_c(cls, ptr: 'GitRepositoryC', owned: bool) -> 'Repository': ...
def __getitem__(self, key: str | Oid) -> Object: ...
def add_worktree(self, name: str, path: str, ref: Reference = ...) -> Worktree: ...
def amend_commit(
self,
commit: Commit | Oid | str,
refname: Reference | str | None,
author: Signature | None = None,
committer: Signature | None = None,
message: str | None = None,
tree: Tree | Oid | str | None = None,
encoding: str = 'UTF-8',
) -> Oid: ...
def applies(
self,
diff: Diff,
Expand Down Expand Up @@ -766,10 +782,12 @@ class Repository:
) -> None: ...
def cherrypick(self, id: _OidArg) -> None: ...
def compress_references(self) -> None: ...
@property
def config(self) -> Config: ...
def create_blob(self, data: bytes) -> Oid: ...
def create_blob_fromdisk(self, path: str) -> Oid: ...
def create_blob_fromiobase(self, iobase: IOBase) -> Oid: ...
def create_blob_fromworkdir(self, path: str) -> Oid: ...
def create_blob_fromworkdir(self, path: str | Path) -> Oid: ...
def create_branch(self, name: str, commit: Commit, force=False) -> Branch: ...
def create_commit(
self,
Expand All @@ -778,7 +796,7 @@ class Repository:
committer: Signature,
message: str | bytes,
tree: _OidArg,
parents: list[_OidArg],
parents: Sequence[_OidArg],
encoding: str = ...,
) -> Oid: ...
def create_commit_string(
Expand Down Expand Up @@ -826,6 +844,7 @@ class Repository:
def descendant_of(self, oid1: _OidArg, oid2: _OidArg) -> bool: ...
def expand_id(self, hex: str) -> Oid: ...
def free(self) -> None: ...
def get(self, key: _OidArg, default: Optional[Commit] = None) -> None | Object: ...
def get_attr(
self,
path: str | bytes | Path,
Expand All @@ -840,7 +859,7 @@ class Repository:
def listall_stashes(self) -> list[Stash]: ...
def listall_submodules(self) -> list[str]: ...
def lookup_branch(
self, branch_name: str, branch_type: BranchType = BranchType.LOCAL
self, branch_name: str | bytes, branch_type: BranchType = BranchType.LOCAL
) -> Branch: ...
def lookup_note(
self, annotated_id: str, ref: str = 'refs/notes/commits'
Expand All @@ -854,6 +873,8 @@ class Repository:
def merge_base(self, oid1: _OidArg, oid2: _OidArg) -> Oid: ...
def merge_base_many(self, oids: list[_OidArg]) -> Oid: ...
def merge_base_octopus(self, oids: list[_OidArg]) -> Oid: ...
@property
def message(self) -> str: ...
def notes(self) -> Iterator[Note]: ...
def path_is_ignored(self, path: str) -> bool: ...
def raw_listall_branches(
Expand All @@ -877,6 +898,8 @@ class Repository:
self, untracked_files: str = 'all', ignored: bool = False
) -> dict[str, int]: ...
def status_file(self, path: str) -> int: ...
def state(self) -> RepositoryState: ...
def state_cleanup(self) -> None: ...
def walk(
self, oid: _OidArg | None, sort_mode: SortMode = SortMode.NONE
) -> Walker: ...
Expand Down
44 changes: 29 additions & 15 deletions pygit2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.

from os import PathLike
from typing import TYPE_CHECKING

try:
from functools import cached_property
except ImportError:
Expand All @@ -33,8 +36,11 @@
from .ffi import C, ffi
from .utils import to_bytes

if TYPE_CHECKING:
from ._libgit2.ffi import GitConfigC, GitConfigEntryC, GitRepositoryC


def str_to_bytes(value, name):
def str_to_bytes(value: str | PathLike[str] | bytes, name: str) -> bytes:
if not isinstance(value, str):
raise TypeError(f'{name} must be a string')

Expand Down Expand Up @@ -72,41 +78,44 @@ def __next__(self):
class Config:
"""Git configuration management."""

def __init__(self, path=None):
_repo: 'GitRepositoryC'
_config: 'GitConfigC'

def __init__(self, path: str | None = None) -> None:
cconfig = ffi.new('git_config **')

if not path:
err = C.git_config_new(cconfig)
else:
path = str_to_bytes(path, 'path')
err = C.git_config_open_ondisk(cconfig, path)
path_bytes = str_to_bytes(path, 'path')
err = C.git_config_open_ondisk(cconfig, path_bytes)

check_error(err, io=True)
self._config = cconfig[0]

@classmethod
def from_c(cls, repo, ptr):
def from_c(cls, repo: 'GitRepositoryC', ptr: 'GitConfigC') -> 'Config':
config = cls.__new__(cls)
config._repo = repo
config._config = ptr

return config

def __del__(self):
def __del__(self) -> None:
try:
C.git_config_free(self._config)
except AttributeError:
pass

def _get(self, key):
def _get(self, key: str | bytes) -> tuple[object, 'ConfigEntry']:
key = str_to_bytes(key, 'key')

entry = ffi.new('git_config_entry **')
err = C.git_config_get_entry(entry, self._config, key)

return err, ConfigEntry._from_c(entry[0])

def _get_entry(self, key):
def _get_entry(self, key: str | bytes) -> 'ConfigEntry':
err, entry = self._get(key)

if err == C.GIT_ENOTFOUND:
Expand Down Expand Up @@ -306,8 +315,13 @@ def get_xdg_config():
class ConfigEntry:
"""An entry in a configuration object."""

_entry: 'GitConfigEntryC'
iterator: ConfigIterator | None

@classmethod
def _from_c(cls, ptr, iterator=None):
def _from_c(
cls, ptr: 'GitConfigEntryC', iterator: ConfigIterator | None = None
) -> 'ConfigEntry':
"""Builds the entry from a ``git_config_entry`` pointer.

``iterator`` must be a ``ConfigIterator`` instance if the entry was
Expand All @@ -330,7 +344,7 @@ def _from_c(cls, ptr, iterator=None):

return entry

def __del__(self):
def __del__(self) -> None:
if self.iterator is None:
C.git_config_entry_free(self._entry)

Expand All @@ -340,24 +354,24 @@ def c_value(self):
return self._entry.value

@cached_property
def raw_name(self):
def raw_name(self) -> bytes:
return ffi.string(self._entry.name)

@cached_property
def raw_value(self):
def raw_value(self) -> bytes:
return ffi.string(self.c_value)

@cached_property
def level(self):
def level(self) -> int:
"""The entry's ``git_config_level_t`` value."""
return self._entry.level

@property
def name(self):
def name(self) -> str:
"""The entry's name."""
return self.raw_name.decode('utf-8')

@property
def value(self):
def value(self) -> str:
"""The entry's value as a string."""
return self.raw_value.decode('utf-8')
2 changes: 1 addition & 1 deletion pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def __iter__(self):
#
# Mapping interface
#
def get(self, key: str, default: Optional[Commit] = None) -> Object:
def get(self, key: str | Oid, default: Optional[Commit] = None) -> Object:
value = self.git_object_lookup_prefix(key)
return value if (value is not None) else default

Expand Down
14 changes: 13 additions & 1 deletion pygit2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import contextlib
import os
from typing import Generic, Iterator, Protocol, TypeVar, Union
from typing import Generic, Iterator, Protocol, TypeVar, Union, overload

# Import from pygit2
from .ffi import C, ffi
Expand All @@ -38,6 +38,18 @@ def maybe_string(ptr):
return ffi.string(ptr).decode('utf8', errors='surrogateescape')


@overload
def to_bytes(
s: Union[str, bytes, os.PathLike[str]],
encoding: str = 'utf-8',
errors: str = 'strict',
) -> bytes: ...
@overload
def to_bytes(
s: Union['ffi.NULL_TYPE', None],
encoding: str = 'utf-8',
errors: str = 'strict',
) -> Union['ffi.NULL_TYPE']: ...
def to_bytes(
s: Union[str, bytes, 'ffi.NULL_TYPE', os.PathLike[str], None],
encoding: str = 'utf-8',
Expand Down
2 changes: 1 addition & 1 deletion src/blob.c
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ static void blob_filter_stream_free(git_writestream *s)


PyDoc_STRVAR(Blob__write_to_queue__doc__,
"_write_to_queue(queue: queue.Queue, closed: threading.Event, chunk_size: int = io.DEFAULT_BUFFER_SIZE, [as_path: str = None, flags: enums.BlobFilter = enums.BlobFilter.CHECK_FOR_BINARY, commit_id: oid = None]) -> None\n"
"_write_to_queue(queue: queue.Queue, ready: threading.Event, done: threading.Event, chunk_size: int = io.DEFAULT_BUFFER_SIZE, [as_path: str = None, flags: enums.BlobFilter = enums.BlobFilter.CHECK_FOR_BINARY, commit_id: oid = None]) -> None\n"
"\n"
"Write the contents of the blob in chunks to `queue`.\n"
"If `as_path` is None, the raw contents of blob will be written to the queue,\n"
Expand Down
Loading
Loading