Skip to content

Commit 56e9423

Browse files
committed
Merge branch 'py3' into 0.3
Conflicts: git/refs/log.py
2 parents d46e3fe + 68f8a43 commit 56e9423

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+721
-452
lines changed

‎README.md

-4
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,6 @@ In short, I want to make a new release of 0.3 with all contributions and fixes i
8383

8484
The goals I have set for myself, in order, are as follows, all on branch 0.3.
8585

86-
* bring the test suite back online to work with the most commonly used git version
87-
* merge all open pull requests, may there be a test-case or not, back. If something breaks, fix it if possible or let the contributor know
88-
* conform git-python's structure and toolchain to the one used in my [other OSS projects](https://github.com/Byron/bcore)
89-
* evaluate all open issues and close them if possible
9086
* evaluate python 3.3 compatibility and establish it if possible
9187

9288
While that is happening, I will try hard to foster community around the project. This means being more responsive on the mailing list and in issues, as well as setting up clear guide lines about the [contribution](http://rfc.zeromq.org/spec:22) and maintenance workflow.

‎VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.3
1+
0.3.4

‎doc/source/changes.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22
Changelog
33
=========
44

5+
0.3.4 - Python 3 Support
6+
========================
7+
* Internally, hexadecimal SHA1 are treated as ascii encoded strings. Binary SHA1 are treated as bytes.
8+
* Id attribute of Commit objects is now `hexsha`, instead of `binsha`. The latter makes no sense in python 3 and I see no application of it anyway besides its artificial usage in test cases.
9+
* **IMPORTANT**: If you were using the config_writer(), you implicitly relied on __del__ to work as expected to flush changes. To be sure changes are flushed under PY3, you will have to call the new `release()` method to trigger a flush. For some reason, __del__ is not called necessarily anymore when a symbol goes out of scope.
10+
* The `Tree` now has a `.join('name')` method which is equivalent to `tree / 'name'`
11+
512
0.3.3
613
=====
714
* When fetching, pulling or pushing, and an error occours, it will not be reported on stdout anymore. However, if there is a fatal error, it will still result in a GitCommandError to be thrown. This goes hand in hand with improved fetch result parsing.
815
* Code Cleanup (in preparation for python 3 support)
16+
917
* Applied autopep8 and cleaned up code
1018
* Using python logging module instead of print statments to signal certain kinds of errors
1119

‎git/cmd.py

+49-30
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,24 @@
77
import os
88
import sys
99
import logging
10-
from util import (
11-
LazyMixin,
12-
stream_copy
13-
)
14-
from exc import GitCommandError
15-
1610
from subprocess import (
1711
call,
1812
Popen,
1913
PIPE
2014
)
2115

16+
17+
from .util import (
18+
LazyMixin,
19+
stream_copy
20+
)
21+
from .exc import GitCommandError
22+
from git.compat import (
23+
string_types,
24+
defenc,
25+
PY3
26+
)
27+
2228
execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
2329
'with_exceptions', 'as_process',
2430
'output_stream')
@@ -114,7 +120,7 @@ def wait(self):
114120
:raise GitCommandError: if the return status is not 0"""
115121
status = self.proc.wait()
116122
if status != 0:
117-
raise GitCommandError(self.args, status, self.proc.stderr.read())
123+
raise GitCommandError(self.args, status, self.proc.stderr.read().decode(defenc))
118124
# END status handling
119125
return status
120126
# END auto interrupt
@@ -314,6 +320,7 @@ def execute(self, command,
314320
always be created with a pipe due to issues with subprocess.
315321
This merely is a workaround as data will be copied from the
316322
output pipe to the given output stream directly.
323+
Judging from the implementation, you shouldn't use this flag !
317324
318325
:param subprocess_kwargs:
319326
Keyword arguments to be passed to subprocess.Popen. Please note that
@@ -365,23 +372,23 @@ def execute(self, command,
365372

366373
# Wait for the process to return
367374
status = 0
368-
stdout_value = ''
369-
stderr_value = ''
375+
stdout_value = b''
376+
stderr_value = b''
370377
try:
371378
if output_stream is None:
372379
stdout_value, stderr_value = proc.communicate()
373380
# strip trailing "\n"
374-
if stdout_value.endswith("\n"):
381+
if stdout_value.endswith(b"\n"):
375382
stdout_value = stdout_value[:-1]
376-
if stderr_value.endswith("\n"):
383+
if stderr_value.endswith(b"\n"):
377384
stderr_value = stderr_value[:-1]
378385
status = proc.returncode
379386
else:
380387
stream_copy(proc.stdout, output_stream, self.max_chunk_size)
381388
stdout_value = output_stream
382389
stderr_value = proc.stderr.read()
383390
# strip trailing "\n"
384-
if stderr_value.endswith("\n"):
391+
if stderr_value.endswith(b"\n"):
385392
stderr_value = stderr_value[:-1]
386393
status = proc.wait()
387394
# END stdout handling
@@ -392,9 +399,10 @@ def execute(self, command,
392399
if self.GIT_PYTHON_TRACE == 'full':
393400
cmdstr = " ".join(command)
394401
if stderr_value:
395-
log.info("%s -> %d; stdout: '%s'; stderr: '%s'", cmdstr, status, stdout_value, stderr_value)
402+
log.info("%s -> %d; stdout: '%s'; stderr: '%s'",
403+
cmdstr, status, stdout_value.decode(defenc), stderr_value.decode(defenc))
396404
elif stdout_value:
397-
log.info("%s -> %d; stdout: '%s'", cmdstr, status, stdout_value)
405+
log.info("%s -> %d; stdout: '%s'", cmdstr, status, stdout_value.decode(defenc))
398406
else:
399407
log.info("%s -> %d", cmdstr, status)
400408
# END handle debug printing
@@ -405,9 +413,12 @@ def execute(self, command,
405413
else:
406414
raise GitCommandError(command, status, stderr_value)
407415

416+
if isinstance(stdout_value, bytes): # could also be output_stream
417+
stdout_value = stdout_value.decode(defenc)
418+
408419
# Allow access to the command's status code
409420
if with_extended_output:
410-
return (status, stdout_value, stderr_value)
421+
return (status, stdout_value, stderr_value.decode(defenc))
411422
else:
412423
return stdout_value
413424

@@ -433,16 +444,18 @@ def transform_kwargs(self, split_single_char_options=False, **kwargs):
433444
@classmethod
434445
def __unpack_args(cls, arg_list):
435446
if not isinstance(arg_list, (list, tuple)):
436-
if isinstance(arg_list, unicode):
437-
return [arg_list.encode('utf-8')]
447+
# This is just required for unicode conversion, as subprocess can't handle it
448+
# However, in any other case, passing strings (usually utf-8 encoded) is totally fine
449+
if not PY3 and isinstance(arg_list, unicode):
450+
return [arg_list.encode(defenc)]
438451
return [str(arg_list)]
439452

440453
outlist = list()
441454
for arg in arg_list:
442455
if isinstance(arg_list, (list, tuple)):
443456
outlist.extend(cls.__unpack_args(arg))
444-
elif isinstance(arg_list, unicode):
445-
outlist.append(arg_list.encode('utf-8'))
457+
elif not PY3 and isinstance(arg_list, unicode):
458+
outlist.append(arg_list.encode(defenc))
446459
# END recursion
447460
else:
448461
outlist.append(str(arg))
@@ -498,8 +511,8 @@ def _call_process(self, method, *args, **kwargs):
498511

499512
# Prepare the argument list
500513
opt_args = self.transform_kwargs(**kwargs)
501-
502514
ext_args = self.__unpack_args([a for a in args if a is not None])
515+
503516
args = opt_args + ext_args
504517

505518
def make_call():
@@ -567,14 +580,20 @@ def _parse_object_header(self, header_line):
567580
raise ValueError("Failed to parse header: %r" % header_line)
568581
return (tokens[0], tokens[1], int(tokens[2]))
569582

570-
def __prepare_ref(self, ref):
571-
# required for command to separate refs on stdin
572-
refstr = str(ref) # could be ref-object
573-
if refstr.endswith("\n"):
574-
return refstr
575-
return refstr + "\n"
583+
def _prepare_ref(self, ref):
584+
# required for command to separate refs on stdin, as bytes
585+
refstr = ref
586+
if isinstance(ref, bytes):
587+
# Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text
588+
refstr = ref.decode('ascii')
589+
elif not isinstance(ref, string_types):
590+
refstr = str(ref) # could be ref-object
591+
592+
if not refstr.endswith("\n"):
593+
refstr += "\n"
594+
return refstr.encode(defenc)
576595

577-
def __get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs):
596+
def _get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs):
578597
cur_val = getattr(self, attr_name)
579598
if cur_val is not None:
580599
return cur_val
@@ -587,7 +606,7 @@ def __get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs):
587606
return cmd
588607

589608
def __get_object_header(self, cmd, ref):
590-
cmd.stdin.write(self.__prepare_ref(ref))
609+
cmd.stdin.write(self._prepare_ref(ref))
591610
cmd.stdin.flush()
592611
return self._parse_object_header(cmd.stdout.readline())
593612

@@ -599,7 +618,7 @@ def get_object_header(self, ref):
599618
once and reuses the command in subsequent calls.
600619
601620
:return: (hexsha, type_string, size_as_int)"""
602-
cmd = self.__get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
621+
cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True)
603622
return self.__get_object_header(cmd, ref)
604623

605624
def get_object_data(self, ref):
@@ -616,7 +635,7 @@ def stream_object_data(self, ref):
616635
:return: (hexsha, type_string, size_as_int, stream)
617636
:note: This method is not threadsafe, you need one independent Command instance
618637
per thread to be safe !"""
619-
cmd = self.__get_persistent_cmd("cat_file_all", "cat_file", batch=True)
638+
cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True)
620639
hexsha, typename, size = self.__get_object_header(cmd, ref)
621640
return (hexsha, typename, size, self.CatFileContentStream(size, cmd.stdout))
622641

‎git/compat.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#-*-coding:utf-8-*-
2+
# config.py
3+
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
4+
#
5+
# This module is part of GitPython and is released under
6+
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
7+
"""utilities to help provide compatibility with python 3"""
8+
# flake8: noqa
9+
10+
import sys
11+
12+
from gitdb.utils.compat import (
13+
PY3,
14+
xrange,
15+
MAXSIZE,
16+
izip,
17+
)
18+
19+
from gitdb.utils.encoding import (
20+
string_types,
21+
text_type,
22+
force_bytes,
23+
force_text
24+
)
25+
26+
defenc = sys.getdefaultencoding()
27+
if PY3:
28+
import io
29+
FileType = io.IOBase
30+
def byte_ord(b):
31+
return b
32+
def bchr(n):
33+
return bytes([n])
34+
def mviter(d):
35+
return d.values()
36+
else:
37+
FileType = file
38+
# usually, this is just ascii, which might not enough for our encoding needs
39+
# Unless it's set specifically, we override it to be utf-8
40+
if defenc == 'ascii':
41+
defenc = 'utf-8'
42+
byte_ord = ord
43+
bchr = chr
44+
def mviter(d):
45+
return d.itervalues()
46+
47+
48+
def with_metaclass(meta, *bases):
49+
"""copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15"""
50+
class metaclass(meta):
51+
__call__ = type.__call__
52+
__init__ = type.__init__
53+
54+
def __new__(cls, name, nbases, d):
55+
if nbases is None:
56+
return type.__new__(cls, name, (), d)
57+
# There may be clients who rely on this attribute to be set to a reasonable value, which is why
58+
# we set the __metaclass__ attribute explicitly
59+
if not PY3 and '___metaclass__' not in d:
60+
d['__metaclass__'] = meta
61+
# end
62+
return meta(name, bases, d)
63+
# end
64+
# end metaclass
65+
return metaclass(meta.__name__ + 'Helper', None, {})
66+
# end handle py2

0 commit comments

Comments
 (0)