Skip to content

Accept Pathlike objects (eg. pathlib.Path) in Py3.6+ #990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pygit2/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def __contains__(self, path):

def __getitem__(self, key):
centry = ffi.NULL
if isinstance(key, str):
if isinstance(key, str) or hasattr(key, '__fspath__'):
centry = C.git_index_get_bypath(self._index, to_bytes(key), 0)
elif isinstance(key, int):
if key >= 0:
Expand Down Expand Up @@ -200,13 +200,13 @@ def add(self, path_or_entry):
Index without checking for the existence of the path or id.
"""

if isinstance(path_or_entry, str):
path = path_or_entry
err = C.git_index_add_bypath(self._index, to_bytes(path))
elif isinstance(path_or_entry, IndexEntry):
if isinstance(path_or_entry, IndexEntry):
entry = path_or_entry
centry, str_ref = entry._to_c()
err = C.git_index_add(self._index, centry)
elif isinstance(path_or_entry, str) or hasattr(path_or_entry, '__fspath__'):
path = path_or_entry
err = C.git_index_add_bypath(self._index, to_bytes(path))
else:
raise AttributeError('argument must be string or IndexEntry')

Expand Down
3 changes: 3 additions & 0 deletions pygit2/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,9 @@ def compress(self):

class Repository(BaseRepository):
def __init__(self, path, *args, **kwargs):
if hasattr(path, "__fspath__"):
path = path.__fspath__()

if not isinstance(path, str):
path = path.decode('utf-8')

Expand Down
15 changes: 12 additions & 3 deletions pygit2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.

import os

# Import from pygit2
from .ffi import ffi

Expand All @@ -31,13 +33,19 @@ def to_bytes(s, encoding='utf-8', errors='strict'):
if s == ffi.NULL or s is None:
return ffi.NULL

if hasattr(s, '__fspath__'):
s = os.fspath(s)

if isinstance(s, bytes):
return s

return s.encode(encoding, errors)


def to_str(s):
if hasattr(s, '__fspath__'):
s = os.fspath(s)

if type(s) is str:
return s

Expand Down Expand Up @@ -76,10 +84,11 @@ def __init__(self, l):

strings = [None] * len(l)
for i in range(len(l)):
if not isinstance(l[i], str):
raise TypeError("Value must be a string")
li = l[i]
if not isinstance(li, str) and not hasattr(li, '__fspath__'):
raise TypeError("Value must be a string or PathLike object")

strings[i] = ffi.new('char []', to_bytes(l[i]))
strings[i] = ffi.new('char []', to_bytes(li))

self._arr = ffi.new('char *[]', strings)
self._strings = strings
Expand Down
32 changes: 26 additions & 6 deletions src/pygit2.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,28 @@ PyObject *
discover_repository(PyObject *self, PyObject *args)
{
git_buf repo_path = {NULL};
const char *path;
PyObject *py_repo_path;
const char *path = NULL;
PyBytesObject *py_path = NULL;
int across_fs = 0;
PyBytesObject *py_ceiling_dirs = NULL;
const char *ceiling_dirs = NULL;
PyObject *py_repo_path = NULL;
int err;

if (!PyArg_ParseTuple(args, "s|Is", &path, &across_fs, &ceiling_dirs))
if (!PyArg_ParseTuple(args, "O&|IO&", PyUnicode_FSConverter, &py_path, &across_fs, PyUnicode_FSConverter, &py_ceiling_dirs))
return NULL;

if (py_path != NULL)
path = PyBytes_AS_STRING(py_path);
if (py_ceiling_dirs != NULL)
ceiling_dirs = PyBytes_AS_STRING(py_ceiling_dirs);

memset(&repo_path, 0, sizeof(git_buf));
err = git_repository_discover(&repo_path, path, across_fs, ceiling_dirs);

Py_XDECREF(py_path);
Py_XDECREF(py_ceiling_dirs);

if (err == GIT_ENOTFOUND)
Py_RETURN_NONE;
if (err < 0)
Expand All @@ -116,13 +127,18 @@ PyObject *
hashfile(PyObject *self, PyObject *args)
{
git_oid oid;
const char* path;
PyBytesObject *py_path = NULL;
const char* path = NULL;
int err;

if (!PyArg_ParseTuple(args, "s", &path))
if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &py_path))
return NULL;

if (py_path != NULL)
path = PyBytes_AS_STRING(py_path);

err = git_odb_hashfile(&oid, path, GIT_OBJ_BLOB);
Py_XDECREF(py_path);
if (err < 0)
return Error_set(err);

Expand Down Expand Up @@ -161,14 +177,18 @@ PyDoc_STRVAR(init_file_backend__doc__,
PyObject *
init_file_backend(PyObject *self, PyObject *args)
{
PyBytesObject *py_path = NULL;
const char* path = NULL;
int err = GIT_OK;
git_repository *repository = NULL;
if (!PyArg_ParseTuple(args, "s", &path)) {
if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &py_path)) {
return NULL;
}
if (py_path != NULL)
path = PyBytes_AS_STRING(py_path);

err = git_repository_open(&repository, path);
Py_XDECREF(py_path);
if (err < 0) {
Error_set_str(err, path);
goto cleanup;
Expand Down
31 changes: 23 additions & 8 deletions src/repository.c
Original file line number Diff line number Diff line change
Expand Up @@ -724,13 +724,18 @@ PyObject *
Repository_create_blob_fromworkdir(Repository *self, PyObject *args)
{
git_oid oid;
const char* path;
PyBytesObject *py_path = NULL;
const char* path = NULL;
int err;

if (!PyArg_ParseTuple(args, "s", &path))
if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &py_path))
return NULL;

if (py_path != NULL)
path = PyBytes_AS_STRING(py_path);

err = git_blob_create_fromworkdir(&oid, self->repo, path);
Py_XDECREF(py_path);
if (err < 0)
return Error_set(err);

Expand All @@ -747,13 +752,18 @@ PyObject *
Repository_create_blob_fromdisk(Repository *self, PyObject *args)
{
git_oid oid;
const char* path;
PyBytesObject *py_path = NULL;
const char* path = NULL;
int err;

if (!PyArg_ParseTuple(args, "s", &path))
if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &py_path))
return NULL;

if (py_path != NULL)
path = PyBytes_AS_STRING(py_path);

err = git_blob_create_fromdisk(&oid, self->repo, path);
Py_XDECREF(py_path);
if (err < 0)
return Error_set(err);

Expand Down Expand Up @@ -1757,20 +1767,25 @@ PyObject *
Repository_add_worktree(Repository *self, PyObject *args)
{
char *c_name;
char *c_path;
PyBytesObject *py_path = NULL;
char *c_path = NULL;
Reference *py_reference = NULL;
git_worktree *wt;
git_worktree_add_options add_opts = GIT_WORKTREE_ADD_OPTIONS_INIT;

int err;

if (!PyArg_ParseTuple(args, "ss|O!", &c_name, &c_path, &ReferenceType, &py_reference))
if (!PyArg_ParseTuple(args, "sO&|O!", &c_name, PyUnicode_FSConverter, &py_path, &ReferenceType, &py_reference))
return NULL;

if (py_path != NULL)
c_path = PyBytes_AS_STRING(py_path);

if(py_reference != NULL)
add_opts.ref = py_reference->reference;

err = git_worktree_add(&wt, self->repo, c_name, c_path, &add_opts);
Py_XDECREF(py_path);
if (err < 0)
return Error_set(err);

Expand Down
27 changes: 20 additions & 7 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,32 @@ pgit_encode_fsdefault(PyObject *value)
const char*
pgit_borrow_encoding(PyObject *value, const char *encoding, PyObject **tvalue)
{
PyObject *py_str;
PyObject *py_value = NULL;
PyObject *py_str = NULL;

#if defined(HAS_FSPATH_SUPPORT)
py_value = PyOS_FSPath(value);
if (py_value == NULL) {
Error_type_error("unexpected %.200s", value);
return NULL;
}
#else
py_value = value;
Py_INCREF(value);
#endif

// Get new PyBytes reference from value
if (PyUnicode_Check(value)) { // Text string
py_str = (encoding) ? PyUnicode_AsEncodedString(value, encoding, "strict")
: PyUnicode_AsUTF8String(value);
if (PyUnicode_Check(py_value)) { // Text string
py_str = (encoding) ? PyUnicode_AsEncodedString(py_value, encoding, "strict")
: PyUnicode_AsUTF8String(py_value);
Py_DECREF(py_value);
if (py_str == NULL)
return NULL;
} else if (PyBytes_Check(value)) { // Byte string
py_str = value;
Py_INCREF(py_str);
} else if (PyBytes_Check(py_value)) { // Byte string
py_str = py_value;
} else { // Type error
Error_type_error("unexpected %.200s", value);
Py_DECREF(py_value);
return NULL;
}

Expand Down
4 changes: 4 additions & 0 deletions src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
# define PYGIT2_FN_UNUSED
#endif

#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 6 && (!defined(PYPY_VERSION) || PYPY_VERSION_NUM >= 0x07030000)
#define HAS_FSPATH_SUPPORT
#endif

#define to_path(x) to_unicode(x, Py_FileSystemDefaultEncoding, "strict")
#define to_encoding(x) PyUnicode_DecodeASCII(x, strlen(x), "strict")

Expand Down
9 changes: 9 additions & 0 deletions test/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
# Boston, MA 02110-1301, USA.

# Standard Library
import unittest
from os.path import join
from pathlib import Path

# pygit2
from . import utils
Expand All @@ -49,3 +51,10 @@ def test_no_attr(self):
assert self.repo.get_attr('file.py', 'text')
assert not self.repo.get_attr('file.jpg', 'text')
assert "lf" == self.repo.get_attr('file.sh', 'eol')

@unittest.skipIf(not utils.has_fspath, "Requires PEP-519 (FSPath) support")
def test_no_attr_aspath(self):
with open(join(self.repo.workdir, '.gitattributes'), 'w+') as f:
print('*.py text\n', file=f)

assert self.repo.get_attr(Path('file.py'), 'text')
9 changes: 9 additions & 0 deletions test/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"""Tests for Blob objects."""

import io
import unittest
from pathlib import Path

import pytest

Expand Down Expand Up @@ -126,6 +128,13 @@ def test_create_blob_fromworkdir(self):
assert len(BLOB_FILE_CONTENT) == blob.size
assert BLOB_FILE_CONTENT == blob.read_raw()

@unittest.skipIf(not utils.has_fspath, "Requires PEP-519 (FSPath) support")
def test_create_blob_fromworkdir_aspath(self):

blob_oid = self.repo.create_blob_fromworkdir(Path("bye.txt"))
blob = self.repo[blob_oid]

assert isinstance(blob, pygit2.Blob)

def test_create_blob_outside_workdir(self):
with pytest.raises(KeyError):
Expand Down
13 changes: 13 additions & 0 deletions test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# Boston, MA 02110-1301, USA.

import os
import unittest
from pathlib import Path

import pytest

Expand Down Expand Up @@ -89,6 +91,17 @@ def test_add(self):
assert 'something.other.here' in config
assert not config.get_bool('something.other.here')

@unittest.skipIf(not utils.has_fspath, "Requires PEP-519 (FSPath) support")
def test_add_aspath(self):
config = Config()

new_file = open(CONFIG_FILENAME, "w")
new_file.write("[this]\n\tthat = true\n")
new_file.close()

config.add_file(Path(CONFIG_FILENAME), 0)
assert 'this.that' in config

def test_read(self):
config = self.repo.config

Expand Down
11 changes: 11 additions & 0 deletions test/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"""Tests for credentials"""

import unittest
from pathlib import Path

import pytest

Expand Down Expand Up @@ -67,6 +68,16 @@ def test_ssh_key(self):
cred = Keypair(username, pubkey, privkey, passphrase)
assert (username, pubkey, privkey, passphrase) == cred.credential_tuple

@unittest.skipIf(not utils.has_fspath, "Requires PEP-519 (FSPath) support")
def test_ssh_key_aspath(self):
username = "git"
pubkey = Path("id_rsa.pub")
privkey = Path("id_rsa")
passphrase = "bad wolf"

cred = Keypair(username, pubkey, privkey, passphrase)
assert (username, pubkey, privkey, passphrase) == cred.credential_tuple

def test_ssh_agent(self):
username = "git"

Expand Down
Loading