From 9d7df463a17ea192c0b31b3c3cbb67032c716077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 11 Aug 2025 12:45:35 +0200 Subject: [PATCH 1/9] Move option handling to CFFI --- .gitignore | 2 + build.sh | 2 +- pygit2/__init__.py | 68 +++---- pygit2/_libgit2/ffi.pyi | 10 + pygit2/_pygit2.pyi | 105 ----------- pygit2/_run.py | 1 + pygit2/decl/options.h | 50 +++++ pygit2/enums.py | 75 ++++---- pygit2/options.py | 395 ++++++++++++++++++++++++++++++++++++++++ pygit2/settings.py | 2 +- src/options.c | 309 ------------------------------- src/options.h | 72 -------- src/pygit2.c | 35 ---- test/conftest.py | 2 +- test/test_options.py | 8 +- 15 files changed, 538 insertions(+), 598 deletions(-) create mode 100644 pygit2/decl/options.h create mode 100644 pygit2/options.py delete mode 100644 src/options.c delete mode 100644 src/options.h diff --git a/.gitignore b/.gitignore index 60e8a5500..0f5d52267 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ __pycache__/ *.pyc *.so *.swp +/pygit2/_libgit2.c +/pygit2/_libgit2.o diff --git a/build.sh b/build.sh index 31fea2443..c710a076c 100644 --- a/build.sh +++ b/build.sh @@ -178,7 +178,7 @@ if [ -n "$LIBGIT2_VERSION" ]; then wget https://github.com/libgit2/libgit2/archive/refs/tags/v$LIBGIT2_VERSION.tar.gz -N -O $FILENAME.tar.gz tar xf $FILENAME.tar.gz cd $FILENAME - mkdir build -p + mkdir -p build cd build if [ "$KERNEL" = "Darwin" ] && [ "$CIBUILDWHEEL" = "1" ]; then CMAKE_PREFIX_PATH=$OPENSSL_PREFIX:$PREFIX cmake .. \ diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 7b01d37c5..9f8acacb1 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -191,38 +191,6 @@ GIT_OID_HEXSZ, GIT_OID_MINPREFIXLEN, GIT_OID_RAWSZ, - GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, - GIT_OPT_ENABLE_CACHING, - GIT_OPT_ENABLE_FSYNC_GITDIR, - GIT_OPT_ENABLE_OFS_DELTA, - GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, - GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, - GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, - GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, - GIT_OPT_GET_CACHED_MEMORY, - GIT_OPT_GET_MWINDOW_FILE_LIMIT, - GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, - GIT_OPT_GET_MWINDOW_SIZE, - GIT_OPT_GET_OWNER_VALIDATION, - GIT_OPT_GET_PACK_MAX_OBJECTS, - GIT_OPT_GET_SEARCH_PATH, - GIT_OPT_GET_TEMPLATE_PATH, - GIT_OPT_GET_USER_AGENT, - GIT_OPT_GET_WINDOWS_SHAREMODE, - GIT_OPT_SET_ALLOCATOR, - GIT_OPT_SET_CACHE_MAX_SIZE, - GIT_OPT_SET_CACHE_OBJECT_LIMIT, - GIT_OPT_SET_MWINDOW_FILE_LIMIT, - GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, - GIT_OPT_SET_MWINDOW_SIZE, - GIT_OPT_SET_OWNER_VALIDATION, - GIT_OPT_SET_PACK_MAX_OBJECTS, - GIT_OPT_SET_SEARCH_PATH, - GIT_OPT_SET_SSL_CERT_LOCATIONS, - GIT_OPT_SET_SSL_CIPHERS, - GIT_OPT_SET_TEMPLATE_PATH, - GIT_OPT_SET_USER_AGENT, - GIT_OPT_SET_WINDOWS_SHAREMODE, GIT_REFERENCES_ALL, GIT_REFERENCES_BRANCHES, GIT_REFERENCES_TAGS, @@ -322,12 +290,46 @@ hash, hashfile, init_file_backend, - option, reference_is_valid_name, tree_entry_cmp, ) from .blame import Blame, BlameHunk from .blob import BlobIO +from .options import ( + option, + GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, + GIT_OPT_ENABLE_CACHING, + GIT_OPT_ENABLE_FSYNC_GITDIR, + GIT_OPT_ENABLE_OFS_DELTA, + GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, + GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, + GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, + GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, + GIT_OPT_GET_CACHED_MEMORY, + GIT_OPT_GET_MWINDOW_FILE_LIMIT, + GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_GET_MWINDOW_SIZE, + GIT_OPT_GET_OWNER_VALIDATION, + GIT_OPT_GET_PACK_MAX_OBJECTS, + GIT_OPT_GET_SEARCH_PATH, + GIT_OPT_GET_TEMPLATE_PATH, + GIT_OPT_GET_USER_AGENT, + GIT_OPT_GET_WINDOWS_SHAREMODE, + GIT_OPT_SET_ALLOCATOR, + GIT_OPT_SET_CACHE_MAX_SIZE, + GIT_OPT_SET_CACHE_OBJECT_LIMIT, + GIT_OPT_SET_MWINDOW_FILE_LIMIT, + GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_SET_MWINDOW_SIZE, + GIT_OPT_SET_OWNER_VALIDATION, + GIT_OPT_SET_PACK_MAX_OBJECTS, + GIT_OPT_SET_SEARCH_PATH, + GIT_OPT_SET_SSL_CERT_LOCATIONS, + GIT_OPT_SET_SSL_CIPHERS, + GIT_OPT_SET_TEMPLATE_PATH, + GIT_OPT_SET_USER_AGENT, + GIT_OPT_SET_WINDOWS_SHAREMODE, +) from .callbacks import ( CheckoutCallbacks, Payload, diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 0e865e311..9feb1791a 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -42,6 +42,9 @@ class int_c: class int64_t: def __getitem__(self, item: Literal[0]) -> int: ... +class ssize_t: + def __getitem__(self, item: Literal[0]) -> int: ... + class _Pointer(Generic[T]): def __setitem__(self, item: Literal[0], a: T) -> None: ... @overload @@ -319,6 +322,8 @@ def new( @overload def new(a: Literal['size_t *', 'size_t*']) -> size_t: ... @overload +def new(a: Literal['ssize_t *', 'ssize_t*']) -> ssize_t: ... +@overload def new(a: Literal['git_stash_save_options *']) -> GitStashSaveOptionsC: ... @overload def new(a: Literal['git_strarray *']) -> GitStrrayC: ... @@ -340,4 +345,9 @@ class buffer(bytes): @overload def __getitem__(self, item: slice[Any, Any, Any]) -> bytes: ... +@overload def cast(a: Literal['int'], b: object) -> int: ... +@overload +def cast(a: Literal['size_t'], b: object) -> int: ... +@overload +def cast(a: Literal['ssize_t'], b: object) -> int: ... diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 105acc2a9..0a2245be9 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -52,38 +52,6 @@ LIBGIT2_VER_MAJOR: int LIBGIT2_VER_MINOR: int LIBGIT2_VER_REVISION: int LIBGIT2_VERSION: str -GIT_OPT_GET_MWINDOW_SIZE: int -GIT_OPT_SET_MWINDOW_SIZE: int -GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: int -GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: int -GIT_OPT_GET_SEARCH_PATH: int -GIT_OPT_SET_SEARCH_PATH: int -GIT_OPT_SET_CACHE_OBJECT_LIMIT: int -GIT_OPT_SET_CACHE_MAX_SIZE: int -GIT_OPT_ENABLE_CACHING: int -GIT_OPT_GET_CACHED_MEMORY: int -GIT_OPT_GET_TEMPLATE_PATH: int -GIT_OPT_SET_TEMPLATE_PATH: int -GIT_OPT_SET_SSL_CERT_LOCATIONS: int -GIT_OPT_SET_USER_AGENT: int -GIT_OPT_ENABLE_STRICT_OBJECT_CREATION: int -GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION: int -GIT_OPT_SET_SSL_CIPHERS: int -GIT_OPT_GET_USER_AGENT: int -GIT_OPT_ENABLE_OFS_DELTA: int -GIT_OPT_ENABLE_FSYNC_GITDIR: int -GIT_OPT_GET_WINDOWS_SHAREMODE: int -GIT_OPT_SET_WINDOWS_SHAREMODE: int -GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION: int -GIT_OPT_SET_ALLOCATOR: int -GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY: int -GIT_OPT_GET_PACK_MAX_OBJECTS: int -GIT_OPT_SET_PACK_MAX_OBJECTS: int -GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS: int -GIT_OPT_GET_OWNER_VALIDATION: int -GIT_OPT_SET_OWNER_VALIDATION: int -GIT_OPT_GET_MWINDOW_FILE_LIMIT: int -GIT_OPT_SET_MWINDOW_FILE_LIMIT: int GIT_OID_RAWSZ: int GIT_OID_HEXSZ: int GIT_OID_HEX_ZERO: str @@ -887,79 +855,6 @@ def discover_repository( def hash(data: bytes | str) -> Oid: ... def hashfile(path: str) -> Oid: ... def init_file_backend(path: str, flags: int = 0) -> object: ... -@overload -def option( - opt: Literal[ - Option.GET_MWINDOW_FILE_LIMIT, - Option.GET_MWINDOW_MAPPED_LIMIT, - Option.GET_MWINDOW_SIZE, - ], -) -> int: ... -@overload -def option( - opt: Literal[ - Option.SET_MWINDOW_FILE_LIMIT, - Option.SET_MWINDOW_MAPPED_LIMIT, - Option.SET_MWINDOW_SIZE, - ], - value: int, -) -> None: ... -@overload -def option(opt: Literal[Option.GET_SEARCH_PATH], level: ConfigLevel) -> str: ... -@overload -def option( - opt: Literal[Option.SET_SEARCH_PATH], level: ConfigLevel, value: str -) -> None: ... -@overload -def option( - opt: Literal[Option.SET_CACHE_OBJECT_LIMIT], object_type: ObjectType, limit: int -) -> None: ... -@overload -def option(opt: Literal[Option.SET_CACHE_MAX_SIZE], max_size: int) -> None: ... -@overload -def option(opt: Literal[Option.GET_CACHED_MEMORY]) -> tuple[int, int]: ... - -# not implemented: -# Option.GET_TEMPLATE_PATH -# Option.SET_TEMPLATE_PATH - -@overload -def option( - opt: Literal[Option.SET_SSL_CERT_LOCATIONS], - file: str | bytes | None, - dir: str | bytes | None, -) -> None: ... - -# not implemented: -# Option.SET_USER_AGENT - -@overload -def option( - opt: Literal[ - Option.ENABLE_CACHING, - Option.ENABLE_STRICT_OBJECT_CREATION, - Option.ENABLE_STRICT_SYMBOLIC_REF_CREATION, - Option.ENABLE_OFS_DELTA, - Option.ENABLE_FSYNC_GITDIR, - Option.ENABLE_STRICT_HASH_VERIFICATION, - Option.ENABLE_UNSAVED_INDEX_SAFETY, - Option.DISABLE_PACK_KEEP_FILE_CHECKS, - Option.SET_OWNER_VALIDATION, - ], - value: bool | Literal[0, 1], -) -> None: ... -@overload -def option(opt: Literal[Option.GET_OWNER_VALIDATION]) -> int: ... - -# not implemented: -# Option.SET_SSL_CIPHERS -# Option.GET_USER_AGENT -# Option.GET_WINDOWS_SHAREMODE -# Option.SET_WINDOWS_SHAREMODE -# Option.SET_ALLOCATOR -# Option.GET_PACK_MAX_OBJECTS -# Option.SET_PACK_MAX_OBJECTS - def reference_is_valid_name(refname: str) -> bool: ... def tree_entry_cmp(a: Object, b: Object) -> int: ... def _cache_enums() -> None: ... diff --git a/pygit2/_run.py b/pygit2/_run.py index f64ba0faf..44f30344e 100644 --- a/pygit2/_run.py +++ b/pygit2/_run.py @@ -81,6 +81,7 @@ 'revert.h', 'stash.h', 'submodule.h', + 'options.h', 'callbacks.h', # Bridge from libgit2 to Python ] h_source = [] diff --git a/pygit2/decl/options.h b/pygit2/decl/options.h new file mode 100644 index 000000000..f6556d5e2 --- /dev/null +++ b/pygit2/decl/options.h @@ -0,0 +1,50 @@ +typedef enum { + GIT_OPT_GET_MWINDOW_SIZE, + GIT_OPT_SET_MWINDOW_SIZE, + GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, + GIT_OPT_GET_SEARCH_PATH, + GIT_OPT_SET_SEARCH_PATH, + GIT_OPT_SET_CACHE_OBJECT_LIMIT, + GIT_OPT_SET_CACHE_MAX_SIZE, + GIT_OPT_ENABLE_CACHING, + GIT_OPT_GET_CACHED_MEMORY, + GIT_OPT_GET_TEMPLATE_PATH, + GIT_OPT_SET_TEMPLATE_PATH, + GIT_OPT_SET_SSL_CERT_LOCATIONS, + GIT_OPT_SET_USER_AGENT, + GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, + GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, + GIT_OPT_SET_SSL_CIPHERS, + GIT_OPT_GET_USER_AGENT, + GIT_OPT_ENABLE_OFS_DELTA, + GIT_OPT_ENABLE_FSYNC_GITDIR, + GIT_OPT_GET_WINDOWS_SHAREMODE, + GIT_OPT_SET_WINDOWS_SHAREMODE, + GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, + GIT_OPT_SET_ALLOCATOR, + GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, + GIT_OPT_GET_PACK_MAX_OBJECTS, + GIT_OPT_SET_PACK_MAX_OBJECTS, + GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, + GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, + GIT_OPT_GET_MWINDOW_FILE_LIMIT, + GIT_OPT_SET_MWINDOW_FILE_LIMIT, + GIT_OPT_SET_ODB_PACKED_PRIORITY, + GIT_OPT_SET_ODB_LOOSE_PRIORITY, + GIT_OPT_GET_EXTENSIONS, + GIT_OPT_SET_EXTENSIONS, + GIT_OPT_GET_OWNER_VALIDATION, + GIT_OPT_SET_OWNER_VALIDATION, + GIT_OPT_GET_HOMEDIR, + GIT_OPT_SET_HOMEDIR, + GIT_OPT_SET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_GET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_SET_SERVER_TIMEOUT, + GIT_OPT_GET_SERVER_TIMEOUT, + GIT_OPT_SET_USER_AGENT_PRODUCT, + GIT_OPT_GET_USER_AGENT_PRODUCT, + GIT_OPT_ADD_SSL_X509_CERT +} git_libgit2_opt_t; + +int git_libgit2_opts(int option, ...); \ No newline at end of file diff --git a/pygit2/enums.py b/pygit2/enums.py index fe6421686..80be63a5b 100644 --- a/pygit2/enums.py +++ b/pygit2/enums.py @@ -26,6 +26,7 @@ from enum import IntEnum, IntFlag from . import _pygit2 +from . import options from .ffi import C @@ -948,45 +949,45 @@ class Option(IntEnum): """Global libgit2 library options""" # Commented out values --> exists in libgit2 but not supported in pygit2's options.c yet - GET_MWINDOW_SIZE = _pygit2.GIT_OPT_GET_MWINDOW_SIZE - SET_MWINDOW_SIZE = _pygit2.GIT_OPT_SET_MWINDOW_SIZE - GET_MWINDOW_MAPPED_LIMIT = _pygit2.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT - SET_MWINDOW_MAPPED_LIMIT = _pygit2.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT - GET_SEARCH_PATH = _pygit2.GIT_OPT_GET_SEARCH_PATH - SET_SEARCH_PATH = _pygit2.GIT_OPT_SET_SEARCH_PATH - SET_CACHE_OBJECT_LIMIT = _pygit2.GIT_OPT_SET_CACHE_OBJECT_LIMIT - SET_CACHE_MAX_SIZE = _pygit2.GIT_OPT_SET_CACHE_MAX_SIZE - ENABLE_CACHING = _pygit2.GIT_OPT_ENABLE_CACHING - GET_CACHED_MEMORY = _pygit2.GIT_OPT_GET_CACHED_MEMORY - GET_TEMPLATE_PATH = _pygit2.GIT_OPT_GET_TEMPLATE_PATH - SET_TEMPLATE_PATH = _pygit2.GIT_OPT_SET_TEMPLATE_PATH - SET_SSL_CERT_LOCATIONS = _pygit2.GIT_OPT_SET_SSL_CERT_LOCATIONS - SET_USER_AGENT = _pygit2.GIT_OPT_SET_USER_AGENT - ENABLE_STRICT_OBJECT_CREATION = _pygit2.GIT_OPT_ENABLE_STRICT_OBJECT_CREATION + GET_MWINDOW_SIZE = options.GIT_OPT_GET_MWINDOW_SIZE + SET_MWINDOW_SIZE = options.GIT_OPT_SET_MWINDOW_SIZE + GET_MWINDOW_MAPPED_LIMIT = options.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT + SET_MWINDOW_MAPPED_LIMIT = options.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT + GET_SEARCH_PATH = options.GIT_OPT_GET_SEARCH_PATH + SET_SEARCH_PATH = options.GIT_OPT_SET_SEARCH_PATH + SET_CACHE_OBJECT_LIMIT = options.GIT_OPT_SET_CACHE_OBJECT_LIMIT + SET_CACHE_MAX_SIZE = options.GIT_OPT_SET_CACHE_MAX_SIZE + ENABLE_CACHING = options.GIT_OPT_ENABLE_CACHING + GET_CACHED_MEMORY = options.GIT_OPT_GET_CACHED_MEMORY + GET_TEMPLATE_PATH = options.GIT_OPT_GET_TEMPLATE_PATH + SET_TEMPLATE_PATH = options.GIT_OPT_SET_TEMPLATE_PATH + SET_SSL_CERT_LOCATIONS = options.GIT_OPT_SET_SSL_CERT_LOCATIONS + SET_USER_AGENT = options.GIT_OPT_SET_USER_AGENT + ENABLE_STRICT_OBJECT_CREATION = options.GIT_OPT_ENABLE_STRICT_OBJECT_CREATION ENABLE_STRICT_SYMBOLIC_REF_CREATION = ( - _pygit2.GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION + options.GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION ) - SET_SSL_CIPHERS = _pygit2.GIT_OPT_SET_SSL_CIPHERS - GET_USER_AGENT = _pygit2.GIT_OPT_GET_USER_AGENT - ENABLE_OFS_DELTA = _pygit2.GIT_OPT_ENABLE_OFS_DELTA - ENABLE_FSYNC_GITDIR = _pygit2.GIT_OPT_ENABLE_FSYNC_GITDIR - GET_WINDOWS_SHAREMODE = _pygit2.GIT_OPT_GET_WINDOWS_SHAREMODE - SET_WINDOWS_SHAREMODE = _pygit2.GIT_OPT_SET_WINDOWS_SHAREMODE - ENABLE_STRICT_HASH_VERIFICATION = _pygit2.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION - SET_ALLOCATOR = _pygit2.GIT_OPT_SET_ALLOCATOR - ENABLE_UNSAVED_INDEX_SAFETY = _pygit2.GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY - GET_PACK_MAX_OBJECTS = _pygit2.GIT_OPT_GET_PACK_MAX_OBJECTS - SET_PACK_MAX_OBJECTS = _pygit2.GIT_OPT_SET_PACK_MAX_OBJECTS - DISABLE_PACK_KEEP_FILE_CHECKS = _pygit2.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS - # ENABLE_HTTP_EXPECT_CONTINUE = _pygit2.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE - GET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_GET_MWINDOW_FILE_LIMIT - SET_MWINDOW_FILE_LIMIT = _pygit2.GIT_OPT_SET_MWINDOW_FILE_LIMIT - # SET_ODB_PACKED_PRIORITY = _pygit2.GIT_OPT_SET_ODB_PACKED_PRIORITY - # SET_ODB_LOOSE_PRIORITY = _pygit2.GIT_OPT_SET_ODB_LOOSE_PRIORITY - # GET_EXTENSIONS = _pygit2.GIT_OPT_GET_EXTENSIONS - # SET_EXTENSIONS = _pygit2.GIT_OPT_SET_EXTENSIONS - GET_OWNER_VALIDATION = _pygit2.GIT_OPT_GET_OWNER_VALIDATION - SET_OWNER_VALIDATION = _pygit2.GIT_OPT_SET_OWNER_VALIDATION + SET_SSL_CIPHERS = options.GIT_OPT_SET_SSL_CIPHERS + GET_USER_AGENT = options.GIT_OPT_GET_USER_AGENT + ENABLE_OFS_DELTA = options.GIT_OPT_ENABLE_OFS_DELTA + ENABLE_FSYNC_GITDIR = options.GIT_OPT_ENABLE_FSYNC_GITDIR + GET_WINDOWS_SHAREMODE = options.GIT_OPT_GET_WINDOWS_SHAREMODE + SET_WINDOWS_SHAREMODE = options.GIT_OPT_SET_WINDOWS_SHAREMODE + ENABLE_STRICT_HASH_VERIFICATION = options.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION + SET_ALLOCATOR = options.GIT_OPT_SET_ALLOCATOR + ENABLE_UNSAVED_INDEX_SAFETY = options.GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY + GET_PACK_MAX_OBJECTS = options.GIT_OPT_GET_PACK_MAX_OBJECTS + SET_PACK_MAX_OBJECTS = options.GIT_OPT_SET_PACK_MAX_OBJECTS + DISABLE_PACK_KEEP_FILE_CHECKS = options.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS + # ENABLE_HTTP_EXPECT_CONTINUE = options.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE + GET_MWINDOW_FILE_LIMIT = options.GIT_OPT_GET_MWINDOW_FILE_LIMIT + SET_MWINDOW_FILE_LIMIT = options.GIT_OPT_SET_MWINDOW_FILE_LIMIT + # SET_ODB_PACKED_PRIORITY = options.GIT_OPT_SET_ODB_PACKED_PRIORITY + # SET_ODB_LOOSE_PRIORITY = options.GIT_OPT_SET_ODB_LOOSE_PRIORITY + # GET_EXTENSIONS = options.GIT_OPT_GET_EXTENSIONS + # SET_EXTENSIONS = options.GIT_OPT_SET_EXTENSIONS + GET_OWNER_VALIDATION = options.GIT_OPT_GET_OWNER_VALIDATION + SET_OWNER_VALIDATION = options.GIT_OPT_SET_OWNER_VALIDATION # GET_HOMEDIR = _pygit2.GIT_OPT_GET_HOMEDIR # SET_HOMEDIR = _pygit2.GIT_OPT_SET_HOMEDIR # SET_SERVER_CONNECT_TIMEOUT = _pygit2.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT diff --git a/pygit2/options.py b/pygit2/options.py new file mode 100644 index 000000000..4f63234e8 --- /dev/null +++ b/pygit2/options.py @@ -0,0 +1,395 @@ +from __future__ import annotations +# Copyright 2010-2025 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +""" +Libgit2 global options management using CFFI. +""" + +from typing import Any, Literal, Optional, Tuple, Union, overload + +from .ffi import C, ffi +from .errors import check_error +from .utils import to_bytes, to_str + +# Import only for type checking to avoid circular imports +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .enums import ConfigLevel, ObjectType, Option + from ._libgit2.ffi import ArrayC, NULL_TYPE, char + +# Export GIT_OPT constants for backward compatibility +GIT_OPT_GET_MWINDOW_SIZE: int = C.GIT_OPT_GET_MWINDOW_SIZE +GIT_OPT_SET_MWINDOW_SIZE: int = C.GIT_OPT_SET_MWINDOW_SIZE +GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: int = C.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT +GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: int = C.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT +GIT_OPT_GET_SEARCH_PATH: int = C.GIT_OPT_GET_SEARCH_PATH +GIT_OPT_SET_SEARCH_PATH: int = C.GIT_OPT_SET_SEARCH_PATH +GIT_OPT_SET_CACHE_OBJECT_LIMIT: int = C.GIT_OPT_SET_CACHE_OBJECT_LIMIT +GIT_OPT_SET_CACHE_MAX_SIZE: int = C.GIT_OPT_SET_CACHE_MAX_SIZE +GIT_OPT_ENABLE_CACHING: int = C.GIT_OPT_ENABLE_CACHING +GIT_OPT_GET_CACHED_MEMORY: int = C.GIT_OPT_GET_CACHED_MEMORY +GIT_OPT_GET_TEMPLATE_PATH: int = C.GIT_OPT_GET_TEMPLATE_PATH +GIT_OPT_SET_TEMPLATE_PATH: int = C.GIT_OPT_SET_TEMPLATE_PATH +GIT_OPT_SET_SSL_CERT_LOCATIONS: int = C.GIT_OPT_SET_SSL_CERT_LOCATIONS +GIT_OPT_SET_USER_AGENT: int = C.GIT_OPT_SET_USER_AGENT +GIT_OPT_ENABLE_STRICT_OBJECT_CREATION: int = C.GIT_OPT_ENABLE_STRICT_OBJECT_CREATION +GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION: int = ( + C.GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION +) +GIT_OPT_SET_SSL_CIPHERS: int = C.GIT_OPT_SET_SSL_CIPHERS +GIT_OPT_GET_USER_AGENT: int = C.GIT_OPT_GET_USER_AGENT +GIT_OPT_ENABLE_OFS_DELTA: int = C.GIT_OPT_ENABLE_OFS_DELTA +GIT_OPT_ENABLE_FSYNC_GITDIR: int = C.GIT_OPT_ENABLE_FSYNC_GITDIR +GIT_OPT_GET_WINDOWS_SHAREMODE: int = C.GIT_OPT_GET_WINDOWS_SHAREMODE +GIT_OPT_SET_WINDOWS_SHAREMODE: int = C.GIT_OPT_SET_WINDOWS_SHAREMODE +GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION: int = C.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION +GIT_OPT_SET_ALLOCATOR: int = C.GIT_OPT_SET_ALLOCATOR +GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY: int = C.GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY +GIT_OPT_GET_PACK_MAX_OBJECTS: int = C.GIT_OPT_GET_PACK_MAX_OBJECTS +GIT_OPT_SET_PACK_MAX_OBJECTS: int = C.GIT_OPT_SET_PACK_MAX_OBJECTS +GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS: int = C.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS +GIT_OPT_GET_MWINDOW_FILE_LIMIT: int = C.GIT_OPT_GET_MWINDOW_FILE_LIMIT +GIT_OPT_SET_MWINDOW_FILE_LIMIT: int = C.GIT_OPT_SET_MWINDOW_FILE_LIMIT +GIT_OPT_GET_OWNER_VALIDATION: int = C.GIT_OPT_GET_OWNER_VALIDATION +GIT_OPT_SET_OWNER_VALIDATION: int = C.GIT_OPT_SET_OWNER_VALIDATION + + +NOT_PASSED = object() + + +def check_args(option: Option, arg1: Any, arg2: Any, expected: int) -> None: + if expected == 0 and (arg1 is not NOT_PASSED or arg2 is not NOT_PASSED): + raise TypeError(f"option({option}) takes no additional arguments") + + if expected == 1 and (arg1 is NOT_PASSED or arg2 is not NOT_PASSED): + raise TypeError(f"option({option}, x) requires 1 additional argument") + + if expected == 2 and (arg1 is NOT_PASSED or arg2 is NOT_PASSED): + raise TypeError(f"option({option}, x, y) requires 2 additional arguments") + + +@overload +def option( + option_type: Union[ + Literal[Option.GET_MWINDOW_SIZE], + Literal[Option.GET_MWINDOW_MAPPED_LIMIT], + Literal[Option.GET_MWINDOW_FILE_LIMIT], + ], +) -> int: ... + + +@overload +def option( + option_type: Union[ + Literal[Option.SET_MWINDOW_SIZE], + Literal[Option.SET_MWINDOW_MAPPED_LIMIT], + Literal[Option.SET_MWINDOW_FILE_LIMIT], + Literal[Option.SET_CACHE_MAX_SIZE], + ], + arg1: int, # value +) -> None: ... + + +@overload +def option( + option_type: Literal[Option.GET_SEARCH_PATH], + arg1: ConfigLevel, # value +) -> str: ... + + +@overload +def option( + option_type: Literal[Option.SET_SEARCH_PATH], + arg1: ConfigLevel, # type + arg2: str, # value +) -> None: ... + + +@overload +def option( + option_type: Literal[Option.SET_CACHE_OBJECT_LIMIT], + arg1: ObjectType, # type + arg2: int, # limit +) -> None: ... + + +@overload +def option(option_type: Literal[Option.GET_CACHED_MEMORY]) -> Tuple[int, int]: ... + + +@overload +def option( + option_type: Literal[Option.SET_SSL_CERT_LOCATIONS], + arg1: Optional[str | bytes], # cert_file + arg2: Optional[str | bytes], # cert_dir +) -> None: ... + + +@overload +def option( + option_type: Union[ + Literal[Option.ENABLE_CACHING], + Literal[Option.ENABLE_STRICT_OBJECT_CREATION], + Literal[Option.ENABLE_STRICT_SYMBOLIC_REF_CREATION], + Literal[Option.ENABLE_OFS_DELTA], + Literal[Option.ENABLE_FSYNC_GITDIR], + Literal[Option.ENABLE_STRICT_HASH_VERIFICATION], + Literal[Option.ENABLE_UNSAVED_INDEX_SAFETY], + Literal[Option.DISABLE_PACK_KEEP_FILE_CHECKS], + Literal[Option.SET_OWNER_VALIDATION], + ], + arg1: bool, # value +) -> None: ... + + +@overload +def option(option_type: Literal[Option.GET_OWNER_VALIDATION]) -> bool: ... + + +# Fallback overload for generic Option values (used in tests) +@overload +def option(option_type: Option, arg1: Any = ..., arg2: Any = ...) -> Any: ... + + +def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) -> Any: + """ + Get or set a libgit2 option. + + Parameters: + + GIT_OPT_GET_SEARCH_PATH, level + Get the config search path for the given level. + + GIT_OPT_SET_SEARCH_PATH, level, path + Set the config search path for the given level. + + GIT_OPT_GET_MWINDOW_SIZE + Get the maximum mmap window size. + + GIT_OPT_SET_MWINDOW_SIZE, size + Set the maximum mmap window size. + + GIT_OPT_GET_MWINDOW_FILE_LIMIT + Get the maximum number of files that will be mapped at any time by the library. + + GIT_OPT_SET_MWINDOW_FILE_LIMIT, size + Set the maximum number of files that can be mapped at any time by the library. The default (0) is unlimited. + + GIT_OPT_GET_OWNER_VALIDATION + Gets the owner validation setting for repository directories. + + GIT_OPT_SET_OWNER_VALIDATION, enabled + Set that repository directories should be owned by the current user. + The default is to validate ownership. + """ + + # Handle GET options with size_t output + if option_type in ( + C.GIT_OPT_GET_MWINDOW_SIZE, + C.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, + C.GIT_OPT_GET_MWINDOW_FILE_LIMIT, + ): + check_args(option_type, arg1, arg2, 0) + + size_ptr = ffi.new("size_t *") + err = C.git_libgit2_opts(option_type, size_ptr) + check_error(err) + return size_ptr[0] + + # Handle SET options with size_t input + elif option_type in ( + C.GIT_OPT_SET_MWINDOW_SIZE, + C.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, + C.GIT_OPT_SET_MWINDOW_FILE_LIMIT, + ): + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1)}" + ) + size = arg1 + if size < 0: + raise ValueError("size must be non-negative") + + err = C.git_libgit2_opts(option_type, ffi.cast("size_t", size)) + check_error(err) + return None + + # Handle GET_SEARCH_PATH + elif option_type == C.GIT_OPT_GET_SEARCH_PATH: + check_args(option_type, arg1, arg2, 1) + + level = int(arg1) # Convert enum to int + buf = ffi.new("git_buf *") + err = C.git_libgit2_opts(option_type, ffi.cast("int", level), buf) + check_error(err) + + try: + if buf.ptr != ffi.NULL: + result = to_str(ffi.string(buf.ptr)) + else: + result = None + finally: + C.git_buf_dispose(buf) + + return result + + # Handle SET_SEARCH_PATH + elif option_type == C.GIT_OPT_SET_SEARCH_PATH: + check_args(option_type, arg1, arg2, 2) + + level = int(arg1) # Convert enum to int + path = arg2 + + path_cdata: ArrayC[char] | NULL_TYPE + if path is None: + path_cdata = ffi.NULL + else: + path_bytes = to_bytes(path) + path_cdata = ffi.new("char[]", path_bytes) + + err = C.git_libgit2_opts(option_type, ffi.cast("int", level), path_cdata) + check_error(err) + return None + + # Handle SET_CACHE_OBJECT_LIMIT + elif option_type == C.GIT_OPT_SET_CACHE_OBJECT_LIMIT: + check_args(option_type, arg1, arg2, 2) + + object_type = int(arg1) # Convert enum to int + if not isinstance(arg2, int): + raise TypeError( + f"option value must be an integer, not {type(arg2).__name__}" + ) + size = arg2 + if size < 0: + raise ValueError("size must be non-negative") + + err = C.git_libgit2_opts( + option_type, ffi.cast("int", object_type), ffi.cast("size_t", size) + ) + check_error(err) + return None + + # Handle SET_CACHE_MAX_SIZE + elif option_type == C.GIT_OPT_SET_CACHE_MAX_SIZE: + check_args(option_type, arg1, arg2, 1) + + size = arg1 + if not isinstance(size, int): + raise TypeError( + f"option value must be an integer, not {type(size).__name__}" + ) + + err = C.git_libgit2_opts(option_type, ffi.cast("ssize_t", size)) + check_error(err) + return None + + # Handle GET_CACHED_MEMORY + elif option_type == C.GIT_OPT_GET_CACHED_MEMORY: + check_args(option_type, arg1, arg2, 0) + + current_ptr = ffi.new("ssize_t *") + allowed_ptr = ffi.new("ssize_t *") + err = C.git_libgit2_opts(option_type, current_ptr, allowed_ptr) + check_error(err) + return (current_ptr[0], allowed_ptr[0]) + + # Handle SET_SSL_CERT_LOCATIONS + elif option_type == C.GIT_OPT_SET_SSL_CERT_LOCATIONS: + check_args(option_type, arg1, arg2, 2) + + cert_file = arg1 + cert_dir = arg2 + + cert_file_cdata: ArrayC[char] | NULL_TYPE + if cert_file is None: + cert_file_cdata = ffi.NULL + else: + cert_file_bytes = to_bytes(cert_file) + cert_file_cdata = ffi.new("char[]", cert_file_bytes) + + cert_dir_cdata: ArrayC[char] | NULL_TYPE + if cert_dir is None: + cert_dir_cdata = ffi.NULL + else: + cert_dir_bytes = to_bytes(cert_dir) + cert_dir_cdata = ffi.new("char[]", cert_dir_bytes) + + err = C.git_libgit2_opts(option_type, cert_file_cdata, cert_dir_cdata) + check_error(err) + return None + + # Handle boolean/int enable/disable options + elif option_type in ( + C.GIT_OPT_ENABLE_CACHING, + C.GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, + C.GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, + C.GIT_OPT_ENABLE_OFS_DELTA, + C.GIT_OPT_ENABLE_FSYNC_GITDIR, + C.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, + C.GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, + C.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, + C.GIT_OPT_SET_OWNER_VALIDATION, + ): + check_args(option_type, arg1, arg2, 1) + + enabled = arg1 + # Convert to int (0 or 1) + value = 1 if enabled else 0 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", value)) + check_error(err) + return None + + # Handle GET_OWNER_VALIDATION + elif option_type == C.GIT_OPT_GET_OWNER_VALIDATION: + check_args(option_type, arg1, arg2, 0) + + enabled_ptr = ffi.new("int *") + err = C.git_libgit2_opts(option_type, enabled_ptr) + check_error(err) + return bool(enabled_ptr[0]) + + # Not implemented options + elif option_type in ( + C.GIT_OPT_GET_TEMPLATE_PATH, + C.GIT_OPT_SET_TEMPLATE_PATH, + C.GIT_OPT_SET_USER_AGENT, + C.GIT_OPT_SET_SSL_CIPHERS, + C.GIT_OPT_GET_USER_AGENT, + C.GIT_OPT_GET_WINDOWS_SHAREMODE, + C.GIT_OPT_SET_WINDOWS_SHAREMODE, + C.GIT_OPT_SET_ALLOCATOR, + C.GIT_OPT_GET_PACK_MAX_OBJECTS, + C.GIT_OPT_SET_PACK_MAX_OBJECTS, + ): + return NotImplemented + + else: + raise ValueError(f"Invalid option {option_type}") diff --git a/pygit2/settings.py b/pygit2/settings.py index da63d9798..a6e2a100c 100644 --- a/pygit2/settings.py +++ b/pygit2/settings.py @@ -32,7 +32,7 @@ import pygit2.enums -from ._pygit2 import option +from .options import option from .enums import ConfigLevel, Option from .errors import GitError diff --git a/src/options.c b/src/options.c deleted file mode 100644 index 11711400b..000000000 --- a/src/options.c +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright 2010-2025 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#define PY_SSIZE_T_CLEAN -#include -#include -#include "error.h" -#include "types.h" -#include "utils.h" - -extern PyObject *GitError; - -static PyObject * -get_search_path(long level) -{ - git_buf buf = {NULL}; - PyObject *py_path; - int err; - - err = git_libgit2_opts(GIT_OPT_GET_SEARCH_PATH, level, &buf); - if (err < 0) - return Error_set(err); - - py_path = to_unicode_n(buf.ptr, buf.size, NULL, NULL); - git_buf_dispose(&buf); - - if (!py_path) - return NULL; - - return py_path; -} - -PyObject * -option(PyObject *self, PyObject *args) -{ - long option; - int error; - PyObject *py_option; - - py_option = PyTuple_GetItem(args, 0); - if (!py_option) - return NULL; - - if (!PyLong_Check(py_option)) - return Error_type_error( - "option should be an integer, got %.200s", py_option); - - option = PyLong_AsLong(py_option); - - switch (option) { - - case GIT_OPT_GET_MWINDOW_FILE_LIMIT: - case GIT_OPT_GET_MWINDOW_MAPPED_LIMIT: - case GIT_OPT_GET_MWINDOW_SIZE: - { - size_t value; - - error = git_libgit2_opts(option, &value); - if (error < 0) - return Error_set(error); - - return PyLong_FromSize_t(value); - } - - case GIT_OPT_SET_MWINDOW_FILE_LIMIT: - case GIT_OPT_SET_MWINDOW_MAPPED_LIMIT: - case GIT_OPT_SET_MWINDOW_SIZE: - { - PyObject *py_value = PyTuple_GetItem(args, 1); - if (!py_value) - return NULL; - - if (!PyLong_Check(py_value)) - return Error_type_error("expected integer, got %.200s", py_value); - - size_t value = PyLong_AsSize_t(py_value); - error = git_libgit2_opts(option, value); - if (error < 0) - return Error_set(error); - - Py_RETURN_NONE; - } - - case GIT_OPT_GET_SEARCH_PATH: - { - PyObject *py_level = PyTuple_GetItem(args, 1); - if (!py_level) - return NULL; - - if (!PyLong_Check(py_level)) - return Error_type_error("level should be an integer, got %.200s", py_level); - - return get_search_path(PyLong_AsLong(py_level)); - } - - case GIT_OPT_SET_SEARCH_PATH: - { - PyObject *py_level = PyTuple_GetItem(args, 1); - if (!py_level) - return NULL; - - PyObject *py_path = PyTuple_GetItem(args, 2); - if (!py_path) - return NULL; - - if (!PyLong_Check(py_level)) - return Error_type_error("level should be an integer, got %.200s", py_level); - - const char *path = pgit_borrow(py_path); - if (!path) - return NULL; - - int err = git_libgit2_opts(option, PyLong_AsLong(py_level), path); - if (err < 0) - return Error_set(err); - - Py_RETURN_NONE; - } - - case GIT_OPT_SET_CACHE_OBJECT_LIMIT: - { - size_t limit; - int object_type; - PyObject *py_object_type, *py_limit; - - py_object_type = PyTuple_GetItem(args, 1); - if (!py_object_type) - return NULL; - - py_limit = PyTuple_GetItem(args, 2); - if (!py_limit) - return NULL; - - if (!PyLong_Check(py_limit)) - return Error_type_error( - "limit should be an integer, got %.200s", py_limit); - - object_type = PyLong_AsLong(py_object_type); - limit = PyLong_AsSize_t(py_limit); - error = git_libgit2_opts(option, object_type, limit); - - if (error < 0) - return Error_set(error); - - Py_RETURN_NONE; - } - - case GIT_OPT_SET_CACHE_MAX_SIZE: - { - size_t max_size; - PyObject *py_max_size; - - py_max_size = PyTuple_GetItem(args, 1); - if (!py_max_size) - return NULL; - - if (!PyLong_Check(py_max_size)) - return Error_type_error( - "max_size should be an integer, got %.200s", py_max_size); - - max_size = PyLong_AsSize_t(py_max_size); - error = git_libgit2_opts(option, max_size); - if (error < 0) - return Error_set(error); - - Py_RETURN_NONE; - } - - case GIT_OPT_GET_CACHED_MEMORY: - { - size_t current; - size_t allowed; - PyObject* tup = PyTuple_New(2); - - error = git_libgit2_opts(option, ¤t, &allowed); - if (error < 0) - return Error_set(error); - - PyTuple_SetItem(tup, 0, PyLong_FromLong(current)); - PyTuple_SetItem(tup, 1, PyLong_FromLong(allowed)); - - return tup; - } - - case GIT_OPT_GET_TEMPLATE_PATH: - case GIT_OPT_SET_TEMPLATE_PATH: - { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - case GIT_OPT_SET_SSL_CERT_LOCATIONS: - { - PyObject *py_file, *py_dir; - char *file_path=NULL, *dir_path=NULL; - int err; - - py_file = PyTuple_GetItem(args, 1); - if (!py_file) - return NULL; - py_dir = PyTuple_GetItem(args, 2); - if (!py_dir) - return NULL; - - /* py_file and py_dir are only valid if they are strings */ - PyObject *tvalue_file = NULL; - if (PyUnicode_Check(py_file) || PyBytes_Check(py_file)) - file_path = pgit_borrow_fsdefault(py_file, &tvalue_file); - - PyObject *tvalue_dir = NULL; - if (PyUnicode_Check(py_dir) || PyBytes_Check(py_dir)) - dir_path = pgit_borrow_fsdefault(py_dir, &tvalue_dir); - - err = git_libgit2_opts(option, file_path, dir_path); - Py_XDECREF(tvalue_file); - Py_XDECREF(tvalue_dir); - - if (err) - return Error_set(err); - - Py_RETURN_NONE; - } - - case GIT_OPT_SET_USER_AGENT: - { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - // int enabled - case GIT_OPT_ENABLE_CACHING: - case GIT_OPT_ENABLE_STRICT_OBJECT_CREATION: - case GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION: - case GIT_OPT_ENABLE_OFS_DELTA: - case GIT_OPT_ENABLE_FSYNC_GITDIR: - case GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION: - case GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY: - case GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS: - case GIT_OPT_SET_OWNER_VALIDATION: - { - PyObject *py_value = PyTuple_GetItem(args, 1); - if (!py_value) - return NULL; - - if (!PyLong_Check(py_value)) - return Error_type_error("expected integer, got %.200s", py_value); - - int value = PyLong_AsSize_t(py_value); - error = git_libgit2_opts(option, value); - if (error < 0) - return Error_set(error); - - Py_RETURN_NONE; - } - - // int enabled getter - case GIT_OPT_GET_OWNER_VALIDATION: - { - int enabled; - - error = git_libgit2_opts(option, &enabled); - if (error < 0) - return Error_set(error); - - return PyLong_FromLong(enabled); - } - - // Not implemented - case GIT_OPT_SET_SSL_CIPHERS: - case GIT_OPT_GET_USER_AGENT: - case GIT_OPT_GET_WINDOWS_SHAREMODE: - case GIT_OPT_SET_WINDOWS_SHAREMODE: - case GIT_OPT_SET_ALLOCATOR: - case GIT_OPT_GET_PACK_MAX_OBJECTS: - case GIT_OPT_SET_PACK_MAX_OBJECTS: - { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - } - - PyErr_SetString(PyExc_ValueError, "unknown/unsupported option value"); - return NULL; -} diff --git a/src/options.h b/src/options.h deleted file mode 100644 index f8b9a08e1..000000000 --- a/src/options.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2010-2025 The pygit2 contributors - * - * This file is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, version 2, - * as published by the Free Software Foundation. - * - * In addition to the permissions in the GNU General Public License, - * the authors give you unlimited permission to link the compiled - * version of this file into combinations with other programs, - * and to distribute those combinations without any restriction - * coming from the use of this file. (The General Public License - * restrictions do apply in other respects; for example, they cover - * modification of the file, and distribution when not linked into - * a combined executable.) - * - * This file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -#ifndef INCLUDE_pygit2_blame_h -#define INCLUDE_pygit2_blame_h - -#define PY_SSIZE_T_CLEAN -#include -#include -#include "types.h" - -PyDoc_STRVAR(option__doc__, - "option(option, ...)\n" - "\n" - "Get or set a libgit2 option.\n" - "\n" - "Parameters:\n" - "\n" - "GIT_OPT_GET_SEARCH_PATH, level\n" - " Get the config search path for the given level.\n" - "\n" - "GIT_OPT_SET_SEARCH_PATH, level, path\n" - " Set the config search path for the given level.\n" - "\n" - "GIT_OPT_GET_MWINDOW_SIZE\n" - " Get the maximum mmap window size.\n" - "\n" - "GIT_OPT_SET_MWINDOW_SIZE, size\n" - " Set the maximum mmap window size.\n" - "\n" - "GIT_OPT_GET_MWINDOW_FILE_LIMIT\n" - " Get the maximum number of files that will be mapped at any time by the library.\n" - "\n" - "GIT_OPT_SET_MWINDOW_FILE_LIMIT, size\n" - " Set the maximum number of files that can be mapped at any time by the library. The default (0) is unlimited.\n" - "\n" - "GIT_OPT_GET_OWNER_VALIDATION\n" - " Gets the owner validation setting for repository directories.\n" - "\n" - "GIT_OPT_SET_OWNER_VALIDATION, enabled\n" - " Set that repository directories should be owned by the current user.\n" - " The default is to validate ownership.\n" - ); - - -PyObject *option(PyObject *self, PyObject *args); - -#endif diff --git a/src/pygit2.c b/src/pygit2.c index ca4739680..c2bac5e5e 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -35,7 +35,6 @@ #include "utils.h" #include "repository.h" #include "oid.h" -#include "options.h" #include "filter.h" PyObject *GitError; @@ -439,7 +438,6 @@ PyMethodDef module_methods[] = { {"hash", hash, METH_VARARGS, hash__doc__}, {"hashfile", hashfile, METH_VARARGS, hashfile__doc__}, {"init_file_backend", init_file_backend, METH_VARARGS, init_file_backend__doc__}, - {"option", option, METH_VARARGS, option__doc__}, {"reference_is_valid_name", reference_is_valid_name, METH_O, reference_is_valid_name__doc__}, {"tree_entry_cmp", tree_entry_cmp, METH_VARARGS, tree_entry_cmp__doc__}, {"filter_register", (PyCFunction)filter_register, METH_VARARGS | METH_KEYWORDS, filter_register__doc__}, @@ -473,39 +471,6 @@ PyInit__pygit2(void) ADD_CONSTANT_INT(m, LIBGIT2_VER_REVISION) ADD_CONSTANT_STR(m, LIBGIT2_VERSION) - /* libgit2 options */ - ADD_CONSTANT_INT(m, GIT_OPT_GET_MWINDOW_SIZE); - ADD_CONSTANT_INT(m, GIT_OPT_SET_MWINDOW_SIZE); - ADD_CONSTANT_INT(m, GIT_OPT_GET_MWINDOW_MAPPED_LIMIT); - ADD_CONSTANT_INT(m, GIT_OPT_SET_MWINDOW_MAPPED_LIMIT); - ADD_CONSTANT_INT(m, GIT_OPT_GET_SEARCH_PATH); - ADD_CONSTANT_INT(m, GIT_OPT_SET_SEARCH_PATH); - ADD_CONSTANT_INT(m, GIT_OPT_SET_CACHE_OBJECT_LIMIT); - ADD_CONSTANT_INT(m, GIT_OPT_SET_CACHE_MAX_SIZE); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_CACHING); - ADD_CONSTANT_INT(m, GIT_OPT_GET_CACHED_MEMORY); - ADD_CONSTANT_INT(m, GIT_OPT_GET_TEMPLATE_PATH); - ADD_CONSTANT_INT(m, GIT_OPT_SET_TEMPLATE_PATH); - ADD_CONSTANT_INT(m, GIT_OPT_SET_SSL_CERT_LOCATIONS); - ADD_CONSTANT_INT(m, GIT_OPT_SET_USER_AGENT); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_STRICT_OBJECT_CREATION); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION); - ADD_CONSTANT_INT(m, GIT_OPT_SET_SSL_CIPHERS); - ADD_CONSTANT_INT(m, GIT_OPT_GET_USER_AGENT); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_OFS_DELTA); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_FSYNC_GITDIR); - ADD_CONSTANT_INT(m, GIT_OPT_GET_WINDOWS_SHAREMODE); - ADD_CONSTANT_INT(m, GIT_OPT_SET_WINDOWS_SHAREMODE); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION); - ADD_CONSTANT_INT(m, GIT_OPT_SET_ALLOCATOR); - ADD_CONSTANT_INT(m, GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY); - ADD_CONSTANT_INT(m, GIT_OPT_GET_PACK_MAX_OBJECTS); - ADD_CONSTANT_INT(m, GIT_OPT_SET_PACK_MAX_OBJECTS); - ADD_CONSTANT_INT(m, GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS); - ADD_CONSTANT_INT(m, GIT_OPT_GET_OWNER_VALIDATION); - ADD_CONSTANT_INT(m, GIT_OPT_SET_OWNER_VALIDATION); - ADD_CONSTANT_INT(m, GIT_OPT_GET_MWINDOW_FILE_LIMIT); - ADD_CONSTANT_INT(m, GIT_OPT_SET_MWINDOW_FILE_LIMIT); /* Exceptions */ ADD_EXC(m, GitError, NULL); diff --git a/test/conftest.py b/test/conftest.py index 6052346f8..6b8a2737d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -24,7 +24,7 @@ def global_git_config() -> None: # Fix tests running in AppVeyor if platform.system() == 'Windows': - pygit2.option(pygit2.enums.Option.SET_OWNER_VALIDATION, 0) + pygit2.option(pygit2.enums.Option.SET_OWNER_VALIDATION, False) @pytest.fixture diff --git a/test/test_options.py b/test/test_options.py index 790f459f4..a9169bd00 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -29,11 +29,11 @@ def __option(getter: Option, setter: Option, value: object) -> None: - old_value = option(getter) # type: ignore[call-overload] - option(setter, value) # type: ignore[call-overload] - assert value == option(getter) # type: ignore[call-overload] + old_value = option(getter) + option(setter, value) + assert value == option(getter) # Reset to avoid side effects in later tests - option(setter, old_value) # type: ignore[call-overload] + option(setter, old_value) def __proxy(name: str, value: object) -> None: From e035a87ca43e502cc38df12dcfd7e2413ae9425c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 11 Aug 2025 15:31:13 +0200 Subject: [PATCH 2/9] Implement not implemented options --- pygit2/_libgit2/ffi.pyi | 15 +- pygit2/enums.py | 25 +- pygit2/options.py | 505 +++++++++++++++++++++++++++++++++++++--- pygit2/utils.py | 4 +- test/test_options.py | 170 ++++++++++++++ 5 files changed, 670 insertions(+), 49 deletions(-) diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 9feb1791a..537bae43f 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -58,7 +58,8 @@ class _MultiPointer(Generic[T]): class ArrayC(Generic[T]): # incomplete! # def _len(self, ?) -> ?: ... - pass + def __getitem__(self, index: int) -> T: ... + def __setitem__(self, index: int, value: T) -> None: ... class GitTimeC: # incomplete @@ -199,7 +200,7 @@ class GitStashSaveOptionsC: class GitStrrayC: # incomplete? - strings: NULL_TYPE | ArrayC[char] + strings: NULL_TYPE | ArrayC[char_pointer] count: int class GitTreeC: @@ -335,6 +336,12 @@ def new(a: Literal['git_buf *'], b: tuple[NULL_TYPE, Literal[0]]) -> GitBufC: .. def new(a: Literal['char **']) -> _Pointer[char_pointer]: ... @overload def new(a: Literal['char[]', 'char []'], b: bytes | NULL_TYPE) -> ArrayC[char]: ... +@overload +def new(a: Literal['char ***']) -> Any: ... # For extensions_ptr in GET_EXTENSIONS +@overload +def new(a: Literal['char *[]'], b: int) -> ArrayC[char_pointer]: ... # For ext_array in SET_EXTENSIONS +@overload +def new(a: Literal['char *[]'], b: list[Any]) -> ArrayC[char_pointer]: ... # For string arrays def addressof(a: object, attribute: str) -> _Pointer[object]: ... class buffer(bytes): @@ -348,6 +355,10 @@ class buffer(bytes): @overload def cast(a: Literal['int'], b: object) -> int: ... @overload +def cast(a: Literal['unsigned int'], b: object) -> int: ... +@overload def cast(a: Literal['size_t'], b: object) -> int: ... @overload def cast(a: Literal['ssize_t'], b: object) -> int: ... +@overload +def cast(a: Literal['char *'], b: object) -> char_pointer: ... diff --git a/pygit2/enums.py b/pygit2/enums.py index 80be63a5b..71239ca91 100644 --- a/pygit2/enums.py +++ b/pygit2/enums.py @@ -979,21 +979,24 @@ class Option(IntEnum): GET_PACK_MAX_OBJECTS = options.GIT_OPT_GET_PACK_MAX_OBJECTS SET_PACK_MAX_OBJECTS = options.GIT_OPT_SET_PACK_MAX_OBJECTS DISABLE_PACK_KEEP_FILE_CHECKS = options.GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS - # ENABLE_HTTP_EXPECT_CONTINUE = options.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE + ENABLE_HTTP_EXPECT_CONTINUE = options.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE GET_MWINDOW_FILE_LIMIT = options.GIT_OPT_GET_MWINDOW_FILE_LIMIT SET_MWINDOW_FILE_LIMIT = options.GIT_OPT_SET_MWINDOW_FILE_LIMIT - # SET_ODB_PACKED_PRIORITY = options.GIT_OPT_SET_ODB_PACKED_PRIORITY - # SET_ODB_LOOSE_PRIORITY = options.GIT_OPT_SET_ODB_LOOSE_PRIORITY - # GET_EXTENSIONS = options.GIT_OPT_GET_EXTENSIONS - # SET_EXTENSIONS = options.GIT_OPT_SET_EXTENSIONS + SET_ODB_PACKED_PRIORITY = options.GIT_OPT_SET_ODB_PACKED_PRIORITY + SET_ODB_LOOSE_PRIORITY = options.GIT_OPT_SET_ODB_LOOSE_PRIORITY + GET_EXTENSIONS = options.GIT_OPT_GET_EXTENSIONS + SET_EXTENSIONS = options.GIT_OPT_SET_EXTENSIONS GET_OWNER_VALIDATION = options.GIT_OPT_GET_OWNER_VALIDATION SET_OWNER_VALIDATION = options.GIT_OPT_SET_OWNER_VALIDATION - # GET_HOMEDIR = _pygit2.GIT_OPT_GET_HOMEDIR - # SET_HOMEDIR = _pygit2.GIT_OPT_SET_HOMEDIR - # SET_SERVER_CONNECT_TIMEOUT = _pygit2.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT - # GET_SERVER_CONNECT_TIMEOUT = _pygit2.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT - # SET_SERVER_TIMEOUT = _pygit2.GIT_OPT_SET_SERVER_TIMEOUT - # GET_SERVER_TIMEOUT = _pygit2.GIT_OPT_GET_SERVER_TIMEOUT + GET_HOMEDIR = options.GIT_OPT_GET_HOMEDIR + SET_HOMEDIR = options.GIT_OPT_SET_HOMEDIR + SET_SERVER_CONNECT_TIMEOUT = options.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT + GET_SERVER_CONNECT_TIMEOUT = options.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT + SET_SERVER_TIMEOUT = options.GIT_OPT_SET_SERVER_TIMEOUT + GET_SERVER_TIMEOUT = options.GIT_OPT_GET_SERVER_TIMEOUT + GET_USER_AGENT_PRODUCT = options.GIT_OPT_GET_USER_AGENT_PRODUCT + SET_USER_AGENT_PRODUCT = options.GIT_OPT_SET_USER_AGENT_PRODUCT + ADD_SSL_X509_CERT = options.GIT_OPT_ADD_SSL_X509_CERT class ReferenceFilter(IntEnum): diff --git a/pygit2/options.py b/pygit2/options.py index 4f63234e8..9fa13b397 100644 --- a/pygit2/options.py +++ b/pygit2/options.py @@ -28,7 +28,7 @@ Libgit2 global options management using CFFI. """ -from typing import Any, Literal, Optional, Tuple, Union, overload +from typing import Any, Literal, overload, cast from .ffi import C, ffi from .errors import check_error @@ -39,7 +39,7 @@ if TYPE_CHECKING: from .enums import ConfigLevel, ObjectType, Option - from ._libgit2.ffi import ArrayC, NULL_TYPE, char + from ._libgit2.ffi import ArrayC, NULL_TYPE, char, char_pointer # Export GIT_OPT constants for backward compatibility GIT_OPT_GET_MWINDOW_SIZE: int = C.GIT_OPT_GET_MWINDOW_SIZE @@ -76,6 +76,20 @@ GIT_OPT_SET_MWINDOW_FILE_LIMIT: int = C.GIT_OPT_SET_MWINDOW_FILE_LIMIT GIT_OPT_GET_OWNER_VALIDATION: int = C.GIT_OPT_GET_OWNER_VALIDATION GIT_OPT_SET_OWNER_VALIDATION: int = C.GIT_OPT_SET_OWNER_VALIDATION +GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE: int = C.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE +GIT_OPT_SET_ODB_PACKED_PRIORITY: int = C.GIT_OPT_SET_ODB_PACKED_PRIORITY +GIT_OPT_SET_ODB_LOOSE_PRIORITY: int = C.GIT_OPT_SET_ODB_LOOSE_PRIORITY +GIT_OPT_GET_EXTENSIONS: int = C.GIT_OPT_GET_EXTENSIONS +GIT_OPT_SET_EXTENSIONS: int = C.GIT_OPT_SET_EXTENSIONS +GIT_OPT_GET_HOMEDIR: int = C.GIT_OPT_GET_HOMEDIR +GIT_OPT_SET_HOMEDIR: int = C.GIT_OPT_SET_HOMEDIR +GIT_OPT_SET_SERVER_CONNECT_TIMEOUT: int = C.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT +GIT_OPT_GET_SERVER_CONNECT_TIMEOUT: int = C.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT +GIT_OPT_SET_SERVER_TIMEOUT: int = C.GIT_OPT_SET_SERVER_TIMEOUT +GIT_OPT_GET_SERVER_TIMEOUT: int = C.GIT_OPT_GET_SERVER_TIMEOUT +GIT_OPT_GET_USER_AGENT_PRODUCT: int = C.GIT_OPT_GET_USER_AGENT_PRODUCT +GIT_OPT_SET_USER_AGENT_PRODUCT: int = C.GIT_OPT_SET_USER_AGENT_PRODUCT +GIT_OPT_ADD_SSL_X509_CERT: int = C.GIT_OPT_ADD_SSL_X509_CERT NOT_PASSED = object() @@ -94,21 +108,21 @@ def check_args(option: Option, arg1: Any, arg2: Any, expected: int) -> None: @overload def option( - option_type: Union[ - Literal[Option.GET_MWINDOW_SIZE], - Literal[Option.GET_MWINDOW_MAPPED_LIMIT], - Literal[Option.GET_MWINDOW_FILE_LIMIT], + option_type: Literal[ + Option.GET_MWINDOW_SIZE, + Option.GET_MWINDOW_MAPPED_LIMIT, + Option.GET_MWINDOW_FILE_LIMIT, ], ) -> int: ... @overload def option( - option_type: Union[ - Literal[Option.SET_MWINDOW_SIZE], - Literal[Option.SET_MWINDOW_MAPPED_LIMIT], - Literal[Option.SET_MWINDOW_FILE_LIMIT], - Literal[Option.SET_CACHE_MAX_SIZE], + option_type: Literal[ + Option.SET_MWINDOW_SIZE, + Option.SET_MWINDOW_MAPPED_LIMIT, + Option.SET_MWINDOW_FILE_LIMIT, + Option.SET_CACHE_MAX_SIZE, ], arg1: int, # value ) -> None: ... @@ -138,29 +152,29 @@ def option( @overload -def option(option_type: Literal[Option.GET_CACHED_MEMORY]) -> Tuple[int, int]: ... +def option(option_type: Literal[Option.GET_CACHED_MEMORY]) -> tuple[int, int]: ... @overload def option( option_type: Literal[Option.SET_SSL_CERT_LOCATIONS], - arg1: Optional[str | bytes], # cert_file - arg2: Optional[str | bytes], # cert_dir + arg1: str | bytes | None, # cert_file + arg2: str | bytes | None, # cert_dir ) -> None: ... @overload def option( - option_type: Union[ - Literal[Option.ENABLE_CACHING], - Literal[Option.ENABLE_STRICT_OBJECT_CREATION], - Literal[Option.ENABLE_STRICT_SYMBOLIC_REF_CREATION], - Literal[Option.ENABLE_OFS_DELTA], - Literal[Option.ENABLE_FSYNC_GITDIR], - Literal[Option.ENABLE_STRICT_HASH_VERIFICATION], - Literal[Option.ENABLE_UNSAVED_INDEX_SAFETY], - Literal[Option.DISABLE_PACK_KEEP_FILE_CHECKS], - Literal[Option.SET_OWNER_VALIDATION], + option_type: Literal[ + Option.ENABLE_CACHING, + Option.ENABLE_STRICT_OBJECT_CREATION, + Option.ENABLE_STRICT_SYMBOLIC_REF_CREATION, + Option.ENABLE_OFS_DELTA, + Option.ENABLE_FSYNC_GITDIR, + Option.ENABLE_STRICT_HASH_VERIFICATION, + Option.ENABLE_UNSAVED_INDEX_SAFETY, + Option.DISABLE_PACK_KEEP_FILE_CHECKS, + Option.SET_OWNER_VALIDATION, ], arg1: bool, # value ) -> None: ... @@ -170,6 +184,75 @@ def option( def option(option_type: Literal[Option.GET_OWNER_VALIDATION]) -> bool: ... +@overload +def option( + option_type: Literal[ + Option.GET_TEMPLATE_PATH, + Option.GET_USER_AGENT, + Option.GET_HOMEDIR, + Option.GET_USER_AGENT_PRODUCT, + ], +) -> str | None: ... + + +@overload +def option( + option_type: Literal[ + Option.SET_TEMPLATE_PATH, + Option.SET_USER_AGENT, + Option.SET_SSL_CIPHERS, + Option.SET_HOMEDIR, + Option.SET_USER_AGENT_PRODUCT, + ], + arg1: str | bytes, # value +) -> None: ... + + +@overload +def option( + option_type: Literal[ + Option.GET_WINDOWS_SHAREMODE, + Option.GET_PACK_MAX_OBJECTS, + Option.GET_SERVER_CONNECT_TIMEOUT, + Option.GET_SERVER_TIMEOUT, + ], +) -> int: ... + + +@overload +def option( + option_type: Literal[ + Option.SET_WINDOWS_SHAREMODE, + Option.SET_PACK_MAX_OBJECTS, + Option.ENABLE_HTTP_EXPECT_CONTINUE, + Option.SET_ODB_PACKED_PRIORITY, + Option.SET_ODB_LOOSE_PRIORITY, + Option.SET_SERVER_CONNECT_TIMEOUT, + Option.SET_SERVER_TIMEOUT, + ], + arg1: int, # value +) -> None: ... + + +@overload +def option(option_type: Literal[Option.GET_EXTENSIONS]) -> list[str]: ... + + +@overload +def option( + option_type: Literal[Option.SET_EXTENSIONS], + arg1: list[str], # extensions + arg2: int, # length +) -> None: ... + + +@overload +def option( + option_type: Literal[Option.ADD_SSL_X509_CERT], + arg1: str | bytes, # certificate +) -> None: ... + + # Fallback overload for generic Option values (used in tests) @overload def option(option_type: Option, arg1: Any = ..., arg2: Any = ...) -> Any: ... @@ -205,6 +288,24 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) GIT_OPT_SET_OWNER_VALIDATION, enabled Set that repository directories should be owned by the current user. The default is to validate ownership. + + GIT_OPT_GET_TEMPLATE_PATH + Get the default template path. + + GIT_OPT_SET_TEMPLATE_PATH, path + Set the default template path. + + GIT_OPT_GET_USER_AGENT + Get the user agent string. + + GIT_OPT_SET_USER_AGENT, user_agent + Set the user agent string. + + GIT_OPT_GET_PACK_MAX_OBJECTS + Get the maximum number of objects to include in a pack. + + GIT_OPT_SET_PACK_MAX_OBJECTS, count + Set the maximum number of objects to include in a pack. """ # Handle GET options with size_t output @@ -376,18 +477,354 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return bool(enabled_ptr[0]) - # Not implemented options + # Handle GET_TEMPLATE_PATH + elif option_type == C.GIT_OPT_GET_TEMPLATE_PATH: + check_args(option_type, arg1, arg2, 0) + + buf = ffi.new("git_buf *") + err = C.git_libgit2_opts(option_type, buf) + check_error(err) + + try: + if buf.ptr != ffi.NULL: + result = to_str(ffi.string(buf.ptr)) + else: + result = None + finally: + C.git_buf_dispose(buf) + + return result + + # Handle SET_TEMPLATE_PATH + elif option_type == C.GIT_OPT_SET_TEMPLATE_PATH: + check_args(option_type, arg1, arg2, 1) + + path = arg1 + template_path_cdata: ArrayC[char] | NULL_TYPE + if path is None: + template_path_cdata = ffi.NULL + else: + path_bytes = to_bytes(path) + template_path_cdata = ffi.new("char[]", path_bytes) + + err = C.git_libgit2_opts(option_type, template_path_cdata) + check_error(err) + return None + + # Handle GET_USER_AGENT + elif option_type == C.GIT_OPT_GET_USER_AGENT: + check_args(option_type, arg1, arg2, 0) + + buf = ffi.new("git_buf *") + err = C.git_libgit2_opts(option_type, buf) + check_error(err) + + try: + if buf.ptr != ffi.NULL: + result = to_str(ffi.string(buf.ptr)) + else: + result = None + finally: + C.git_buf_dispose(buf) + + return result + + # Handle SET_USER_AGENT + elif option_type == C.GIT_OPT_SET_USER_AGENT: + check_args(option_type, arg1, arg2, 1) + + agent = arg1 + agent_bytes = to_bytes(agent) + agent_cdata = ffi.new("char[]", agent_bytes) + + err = C.git_libgit2_opts(option_type, agent_cdata) + check_error(err) + return None + + # Handle SET_SSL_CIPHERS + elif option_type == C.GIT_OPT_SET_SSL_CIPHERS: + check_args(option_type, arg1, arg2, 1) + + ciphers = arg1 + ciphers_bytes = to_bytes(ciphers) + ciphers_cdata = ffi.new("char[]", ciphers_bytes) + + err = C.git_libgit2_opts(option_type, ciphers_cdata) + check_error(err) + return None + + # Handle GET_WINDOWS_SHAREMODE + elif option_type == C.GIT_OPT_GET_WINDOWS_SHAREMODE: + check_args(option_type, arg1, arg2, 0) + + value_ptr = ffi.new("unsigned int *") + err = C.git_libgit2_opts(option_type, value_ptr) + check_error(err) + return value_ptr[0] + + # Handle SET_WINDOWS_SHAREMODE + elif option_type == C.GIT_OPT_SET_WINDOWS_SHAREMODE: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + value = arg1 + if value < 0: + raise ValueError("value must be non-negative") + + err = C.git_libgit2_opts(option_type, ffi.cast("unsigned int", value)) + check_error(err) + return None + + # Handle GET_PACK_MAX_OBJECTS + elif option_type == C.GIT_OPT_GET_PACK_MAX_OBJECTS: + check_args(option_type, arg1, arg2, 0) + + size_ptr = ffi.new("size_t *") + err = C.git_libgit2_opts(option_type, size_ptr) + check_error(err) + return size_ptr[0] + + # Handle SET_PACK_MAX_OBJECTS + elif option_type == C.GIT_OPT_SET_PACK_MAX_OBJECTS: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + size = arg1 + if size < 0: + raise ValueError("size must be non-negative") + + err = C.git_libgit2_opts(option_type, ffi.cast("size_t", size)) + check_error(err) + return None + + # Handle ENABLE_HTTP_EXPECT_CONTINUE + elif option_type == C.GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE: + check_args(option_type, arg1, arg2, 1) + + enabled = arg1 + # Convert to int (0 or 1) + value = 1 if enabled else 0 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", value)) + check_error(err) + return None + + # Handle SET_ODB_PACKED_PRIORITY + elif option_type == C.GIT_OPT_SET_ODB_PACKED_PRIORITY: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + priority = arg1 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", priority)) + check_error(err) + return None + + # Handle SET_ODB_LOOSE_PRIORITY + elif option_type == C.GIT_OPT_SET_ODB_LOOSE_PRIORITY: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + priority = arg1 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", priority)) + check_error(err) + return None + + # Handle GET_EXTENSIONS + elif option_type == C.GIT_OPT_GET_EXTENSIONS: + check_args(option_type, arg1, arg2, 0) + + # GET_EXTENSIONS expects a git_strarray pointer + strarray = ffi.new("git_strarray *") + err = C.git_libgit2_opts(option_type, strarray) + check_error(err) + + result = [] + try: + if strarray.strings != ffi.NULL: + # Cast to the non-NULL type for type checking + strings = cast('ArrayC[char_pointer]', strarray.strings) + for i in range(strarray.count): + if strings[i] != ffi.NULL: + result.append(to_str(ffi.string(strings[i]))) + finally: + # Must dispose of the strarray to free the memory + C.git_strarray_dispose(strarray) + + return result + + # Handle SET_EXTENSIONS + elif option_type == C.GIT_OPT_SET_EXTENSIONS: + check_args(option_type, arg1, arg2, 2) + + extensions = arg1 + length = arg2 + + if not isinstance(extensions, list): + raise TypeError("extensions must be a list of strings") + if not isinstance(length, int): + raise TypeError("length must be an integer") + + # Create array of char pointers + # libgit2 will make its own copies with git__strdup + ext_array: ArrayC[char_pointer] = ffi.new("char *[]", len(extensions)) + ext_strings: list[ArrayC[char]] = [] # Keep references during the call + + for i, ext in enumerate(extensions): + ext_bytes = to_bytes(ext) + ext_string: ArrayC[char] = ffi.new("char[]", ext_bytes) + ext_strings.append(ext_string) + ext_array[i] = ffi.cast("char *", ext_string) + + err = C.git_libgit2_opts(option_type, ext_array, ffi.cast("size_t", length)) + check_error(err) + return None + + # Handle GET_HOMEDIR + elif option_type == C.GIT_OPT_GET_HOMEDIR: + check_args(option_type, arg1, arg2, 0) + + buf = ffi.new("git_buf *") + err = C.git_libgit2_opts(option_type, buf) + check_error(err) + + try: + if buf.ptr != ffi.NULL: + result = to_str(ffi.string(buf.ptr)) + else: + result = None + finally: + C.git_buf_dispose(buf) + + return result + + # Handle SET_HOMEDIR + elif option_type == C.GIT_OPT_SET_HOMEDIR: + check_args(option_type, arg1, arg2, 1) + + path = arg1 + homedir_cdata: ArrayC[char] | NULL_TYPE + if path is None: + homedir_cdata = ffi.NULL + else: + path_bytes = to_bytes(path) + homedir_cdata = ffi.new("char[]", path_bytes) + + err = C.git_libgit2_opts(option_type, homedir_cdata) + check_error(err) + return None + + # Handle GET_SERVER_CONNECT_TIMEOUT + elif option_type == C.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT: + check_args(option_type, arg1, arg2, 0) + + timeout_ptr = ffi.new("int *") + err = C.git_libgit2_opts(option_type, timeout_ptr) + check_error(err) + return timeout_ptr[0] + + # Handle SET_SERVER_CONNECT_TIMEOUT + elif option_type == C.GIT_OPT_SET_SERVER_CONNECT_TIMEOUT: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + timeout = arg1 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", timeout)) + check_error(err) + return None + + # Handle GET_SERVER_TIMEOUT + elif option_type == C.GIT_OPT_GET_SERVER_TIMEOUT: + check_args(option_type, arg1, arg2, 0) + + timeout_ptr = ffi.new("int *") + err = C.git_libgit2_opts(option_type, timeout_ptr) + check_error(err) + return timeout_ptr[0] + + # Handle SET_SERVER_TIMEOUT + elif option_type == C.GIT_OPT_SET_SERVER_TIMEOUT: + check_args(option_type, arg1, arg2, 1) + + if not isinstance(arg1, int): + raise TypeError( + f"option value must be an integer, not {type(arg1).__name__}" + ) + timeout = arg1 + + err = C.git_libgit2_opts(option_type, ffi.cast("int", timeout)) + check_error(err) + return None + + # Handle GET_USER_AGENT_PRODUCT + elif option_type == C.GIT_OPT_GET_USER_AGENT_PRODUCT: + check_args(option_type, arg1, arg2, 0) + + buf = ffi.new("git_buf *") + err = C.git_libgit2_opts(option_type, buf) + check_error(err) + + try: + if buf.ptr != ffi.NULL: + result = to_str(ffi.string(buf.ptr)) + else: + result = None + finally: + C.git_buf_dispose(buf) + + return result + + # Handle SET_USER_AGENT_PRODUCT + elif option_type == C.GIT_OPT_SET_USER_AGENT_PRODUCT: + check_args(option_type, arg1, arg2, 1) + + product = arg1 + product_bytes = to_bytes(product) + product_cdata = ffi.new("char[]", product_bytes) + + err = C.git_libgit2_opts(option_type, product_cdata) + check_error(err) + return None + + # Handle ADD_SSL_X509_CERT + elif option_type == C.GIT_OPT_ADD_SSL_X509_CERT: + check_args(option_type, arg1, arg2, 1) + + cert = arg1 + if isinstance(cert, (str, bytes)): + cert_bytes = to_bytes(cert) + cert_cdata = ffi.new("char[]", cert_bytes) + cert_len = len(cert_bytes) + else: + raise TypeError("certificate must be a string or bytes") + + err = C.git_libgit2_opts(option_type, cert_cdata, ffi.cast("size_t", cert_len)) + check_error(err) + return None + + # Not implemented - SET_ALLOCATOR is not feasible from Python level + # because it requires providing C function pointers for memory management + # (malloc, free, etc.) that must handle raw memory at the C level, + # which cannot be safely implemented in pure Python. elif option_type in ( - C.GIT_OPT_GET_TEMPLATE_PATH, - C.GIT_OPT_SET_TEMPLATE_PATH, - C.GIT_OPT_SET_USER_AGENT, - C.GIT_OPT_SET_SSL_CIPHERS, - C.GIT_OPT_GET_USER_AGENT, - C.GIT_OPT_GET_WINDOWS_SHAREMODE, - C.GIT_OPT_SET_WINDOWS_SHAREMODE, C.GIT_OPT_SET_ALLOCATOR, - C.GIT_OPT_GET_PACK_MAX_OBJECTS, - C.GIT_OPT_SET_PACK_MAX_OBJECTS, ): return NotImplemented diff --git a/pygit2/utils.py b/pygit2/utils.py index b3d5d1416..dcc77167c 100644 --- a/pygit2/utils.py +++ b/pygit2/utils.py @@ -145,7 +145,7 @@ class StrArray: __array: 'GitStrrayC | ffi.NULL_TYPE' __strings: list['None | ArrayC[char]'] - __arr: 'ArrayC[char]' + __arr: 'ArrayC[char_pointer]' def __init__(self, lst: None | Sequence[str | os.PathLike[str]]): # Allow passing in None as lg2 typically considers them the same as empty @@ -164,7 +164,7 @@ def __init__(self, lst: None | Sequence[str | os.PathLike[str]]): strings[i] = ffi.new('char []', to_bytes(li)) - self.__arr = ffi.new('char *[]', strings) # type: ignore[call-overload] + self.__arr = ffi.new('char *[]', strings) self.__strings = strings self.__array = ffi.new('git_strarray *', [self.__arr, len(strings)]) # type: ignore[call-overload] diff --git a/test/test_options.py b/test/test_options.py index a9169bd00..ddb262c77 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -130,3 +130,173 @@ def test_search_path_proxy() -> None: def test_owner_validation() -> None: __option(Option.GET_OWNER_VALIDATION, Option.SET_OWNER_VALIDATION, 0) + + +def test_template_path() -> None: + # Get the initial template path + original_path = option(Option.GET_TEMPLATE_PATH) + + # Set a new template path + test_path = '/tmp/test_templates' + option(Option.SET_TEMPLATE_PATH, test_path) + assert option(Option.GET_TEMPLATE_PATH) == test_path + + # Reset to original path + if original_path: + option(Option.SET_TEMPLATE_PATH, original_path) + else: + option(Option.SET_TEMPLATE_PATH, None) + + +def test_user_agent() -> None: + # Get the initial user agent + original_agent = option(Option.GET_USER_AGENT) + + # Set a new user agent + test_agent = 'test-agent/1.0' + option(Option.SET_USER_AGENT, test_agent) + assert option(Option.GET_USER_AGENT) == test_agent + + # Reset to original agent + if original_agent: + option(Option.SET_USER_AGENT, original_agent) + + +def test_pack_max_objects() -> None: + __option(Option.GET_PACK_MAX_OBJECTS, Option.SET_PACK_MAX_OBJECTS, 100000) + + +def test_windows_sharemode() -> None: + # This test might not work on non-Windows platforms + try: + __option(Option.GET_WINDOWS_SHAREMODE, Option.SET_WINDOWS_SHAREMODE, 1) + except Exception: + # May fail on non-Windows platforms + pass + + +def test_ssl_ciphers() -> None: + # Setting SSL ciphers (no getter available) + try: + option(Option.SET_SSL_CIPHERS, 'DEFAULT') + except pygit2.GitError as e: + # May fail if TLS backend doesn't support custom ciphers + if "TLS backend doesn't support custom ciphers" not in str(e): + raise + + +def test_enable_http_expect_continue() -> None: + # Enable and disable HTTP expect continue + option(Option.ENABLE_HTTP_EXPECT_CONTINUE, True) + option(Option.ENABLE_HTTP_EXPECT_CONTINUE, False) + + +def test_odb_priorities() -> None: + # Set ODB priorities + option(Option.SET_ODB_PACKED_PRIORITY, 1) + option(Option.SET_ODB_LOOSE_PRIORITY, 2) + + +def test_extensions() -> None: + # Get initial extensions list + original_extensions = option(Option.GET_EXTENSIONS) + assert isinstance(original_extensions, list) + + # Try to set extensions (this might fail depending on the setup) + try: + test_extensions = ['objectformat', 'worktreeconfig'] + option(Option.SET_EXTENSIONS, test_extensions, len(test_extensions)) + + # Verify they were set + new_extensions = option(Option.GET_EXTENSIONS) + assert isinstance(new_extensions, list) + + # Check that our extensions are present + # Note: libgit2 may add its own built-in extensions and sort them + for ext in test_extensions: + assert ext in new_extensions, f"Extension '{ext}' not found in {new_extensions}" + + # Test with empty list + option(Option.SET_EXTENSIONS, [], 0) + empty_extensions = option(Option.GET_EXTENSIONS) + assert isinstance(empty_extensions, list) + # Even with empty input, libgit2 may have built-in extensions + + # Test with a custom extension + custom_extensions = ['myextension', 'objectformat'] + option(Option.SET_EXTENSIONS, custom_extensions, len(custom_extensions)) + custom_result = option(Option.GET_EXTENSIONS) + assert 'myextension' in custom_result + assert 'objectformat' in custom_result + + # Restore original extensions + if original_extensions: + option(Option.SET_EXTENSIONS, original_extensions, len(original_extensions)) + else: + # Reset to empty list if there were no extensions + option(Option.SET_EXTENSIONS, [], 0) + + # Verify restoration + final_extensions = option(Option.GET_EXTENSIONS) + assert set(final_extensions) == set(original_extensions) + except Exception: + # May fail if extensions cannot be modified + pass + + +def test_homedir() -> None: + # Get the initial home directory + original_homedir = option(Option.GET_HOMEDIR) + + # Set a new home directory + test_homedir = '/tmp/test_home' + option(Option.SET_HOMEDIR, test_homedir) + assert option(Option.GET_HOMEDIR) == test_homedir + + # Reset to original home directory + if original_homedir: + option(Option.SET_HOMEDIR, original_homedir) + else: + option(Option.SET_HOMEDIR, None) + + +def test_server_timeouts() -> None: + # Test connect timeout + original_connect = option(Option.GET_SERVER_CONNECT_TIMEOUT) + option(Option.SET_SERVER_CONNECT_TIMEOUT, 5000) + assert option(Option.GET_SERVER_CONNECT_TIMEOUT) == 5000 + option(Option.SET_SERVER_CONNECT_TIMEOUT, original_connect) + + # Test server timeout + original_timeout = option(Option.GET_SERVER_TIMEOUT) + option(Option.SET_SERVER_TIMEOUT, 10000) + assert option(Option.GET_SERVER_TIMEOUT) == 10000 + option(Option.SET_SERVER_TIMEOUT, original_timeout) + + +def test_user_agent_product() -> None: + # Get the initial user agent product + original_product = option(Option.GET_USER_AGENT_PRODUCT) + + # Set a new user agent product + test_product = 'test-product' + option(Option.SET_USER_AGENT_PRODUCT, test_product) + assert option(Option.GET_USER_AGENT_PRODUCT) == test_product + + # Reset to original product + if original_product: + option(Option.SET_USER_AGENT_PRODUCT, original_product) + + +def test_add_ssl_x509_cert() -> None: + # Test adding an SSL certificate (basic test, just ensure it doesn't crash) + test_cert = "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----" + try: + option(Option.ADD_SSL_X509_CERT, test_cert) + except Exception: + # May fail depending on SSL backend + pass + + +def test_mwindow_file_limit() -> None: + __option(Option.GET_MWINDOW_FILE_LIMIT, Option.SET_MWINDOW_FILE_LIMIT, 100) From cfd7d94daebf5cb059a0deb09d22c1e3e80376ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 11 Aug 2025 17:41:09 +0200 Subject: [PATCH 3/9] Fix nits I noticed --- .gitignore | 1 + pygit2/_libgit2/ffi.pyi | 2 - test/test_options.py | 91 +++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 0f5d52267..12c3a9701 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.envrc /.tox/ /build/ +/ci/ /dist/ /docs/_build/ /MANIFEST diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 537bae43f..5936d2f21 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -337,8 +337,6 @@ def new(a: Literal['char **']) -> _Pointer[char_pointer]: ... @overload def new(a: Literal['char[]', 'char []'], b: bytes | NULL_TYPE) -> ArrayC[char]: ... @overload -def new(a: Literal['char ***']) -> Any: ... # For extensions_ptr in GET_EXTENSIONS -@overload def new(a: Literal['char *[]'], b: int) -> ArrayC[char_pointer]: ... # For ext_array in SET_EXTENSIONS @overload def new(a: Literal['char *[]'], b: list[Any]) -> ArrayC[char_pointer]: ... # For string arrays diff --git a/test/test_options.py b/test/test_options.py index ddb262c77..78a533688 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -201,47 +201,43 @@ def test_extensions() -> None: # Get initial extensions list original_extensions = option(Option.GET_EXTENSIONS) assert isinstance(original_extensions, list) - - # Try to set extensions (this might fail depending on the setup) - try: - test_extensions = ['objectformat', 'worktreeconfig'] - option(Option.SET_EXTENSIONS, test_extensions, len(test_extensions)) - - # Verify they were set - new_extensions = option(Option.GET_EXTENSIONS) - assert isinstance(new_extensions, list) - - # Check that our extensions are present - # Note: libgit2 may add its own built-in extensions and sort them - for ext in test_extensions: - assert ext in new_extensions, f"Extension '{ext}' not found in {new_extensions}" - - # Test with empty list + + # Set extensions + test_extensions = ['objectformat', 'worktreeconfig'] + option(Option.SET_EXTENSIONS, test_extensions, len(test_extensions)) + + # Verify they were set + new_extensions = option(Option.GET_EXTENSIONS) + assert isinstance(new_extensions, list) + + # Check that our extensions are present + # Note: libgit2 may add its own built-in extensions and sort them + for ext in test_extensions: + assert ext in new_extensions, f"Extension '{ext}' not found in {new_extensions}" + + # Test with empty list + option(Option.SET_EXTENSIONS, [], 0) + empty_extensions = option(Option.GET_EXTENSIONS) + assert isinstance(empty_extensions, list) + # Even with empty input, libgit2 may have built-in extensions + + # Test with a custom extension + custom_extensions = ['myextension', 'objectformat'] + option(Option.SET_EXTENSIONS, custom_extensions, len(custom_extensions)) + custom_result = option(Option.GET_EXTENSIONS) + assert 'myextension' in custom_result + assert 'objectformat' in custom_result + + # Restore original extensions + if original_extensions: + option(Option.SET_EXTENSIONS, original_extensions, len(original_extensions)) + else: + # Reset to empty list if there were no extensions option(Option.SET_EXTENSIONS, [], 0) - empty_extensions = option(Option.GET_EXTENSIONS) - assert isinstance(empty_extensions, list) - # Even with empty input, libgit2 may have built-in extensions - - # Test with a custom extension - custom_extensions = ['myextension', 'objectformat'] - option(Option.SET_EXTENSIONS, custom_extensions, len(custom_extensions)) - custom_result = option(Option.GET_EXTENSIONS) - assert 'myextension' in custom_result - assert 'objectformat' in custom_result - - # Restore original extensions - if original_extensions: - option(Option.SET_EXTENSIONS, original_extensions, len(original_extensions)) - else: - # Reset to empty list if there were no extensions - option(Option.SET_EXTENSIONS, [], 0) - - # Verify restoration - final_extensions = option(Option.GET_EXTENSIONS) - assert set(final_extensions) == set(original_extensions) - except Exception: - # May fail if extensions cannot be modified - pass + + # Verify restoration + final_extensions = option(Option.GET_EXTENSIONS) + assert set(final_extensions) == set(original_extensions) def test_homedir() -> None: @@ -289,13 +285,20 @@ def test_user_agent_product() -> None: def test_add_ssl_x509_cert() -> None: - # Test adding an SSL certificate (basic test, just ensure it doesn't crash) + # Test adding an SSL certificate + # This is a minimal test certificate (not valid, but tests the API) test_cert = "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----" + try: option(Option.ADD_SSL_X509_CERT, test_cert) - except Exception: - # May fail depending on SSL backend - pass + except pygit2.GitError as e: + # May fail if TLS backend doesn't support adding raw certs + # or if the certificate format is invalid + if ( + "TLS backend doesn't support" not in str(e) + and "invalid" not in str(e).lower() + ): + raise def test_mwindow_file_limit() -> None: From 09ce9c48855cbe454aa6d9365a9b784c94b366a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 11 Aug 2025 21:37:35 +0200 Subject: [PATCH 4/9] Appease formatting gods --- pygit2/_libgit2/ffi.pyi | 8 ++- pygit2/options.py | 132 +++++++++++++++++++--------------------- test/test_options.py | 6 +- 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 5936d2f21..c1156fa81 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -337,9 +337,13 @@ def new(a: Literal['char **']) -> _Pointer[char_pointer]: ... @overload def new(a: Literal['char[]', 'char []'], b: bytes | NULL_TYPE) -> ArrayC[char]: ... @overload -def new(a: Literal['char *[]'], b: int) -> ArrayC[char_pointer]: ... # For ext_array in SET_EXTENSIONS +def new( + a: Literal['char *[]'], b: int +) -> ArrayC[char_pointer]: ... # For ext_array in SET_EXTENSIONS @overload -def new(a: Literal['char *[]'], b: list[Any]) -> ArrayC[char_pointer]: ... # For string arrays +def new( + a: Literal['char *[]'], b: list[Any] +) -> ArrayC[char_pointer]: ... # For string arrays def addressof(a: object, attribute: str) -> _Pointer[object]: ... class buffer(bytes): diff --git a/pygit2/options.py b/pygit2/options.py index 9fa13b397..036b8b0ba 100644 --- a/pygit2/options.py +++ b/pygit2/options.py @@ -97,13 +97,13 @@ def check_args(option: Option, arg1: Any, arg2: Any, expected: int) -> None: if expected == 0 and (arg1 is not NOT_PASSED or arg2 is not NOT_PASSED): - raise TypeError(f"option({option}) takes no additional arguments") + raise TypeError(f'option({option}) takes no additional arguments') if expected == 1 and (arg1 is NOT_PASSED or arg2 is not NOT_PASSED): - raise TypeError(f"option({option}, x) requires 1 additional argument") + raise TypeError(f'option({option}, x) requires 1 additional argument') if expected == 2 and (arg1 is NOT_PASSED or arg2 is NOT_PASSED): - raise TypeError(f"option({option}, x, y) requires 2 additional arguments") + raise TypeError(f'option({option}, x, y) requires 2 additional arguments') @overload @@ -316,7 +316,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) ): check_args(option_type, arg1, arg2, 0) - size_ptr = ffi.new("size_t *") + size_ptr = ffi.new('size_t *') err = C.git_libgit2_opts(option_type, size_ptr) check_error(err) return size_ptr[0] @@ -330,14 +330,12 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_args(option_type, arg1, arg2, 1) if not isinstance(arg1, int): - raise TypeError( - f"option value must be an integer, not {type(arg1)}" - ) + raise TypeError(f'option value must be an integer, not {type(arg1)}') size = arg1 if size < 0: - raise ValueError("size must be non-negative") + raise ValueError('size must be non-negative') - err = C.git_libgit2_opts(option_type, ffi.cast("size_t", size)) + err = C.git_libgit2_opts(option_type, ffi.cast('size_t', size)) check_error(err) return None @@ -346,8 +344,8 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_args(option_type, arg1, arg2, 1) level = int(arg1) # Convert enum to int - buf = ffi.new("git_buf *") - err = C.git_libgit2_opts(option_type, ffi.cast("int", level), buf) + buf = ffi.new('git_buf *') + err = C.git_libgit2_opts(option_type, ffi.cast('int', level), buf) check_error(err) try: @@ -372,9 +370,9 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) path_cdata = ffi.NULL else: path_bytes = to_bytes(path) - path_cdata = ffi.new("char[]", path_bytes) + path_cdata = ffi.new('char[]', path_bytes) - err = C.git_libgit2_opts(option_type, ffi.cast("int", level), path_cdata) + err = C.git_libgit2_opts(option_type, ffi.cast('int', level), path_cdata) check_error(err) return None @@ -385,14 +383,14 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) object_type = int(arg1) # Convert enum to int if not isinstance(arg2, int): raise TypeError( - f"option value must be an integer, not {type(arg2).__name__}" + f'option value must be an integer, not {type(arg2).__name__}' ) size = arg2 if size < 0: - raise ValueError("size must be non-negative") + raise ValueError('size must be non-negative') err = C.git_libgit2_opts( - option_type, ffi.cast("int", object_type), ffi.cast("size_t", size) + option_type, ffi.cast('int', object_type), ffi.cast('size_t', size) ) check_error(err) return None @@ -404,10 +402,10 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) size = arg1 if not isinstance(size, int): raise TypeError( - f"option value must be an integer, not {type(size).__name__}" + f'option value must be an integer, not {type(size).__name__}' ) - err = C.git_libgit2_opts(option_type, ffi.cast("ssize_t", size)) + err = C.git_libgit2_opts(option_type, ffi.cast('ssize_t', size)) check_error(err) return None @@ -415,8 +413,8 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_CACHED_MEMORY: check_args(option_type, arg1, arg2, 0) - current_ptr = ffi.new("ssize_t *") - allowed_ptr = ffi.new("ssize_t *") + current_ptr = ffi.new('ssize_t *') + allowed_ptr = ffi.new('ssize_t *') err = C.git_libgit2_opts(option_type, current_ptr, allowed_ptr) check_error(err) return (current_ptr[0], allowed_ptr[0]) @@ -433,14 +431,14 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) cert_file_cdata = ffi.NULL else: cert_file_bytes = to_bytes(cert_file) - cert_file_cdata = ffi.new("char[]", cert_file_bytes) + cert_file_cdata = ffi.new('char[]', cert_file_bytes) cert_dir_cdata: ArrayC[char] | NULL_TYPE if cert_dir is None: cert_dir_cdata = ffi.NULL else: cert_dir_bytes = to_bytes(cert_dir) - cert_dir_cdata = ffi.new("char[]", cert_dir_bytes) + cert_dir_cdata = ffi.new('char[]', cert_dir_bytes) err = C.git_libgit2_opts(option_type, cert_file_cdata, cert_dir_cdata) check_error(err) @@ -464,7 +462,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) # Convert to int (0 or 1) value = 1 if enabled else 0 - err = C.git_libgit2_opts(option_type, ffi.cast("int", value)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', value)) check_error(err) return None @@ -472,7 +470,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_OWNER_VALIDATION: check_args(option_type, arg1, arg2, 0) - enabled_ptr = ffi.new("int *") + enabled_ptr = ffi.new('int *') err = C.git_libgit2_opts(option_type, enabled_ptr) check_error(err) return bool(enabled_ptr[0]) @@ -481,7 +479,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_TEMPLATE_PATH: check_args(option_type, arg1, arg2, 0) - buf = ffi.new("git_buf *") + buf = ffi.new('git_buf *') err = C.git_libgit2_opts(option_type, buf) check_error(err) @@ -505,7 +503,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) template_path_cdata = ffi.NULL else: path_bytes = to_bytes(path) - template_path_cdata = ffi.new("char[]", path_bytes) + template_path_cdata = ffi.new('char[]', path_bytes) err = C.git_libgit2_opts(option_type, template_path_cdata) check_error(err) @@ -515,7 +513,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_USER_AGENT: check_args(option_type, arg1, arg2, 0) - buf = ffi.new("git_buf *") + buf = ffi.new('git_buf *') err = C.git_libgit2_opts(option_type, buf) check_error(err) @@ -535,7 +533,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) agent = arg1 agent_bytes = to_bytes(agent) - agent_cdata = ffi.new("char[]", agent_bytes) + agent_cdata = ffi.new('char[]', agent_bytes) err = C.git_libgit2_opts(option_type, agent_cdata) check_error(err) @@ -547,7 +545,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) ciphers = arg1 ciphers_bytes = to_bytes(ciphers) - ciphers_cdata = ffi.new("char[]", ciphers_bytes) + ciphers_cdata = ffi.new('char[]', ciphers_bytes) err = C.git_libgit2_opts(option_type, ciphers_cdata) check_error(err) @@ -557,7 +555,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_WINDOWS_SHAREMODE: check_args(option_type, arg1, arg2, 0) - value_ptr = ffi.new("unsigned int *") + value_ptr = ffi.new('unsigned int *') err = C.git_libgit2_opts(option_type, value_ptr) check_error(err) return value_ptr[0] @@ -568,13 +566,13 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) value = arg1 if value < 0: - raise ValueError("value must be non-negative") + raise ValueError('value must be non-negative') - err = C.git_libgit2_opts(option_type, ffi.cast("unsigned int", value)) + err = C.git_libgit2_opts(option_type, ffi.cast('unsigned int', value)) check_error(err) return None @@ -582,7 +580,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_PACK_MAX_OBJECTS: check_args(option_type, arg1, arg2, 0) - size_ptr = ffi.new("size_t *") + size_ptr = ffi.new('size_t *') err = C.git_libgit2_opts(option_type, size_ptr) check_error(err) return size_ptr[0] @@ -593,13 +591,13 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) size = arg1 if size < 0: - raise ValueError("size must be non-negative") + raise ValueError('size must be non-negative') - err = C.git_libgit2_opts(option_type, ffi.cast("size_t", size)) + err = C.git_libgit2_opts(option_type, ffi.cast('size_t', size)) check_error(err) return None @@ -611,7 +609,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) # Convert to int (0 or 1) value = 1 if enabled else 0 - err = C.git_libgit2_opts(option_type, ffi.cast("int", value)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', value)) check_error(err) return None @@ -621,11 +619,11 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) priority = arg1 - err = C.git_libgit2_opts(option_type, ffi.cast("int", priority)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', priority)) check_error(err) return None @@ -635,11 +633,11 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) priority = arg1 - err = C.git_libgit2_opts(option_type, ffi.cast("int", priority)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', priority)) check_error(err) return None @@ -648,7 +646,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_args(option_type, arg1, arg2, 0) # GET_EXTENSIONS expects a git_strarray pointer - strarray = ffi.new("git_strarray *") + strarray = ffi.new('git_strarray *') err = C.git_libgit2_opts(option_type, strarray) check_error(err) @@ -663,7 +661,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) finally: # Must dispose of the strarray to free the memory C.git_strarray_dispose(strarray) - + return result # Handle SET_EXTENSIONS @@ -674,22 +672,22 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) length = arg2 if not isinstance(extensions, list): - raise TypeError("extensions must be a list of strings") + raise TypeError('extensions must be a list of strings') if not isinstance(length, int): - raise TypeError("length must be an integer") + raise TypeError('length must be an integer') # Create array of char pointers # libgit2 will make its own copies with git__strdup - ext_array: ArrayC[char_pointer] = ffi.new("char *[]", len(extensions)) + ext_array: ArrayC[char_pointer] = ffi.new('char *[]', len(extensions)) ext_strings: list[ArrayC[char]] = [] # Keep references during the call - + for i, ext in enumerate(extensions): ext_bytes = to_bytes(ext) - ext_string: ArrayC[char] = ffi.new("char[]", ext_bytes) + ext_string: ArrayC[char] = ffi.new('char[]', ext_bytes) ext_strings.append(ext_string) - ext_array[i] = ffi.cast("char *", ext_string) + ext_array[i] = ffi.cast('char *', ext_string) - err = C.git_libgit2_opts(option_type, ext_array, ffi.cast("size_t", length)) + err = C.git_libgit2_opts(option_type, ext_array, ffi.cast('size_t', length)) check_error(err) return None @@ -697,7 +695,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_HOMEDIR: check_args(option_type, arg1, arg2, 0) - buf = ffi.new("git_buf *") + buf = ffi.new('git_buf *') err = C.git_libgit2_opts(option_type, buf) check_error(err) @@ -721,7 +719,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) homedir_cdata = ffi.NULL else: path_bytes = to_bytes(path) - homedir_cdata = ffi.new("char[]", path_bytes) + homedir_cdata = ffi.new('char[]', path_bytes) err = C.git_libgit2_opts(option_type, homedir_cdata) check_error(err) @@ -731,7 +729,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_SERVER_CONNECT_TIMEOUT: check_args(option_type, arg1, arg2, 0) - timeout_ptr = ffi.new("int *") + timeout_ptr = ffi.new('int *') err = C.git_libgit2_opts(option_type, timeout_ptr) check_error(err) return timeout_ptr[0] @@ -742,11 +740,11 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) timeout = arg1 - err = C.git_libgit2_opts(option_type, ffi.cast("int", timeout)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', timeout)) check_error(err) return None @@ -754,7 +752,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_SERVER_TIMEOUT: check_args(option_type, arg1, arg2, 0) - timeout_ptr = ffi.new("int *") + timeout_ptr = ffi.new('int *') err = C.git_libgit2_opts(option_type, timeout_ptr) check_error(err) return timeout_ptr[0] @@ -765,11 +763,11 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) if not isinstance(arg1, int): raise TypeError( - f"option value must be an integer, not {type(arg1).__name__}" + f'option value must be an integer, not {type(arg1).__name__}' ) timeout = arg1 - err = C.git_libgit2_opts(option_type, ffi.cast("int", timeout)) + err = C.git_libgit2_opts(option_type, ffi.cast('int', timeout)) check_error(err) return None @@ -777,7 +775,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) elif option_type == C.GIT_OPT_GET_USER_AGENT_PRODUCT: check_args(option_type, arg1, arg2, 0) - buf = ffi.new("git_buf *") + buf = ffi.new('git_buf *') err = C.git_libgit2_opts(option_type, buf) check_error(err) @@ -797,7 +795,7 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) product = arg1 product_bytes = to_bytes(product) - product_cdata = ffi.new("char[]", product_bytes) + product_cdata = ffi.new('char[]', product_bytes) err = C.git_libgit2_opts(option_type, product_cdata) check_error(err) @@ -810,12 +808,12 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) cert = arg1 if isinstance(cert, (str, bytes)): cert_bytes = to_bytes(cert) - cert_cdata = ffi.new("char[]", cert_bytes) + cert_cdata = ffi.new('char[]', cert_bytes) cert_len = len(cert_bytes) else: - raise TypeError("certificate must be a string or bytes") + raise TypeError('certificate must be a string or bytes') - err = C.git_libgit2_opts(option_type, cert_cdata, ffi.cast("size_t", cert_len)) + err = C.git_libgit2_opts(option_type, cert_cdata, ffi.cast('size_t', cert_len)) check_error(err) return None @@ -823,10 +821,8 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) # because it requires providing C function pointers for memory management # (malloc, free, etc.) that must handle raw memory at the C level, # which cannot be safely implemented in pure Python. - elif option_type in ( - C.GIT_OPT_SET_ALLOCATOR, - ): + elif option_type in (C.GIT_OPT_SET_ALLOCATOR,): return NotImplemented else: - raise ValueError(f"Invalid option {option_type}") + raise ValueError(f'Invalid option {option_type}') diff --git a/test/test_options.py b/test/test_options.py index 78a533688..a92a62318 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -294,9 +294,11 @@ def test_add_ssl_x509_cert() -> None: except pygit2.GitError as e: # May fail if TLS backend doesn't support adding raw certs # or if the certificate format is invalid + msg = str(e).lower() if ( - "TLS backend doesn't support" not in str(e) - and "invalid" not in str(e).lower() + "tls backend doesn't support" not in msg + and "invalid" not in msg + and "failed to add raw x509 certificate" not in msg ): raise From c4ad77db2c807209768dddde3b43002dea45dc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 12 Aug 2025 11:18:57 +0200 Subject: [PATCH 5/9] Include the new names in `__all__` and the high-level `settings` sub-module --- pygit2/__init__.py | 28 ++++ pygit2/settings.py | 143 +++++++++++++++++++ test/test_settings.py | 312 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 483 insertions(+) create mode 100644 test/test_settings.py diff --git a/pygit2/__init__.py b/pygit2/__init__.py index 9f8acacb1..f5250a604 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -297,37 +297,51 @@ from .blob import BlobIO from .options import ( option, + GIT_OPT_ADD_SSL_X509_CERT, GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, GIT_OPT_ENABLE_CACHING, GIT_OPT_ENABLE_FSYNC_GITDIR, + GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, GIT_OPT_ENABLE_OFS_DELTA, GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, GIT_OPT_ENABLE_STRICT_OBJECT_CREATION, GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION, GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, GIT_OPT_GET_CACHED_MEMORY, + GIT_OPT_GET_EXTENSIONS, + GIT_OPT_GET_HOMEDIR, GIT_OPT_GET_MWINDOW_FILE_LIMIT, GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, GIT_OPT_GET_MWINDOW_SIZE, GIT_OPT_GET_OWNER_VALIDATION, GIT_OPT_GET_PACK_MAX_OBJECTS, GIT_OPT_GET_SEARCH_PATH, + GIT_OPT_GET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_GET_SERVER_TIMEOUT, GIT_OPT_GET_TEMPLATE_PATH, GIT_OPT_GET_USER_AGENT, + GIT_OPT_GET_USER_AGENT_PRODUCT, GIT_OPT_GET_WINDOWS_SHAREMODE, GIT_OPT_SET_ALLOCATOR, GIT_OPT_SET_CACHE_MAX_SIZE, GIT_OPT_SET_CACHE_OBJECT_LIMIT, + GIT_OPT_SET_EXTENSIONS, + GIT_OPT_SET_HOMEDIR, GIT_OPT_SET_MWINDOW_FILE_LIMIT, GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, GIT_OPT_SET_MWINDOW_SIZE, + GIT_OPT_SET_ODB_LOOSE_PRIORITY, + GIT_OPT_SET_ODB_PACKED_PRIORITY, GIT_OPT_SET_OWNER_VALIDATION, GIT_OPT_SET_PACK_MAX_OBJECTS, GIT_OPT_SET_SEARCH_PATH, + GIT_OPT_SET_SERVER_CONNECT_TIMEOUT, + GIT_OPT_SET_SERVER_TIMEOUT, GIT_OPT_SET_SSL_CERT_LOCATIONS, GIT_OPT_SET_SSL_CIPHERS, GIT_OPT_SET_TEMPLATE_PATH, GIT_OPT_SET_USER_AGENT, + GIT_OPT_SET_USER_AGENT_PRODUCT, GIT_OPT_SET_WINDOWS_SHAREMODE, ) from .callbacks import ( @@ -747,37 +761,51 @@ def clone_repository( 'GIT_OBJECT_REF_DELTA', 'GIT_OBJECT_TAG', 'GIT_OBJECT_TREE', + 'GIT_OPT_ADD_SSL_X509_CERT', 'GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS', 'GIT_OPT_ENABLE_CACHING', 'GIT_OPT_ENABLE_FSYNC_GITDIR', + 'GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE', 'GIT_OPT_ENABLE_OFS_DELTA', 'GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION', 'GIT_OPT_ENABLE_STRICT_OBJECT_CREATION', 'GIT_OPT_ENABLE_STRICT_SYMBOLIC_REF_CREATION', 'GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY', 'GIT_OPT_GET_CACHED_MEMORY', + 'GIT_OPT_GET_EXTENSIONS', + 'GIT_OPT_GET_HOMEDIR', 'GIT_OPT_GET_MWINDOW_FILE_LIMIT', 'GIT_OPT_GET_MWINDOW_MAPPED_LIMIT', 'GIT_OPT_GET_MWINDOW_SIZE', 'GIT_OPT_GET_OWNER_VALIDATION', 'GIT_OPT_GET_PACK_MAX_OBJECTS', 'GIT_OPT_GET_SEARCH_PATH', + 'GIT_OPT_GET_SERVER_CONNECT_TIMEOUT', + 'GIT_OPT_GET_SERVER_TIMEOUT', 'GIT_OPT_GET_TEMPLATE_PATH', 'GIT_OPT_GET_USER_AGENT', + 'GIT_OPT_GET_USER_AGENT_PRODUCT', 'GIT_OPT_GET_WINDOWS_SHAREMODE', 'GIT_OPT_SET_ALLOCATOR', 'GIT_OPT_SET_CACHE_MAX_SIZE', 'GIT_OPT_SET_CACHE_OBJECT_LIMIT', + 'GIT_OPT_SET_EXTENSIONS', + 'GIT_OPT_SET_HOMEDIR', 'GIT_OPT_SET_MWINDOW_FILE_LIMIT', 'GIT_OPT_SET_MWINDOW_MAPPED_LIMIT', 'GIT_OPT_SET_MWINDOW_SIZE', + 'GIT_OPT_SET_ODB_LOOSE_PRIORITY', + 'GIT_OPT_SET_ODB_PACKED_PRIORITY', 'GIT_OPT_SET_OWNER_VALIDATION', 'GIT_OPT_SET_PACK_MAX_OBJECTS', 'GIT_OPT_SET_SEARCH_PATH', + 'GIT_OPT_SET_SERVER_CONNECT_TIMEOUT', + 'GIT_OPT_SET_SERVER_TIMEOUT', 'GIT_OPT_SET_SSL_CERT_LOCATIONS', 'GIT_OPT_SET_SSL_CIPHERS', 'GIT_OPT_SET_TEMPLATE_PATH', 'GIT_OPT_SET_USER_AGENT', + 'GIT_OPT_SET_USER_AGENT_PRODUCT', 'GIT_OPT_SET_WINDOWS_SHAREMODE', 'GIT_REFERENCES_ALL', 'GIT_REFERENCES_BRANCHES', diff --git a/pygit2/settings.py b/pygit2/settings.py index a6e2a100c..a3d1e186d 100644 --- a/pygit2/settings.py +++ b/pygit2/settings.py @@ -105,6 +105,15 @@ def mwindow_mapped_limit(self) -> int: def mwindow_mapped_limit(self, value: int) -> None: option(Option.SET_MWINDOW_MAPPED_LIMIT, value) + @property + def mwindow_file_limit(self) -> int: + """Get or set the maximum number of files to be mapped at any time""" + return option(Option.GET_MWINDOW_FILE_LIMIT) + + @mwindow_file_limit.setter + def mwindow_file_limit(self, value: int) -> None: + option(Option.SET_MWINDOW_FILE_LIMIT, value) + @property def cached_memory(self) -> tuple[int, int]: """ @@ -207,3 +216,137 @@ def set_ssl_cert_locations( option(Option.SET_SSL_CERT_LOCATIONS, cert_file, cert_dir) self._ssl_cert_file = cert_file self._ssl_cert_dir = cert_dir + + @property + def template_path(self) -> str | None: + """Get or set the default template path for new repositories""" + return option(Option.GET_TEMPLATE_PATH) + + @template_path.setter + def template_path(self, value: str | bytes) -> None: + option(Option.SET_TEMPLATE_PATH, value) + + @property + def user_agent(self) -> str | None: + """Get or set the user agent string for network operations""" + return option(Option.GET_USER_AGENT) + + @user_agent.setter + def user_agent(self, value: str | bytes) -> None: + option(Option.SET_USER_AGENT, value) + + @property + def user_agent_product(self) -> str | None: + """Get or set the user agent product name""" + return option(Option.GET_USER_AGENT_PRODUCT) + + @user_agent_product.setter + def user_agent_product(self, value: str | bytes) -> None: + option(Option.SET_USER_AGENT_PRODUCT, value) + + def set_ssl_ciphers(self, ciphers: str | bytes) -> None: + """Set the SSL ciphers to use for HTTPS connections""" + option(Option.SET_SSL_CIPHERS, ciphers) + + def add_ssl_x509_cert(self, certificate: str | bytes) -> None: + """Add a trusted X.509 certificate for SSL/TLS connections""" + option(Option.ADD_SSL_X509_CERT, certificate) + + def enable_strict_object_creation(self, value: bool = True) -> None: + """Enable or disable strict object creation validation""" + option(Option.ENABLE_STRICT_OBJECT_CREATION, value) + + def enable_strict_symbolic_ref_creation(self, value: bool = True) -> None: + """Enable or disable strict symbolic reference creation validation""" + option(Option.ENABLE_STRICT_SYMBOLIC_REF_CREATION, value) + + def enable_ofs_delta(self, value: bool = True) -> None: + """Enable or disable offset delta encoding""" + option(Option.ENABLE_OFS_DELTA, value) + + def enable_fsync_gitdir(self, value: bool = True) -> None: + """Enable or disable fsync for git directory operations""" + option(Option.ENABLE_FSYNC_GITDIR, value) + + def enable_strict_hash_verification(self, value: bool = True) -> None: + """Enable or disable strict hash verification""" + option(Option.ENABLE_STRICT_HASH_VERIFICATION, value) + + def enable_unsaved_index_safety(self, value: bool = True) -> None: + """Enable or disable unsaved index safety checks""" + option(Option.ENABLE_UNSAVED_INDEX_SAFETY, value) + + def enable_http_expect_continue(self, value: bool = True) -> None: + """Enable or disable HTTP Expect/Continue for large pushes""" + option(Option.ENABLE_HTTP_EXPECT_CONTINUE, value) + + @property + def windows_sharemode(self) -> int: + """Get or set the Windows share mode for opening files""" + return option(Option.GET_WINDOWS_SHAREMODE) + + @windows_sharemode.setter + def windows_sharemode(self, value: int) -> None: + option(Option.SET_WINDOWS_SHAREMODE, value) + + @property + def pack_max_objects(self) -> int: + """Get or set the maximum number of objects in a pack""" + return option(Option.GET_PACK_MAX_OBJECTS) + + @pack_max_objects.setter + def pack_max_objects(self, value: int) -> None: + option(Option.SET_PACK_MAX_OBJECTS, value) + + @property + def owner_validation(self) -> bool: + """Get or set repository directory ownership validation""" + return option(Option.GET_OWNER_VALIDATION) + + @owner_validation.setter + def owner_validation(self, value: bool) -> None: + option(Option.SET_OWNER_VALIDATION, value) + + def set_odb_packed_priority(self, priority: int) -> None: + """Set the priority for packed ODB backend (default 1)""" + option(Option.SET_ODB_PACKED_PRIORITY, priority) + + def set_odb_loose_priority(self, priority: int) -> None: + """Set the priority for loose ODB backend (default 2)""" + option(Option.SET_ODB_LOOSE_PRIORITY, priority) + + @property + def extensions(self) -> list[str]: + """Get the list of enabled extensions""" + return option(Option.GET_EXTENSIONS) + + def set_extensions(self, extensions: list[str]) -> None: + """Set the list of enabled extensions""" + option(Option.SET_EXTENSIONS, extensions, len(extensions)) + + @property + def homedir(self) -> str | None: + """Get or set the home directory""" + return option(Option.GET_HOMEDIR) + + @homedir.setter + def homedir(self, value: str | bytes) -> None: + option(Option.SET_HOMEDIR, value) + + @property + def server_connect_timeout(self) -> int: + """Get or set the server connection timeout in milliseconds""" + return option(Option.GET_SERVER_CONNECT_TIMEOUT) + + @server_connect_timeout.setter + def server_connect_timeout(self, value: int) -> None: + option(Option.SET_SERVER_CONNECT_TIMEOUT, value) + + @property + def server_timeout(self) -> int: + """Get or set the server timeout in milliseconds""" + return option(Option.GET_SERVER_TIMEOUT) + + @server_timeout.setter + def server_timeout(self, value: int) -> None: + option(Option.SET_SERVER_TIMEOUT, value) diff --git a/test/test_settings.py b/test/test_settings.py new file mode 100644 index 000000000..dfe1d4c29 --- /dev/null +++ b/test/test_settings.py @@ -0,0 +1,312 @@ +# Copyright 2010-2025 The pygit2 contributors +# +# This file is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# In addition to the permissions in the GNU General Public License, +# the authors give you unlimited permission to link the compiled +# version of this file into combinations with other programs, +# and to distribute those combinations without any restriction +# coming from the use of this file. (The General Public License +# restrictions do apply in other respects; for example, they cover +# modification of the file, and distribution when not linked into +# a combined executable.) +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +"""Test the Settings class.""" + +import sys +import pytest +import pygit2 +from pygit2.enums import ConfigLevel, ObjectType + + +def test_mwindow_size() -> None: + """Test get/set mwindow size""" + original = pygit2.settings.mwindow_size + try: + test_size = 200 * 1024 + pygit2.settings.mwindow_size = test_size + assert pygit2.settings.mwindow_size == test_size + finally: + pygit2.settings.mwindow_size = original + + +def test_mwindow_mapped_limit() -> None: + """Test get/set mwindow mapped limit""" + original = pygit2.settings.mwindow_mapped_limit + try: + test_limit = 300 * 1024 + pygit2.settings.mwindow_mapped_limit = test_limit + assert pygit2.settings.mwindow_mapped_limit == test_limit + finally: + pygit2.settings.mwindow_mapped_limit = original + + +def test_cached_memory() -> None: + """Test get cached memory""" + cached = pygit2.settings.cached_memory + assert isinstance(cached, tuple) + assert len(cached) == 2 + assert isinstance(cached[0], int) + assert isinstance(cached[1], int) + + +def test_enable_caching() -> None: + """Test enable/disable caching""" + # Verify the method exists and accepts boolean values without raising + assert hasattr(pygit2.settings, 'enable_caching') + assert callable(pygit2.settings.enable_caching) + + # Should not raise exceptions + pygit2.settings.enable_caching(False) + pygit2.settings.enable_caching(True) + + +def test_disable_pack_keep_file_checks() -> None: + """Test disable pack keep file checks""" + assert hasattr(pygit2.settings, 'disable_pack_keep_file_checks') + assert callable(pygit2.settings.disable_pack_keep_file_checks) + + # Should not raise exceptions + pygit2.settings.disable_pack_keep_file_checks(False) + pygit2.settings.disable_pack_keep_file_checks(True) + pygit2.settings.disable_pack_keep_file_checks(False) + + +def test_cache_max_size() -> None: + """Test set cache max size""" + original_max_size = pygit2.settings.cached_memory[1] + try: + pygit2.settings.cache_max_size(128 * 1024**2) + assert pygit2.settings.cached_memory[1] == 128 * 1024**2 + pygit2.settings.cache_max_size(256 * 1024**2) + assert pygit2.settings.cached_memory[1] == 256 * 1024**2 + finally: + pygit2.settings.cache_max_size(original_max_size) + + +@pytest.mark.parametrize("object_type,test_size,default_size", [ + (ObjectType.BLOB, 2 * 1024, 0), + (ObjectType.COMMIT, 8 * 1024, 4096), + (ObjectType.TREE, 8 * 1024, 4096), + (ObjectType.TAG, 8 * 1024, 4096), + (ObjectType.BLOB, 0, 0), +]) +def test_cache_object_limit(object_type: ObjectType, test_size: int, default_size: int) -> None: + """Test set cache object limit""" + assert callable(pygit2.settings.cache_object_limit) + + pygit2.settings.cache_object_limit(object_type, test_size) + pygit2.settings.cache_object_limit(object_type, default_size) + + +@pytest.mark.parametrize("level,test_path", [ + (ConfigLevel.GLOBAL, '/tmp/test_global'), + (ConfigLevel.XDG, '/tmp/test_xdg'), + (ConfigLevel.SYSTEM, '/tmp/test_system'), +]) +def test_search_path(level: ConfigLevel, test_path: str) -> None: + """Test get/set search paths""" + original = pygit2.settings.search_path[level] + try: + pygit2.settings.search_path[level] = test_path + assert pygit2.settings.search_path[level] == test_path + finally: + pygit2.settings.search_path[level] = original + + +def test_template_path() -> None: + """Test get/set template path""" + original = pygit2.settings.template_path + try: + pygit2.settings.template_path = '/tmp/test_templates' + assert pygit2.settings.template_path == '/tmp/test_templates' + finally: + if original: + pygit2.settings.template_path = original + + +def test_user_agent() -> None: + """Test get/set user agent""" + original = pygit2.settings.user_agent + try: + pygit2.settings.user_agent = 'test-agent/1.0' + assert pygit2.settings.user_agent == 'test-agent/1.0' + finally: + if original: + pygit2.settings.user_agent = original + + +def test_user_agent_product() -> None: + """Test get/set user agent product""" + original = pygit2.settings.user_agent_product + try: + pygit2.settings.user_agent_product = 'test-product' + assert pygit2.settings.user_agent_product == 'test-product' + finally: + if original: + pygit2.settings.user_agent_product = original + + +def test_pack_max_objects() -> None: + """Test get/set pack max objects""" + original = pygit2.settings.pack_max_objects + try: + pygit2.settings.pack_max_objects = 100000 + assert pygit2.settings.pack_max_objects == 100000 + finally: + pygit2.settings.pack_max_objects = original + + +def test_owner_validation() -> None: + """Test get/set owner validation""" + original = pygit2.settings.owner_validation + try: + pygit2.settings.owner_validation = False + assert pygit2.settings.owner_validation == False # noqa: E712 + pygit2.settings.owner_validation = True + assert pygit2.settings.owner_validation == True # noqa: E712 + finally: + pygit2.settings.owner_validation = original + + +def test_mwindow_file_limit() -> None: + """Test get/set mwindow file limit""" + original = pygit2.settings.mwindow_file_limit + try: + pygit2.settings.mwindow_file_limit = 100 + assert pygit2.settings.mwindow_file_limit == 100 + finally: + pygit2.settings.mwindow_file_limit = original + + +def test_homedir() -> None: + """Test get/set home directory""" + original = pygit2.settings.homedir + try: + pygit2.settings.homedir = '/tmp/test_home' + assert pygit2.settings.homedir == '/tmp/test_home' + finally: + if original: + pygit2.settings.homedir = original + + +def test_server_timeouts() -> None: + """Test get/set server timeouts""" + original_connect = pygit2.settings.server_connect_timeout + original_timeout = pygit2.settings.server_timeout + try: + pygit2.settings.server_connect_timeout = 5000 + assert pygit2.settings.server_connect_timeout == 5000 + + pygit2.settings.server_timeout = 10000 + assert pygit2.settings.server_timeout == 10000 + finally: + pygit2.settings.server_connect_timeout = original_connect + pygit2.settings.server_timeout = original_timeout + + +def test_extensions() -> None: + """Test get/set extensions""" + original = pygit2.settings.extensions + try: + test_extensions = ['objectformat', 'worktreeconfig'] + pygit2.settings.set_extensions(test_extensions) + + new_extensions = pygit2.settings.extensions + for ext in test_extensions: + assert ext in new_extensions + finally: + if original: + pygit2.settings.set_extensions(original) + + +@pytest.mark.parametrize("method_name,default_value", [ + ('enable_strict_object_creation', True), + ('enable_strict_symbolic_ref_creation', True), + ('enable_ofs_delta', True), + ('enable_fsync_gitdir', False), + ('enable_strict_hash_verification', True), + ('enable_unsaved_index_safety', False), + ('enable_http_expect_continue', False), +]) +def test_enable_methods(method_name: str, default_value: bool) -> None: + """Test various enable methods""" + assert hasattr(pygit2.settings, method_name) + method = getattr(pygit2.settings, method_name) + assert callable(method) + + method(True) + method(False) + method(default_value) + + +@pytest.mark.parametrize("priority", [1, 5, 10, 0, -1, -2]) +def test_odb_priorities(priority: int) -> None: + """Test setting ODB priorities""" + assert hasattr(pygit2.settings, 'set_odb_packed_priority') + assert hasattr(pygit2.settings, 'set_odb_loose_priority') + assert callable(pygit2.settings.set_odb_packed_priority) + assert callable(pygit2.settings.set_odb_loose_priority) + + pygit2.settings.set_odb_packed_priority(priority) + pygit2.settings.set_odb_loose_priority(priority) + + pygit2.settings.set_odb_packed_priority(1) + pygit2.settings.set_odb_loose_priority(2) + + +def test_ssl_methods() -> None: + """Test SSL-related methods""" + assert callable(pygit2.settings.set_ssl_ciphers) + assert callable(pygit2.settings.add_ssl_x509_cert) + + ssl_ciphers_supported = True + try: + pygit2.settings.set_ssl_ciphers('DEFAULT') + except pygit2.GitError as e: + msg = str(e).lower() + if "tls backend doesn't support" not in msg: + raise + ssl_ciphers_supported = False + + test_cert = "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----" + x509_cert_supported = True + try: + pygit2.settings.add_ssl_x509_cert(test_cert) + except pygit2.GitError as e: + msg = str(e).lower() + if ( + "tls backend doesn't support" not in msg + and "invalid" not in msg + and "failed to add raw x509 certificate" not in msg + ): + raise + x509_cert_supported = False + + # At least verify the methods exist even if not supported + assert not ssl_ciphers_supported or not x509_cert_supported or True + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific feature") +def test_windows_sharemode() -> None: + """Test get/set Windows share mode""" + original = pygit2.settings.windows_sharemode + try: + pygit2.settings.windows_sharemode = 1 + assert pygit2.settings.windows_sharemode == 1 + pygit2.settings.windows_sharemode = 2 + assert pygit2.settings.windows_sharemode == 2 + finally: + pygit2.settings.windows_sharemode = original \ No newline at end of file From 230fad999765a118cfe06c7cc74577b5bd25c3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 12 Aug 2025 11:19:40 +0200 Subject: [PATCH 6/9] More reformatting --- test/test_options.py | 6 +-- test/test_settings.py | 89 ++++++++++++++++++++++++------------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/test/test_options.py b/test/test_options.py index a92a62318..ed746824b 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -287,7 +287,7 @@ def test_user_agent_product() -> None: def test_add_ssl_x509_cert() -> None: # Test adding an SSL certificate # This is a minimal test certificate (not valid, but tests the API) - test_cert = "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----" + test_cert = '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----' try: option(Option.ADD_SSL_X509_CERT, test_cert) @@ -297,8 +297,8 @@ def test_add_ssl_x509_cert() -> None: msg = str(e).lower() if ( "tls backend doesn't support" not in msg - and "invalid" not in msg - and "failed to add raw x509 certificate" not in msg + and 'invalid' not in msg + and 'failed to add raw x509 certificate' not in msg ): raise diff --git a/test/test_settings.py b/test/test_settings.py index dfe1d4c29..f24ddfb3f 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -67,7 +67,7 @@ def test_enable_caching() -> None: # Verify the method exists and accepts boolean values without raising assert hasattr(pygit2.settings, 'enable_caching') assert callable(pygit2.settings.enable_caching) - + # Should not raise exceptions pygit2.settings.enable_caching(False) pygit2.settings.enable_caching(True) @@ -77,7 +77,7 @@ def test_disable_pack_keep_file_checks() -> None: """Test disable pack keep file checks""" assert hasattr(pygit2.settings, 'disable_pack_keep_file_checks') assert callable(pygit2.settings.disable_pack_keep_file_checks) - + # Should not raise exceptions pygit2.settings.disable_pack_keep_file_checks(False) pygit2.settings.disable_pack_keep_file_checks(True) @@ -96,26 +96,34 @@ def test_cache_max_size() -> None: pygit2.settings.cache_max_size(original_max_size) -@pytest.mark.parametrize("object_type,test_size,default_size", [ - (ObjectType.BLOB, 2 * 1024, 0), - (ObjectType.COMMIT, 8 * 1024, 4096), - (ObjectType.TREE, 8 * 1024, 4096), - (ObjectType.TAG, 8 * 1024, 4096), - (ObjectType.BLOB, 0, 0), -]) -def test_cache_object_limit(object_type: ObjectType, test_size: int, default_size: int) -> None: +@pytest.mark.parametrize( + 'object_type,test_size,default_size', + [ + (ObjectType.BLOB, 2 * 1024, 0), + (ObjectType.COMMIT, 8 * 1024, 4096), + (ObjectType.TREE, 8 * 1024, 4096), + (ObjectType.TAG, 8 * 1024, 4096), + (ObjectType.BLOB, 0, 0), + ], +) +def test_cache_object_limit( + object_type: ObjectType, test_size: int, default_size: int +) -> None: """Test set cache object limit""" assert callable(pygit2.settings.cache_object_limit) - + pygit2.settings.cache_object_limit(object_type, test_size) pygit2.settings.cache_object_limit(object_type, default_size) -@pytest.mark.parametrize("level,test_path", [ - (ConfigLevel.GLOBAL, '/tmp/test_global'), - (ConfigLevel.XDG, '/tmp/test_xdg'), - (ConfigLevel.SYSTEM, '/tmp/test_system'), -]) +@pytest.mark.parametrize( + 'level,test_path', + [ + (ConfigLevel.GLOBAL, '/tmp/test_global'), + (ConfigLevel.XDG, '/tmp/test_xdg'), + (ConfigLevel.SYSTEM, '/tmp/test_system'), + ], +) def test_search_path(level: ConfigLevel, test_path: str) -> None: """Test get/set search paths""" original = pygit2.settings.search_path[level] @@ -209,7 +217,7 @@ def test_server_timeouts() -> None: try: pygit2.settings.server_connect_timeout = 5000 assert pygit2.settings.server_connect_timeout == 5000 - + pygit2.settings.server_timeout = 10000 assert pygit2.settings.server_timeout == 10000 finally: @@ -223,7 +231,7 @@ def test_extensions() -> None: try: test_extensions = ['objectformat', 'worktreeconfig'] pygit2.settings.set_extensions(test_extensions) - + new_extensions = pygit2.settings.extensions for ext in test_extensions: assert ext in new_extensions @@ -232,37 +240,40 @@ def test_extensions() -> None: pygit2.settings.set_extensions(original) -@pytest.mark.parametrize("method_name,default_value", [ - ('enable_strict_object_creation', True), - ('enable_strict_symbolic_ref_creation', True), - ('enable_ofs_delta', True), - ('enable_fsync_gitdir', False), - ('enable_strict_hash_verification', True), - ('enable_unsaved_index_safety', False), - ('enable_http_expect_continue', False), -]) +@pytest.mark.parametrize( + 'method_name,default_value', + [ + ('enable_strict_object_creation', True), + ('enable_strict_symbolic_ref_creation', True), + ('enable_ofs_delta', True), + ('enable_fsync_gitdir', False), + ('enable_strict_hash_verification', True), + ('enable_unsaved_index_safety', False), + ('enable_http_expect_continue', False), + ], +) def test_enable_methods(method_name: str, default_value: bool) -> None: """Test various enable methods""" assert hasattr(pygit2.settings, method_name) method = getattr(pygit2.settings, method_name) assert callable(method) - + method(True) method(False) method(default_value) -@pytest.mark.parametrize("priority", [1, 5, 10, 0, -1, -2]) +@pytest.mark.parametrize('priority', [1, 5, 10, 0, -1, -2]) def test_odb_priorities(priority: int) -> None: """Test setting ODB priorities""" assert hasattr(pygit2.settings, 'set_odb_packed_priority') assert hasattr(pygit2.settings, 'set_odb_loose_priority') assert callable(pygit2.settings.set_odb_packed_priority) assert callable(pygit2.settings.set_odb_loose_priority) - + pygit2.settings.set_odb_packed_priority(priority) pygit2.settings.set_odb_loose_priority(priority) - + pygit2.settings.set_odb_packed_priority(1) pygit2.settings.set_odb_loose_priority(2) @@ -271,7 +282,7 @@ def test_ssl_methods() -> None: """Test SSL-related methods""" assert callable(pygit2.settings.set_ssl_ciphers) assert callable(pygit2.settings.add_ssl_x509_cert) - + ssl_ciphers_supported = True try: pygit2.settings.set_ssl_ciphers('DEFAULT') @@ -280,8 +291,8 @@ def test_ssl_methods() -> None: if "tls backend doesn't support" not in msg: raise ssl_ciphers_supported = False - - test_cert = "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----" + + test_cert = '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----' x509_cert_supported = True try: pygit2.settings.add_ssl_x509_cert(test_cert) @@ -289,17 +300,17 @@ def test_ssl_methods() -> None: msg = str(e).lower() if ( "tls backend doesn't support" not in msg - and "invalid" not in msg - and "failed to add raw x509 certificate" not in msg + and 'invalid' not in msg + and 'failed to add raw x509 certificate' not in msg ): raise x509_cert_supported = False - + # At least verify the methods exist even if not supported assert not ssl_ciphers_supported or not x509_cert_supported or True -@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific feature") +@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-specific feature') def test_windows_sharemode() -> None: """Test get/set Windows share mode""" original = pygit2.settings.windows_sharemode @@ -309,4 +320,4 @@ def test_windows_sharemode() -> None: pygit2.settings.windows_sharemode = 2 assert pygit2.settings.windows_sharemode == 2 finally: - pygit2.settings.windows_sharemode = original \ No newline at end of file + pygit2.settings.windows_sharemode = original From 6007f9725f920cfde35f0ec3dc1189431dc850ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 12 Aug 2025 11:23:38 +0200 Subject: [PATCH 7/9] ruff check --fix --- pygit2/__init__.py | 36 ++++++++++++++++++------------------ pygit2/_pygit2.pyi | 2 -- pygit2/enums.py | 3 +-- pygit2/options.py | 13 ++++++------- pygit2/settings.py | 2 +- test/test_settings.py | 2 ++ 6 files changed, 28 insertions(+), 30 deletions(-) diff --git a/pygit2/__init__.py b/pygit2/__init__.py index f5250a604..d5edd2a10 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -295,8 +295,24 @@ ) from .blame import Blame, BlameHunk from .blob import BlobIO +from .callbacks import ( + CheckoutCallbacks, + Payload, + RemoteCallbacks, + StashApplyCallbacks, + get_credentials, + git_clone_options, + git_fetch_options, + git_proxy_options, +) +from .config import Config +from .credentials import * +from .errors import Passthrough, check_error +from .ffi import C, ffi +from .filter import Filter +from .index import Index, IndexEntry +from .legacyenums import * from .options import ( - option, GIT_OPT_ADD_SSL_X509_CERT, GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, GIT_OPT_ENABLE_CACHING, @@ -343,24 +359,8 @@ GIT_OPT_SET_USER_AGENT, GIT_OPT_SET_USER_AGENT_PRODUCT, GIT_OPT_SET_WINDOWS_SHAREMODE, + option, ) -from .callbacks import ( - CheckoutCallbacks, - Payload, - RemoteCallbacks, - StashApplyCallbacks, - get_credentials, - git_clone_options, - git_fetch_options, - git_proxy_options, -) -from .config import Config -from .credentials import * -from .errors import Passthrough, check_error -from .ffi import C, ffi -from .filter import Filter -from .index import Index, IndexEntry -from .legacyenums import * from .packbuilder import PackBuilder from .remotes import Remote from .repository import Repository diff --git a/pygit2/_pygit2.pyi b/pygit2/_pygit2.pyi index 0a2245be9..1172f965a 100644 --- a/pygit2/_pygit2.pyi +++ b/pygit2/_pygit2.pyi @@ -25,7 +25,6 @@ from .enums import ( ApplyLocation, BlobFilter, BranchType, - ConfigLevel, DeltaStatus, DiffFind, DiffFlag, @@ -35,7 +34,6 @@ from .enums import ( MergeAnalysis, MergePreference, ObjectType, - Option, ReferenceFilter, ReferenceType, ResetMode, diff --git a/pygit2/enums.py b/pygit2/enums.py index 71239ca91..d690e73ae 100644 --- a/pygit2/enums.py +++ b/pygit2/enums.py @@ -25,8 +25,7 @@ from enum import IntEnum, IntFlag -from . import _pygit2 -from . import options +from . import _pygit2, options from .ffi import C diff --git a/pygit2/options.py b/pygit2/options.py index 036b8b0ba..30bdd13cf 100644 --- a/pygit2/options.py +++ b/pygit2/options.py @@ -1,4 +1,3 @@ -from __future__ import annotations # Copyright 2010-2025 The pygit2 contributors # # This file is free software; you can redistribute it and/or modify @@ -28,18 +27,18 @@ Libgit2 global options management using CFFI. """ -from typing import Any, Literal, overload, cast +from __future__ import annotations + +# Import only for type checking to avoid circular imports +from typing import TYPE_CHECKING, Any, Literal, cast, overload -from .ffi import C, ffi from .errors import check_error +from .ffi import C, ffi from .utils import to_bytes, to_str -# Import only for type checking to avoid circular imports -from typing import TYPE_CHECKING - if TYPE_CHECKING: + from ._libgit2.ffi import NULL_TYPE, ArrayC, char, char_pointer from .enums import ConfigLevel, ObjectType, Option - from ._libgit2.ffi import ArrayC, NULL_TYPE, char, char_pointer # Export GIT_OPT constants for backward compatibility GIT_OPT_GET_MWINDOW_SIZE: int = C.GIT_OPT_GET_MWINDOW_SIZE diff --git a/pygit2/settings.py b/pygit2/settings.py index a3d1e186d..feecea42c 100644 --- a/pygit2/settings.py +++ b/pygit2/settings.py @@ -32,9 +32,9 @@ import pygit2.enums -from .options import option from .enums import ConfigLevel, Option from .errors import GitError +from .options import option class SearchPathList: diff --git a/test/test_settings.py b/test/test_settings.py index f24ddfb3f..5d0dedb5e 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -26,7 +26,9 @@ """Test the Settings class.""" import sys + import pytest + import pygit2 from pygit2.enums import ConfigLevel, ObjectType From 13dc3016e1998d1fe822c19c2b6aab44ff0b63bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 12 Aug 2025 14:56:00 +0200 Subject: [PATCH 8/9] Drop (broken) support for GIT_OPT_ADD_SSL_X509_CERT --- pygit2/options.py | 36 +++++-------------------- pygit2/settings.py | 4 --- test/test_options.py | 63 +++++++------------------------------------ test/test_settings.py | 49 +++------------------------------ 4 files changed, 19 insertions(+), 133 deletions(-) diff --git a/pygit2/options.py b/pygit2/options.py index 30bdd13cf..d238a2999 100644 --- a/pygit2/options.py +++ b/pygit2/options.py @@ -307,7 +307,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) Set the maximum number of objects to include in a pack. """ - # Handle GET options with size_t output if option_type in ( C.GIT_OPT_GET_MWINDOW_SIZE, C.GIT_OPT_GET_MWINDOW_MAPPED_LIMIT, @@ -320,7 +319,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return size_ptr[0] - # Handle SET options with size_t input elif option_type in ( C.GIT_OPT_SET_MWINDOW_SIZE, C.GIT_OPT_SET_MWINDOW_MAPPED_LIMIT, @@ -338,7 +336,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle GET_SEARCH_PATH elif option_type == C.GIT_OPT_GET_SEARCH_PATH: check_args(option_type, arg1, arg2, 1) @@ -357,7 +354,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) return result - # Handle SET_SEARCH_PATH elif option_type == C.GIT_OPT_SET_SEARCH_PATH: check_args(option_type, arg1, arg2, 2) @@ -375,7 +371,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle SET_CACHE_OBJECT_LIMIT elif option_type == C.GIT_OPT_SET_CACHE_OBJECT_LIMIT: check_args(option_type, arg1, arg2, 2) @@ -394,7 +389,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle SET_CACHE_MAX_SIZE elif option_type == C.GIT_OPT_SET_CACHE_MAX_SIZE: check_args(option_type, arg1, arg2, 1) @@ -408,7 +402,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle GET_CACHED_MEMORY elif option_type == C.GIT_OPT_GET_CACHED_MEMORY: check_args(option_type, arg1, arg2, 0) @@ -418,7 +411,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return (current_ptr[0], allowed_ptr[0]) - # Handle SET_SSL_CERT_LOCATIONS elif option_type == C.GIT_OPT_SET_SSL_CERT_LOCATIONS: check_args(option_type, arg1, arg2, 2) @@ -465,7 +457,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle GET_OWNER_VALIDATION elif option_type == C.GIT_OPT_GET_OWNER_VALIDATION: check_args(option_type, arg1, arg2, 0) @@ -474,7 +465,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return bool(enabled_ptr[0]) - # Handle GET_TEMPLATE_PATH elif option_type == C.GIT_OPT_GET_TEMPLATE_PATH: check_args(option_type, arg1, arg2, 0) @@ -492,7 +482,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) return result - # Handle SET_TEMPLATE_PATH elif option_type == C.GIT_OPT_SET_TEMPLATE_PATH: check_args(option_type, arg1, arg2, 1) @@ -508,7 +497,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle GET_USER_AGENT elif option_type == C.GIT_OPT_GET_USER_AGENT: check_args(option_type, arg1, arg2, 0) @@ -526,7 +514,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) return result - # Handle SET_USER_AGENT elif option_type == C.GIT_OPT_SET_USER_AGENT: check_args(option_type, arg1, arg2, 1) @@ -538,7 +525,6 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle SET_SSL_CIPHERS elif option_type == C.GIT_OPT_SET_SSL_CIPHERS: check_args(option_type, arg1, arg2, 1) @@ -800,28 +786,18 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) check_error(err) return None - # Handle ADD_SSL_X509_CERT + # Not implemented - ADD_SSL_X509_CERT requires directly binding with OpenSSL + # as the API works accepts a X509* struct. Use GIT_OPT_SET_SSL_CERT_LOCATIONS + # instead. elif option_type == C.GIT_OPT_ADD_SSL_X509_CERT: - check_args(option_type, arg1, arg2, 1) - - cert = arg1 - if isinstance(cert, (str, bytes)): - cert_bytes = to_bytes(cert) - cert_cdata = ffi.new('char[]', cert_bytes) - cert_len = len(cert_bytes) - else: - raise TypeError('certificate must be a string or bytes') - - err = C.git_libgit2_opts(option_type, cert_cdata, ffi.cast('size_t', cert_len)) - check_error(err) - return None + raise NotImplementedError("Use GIT_OPT_SET_SSL_CERT_LOCATIONS instead") # Not implemented - SET_ALLOCATOR is not feasible from Python level # because it requires providing C function pointers for memory management # (malloc, free, etc.) that must handle raw memory at the C level, # which cannot be safely implemented in pure Python. - elif option_type in (C.GIT_OPT_SET_ALLOCATOR,): - return NotImplemented + elif option_type == C.GIT_OPT_SET_ALLOCATOR: + raise NotImplementedError("Setting a custom allocator not possible from Python") else: raise ValueError(f'Invalid option {option_type}') diff --git a/pygit2/settings.py b/pygit2/settings.py index feecea42c..91b5d3191 100644 --- a/pygit2/settings.py +++ b/pygit2/settings.py @@ -248,10 +248,6 @@ def set_ssl_ciphers(self, ciphers: str | bytes) -> None: """Set the SSL ciphers to use for HTTPS connections""" option(Option.SET_SSL_CIPHERS, ciphers) - def add_ssl_x509_cert(self, certificate: str | bytes) -> None: - """Add a trusted X.509 certificate for SSL/TLS connections""" - option(Option.ADD_SSL_X509_CERT, certificate) - def enable_strict_object_creation(self, value: bool = True) -> None: """Enable or disable strict object creation validation""" option(Option.ENABLE_STRICT_OBJECT_CREATION, value) diff --git a/test/test_options.py b/test/test_options.py index ed746824b..6a1c1ac08 100644 --- a/test/test_options.py +++ b/test/test_options.py @@ -23,6 +23,10 @@ # the Free Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. +import sys + +import pytest + import pygit2 from pygit2 import option from pygit2.enums import ConfigLevel, ObjectType, Option @@ -133,15 +137,12 @@ def test_owner_validation() -> None: def test_template_path() -> None: - # Get the initial template path original_path = option(Option.GET_TEMPLATE_PATH) - # Set a new template path test_path = '/tmp/test_templates' option(Option.SET_TEMPLATE_PATH, test_path) assert option(Option.GET_TEMPLATE_PATH) == test_path - # Reset to original path if original_path: option(Option.SET_TEMPLATE_PATH, original_path) else: @@ -149,15 +150,12 @@ def test_template_path() -> None: def test_user_agent() -> None: - # Get the initial user agent original_agent = option(Option.GET_USER_AGENT) - # Set a new user agent test_agent = 'test-agent/1.0' option(Option.SET_USER_AGENT, test_agent) assert option(Option.GET_USER_AGENT) == test_agent - # Reset to original agent if original_agent: option(Option.SET_USER_AGENT, original_agent) @@ -166,13 +164,9 @@ def test_pack_max_objects() -> None: __option(Option.GET_PACK_MAX_OBJECTS, Option.SET_PACK_MAX_OBJECTS, 100000) +@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-specific feature') def test_windows_sharemode() -> None: - # This test might not work on non-Windows platforms - try: - __option(Option.GET_WINDOWS_SHAREMODE, Option.SET_WINDOWS_SHAREMODE, 1) - except Exception: - # May fail on non-Windows platforms - pass + __option(Option.GET_WINDOWS_SHAREMODE, Option.SET_WINDOWS_SHAREMODE, 1) def test_ssl_ciphers() -> None: @@ -180,76 +174,61 @@ def test_ssl_ciphers() -> None: try: option(Option.SET_SSL_CIPHERS, 'DEFAULT') except pygit2.GitError as e: - # May fail if TLS backend doesn't support custom ciphers - if "TLS backend doesn't support custom ciphers" not in str(e): - raise + if "TLS backend doesn't support custom ciphers" in str(e): + pytest.skip(str(e)) + raise def test_enable_http_expect_continue() -> None: - # Enable and disable HTTP expect continue option(Option.ENABLE_HTTP_EXPECT_CONTINUE, True) option(Option.ENABLE_HTTP_EXPECT_CONTINUE, False) def test_odb_priorities() -> None: - # Set ODB priorities option(Option.SET_ODB_PACKED_PRIORITY, 1) option(Option.SET_ODB_LOOSE_PRIORITY, 2) def test_extensions() -> None: - # Get initial extensions list original_extensions = option(Option.GET_EXTENSIONS) assert isinstance(original_extensions, list) - # Set extensions test_extensions = ['objectformat', 'worktreeconfig'] option(Option.SET_EXTENSIONS, test_extensions, len(test_extensions)) - # Verify they were set new_extensions = option(Option.GET_EXTENSIONS) assert isinstance(new_extensions, list) - # Check that our extensions are present # Note: libgit2 may add its own built-in extensions and sort them for ext in test_extensions: assert ext in new_extensions, f"Extension '{ext}' not found in {new_extensions}" - # Test with empty list option(Option.SET_EXTENSIONS, [], 0) empty_extensions = option(Option.GET_EXTENSIONS) assert isinstance(empty_extensions, list) - # Even with empty input, libgit2 may have built-in extensions - # Test with a custom extension custom_extensions = ['myextension', 'objectformat'] option(Option.SET_EXTENSIONS, custom_extensions, len(custom_extensions)) custom_result = option(Option.GET_EXTENSIONS) assert 'myextension' in custom_result assert 'objectformat' in custom_result - # Restore original extensions if original_extensions: option(Option.SET_EXTENSIONS, original_extensions, len(original_extensions)) else: - # Reset to empty list if there were no extensions option(Option.SET_EXTENSIONS, [], 0) - # Verify restoration final_extensions = option(Option.GET_EXTENSIONS) assert set(final_extensions) == set(original_extensions) def test_homedir() -> None: - # Get the initial home directory original_homedir = option(Option.GET_HOMEDIR) - # Set a new home directory test_homedir = '/tmp/test_home' option(Option.SET_HOMEDIR, test_homedir) assert option(Option.GET_HOMEDIR) == test_homedir - # Reset to original home directory if original_homedir: option(Option.SET_HOMEDIR, original_homedir) else: @@ -257,13 +236,11 @@ def test_homedir() -> None: def test_server_timeouts() -> None: - # Test connect timeout original_connect = option(Option.GET_SERVER_CONNECT_TIMEOUT) option(Option.SET_SERVER_CONNECT_TIMEOUT, 5000) assert option(Option.GET_SERVER_CONNECT_TIMEOUT) == 5000 option(Option.SET_SERVER_CONNECT_TIMEOUT, original_connect) - # Test server timeout original_timeout = option(Option.GET_SERVER_TIMEOUT) option(Option.SET_SERVER_TIMEOUT, 10000) assert option(Option.GET_SERVER_TIMEOUT) == 10000 @@ -271,37 +248,15 @@ def test_server_timeouts() -> None: def test_user_agent_product() -> None: - # Get the initial user agent product original_product = option(Option.GET_USER_AGENT_PRODUCT) - # Set a new user agent product test_product = 'test-product' option(Option.SET_USER_AGENT_PRODUCT, test_product) assert option(Option.GET_USER_AGENT_PRODUCT) == test_product - # Reset to original product if original_product: option(Option.SET_USER_AGENT_PRODUCT, original_product) -def test_add_ssl_x509_cert() -> None: - # Test adding an SSL certificate - # This is a minimal test certificate (not valid, but tests the API) - test_cert = '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----' - - try: - option(Option.ADD_SSL_X509_CERT, test_cert) - except pygit2.GitError as e: - # May fail if TLS backend doesn't support adding raw certs - # or if the certificate format is invalid - msg = str(e).lower() - if ( - "tls backend doesn't support" not in msg - and 'invalid' not in msg - and 'failed to add raw x509 certificate' not in msg - ): - raise - - def test_mwindow_file_limit() -> None: __option(Option.GET_MWINDOW_FILE_LIMIT, Option.SET_MWINDOW_FILE_LIMIT, 100) diff --git a/test/test_settings.py b/test/test_settings.py index 5d0dedb5e..5c5211019 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -34,7 +34,6 @@ def test_mwindow_size() -> None: - """Test get/set mwindow size""" original = pygit2.settings.mwindow_size try: test_size = 200 * 1024 @@ -45,7 +44,6 @@ def test_mwindow_size() -> None: def test_mwindow_mapped_limit() -> None: - """Test get/set mwindow mapped limit""" original = pygit2.settings.mwindow_mapped_limit try: test_limit = 300 * 1024 @@ -56,7 +54,6 @@ def test_mwindow_mapped_limit() -> None: def test_cached_memory() -> None: - """Test get cached memory""" cached = pygit2.settings.cached_memory assert isinstance(cached, tuple) assert len(cached) == 2 @@ -65,8 +62,6 @@ def test_cached_memory() -> None: def test_enable_caching() -> None: - """Test enable/disable caching""" - # Verify the method exists and accepts boolean values without raising assert hasattr(pygit2.settings, 'enable_caching') assert callable(pygit2.settings.enable_caching) @@ -76,7 +71,6 @@ def test_enable_caching() -> None: def test_disable_pack_keep_file_checks() -> None: - """Test disable pack keep file checks""" assert hasattr(pygit2.settings, 'disable_pack_keep_file_checks') assert callable(pygit2.settings.disable_pack_keep_file_checks) @@ -87,7 +81,6 @@ def test_disable_pack_keep_file_checks() -> None: def test_cache_max_size() -> None: - """Test set cache max size""" original_max_size = pygit2.settings.cached_memory[1] try: pygit2.settings.cache_max_size(128 * 1024**2) @@ -111,7 +104,6 @@ def test_cache_max_size() -> None: def test_cache_object_limit( object_type: ObjectType, test_size: int, default_size: int ) -> None: - """Test set cache object limit""" assert callable(pygit2.settings.cache_object_limit) pygit2.settings.cache_object_limit(object_type, test_size) @@ -127,7 +119,6 @@ def test_cache_object_limit( ], ) def test_search_path(level: ConfigLevel, test_path: str) -> None: - """Test get/set search paths""" original = pygit2.settings.search_path[level] try: pygit2.settings.search_path[level] = test_path @@ -137,7 +128,6 @@ def test_search_path(level: ConfigLevel, test_path: str) -> None: def test_template_path() -> None: - """Test get/set template path""" original = pygit2.settings.template_path try: pygit2.settings.template_path = '/tmp/test_templates' @@ -148,7 +138,6 @@ def test_template_path() -> None: def test_user_agent() -> None: - """Test get/set user agent""" original = pygit2.settings.user_agent try: pygit2.settings.user_agent = 'test-agent/1.0' @@ -159,7 +148,6 @@ def test_user_agent() -> None: def test_user_agent_product() -> None: - """Test get/set user agent product""" original = pygit2.settings.user_agent_product try: pygit2.settings.user_agent_product = 'test-product' @@ -170,7 +158,6 @@ def test_user_agent_product() -> None: def test_pack_max_objects() -> None: - """Test get/set pack max objects""" original = pygit2.settings.pack_max_objects try: pygit2.settings.pack_max_objects = 100000 @@ -180,7 +167,6 @@ def test_pack_max_objects() -> None: def test_owner_validation() -> None: - """Test get/set owner validation""" original = pygit2.settings.owner_validation try: pygit2.settings.owner_validation = False @@ -192,7 +178,6 @@ def test_owner_validation() -> None: def test_mwindow_file_limit() -> None: - """Test get/set mwindow file limit""" original = pygit2.settings.mwindow_file_limit try: pygit2.settings.mwindow_file_limit = 100 @@ -202,7 +187,6 @@ def test_mwindow_file_limit() -> None: def test_homedir() -> None: - """Test get/set home directory""" original = pygit2.settings.homedir try: pygit2.settings.homedir = '/tmp/test_home' @@ -213,7 +197,6 @@ def test_homedir() -> None: def test_server_timeouts() -> None: - """Test get/set server timeouts""" original_connect = pygit2.settings.server_connect_timeout original_timeout = pygit2.settings.server_timeout try: @@ -228,7 +211,6 @@ def test_server_timeouts() -> None: def test_extensions() -> None: - """Test get/set extensions""" original = pygit2.settings.extensions try: test_extensions = ['objectformat', 'worktreeconfig'] @@ -255,7 +237,6 @@ def test_extensions() -> None: ], ) def test_enable_methods(method_name: str, default_value: bool) -> None: - """Test various enable methods""" assert hasattr(pygit2.settings, method_name) method = getattr(pygit2.settings, method_name) assert callable(method) @@ -280,41 +261,19 @@ def test_odb_priorities(priority: int) -> None: pygit2.settings.set_odb_loose_priority(2) -def test_ssl_methods() -> None: - """Test SSL-related methods""" +def test_ssl_ciphers() -> None: assert callable(pygit2.settings.set_ssl_ciphers) - assert callable(pygit2.settings.add_ssl_x509_cert) - ssl_ciphers_supported = True try: pygit2.settings.set_ssl_ciphers('DEFAULT') except pygit2.GitError as e: - msg = str(e).lower() - if "tls backend doesn't support" not in msg: - raise - ssl_ciphers_supported = False - - test_cert = '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----' - x509_cert_supported = True - try: - pygit2.settings.add_ssl_x509_cert(test_cert) - except pygit2.GitError as e: - msg = str(e).lower() - if ( - "tls backend doesn't support" not in msg - and 'invalid' not in msg - and 'failed to add raw x509 certificate' not in msg - ): - raise - x509_cert_supported = False - - # At least verify the methods exist even if not supported - assert not ssl_ciphers_supported or not x509_cert_supported or True + if "TLS backend doesn't support" in str(e): + pytest.skip(str(e)) + raise @pytest.mark.skipif(sys.platform != 'win32', reason='Windows-specific feature') def test_windows_sharemode() -> None: - """Test get/set Windows share mode""" original = pygit2.settings.windows_sharemode try: pygit2.settings.windows_sharemode = 1 From 7a7895f06a7fee3e57b3be2561ec1c63737724d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 12 Aug 2025 19:40:06 +0200 Subject: [PATCH 9/9] Halve the number of quotes in NotImplementedError messages --- pygit2/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygit2/options.py b/pygit2/options.py index d238a2999..06216aee3 100644 --- a/pygit2/options.py +++ b/pygit2/options.py @@ -790,14 +790,14 @@ def option(option_type: Option, arg1: Any = NOT_PASSED, arg2: Any = NOT_PASSED) # as the API works accepts a X509* struct. Use GIT_OPT_SET_SSL_CERT_LOCATIONS # instead. elif option_type == C.GIT_OPT_ADD_SSL_X509_CERT: - raise NotImplementedError("Use GIT_OPT_SET_SSL_CERT_LOCATIONS instead") + raise NotImplementedError('Use GIT_OPT_SET_SSL_CERT_LOCATIONS instead') # Not implemented - SET_ALLOCATOR is not feasible from Python level # because it requires providing C function pointers for memory management # (malloc, free, etc.) that must handle raw memory at the C level, # which cannot be safely implemented in pure Python. elif option_type == C.GIT_OPT_SET_ALLOCATOR: - raise NotImplementedError("Setting a custom allocator not possible from Python") + raise NotImplementedError('Setting a custom allocator not possible from Python') else: raise ValueError(f'Invalid option {option_type}')