Skip to content

Commit 608c825

Browse files
committed
Add specific exception classes for libgit2 error codes
The following libgit2 errors are now raised as specific pygit2 exceptions instead of plain ValueError or KeyError: - AlreadyExistsError (also ValueError) - InvalidSpecError (also ValueError) - InvalidError (also ValueError) - NotFoundError (also KeyError) - AmbiguousError (also ValueError) - AuthError - CertificateError All new exceptions inherit from GitError, so existing code catching GitError, ValueError, KeyError, or Exception continues to work. Fixes #830 Closes #998 Assisted-by: Kimi Code
1 parent ba21cf5 commit 608c825

9 files changed

Lines changed: 340 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# 1.20.1 (UNRELEASED)
2+
3+
- New exception hierarchy: `AlreadyExistsError`, `InvalidSpecError`,
4+
`InvalidError`, `NotFoundError`, `AmbiguousError`, `AuthError`, and
5+
`CertificateError` all inherit from `GitError` and the appropriate Python
6+
built-in exception (`ValueError`/`KeyError`) for backward compatibility
7+
[#830](https://github.com/libgit2/pygit2/issues/830).
8+
9+
110
# 1.20.0 (2026-08-08)
211

312
- New `RemoteCallbacks.custom_headers()`

docs/general.rst

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,92 @@ Exception when trying to create an object (reference, etc) that already exists.
8181

8282
Exception when an input specification such as a reference name is invalid.
8383

84+
.. autoexception:: pygit2.InvalidError
85+
:members:
86+
:show-inheritance:
87+
:undoc-members:
88+
89+
Exception when an operation or input is invalid.
90+
91+
.. autoexception:: pygit2.NotFoundError
92+
:members:
93+
:show-inheritance:
94+
:undoc-members:
95+
96+
Exception when a requested object could not be found.
97+
98+
.. autoexception:: pygit2.AmbiguousError
99+
:members:
100+
:show-inheritance:
101+
:undoc-members:
102+
103+
Exception when more than one object matches.
104+
105+
.. autoexception:: pygit2.AuthError
106+
:members:
107+
:show-inheritance:
108+
:undoc-members:
109+
110+
Exception when an authentication error occurs.
111+
112+
.. autoexception:: pygit2.CertificateError
113+
:members:
114+
:show-inheritance:
115+
:undoc-members:
116+
117+
Exception when a server certificate is invalid.
118+
84119
.. autoexception:: pygit2.Passthrough
85120
:members:
86121
:show-inheritance:
87122
:undoc-members:
88123

89124
Exception that can be raised from a callback to tell libgit2 to behave as if
90125
that callback had not been set. See :doc:`callbacks` for details.
126+
127+
Error mapping
128+
=============
129+
130+
The following table shows how libgit2 error codes map to pygit2 exceptions.
131+
The new exception classes inherit from :py:exc:`pygit2.GitError` and, where
132+
noted, from a Python built-in exception for backward compatibility.
133+
134+
.. list-table::
135+
:header-rows: 1
136+
:widths: 35 35 30
137+
138+
* - pygit2 exception
139+
- libgit2 code / class
140+
- Built-in base
141+
142+
* - :py:exc:`AlreadyExistsError`
143+
- ``GIT_EEXISTS``
144+
- ``ValueError``
145+
146+
* - :py:exc:`InvalidSpecError`
147+
- ``GIT_EINVALIDSPEC``
148+
- ``ValueError``
149+
150+
* - :py:exc:`InvalidError`
151+
- ``GIT_EINVALID``, ``GIT_ERROR_INVALID``
152+
- ``ValueError``
153+
154+
* - :py:exc:`NotFoundError`
155+
- ``GIT_ENOTFOUND``
156+
- ``KeyError``
157+
158+
* - :py:exc:`AmbiguousError`
159+
- ``GIT_EAMBIGUOUS``
160+
- ``ValueError``
161+
162+
* - :py:exc:`AuthError`
163+
- ``GIT_EAUTH``
164+
-
165+
166+
* - :py:exc:`CertificateError`
167+
- ``GIT_ECERTIFICATE``
168+
-
169+
170+
* - :py:exc:`GitError`
171+
- generic / other errors
172+
-

pygit2/__init__.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@
248248
LIBGIT2_VER_MINOR,
249249
LIBGIT2_VER_REVISION,
250250
LIBGIT2_VERSION,
251-
AlreadyExistsError,
252251
Blob,
253252
Branch,
254253
Commit,
@@ -259,8 +258,6 @@
259258
DiffLine,
260259
DiffStats,
261260
FilterSource,
262-
GitError,
263-
InvalidSpecError,
264261
Mailmap,
265262
Note,
266263
Object,
@@ -306,7 +303,18 @@
306303
)
307304
from .config import Config
308305
from .credentials import *
309-
from .errors import Passthrough, check_error
306+
from .errors import (
307+
AlreadyExistsError,
308+
AmbiguousError,
309+
AuthError,
310+
CertificateError,
311+
GitError,
312+
InvalidError,
313+
InvalidSpecError,
314+
NotFoundError,
315+
Passthrough,
316+
check_error,
317+
)
310318
from .ffi import C, ffi
311319
from .filter import Filter
312320
from .index import Index, IndexEntry
@@ -592,8 +600,11 @@ def filter_unregister(name: str) -> None:
592600
'Object',
593601
'Reference',
594602
'AlreadyExistsError',
603+
'AmbiguousError',
604+
'AuthError',
595605
'Blob',
596606
'Branch',
607+
'CertificateError',
597608
'Commit',
598609
'Diff',
599610
'DiffDelta',
@@ -602,9 +613,11 @@ def filter_unregister(name: str) -> None:
602613
'DiffLine',
603614
'DiffStats',
604615
'GitError',
616+
'InvalidError',
605617
'InvalidSpecError',
606618
'Mailmap',
607619
'Note',
620+
'NotFoundError',
608621
'Odb',
609622
'OdbBackend',
610623
'OdbBackendLoose',

pygit2/_pygit2.pyi

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,10 @@ class Reference:
331331
def __lt__(self, other, /) -> bool: ...
332332
def __ne__(self, other, /) -> bool: ...
333333

334-
class AlreadyExistsError(ValueError): ...
334+
class AlreadyExistsError(GitError, ValueError): ...
335+
class AmbiguousError(GitError, ValueError): ...
336+
class AuthError(GitError): ...
337+
class InvalidError(GitError, ValueError): ...
335338

336339
@final
337340
class Blob(Object):
@@ -376,6 +379,8 @@ class Branch(Reference): # type: ignore[misc]
376379
def is_head(self) -> bool: ...
377380
def rename(self, name: str, force: bool = False) -> 'Branch': ... # type: ignore[override]
378381

382+
class CertificateError(GitError): ...
383+
379384
@final
380385
class Commit(_ObjectBase[GitCommitC]):
381386
_pointer: _Pointer[GitCommitC]
@@ -476,7 +481,7 @@ class FilterSource:
476481
flags: int
477482

478483
class GitError(Exception): ...
479-
class InvalidSpecError(ValueError): ...
484+
class InvalidSpecError(GitError, ValueError): ...
480485

481486
@final
482487
class Mailmap:
@@ -505,6 +510,8 @@ class Note:
505510
self, author: Signature, committer: Signature, ref: str = 'refs/notes/commits'
506511
) -> None: ...
507512

513+
class NotFoundError(GitError, KeyError): ...
514+
508515
@final
509516
class Odb:
510517
backends: Iterator[OdbBackend]

pygit2/errors.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,39 @@
2424
# Boston, MA 02110-1301, USA.
2525

2626
# Import from pygit2
27-
from ._pygit2 import GitError
27+
from ._pygit2 import (
28+
AlreadyExistsError,
29+
AmbiguousError,
30+
AuthError,
31+
CertificateError,
32+
GitError,
33+
InvalidError,
34+
InvalidSpecError,
35+
NotFoundError,
36+
)
2837
from .ffi import C, ffi
2938

30-
__all__ = ['GitError']
39+
__all__ = [
40+
'AlreadyExistsError',
41+
'AmbiguousError',
42+
'AuthError',
43+
'CertificateError',
44+
'GitError',
45+
'InvalidError',
46+
'InvalidSpecError',
47+
'NotFoundError',
48+
'Passthrough',
49+
]
3150

32-
value_errors = set([C.GIT_EEXISTS, C.GIT_EINVALIDSPEC, C.GIT_EAMBIGUOUS])
51+
# Docstrings for C-defined exception classes
52+
GitError.__doc__ = 'Generic libgit2 error.'
53+
AlreadyExistsError.__doc__ = 'Object already exists.'
54+
InvalidSpecError.__doc__ = 'Invalid name/ref spec.'
55+
NotFoundError.__doc__ = 'Requested object could not be found.'
56+
AmbiguousError.__doc__ = 'More than one object matches.'
57+
AuthError.__doc__ = 'Authentication error.'
58+
CertificateError.__doc__ = 'Server certificate is invalid.'
59+
InvalidError.__doc__ = 'Invalid operation or input.'
3360

3461

3562
def check_error(err: int, io: bool = False) -> None:
@@ -48,17 +75,32 @@ def check_error(err: int, io: bool = False) -> None:
4875
message = f'err {err} (no message provided)'
4976

5077
# Translate to Python errors
51-
if err in value_errors:
78+
if err == C.GIT_EEXISTS:
79+
raise AlreadyExistsError(message)
80+
81+
if err == C.GIT_EINVALIDSPEC:
82+
raise InvalidSpecError(message)
83+
84+
if err == C.GIT_EINVALID:
85+
raise InvalidError(message)
86+
87+
if err == C.GIT_EAMBIGUOUS:
88+
raise AmbiguousError(message)
89+
90+
if err == C.GIT_EBUFS:
5291
raise ValueError(message)
5392

93+
if err == C.GIT_EAUTH:
94+
raise AuthError(message)
95+
96+
if err == C.GIT_ECERTIFICATE:
97+
raise CertificateError(message)
98+
5499
if err == C.GIT_ENOTFOUND:
55100
if io:
56101
raise IOError(message)
57102

58-
raise KeyError(message)
59-
60-
if err == C.GIT_EINVALIDSPEC:
61-
raise ValueError(message)
103+
raise NotFoundError(message)
62104

63105
if err == C.GIT_ITEROVER:
64106
raise StopIteration()

src/error.c

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
extern PyObject *GitError;
3131
extern PyObject *AlreadyExistsError;
3232
extern PyObject *InvalidSpecError;
33+
extern PyObject *InvalidError;
34+
extern PyObject *NotFoundError;
35+
extern PyObject *AmbiguousError;
36+
extern PyObject *AuthError;
37+
extern PyObject *CertificateError;
3338

3439
PyObject *
3540
Error_type(int type)
@@ -39,15 +44,15 @@ Error_type(int type)
3944
switch (type) {
4045
/* Input does not exist in the scope searched. */
4146
case GIT_ENOTFOUND:
42-
return PyExc_KeyError;
47+
return NotFoundError;
4348

4449
/* A reference with this name already exists */
4550
case GIT_EEXISTS:
4651
return AlreadyExistsError;
4752

4853
/* The given short oid is ambiguous */
4954
case GIT_EAMBIGUOUS:
50-
return PyExc_ValueError;
55+
return AmbiguousError;
5156

5257
/* The buffer is too short to satisfy the request */
5358
case GIT_EBUFS:
@@ -57,6 +62,18 @@ Error_type(int type)
5762
case GIT_EINVALIDSPEC:
5863
return InvalidSpecError;
5964

65+
/* Invalid operation or input */
66+
case GIT_EINVALID:
67+
return InvalidError;
68+
69+
/* Authentication error */
70+
case GIT_EAUTH:
71+
return AuthError;
72+
73+
/* Server certificate is invalid */
74+
case GIT_ECERTIFICATE:
75+
return CertificateError;
76+
6077
/* Skip and passthrough the given ODB backend */
6178
case GIT_PASSTHROUGH:
6279
return GitError;
@@ -75,7 +92,7 @@ Error_type(int type)
7592
case GITERR_OS:
7693
return PyExc_OSError;
7794
case GITERR_INVALID:
78-
return PyExc_ValueError;
95+
return InvalidError;
7996
}
8097
}
8198
return GitError;
@@ -106,8 +123,8 @@ PyObject *
106123
Error_set_str(int err, const char *str)
107124
{
108125
if (err == GIT_ENOTFOUND) {
109-
/* KeyError expects the arg to be the missing key. */
110-
PyErr_SetString(PyExc_KeyError, str);
126+
/* NotFoundError inherits from KeyError; the argument is the missing key. */
127+
PyErr_SetString(NotFoundError, str);
111128
return NULL;
112129
}
113130

src/pygit2.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
PyObject *GitError;
4141
PyObject *AlreadyExistsError;
4242
PyObject *InvalidSpecError;
43+
PyObject *InvalidError;
44+
PyObject *NotFoundError;
45+
PyObject *AmbiguousError;
46+
PyObject *AuthError;
47+
PyObject *CertificateError;
4348

4449
PyObject *DeltaStatusEnum;
4550
PyObject *DiffFlagEnum;
@@ -461,8 +466,13 @@ PyInit__pygit2(void)
461466

462467
/* Exceptions */
463468
ADD_EXC(m, GitError, NULL);
464-
ADD_EXC(m, AlreadyExistsError, PyExc_ValueError);
465-
ADD_EXC(m, InvalidSpecError, PyExc_ValueError);
469+
ADD_EXC2(m, AlreadyExistsError, GitError, PyExc_ValueError);
470+
ADD_EXC2(m, InvalidSpecError, GitError, PyExc_ValueError);
471+
ADD_EXC2(m, InvalidError, GitError, PyExc_ValueError);
472+
ADD_EXC2(m, NotFoundError, GitError, PyExc_KeyError);
473+
ADD_EXC2(m, AmbiguousError, GitError, PyExc_ValueError);
474+
ADD_EXC(m, AuthError, GitError);
475+
ADD_EXC(m, CertificateError, GitError);
466476

467477
/* Repository */
468478
INIT_TYPE(RepositoryType, NULL, PyType_GenericNew)

0 commit comments

Comments
 (0)