From cb70d41753bc916599ef49ae78490b70b70f998e Mon Sep 17 00:00:00 2001 From: Darragh Bailey Date: Thu, 6 Nov 2014 16:08:59 +0000 Subject: [PATCH 1/3] Fix pep8 issues with autopep8 --- gitdb/__init__.py | 21 +- gitdb/base.py | 237 +++--- gitdb/db/__init__.py | 13 +- gitdb/db/base.py | 183 ++--- gitdb/db/git.py | 57 +- gitdb/db/loose.py | 157 ++-- gitdb/db/mem.py | 76 +- gitdb/db/pack.py | 115 +-- gitdb/db/ref.py | 38 +- gitdb/exc.py | 26 +- gitdb/fun.py | 351 +++++---- gitdb/pack.py | 688 ++++++++++-------- gitdb/stream.py | 469 ++++++------ gitdb/test/__init__.py | 4 +- gitdb/test/db/lib.py | 91 +-- gitdb/test/db/test_git.py | 33 +- gitdb/test/db/test_loose.py | 24 +- gitdb/test/db/test_mem.py | 21 +- gitdb/test/db/test_pack.py | 38 +- gitdb/test/db/test_ref.py | 35 +- gitdb/test/lib.py | 76 +- gitdb/test/performance/lib.py | 20 +- gitdb/test/performance/test_pack.py | 41 +- gitdb/test/performance/test_pack_streaming.py | 43 +- gitdb/test/performance/test_stream.py | 123 ++-- gitdb/test/test_base.py | 43 +- gitdb/test/test_example.py | 30 +- gitdb/test/test_pack.py | 178 +++-- gitdb/test/test_stream.py | 107 +-- gitdb/test/test_util.py | 48 +- gitdb/typ.py | 2 +- gitdb/util.py | 175 +++-- setup.py | 160 ++-- 33 files changed, 2011 insertions(+), 1712 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index ff750d1..97e2aed 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -8,18 +8,22 @@ import os #{ Initialization + + def _init_externals(): """Initialize external projects by putting them into the path""" for module in ('async', 'smmap'): sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', module)) - + try: __import__(module) except ImportError: - raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) - #END verify import - #END handel imports - + raise ImportError( + "'%s' could not be imported, assure it is located in your PYTHONPATH" % + module) + # END verify import + # END handel imports + #} END initialization _init_externals() @@ -32,7 +36,6 @@ def _init_externals(): # default imports -from db import * -from base import * -from stream import * - +from .db import * +from .base import * +from .stream import * diff --git a/gitdb/base.py b/gitdb/base.py index bad5f74..b8713bf 100644 --- a/gitdb/base.py +++ b/gitdb/base.py @@ -3,193 +3,199 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with basic data structures - they are designed to be lightweight and fast""" -from util import ( - bin_to_hex, - zlib - ) +from .util import ( + bin_to_hex, + zlib +) -from fun import ( - type_id_to_type_map, - type_to_type_id_map - ) +from .fun import ( + type_id_to_type_map, + type_to_type_id_map +) -__all__ = ('OInfo', 'OPackInfo', 'ODeltaPackInfo', - 'OStream', 'OPackStream', 'ODeltaPackStream', - 'IStream', 'InvalidOInfo', 'InvalidOStream' ) +__all__ = ('OInfo', 'OPackInfo', 'ODeltaPackInfo', + 'OStream', 'OPackStream', 'ODeltaPackStream', + 'IStream', 'InvalidOInfo', 'InvalidOStream') #{ ODB Bases + class OInfo(tuple): - """Carries information about an object in an ODB, provding information + + """Carries information about an object in an ODB, provding information about the binary sha of the object, the type_string as well as the uncompressed size in bytes. - + It can be accessed using tuple notation and using attribute access notation:: - + assert dbi[0] == dbi.binsha assert dbi[1] == dbi.type assert dbi[2] == dbi.size - + The type is designed to be as lighteight as possible.""" __slots__ = tuple() - + def __new__(cls, sha, type, size): return tuple.__new__(cls, (sha, type, size)) - + def __init__(self, *args): tuple.__init__(self) - - #{ Interface + + #{ Interface @property def binsha(self): """:return: our sha as binary, 20 bytes""" return self[0] - + @property def hexsha(self): """:return: our sha, hex encoded, 40 bytes""" return bin_to_hex(self[0]) - + @property def type(self): return self[1] - + @property def type_id(self): return type_to_type_id_map[self[1]] - + @property def size(self): return self[2] #} END interface - - + + class OPackInfo(tuple): - """As OInfo, but provides a type_id property to retrieve the numerical type id, and + + """As OInfo, but provides a type_id property to retrieve the numerical type id, and does not include a sha. - - Additionally, the pack_offset is the absolute offset into the packfile at which + + Additionally, the pack_offset is the absolute offset into the packfile at which all object information is located. The data_offset property points to the abosolute location in the pack at which that actual data stream can be found.""" __slots__ = tuple() - + def __new__(cls, packoffset, type, size): - return tuple.__new__(cls, (packoffset,type, size)) - + return tuple.__new__(cls, (packoffset, type, size)) + def __init__(self, *args): tuple.__init__(self) - - #{ Interface - + + #{ Interface + @property def pack_offset(self): return self[0] - + @property def type(self): return type_id_to_type_map[self[1]] - + @property def type_id(self): return self[1] - + @property def size(self): return self[2] - + #} END interface - - + + class ODeltaPackInfo(OPackInfo): - """Adds delta specific information, - Either the 20 byte sha which points to some object in the database, + + """Adds delta specific information, + Either the 20 byte sha which points to some object in the database, or the negative offset from the pack_offset, so that pack_offset - delta_info yields the pack offset of the base object""" __slots__ = tuple() - + def __new__(cls, packoffset, type, size, delta_info): return tuple.__new__(cls, (packoffset, type, size, delta_info)) - - #{ Interface + + #{ Interface @property def delta_info(self): return self[3] - #} END interface - - + #} END interface + + class OStream(OInfo): - """Base for object streams retrieved from the database, providing additional + + """Base for object streams retrieved from the database, providing additional information about the stream. Generally, ODB streams are read-only as objects are immutable""" __slots__ = tuple() - + def __new__(cls, sha, type, size, stream, *args, **kwargs): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (sha, type, size, stream)) - - + def __init__(self, *args, **kwargs): tuple.__init__(self) - - #{ Stream Reader Interface - + + #{ Stream Reader Interface + def read(self, size=-1): return self[3].read(size) - + @property def stream(self): return self[3] - + #} END stream reader interface - - + + class ODeltaStream(OStream): + """Uses size info of its stream, delaying reads""" - + def __new__(cls, sha, type, size, stream, *args, **kwargs): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (sha, type, size, stream)) - + #{ Stream Reader Interface - + @property def size(self): return self[3].size - + #} END stream reader interface - - + + class OPackStream(OPackInfo): + """Next to pack object information, a stream outputting an undeltified base object is provided""" __slots__ = tuple() - + def __new__(cls, packoffset, type, size, stream, *args): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (packoffset, type, size, stream)) - - #{ Stream Reader Interface + + #{ Stream Reader Interface def read(self, size=-1): return self[3].read(size) - + @property def stream(self): return self[3] #} END stream reader interface - + class ODeltaPackStream(ODeltaPackInfo): + """Provides a stream outputting the uncompressed offset delta information""" __slots__ = tuple() - + def __new__(cls, packoffset, type, size, delta_info, stream): return tuple.__new__(cls, (packoffset, type, size, delta_info, stream)) - - #{ Stream Reader Interface + #{ Stream Reader Interface def read(self, size=-1): return self[4].read(size) - + @property def stream(self): return self[4] @@ -197,106 +203,107 @@ def stream(self): class IStream(list): - """Represents an input content stream to be fed into the ODB. It is mutable to allow + + """Represents an input content stream to be fed into the ODB. It is mutable to allow the ODB to record information about the operations outcome right in this instance. - + It provides interfaces for the OStream and a StreamReader to allow the instance to blend in without prior conversion. - + The only method your content stream must support is 'read'""" __slots__ = tuple() - + def __new__(cls, type, size, stream, sha=None): return list.__new__(cls, (sha, type, size, stream, None)) - + def __init__(self, type, size, stream, sha=None): list.__init__(self, (sha, type, size, stream, None)) - - #{ Interface + + #{ Interface @property def hexsha(self): """:return: our sha, hex encoded, 40 bytes""" return bin_to_hex(self[0]) - + def _error(self): """:return: the error that occurred when processing the stream, or None""" return self[4] - + def _set_error(self, exc): """Set this input stream to the given exc, may be None to reset the error""" self[4] = exc - + error = property(_error, _set_error) - + #} END interface - + #{ Stream Reader Interface - + def read(self, size=-1): - """Implements a simple stream reader interface, passing the read call on + """Implements a simple stream reader interface, passing the read call on to our internal stream""" return self[3].read(size) - - #} END stream reader interface - + + #} END stream reader interface + #{ interface - + def _set_binsha(self, binsha): self[0] = binsha - + def _binsha(self): return self[0] - + binsha = property(_binsha, _set_binsha) - - + def _type(self): return self[1] - + def _set_type(self, type): self[1] = type - + type = property(_type, _set_type) - + def _size(self): return self[2] - + def _set_size(self, size): self[2] = size - + size = property(_size, _set_size) - + def _stream(self): return self[3] - + def _set_stream(self, stream): self[3] = stream - + stream = property(_stream, _set_stream) - - #} END odb info interface - + + #} END odb info interface + class InvalidOInfo(tuple): - """Carries information about a sha identifying an object which is invalid in + + """Carries information about a sha identifying an object which is invalid in the queried database. The exception attribute provides more information about the cause of the issue""" __slots__ = tuple() - + def __new__(cls, sha, exc): return tuple.__new__(cls, (sha, exc)) - + def __init__(self, sha, exc): tuple.__init__(self, (sha, exc)) - + @property def binsha(self): return self[0] - + @property def hexsha(self): return bin_to_hex(self[0]) - + @property def error(self): """:return: exception instance explaining the failure""" @@ -304,8 +311,8 @@ def error(self): class InvalidOStream(InvalidOInfo): + """Carries information about an invalid ODB stream""" __slots__ = tuple() - -#} END ODB Bases +#} END ODB Bases diff --git a/gitdb/db/__init__.py b/gitdb/db/__init__.py index e5935b7..2b372b1 100644 --- a/gitdb/db/__init__.py +++ b/gitdb/db/__init__.py @@ -3,10 +3,9 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from base import * -from loose import * -from mem import * -from pack import * -from git import * -from ref import * - +from .base import * +from .loose import * +from .mem import * +from .pack import * +from .git import * +from .ref import * diff --git a/gitdb/db/base.py b/gitdb/db/base.py index 867e93a..bc866a9 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -4,104 +4,110 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Contains implementations of database retrieveing objects""" from gitdb.util import ( - pool, - join, - LazyMixin, - hex_to_bin - ) + pool, + join, + LazyMixin, + hex_to_bin +) from gitdb.exc import ( - BadObject, - AmbiguousObjectName - ) + BadObject, + AmbiguousObjectName +) from async import ( - ChannelThreadTask - ) + ChannelThreadTask +) from itertools import chain +from functools import reduce __all__ = ('ObjectDBR', 'ObjectDBW', 'FileDBBase', 'CompoundDB', 'CachingDB') class ObjectDBR(object): + """Defines an interface for object database lookup. Objects are identified either by their 20 byte bin sha""" - + def __contains__(self, sha): return self.has_obj - - #{ Query Interface + + #{ Query Interface def has_object(self, sha): """ :return: True if the object identified by the given 20 bytes binary sha is contained in the database""" raise NotImplementedError("To be implemented in subclass") - + def has_object_async(self, reader): """Return a reader yielding information about the membership of objects as identified by shas :param reader: Reader yielding 20 byte shas. :return: async.Reader yielding tuples of (sha, bool) pairs which indicate whether the given sha exists in the database or not""" - task = ChannelThreadTask(reader, str(self.has_object_async), lambda sha: (sha, self.has_object(sha))) - return pool.add_task(task) - + task = ChannelThreadTask( + reader, str( + self.has_object_async), lambda sha: ( + sha, self.has_object(sha))) + return pool.add_task(task) + def info(self, sha): """ :return: OInfo instance :param sha: bytes binary sha :raise BadObject:""" raise NotImplementedError("To be implemented in subclass") - + def info_async(self, reader): """Retrieve information of a multitude of objects asynchronously :param reader: Channel yielding the sha's of the objects of interest :return: async.Reader yielding OInfo|InvalidOInfo, in any order""" task = ChannelThreadTask(reader, str(self.info_async), self.info) return pool.add_task(task) - + def stream(self, sha): """:return: OStream instance :param sha: 20 bytes binary sha :raise BadObject:""" raise NotImplementedError("To be implemented in subclass") - + def stream_async(self, reader): """Retrieve the OStream of multiple objects :param reader: see ``info`` :param max_threads: see ``ObjectDBW.store`` :return: async.Reader yielding OStream|InvalidOStream instances in any order - - **Note:** depending on the system configuration, it might not be possible to + + **Note:** depending on the system configuration, it might not be possible to read all OStreams at once. Instead, read them individually using reader.read(x) where x is small enough.""" # base implementation just uses the stream method repeatedly task = ChannelThreadTask(reader, str(self.stream_async), self.stream) return pool.add_task(task) - + def size(self): """:return: amount of objects in this database""" raise NotImplementedError() - + def sha_iter(self): """Return iterator yielding 20 byte shas for all objects in this data base""" raise NotImplementedError() - + #} END query interface - - + + class ObjectDBW(object): + """Defines an interface to create objects in the database""" - + def __init__(self, *args, **kwargs): self._ostream = None - + #{ Edit Interface def set_ostream(self, stream): """ Adjusts the stream to which all data should be sent when storing new objects - + :param stream: if not None, the stream to use, if None the default stream will be used. :return: previously installed stream, or None if there was no override @@ -109,96 +115,95 @@ def set_ostream(self, stream): cstream = self._ostream self._ostream = stream return cstream - + def ostream(self): """ :return: overridden output stream this instance will write to, or None if it will write to the default stream""" return self._ostream - + def store(self, istream): """ Create a new object in the database :return: the input istream object with its sha set to its corresponding value - - :param istream: IStream compatible instance. If its sha is already set - to a value, the object will just be stored in the our database format, + + :param istream: IStream compatible instance. If its sha is already set + to a value, the object will just be stored in the our database format, in which case the input stream is expected to be in object format ( header + contents ). :raise IOError: if data could not be written""" raise NotImplementedError("To be implemented in subclass") - + def store_async(self, reader): """ - Create multiple new objects in the database asynchronously. The method will - return right away, returning an output channel which receives the results as + Create multiple new objects in the database asynchronously. The method will + return right away, returning an output channel which receives the results as they are computed. - + :return: Channel yielding your IStream which served as input, in any order. - The IStreams sha will be set to the sha it received during the process, + The IStreams sha will be set to the sha it received during the process, or its error attribute will be set to the exception informing about the error. - + :param reader: async.Reader yielding IStream instances. The same instances will be used in the output channel as were received in by the Reader. - - **Note:** As some ODB implementations implement this operation atomic, they might - abort the whole operation if one item could not be processed. Hence check how + + **Note:** As some ODB implementations implement this operation atomic, they might + abort the whole operation if one item could not be processed. Hence check how many items have actually been produced.""" # base implementation uses store to perform the work - task = ChannelThreadTask(reader, str(self.store_async), self.store) + task = ChannelThreadTask(reader, str(self.store_async), self.store) return pool.add_task(task) - + #} END edit interface - + class FileDBBase(object): - """Provides basic facilities to retrieve files of interest, including + + """Provides basic facilities to retrieve files of interest, including caching facilities to help mapping hexsha's to objects""" - + def __init__(self, root_path): """Initialize this instance to look for its files at the given root path All subsequent operations will be relative to this path - :raise InvalidDBRoot: + :raise InvalidDBRoot: **Note:** The base will not perform any accessablity checking as the base - might not yet be accessible, but become accessible before the first + might not yet be accessible, but become accessible before the first access.""" super(FileDBBase, self).__init__() self._root_path = root_path - - - #{ Interface + + #{ Interface def root_path(self): """:return: path at which this db operates""" return self._root_path - + def db_path(self, rela_path): """ - :return: the given relative path relative to our database root, allowing + :return: the given relative path relative to our database root, allowing to pontentially access datafiles""" return join(self._root_path, rela_path) #} END interface - + class CachingDB(object): + """A database which uses caches to speed-up access""" - - #{ Interface + + #{ Interface def update_cache(self, force=False): """ Call this method if the underlying data changed to trigger an update of the internal caching structures. - + :param force: if True, the update must be performed. Otherwise the implementation may decide not to perform an update if it thinks nothing has changed. :return: True if an update was performed as something change indeed""" - - # END interface - + # END interface def _databases_recursive(database, output): - """Fill output list with database from db, in order. Deals with Loose, Packed + """Fill output list with database from db, in order. Deals with Loose, Packed and compound databases.""" if isinstance(database, CompoundDB): compounds = list() @@ -209,13 +214,15 @@ def _databases_recursive(database, output): else: output.append(database) # END handle database type - + class CompoundDB(ObjectDBR, LazyMixin, CachingDB): + """A database which delegates calls to sub-databases. - + Databases are stored in the lazy-loaded _dbs attribute. Define _set_cache_ to update it with your databases""" + def _set_cache_(self, attr): if attr == '_dbs': self._dbs = list() @@ -223,27 +230,27 @@ def _set_cache_(self, attr): self._db_cache = dict() else: super(CompoundDB, self)._set_cache_(attr) - + def _db_query(self, sha): """:return: database containing the given 20 byte sha :raise BadObject:""" - # most databases use binary representations, prevent converting + # most databases use binary representations, prevent converting # it everytime a database is being queried try: return self._db_cache[sha] except KeyError: pass # END first level cache - + for db in self._dbs: if db.has_object(sha): self._db_cache[sha] = db return db # END for each database raise BadObject(sha) - - #{ ObjectDBR interface - + + #{ ObjectDBR interface + def has_object(self, sha): try: self._db_query(sha) @@ -251,24 +258,24 @@ def has_object(self, sha): except BadObject: return False # END handle exceptions - + def info(self, sha): return self._db_query(sha).info(sha) - + def stream(self, sha): return self._db_query(sha).stream(sha) def size(self): """:return: total size of all contained databases""" - return reduce(lambda x,y: x+y, (db.size() for db in self._dbs), 0) - + return reduce(lambda x, y: x + y, (db.size() for db in self._dbs), 0) + def sha_iter(self): return chain(*(db.sha_iter() for db in self._dbs)) - + #} END object DBR Interface - + #{ Interface - + def databases(self): """:return: tuple of database instances we use for lookups""" return tuple(self._dbs) @@ -283,7 +290,7 @@ def update_cache(self, force=False): # END if is caching db # END for each database to update return stat - + def partial_to_complete_sha_hex(self, partial_hexsha): """ :return: 20 byte binary sha1 from the given less-than-40 byte hexsha @@ -291,22 +298,24 @@ def partial_to_complete_sha_hex(self, partial_hexsha): :raise AmbiguousObjectName: """ databases = list() _databases_recursive(self, databases) - + len_partial_hexsha = len(partial_hexsha) if len_partial_hexsha % 2 != 0: partial_binsha = hex_to_bin(partial_hexsha + "0") else: partial_binsha = hex_to_bin(partial_hexsha) - # END assure successful binary conversion - + # END assure successful binary conversion + candidate = None for db in databases: full_bin_sha = None try: if hasattr(db, 'partial_to_complete_sha_hex'): - full_bin_sha = db.partial_to_complete_sha_hex(partial_hexsha) + full_bin_sha = db.partial_to_complete_sha_hex( + partial_hexsha) else: - full_bin_sha = db.partial_to_complete_sha(partial_binsha, len_partial_hexsha) + full_bin_sha = db.partial_to_complete_sha( + partial_binsha, len_partial_hexsha) # END handle database type except BadObject: continue @@ -320,7 +329,5 @@ def partial_to_complete_sha_hex(self, partial_hexsha): if not candidate: raise BadObject(partial_binsha) return candidate - - #} END interface - + #} END interface diff --git a/gitdb/db/git.py b/gitdb/db/git.py index 1d6ad0f..57a6631 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -2,51 +2,52 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from base import ( - CompoundDB, - ObjectDBW, - FileDBBase - ) +from .base import ( + CompoundDB, + ObjectDBW, + FileDBBase +) -from loose import LooseObjectDB -from pack import PackedDB -from ref import ReferenceDB +from .loose import LooseObjectDB +from .pack import PackedDB +from .ref import ReferenceDB from gitdb.util import LazyMixin from gitdb.exc import ( - InvalidDBRoot, - BadObject, - AmbiguousObjectName - ) + InvalidDBRoot, + BadObject, + AmbiguousObjectName +) import os __all__ = ('GitDB', ) class GitDB(FileDBBase, ObjectDBW, CompoundDB): + """A git-style object database, which contains all objects in the 'objects' subdirectory""" # Configuration PackDBCls = PackedDB LooseDBCls = LooseObjectDB ReferenceDBCls = ReferenceDB - + # Directories packs_dir = 'pack' loose_dir = '' alternates_dir = os.path.join('info', 'alternates') - + def __init__(self, root_path): """Initialize ourselves on a git objects directory""" super(GitDB, self).__init__(root_path) - + def _set_cache_(self, attr): if attr == '_dbs' or attr == '_loose_db': self._dbs = list() loose_db = None - for subpath, dbcls in ((self.packs_dir, self.PackDBCls), - (self.loose_dir, self.LooseDBCls), - (self.alternates_dir, self.ReferenceDBCls)): + for subpath, dbcls in ((self.packs_dir, self.PackDBCls), + (self.loose_dir, self.LooseDBCls), + (self.alternates_dir, self.ReferenceDBCls)): path = self.db_path(subpath) if os.path.exists(path): self._dbs.append(dbcls(path)) @@ -55,31 +56,31 @@ def _set_cache_(self, attr): # END remember loose db # END check path exists # END for each db type - + # should have at least one subdb if not self._dbs: raise InvalidDBRoot(self.root_path()) # END handle error - + # we the first one should have the store method - assert loose_db is not None and hasattr(loose_db, 'store'), "First database needs store functionality" - + assert loose_db is not None and hasattr( + loose_db, 'store'), "First database needs store functionality" + # finally set the value self._loose_db = loose_db else: super(GitDB, self)._set_cache_(attr) # END handle attrs - + #{ ObjectDBW interface - + def store(self, istream): return self._loose_db.store(istream) - + def ostream(self): return self._loose_db.ostream() - + def set_ostream(self, ostream): return self._loose_db.set_ostream(ostream) - + #} END objectdbw interface - diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index dc0ea0e..900092a 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -2,54 +2,54 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from base import ( - FileDBBase, - ObjectDBR, - ObjectDBW - ) +from .base import ( + FileDBBase, + ObjectDBR, + ObjectDBW +) from gitdb.exc import ( - InvalidDBRoot, + InvalidDBRoot, BadObject, AmbiguousObjectName - ) +) from gitdb.stream import ( - DecompressMemMapReader, - FDCompressedSha1Writer, - FDStream, - Sha1Writer - ) + DecompressMemMapReader, + FDCompressedSha1Writer, + FDStream, + Sha1Writer +) from gitdb.base import ( - OStream, - OInfo - ) + OStream, + OInfo +) from gitdb.util import ( - file_contents_ro_filepath, - ENOENT, - hex_to_bin, - bin_to_hex, - exists, - chmod, - isdir, - isfile, - remove, - mkdir, - rename, - dirname, - basename, - join - ) - -from gitdb.fun import ( + file_contents_ro_filepath, + ENOENT, + hex_to_bin, + bin_to_hex, + exists, + chmod, + isdir, + isfile, + remove, + mkdir, + rename, + dirname, + basename, + join +) + +from gitdb.fun import ( chunk_size, - loose_object_header_info, + loose_object_header_info, write_object, stream_copy - ) +) import tempfile import mmap @@ -57,23 +57,23 @@ import os -__all__ = ( 'LooseObjectDB', ) +__all__ = ('LooseObjectDB', ) class LooseObjectDB(FileDBBase, ObjectDBR, ObjectDBW): + """A database which operates on loose object files""" - + # CONFIGURATION # chunks in which data will be copied between streams stream_chunk_size = chunk_size - + # On windows we need to keep it writable, otherwise it cannot be removed # either - new_objects_mode = 0444 + new_objects_mode = 0o444 if os.name == 'nt': - new_objects_mode = 0644 - - + new_objects_mode = 0o644 + def __init__(self, root_path): super(LooseObjectDB, self).__init__(root_path) self._hexsha_to_file = dict() @@ -81,14 +81,14 @@ def __init__(self, root_path): # Depending on the root, this might work for some mounts, for others not, which # is why it is per instance self._fd_open_flags = getattr(os, 'O_NOATIME', 0) - - #{ Interface + + #{ Interface def object_path(self, hexsha): """ - :return: path at which the object with the given hexsha would be stored, + :return: path at which the object with the given hexsha would be stored, relative to the database root""" return join(hexsha[:2], hexsha[2:]) - + def readable_db_object_path(self, hexsha): """ :return: readable object path to the object identified by hexsha @@ -97,8 +97,8 @@ def readable_db_object_path(self, hexsha): return self._hexsha_to_file[hexsha] except KeyError: pass - # END ignore cache misses - + # END ignore cache misses + # try filesystem path = self.db_path(self.object_path(hexsha)) if exists(path): @@ -106,11 +106,11 @@ def readable_db_object_path(self, hexsha): return path # END handle cache raise BadObject(hexsha) - + def partial_to_complete_sha_hex(self, partial_hexsha): """:return: 20 byte binary sha1 string which matches the given name uniquely :param name: hexadecimal partial name - :raise AmbiguousObjectName: + :raise AmbiguousObjectName: :raise BadObject: """ candidate = None for binsha in self.sha_iter(): @@ -123,17 +123,18 @@ def partial_to_complete_sha_hex(self, partial_hexsha): if candidate is None: raise BadObject(partial_hexsha) return candidate - + #} END interface - + def _map_loose_object(self, sha): """ :return: memory map of that file to allow random read access :raise BadObject: if object could not be located""" db_path = self.db_path(self.object_path(bin_to_hex(sha))) try: - return file_contents_ro_filepath(db_path, flags=self._fd_open_flags) - except OSError,e: + return file_contents_ro_filepath( + db_path, flags=self._fd_open_flags) + except OSError as e: if e.errno != ENOENT: # try again without noatime try: @@ -151,13 +152,15 @@ def _map_loose_object(self, sha): finally: os.close(fd) # END assure file is closed - + def set_ostream(self, stream): """:raise TypeError: if the stream does not support the Sha1Writer interface""" if stream is not None and not isinstance(stream, Sha1Writer): - raise TypeError("Output stream musst support the %s interface" % Sha1Writer.__name__) + raise TypeError( + "Output stream musst support the %s interface" % + Sha1Writer.__name__) return super(LooseObjectDB, self).set_ostream(stream) - + def info(self, sha): m = self._map_loose_object(sha) try: @@ -166,12 +169,13 @@ def info(self, sha): finally: m.close() # END assure release of system resources - + def stream(self, sha): m = self._map_loose_object(sha) - type, size, stream = DecompressMemMapReader.new(m, close_on_deletion = True) + type, size, stream = DecompressMemMapReader.new( + m, close_on_deletion=True) return OStream(sha, type, size, stream) - + def has_object(self, sha): try: self.readable_db_object_path(bin_to_hex(sha)) @@ -179,7 +183,7 @@ def has_object(self, sha): except BadObject: return False # END check existance - + def store(self, istream): """note: The sha we produce will be hex by nature""" tmp_path = None @@ -187,24 +191,32 @@ def store(self, istream): if writer is None: # open a tmp file to write the data to fd, tmp_path = tempfile.mkstemp(prefix='obj', dir=self._root_path) - + if istream.binsha is None: writer = FDCompressedSha1Writer(fd) else: writer = FDStream(fd) # END handle direct stream copies # END handle custom writer - + try: try: if istream.binsha is not None: # copy as much as possible, the actual uncompressed item size might # be smaller than the compressed version - stream_copy(istream.read, writer.write, sys.maxint, self.stream_chunk_size) + stream_copy( + istream.read, + writer.write, + sys.maxsize, + self.stream_chunk_size) else: # write object with header, we have to make a new one - write_object(istream.type, istream.size, istream.read, writer.write, - chunk_size=self.stream_chunk_size) + write_object( + istream.type, + istream.size, + istream.read, + writer.write, + chunk_size=self.stream_chunk_size) # END handle direct stream copies finally: if tmp_path: @@ -215,14 +227,14 @@ def store(self, istream): os.remove(tmp_path) raise # END assure tmpfile removal on error - + hexsha = None if istream.binsha: hexsha = istream.hexsha else: hexsha = writer.sha(as_hex=True) # END handle sha - + if tmp_path: obj_path = self.db_path(self.object_path(hexsha)) obj_dir = dirname(obj_path) @@ -234,29 +246,28 @@ def store(self, istream): remove(obj_path) # END handle win322 rename(tmp_path, obj_path) - + # make sure its readable for all ! It started out as rw-- tmp file # but needs to be rwrr chmod(obj_path, self.new_objects_mode) # END handle dry_run - + istream.binsha = hex_to_bin(hexsha) return istream - + def sha_iter(self): # find all files which look like an object, extract sha from there for root, dirs, files in os.walk(self.root_path()): root_base = basename(root) if len(root_base) != 2: continue - + for f in files: if len(f) != 38: continue yield hex_to_bin(root_base + f) # END for each file # END for each walk iteration - + def size(self): return len(tuple(self.sha_iter())) - diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index b9b2b89..69a523c 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -3,74 +3,79 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Contains the MemoryDatabase implementation""" -from loose import LooseObjectDB -from base import ( - ObjectDBR, - ObjectDBW - ) +from .loose import LooseObjectDB +from .base import ( + ObjectDBR, + ObjectDBW +) from gitdb.base import ( - OStream, - IStream, - ) + OStream, + IStream, +) from gitdb.exc import ( - BadObject, - UnsupportedOperation - ) + BadObject, + UnsupportedOperation +) from gitdb.stream import ( - ZippedStoreShaWriter, - DecompressMemMapReader, - ) + ZippedStoreShaWriter, + DecompressMemMapReader, +) from cStringIO import StringIO __all__ = ("MemoryDB", ) + class MemoryDB(ObjectDBR, ObjectDBW): + """A memory database stores everything to memory, providing fast IO and object retrieval. It should be used to buffer results and obtain SHAs before writing it to the actual physical storage, as it allows to query whether object already exists in the target storage before introducing actual IO - + **Note:** memory is currently not threadsafe, hence the async methods cannot be used for storing""" - + def __init__(self): super(MemoryDB, self).__init__() self._db = LooseObjectDB("path/doesnt/matter") - + # maps 20 byte shas to their OStream objects self._cache = dict() - + def set_ostream(self, stream): raise UnsupportedOperation("MemoryDB's always stream into memory") - + def store(self, istream): zstream = ZippedStoreShaWriter() self._db.set_ostream(zstream) - + istream = self._db.store(istream) zstream.close() # close to flush zstream.seek(0) - - # don't provide a size, the stream is written in object format, hence the + + # don't provide a size, the stream is written in object format, hence the # header needs decompression - decomp_stream = DecompressMemMapReader(zstream.getvalue(), close_on_deletion=False) - self._cache[istream.binsha] = OStream(istream.binsha, istream.type, istream.size, decomp_stream) - + decomp_stream = DecompressMemMapReader( + zstream.getvalue(), close_on_deletion=False) + self._cache[istream.binsha] = OStream( + istream.binsha, istream.type, istream.size, decomp_stream) + return istream - + def store_async(self, reader): - raise UnsupportedOperation("MemoryDBs cannot currently be used for async write access") - + raise UnsupportedOperation( + "MemoryDBs cannot currently be used for async write access") + def has_object(self, sha): return sha in self._cache def info(self, sha): # we always return streams, which are infos as well return self.stream(sha) - + def stream(self, sha): try: ostream = self._cache[sha] @@ -80,15 +85,14 @@ def stream(self, sha): except KeyError: raise BadObject(sha) # END exception handling - + def size(self): return len(self._cache) - + def sha_iter(self): return self._cache.iterkeys() - - - #{ Interface + + #{ Interface def stream_copy(self, sha_iter, odb): """Copy the streams as identified by sha's yielded by sha_iter into the given odb The streams will be copied directly @@ -100,12 +104,12 @@ def stream_copy(self, sha_iter, odb): if odb.has_object(sha): continue # END check object existance - + ostream = self.stream(sha) # compressed data including header sio = StringIO(ostream.stream.data()) istream = IStream(ostream.type, ostream.size, sio, sha) - + odb.store(istream) count += 1 # END for each sha diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 9287319..81b7cc0 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -3,24 +3,25 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module containing a database to deal with packs""" -from base import ( - FileDBBase, - ObjectDBR, - CachingDB - ) +from .base import ( + FileDBBase, + ObjectDBR, + CachingDB +) from gitdb.util import LazyMixin from gitdb.exc import ( - BadObject, - UnsupportedOperation, - AmbiguousObjectName - ) + BadObject, + UnsupportedOperation, + AmbiguousObjectName +) from gitdb.pack import PackEntity import os import glob +from functools import reduce __all__ = ('PackedDB', ) @@ -28,13 +29,14 @@ class PackedDB(FileDBBase, ObjectDBR, CachingDB, LazyMixin): + """A database operating on a set of object packs""" - + # sort the priority list every N queries - # Higher values are better, performance tests don't show this has + # Higher values are better, performance tests don't show this has # any effect, but it should have one _sort_interval = 500 - + def __init__(self, root_path): super(PackedDB, self).__init__(root_path) # list of lists with three items: @@ -43,30 +45,31 @@ def __init__(self, root_path): # * sha_to_index - PackIndexFile.sha_to_index method for direct cache query # self._entities = list() # lazy loaded list self._hit_count = 0 # amount of hits - self._st_mtime = 0 # last modification data of our root path - + # last modification data of our root path + self._st_mtime = 0 + def _set_cache_(self, attr): if attr == '_entities': self._entities = list() self.update_cache(force=True) # END handle entities initialization - + def _sort_entities(self): self._entities.sort(key=lambda l: l[0], reverse=True) - + def _pack_info(self, sha): """:return: tuple(entity, index) for an item at the given sha :param sha: 20 or 40 byte sha :raise BadObject: **Note:** This method is not thread-safe, but may be hit in multi-threaded - operation. The worst thing that can happen though is a counter that + operation. The worst thing that can happen though is a counter that was not incremented, or the list being in wrong order. So we safe the time for locking here, lets see how that goes""" # presort ? if self._hit_count % self._sort_interval == 0: self._sort_entities() # END update sorting - + for item in self._entities: index = item[2](sha) if index is not None: @@ -75,14 +78,14 @@ def _pack_info(self, sha): return (item[1], index) # END index found in pack # END for each item - + # no hit, see whether we have to update packs # NOTE: considering packs don't change very often, we safe this call # and leave it to the super-caller to trigger that raise BadObject(sha) - - #{ Object DB Read - + + #{ Object DB Read + def has_object(self, sha): try: self._pack_info(sha) @@ -90,15 +93,15 @@ def has_object(self, sha): except BadObject: return False # END exception handling - + def info(self, sha): entity, index = self._pack_info(sha) return entity.info_at_index(index) - + def stream(self, sha): entity, index = self._pack_info(sha) return entity.stream_at_index(index) - + def sha_iter(self): sha_list = list() for entity in self.entities(): @@ -108,58 +111,59 @@ def sha_iter(self): yield sha_by_index(index) # END for each index # END for each entity - + def size(self): sizes = [item[1].index().size() for item in self._entities] - return reduce(lambda x,y: x+y, sizes, 0) - + return reduce(lambda x, y: x + y, sizes, 0) + #} END object db read - + #{ object db write - + def store(self, istream): - """Storing individual objects is not feasible as a pack is designed to + """Storing individual objects is not feasible as a pack is designed to hold multiple objects. Writing or rewriting packs for single objects is inefficient""" raise UnsupportedOperation() - + def store_async(self, reader): # TODO: add ObjectDBRW before implementing this raise NotImplementedError() - + #} END object db write - - - #{ Interface - + + #{ Interface + def update_cache(self, force=False): """ - Update our cache with the acutally existing packs on disk. Add new ones, + Update our cache with the acutally existing packs on disk. Add new ones, and remove deleted ones. We keep the unchanged ones - + :param force: If True, the cache will be updated even though the directory does not appear to have changed according to its modification timestamp. - :return: True if the packs have been updated so there is new information, + :return: True if the packs have been updated so there is new information, False if there was no change to the pack database""" stat = os.stat(self.root_path()) if not force and stat.st_mtime <= self._st_mtime: return False # END abort early on no change self._st_mtime = stat.st_mtime - + # packs are supposed to be prefixed with pack- by git-convention # get all pack files, figure out what changed - pack_files = set(glob.glob(os.path.join(self.root_path(), "pack-*.pack"))) + pack_files = set( + glob.glob(os.path.join(self.root_path(), "pack-*.pack"))) our_pack_files = set(item[1].pack().path() for item in self._entities) - + # new packs for pack_file in (pack_files - our_pack_files): # init the hit-counter/priority with the size, a good measure for hit- # probability. Its implemented so that only 12 bytes will be read entity = PackEntity(pack_file) - self._entities.append([entity.pack().size(), entity, entity.index().sha_to_index]) + self._entities.append( + [entity.pack().size(), entity, entity.index().sha_to_index]) # END for each new packfile - + # removed packs for pack_file in (our_pack_files - pack_files): del_index = -1 @@ -172,26 +176,27 @@ def update_cache(self, force=False): assert del_index != -1 del(self._entities[del_index]) # END for each removed pack - + # reinitialize prioritiess self._sort_entities() return True - + def entities(self): """:return: list of pack entities operated upon by this database""" - return [ item[1] for item in self._entities ] - + return [item[1] for item in self._entities] + def partial_to_complete_sha(self, partial_binsha, canonical_length): """:return: 20 byte sha as inferred by the given partial binary sha - :param partial_binsha: binary sha with less than 20 bytes + :param partial_binsha: binary sha with less than 20 bytes :param canonical_length: length of the corresponding canonical representation. It is required as binary sha's cannot display whether the original hex sha had an odd or even number of characters - :raise AmbiguousObjectName: + :raise AmbiguousObjectName: :raise BadObject: """ candidate = None for item in self._entities: - item_index = item[1].index().partial_sha_to_index(partial_binsha, canonical_length) + item_index = item[1].index().partial_sha_to_index( + partial_binsha, canonical_length) if item_index is not None: sha = item[1].index().sha(item_index) if candidate and candidate != sha: @@ -199,11 +204,11 @@ def partial_to_complete_sha(self, partial_binsha, canonical_length): candidate = sha # END handle full sha could be found # END for each entity - + if candidate: return candidate - + # still not found ? raise BadObject(partial_binsha) - + #} END interface diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 60004a7..df214af 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -2,25 +2,27 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from base import ( - CompoundDB, - ) +from .base import ( + CompoundDB, +) import os __all__ = ('ReferenceDB', ) + class ReferenceDB(CompoundDB): + """A database consisting of database referred to in a file""" - + # Configuration # Specifies the object database to use for the paths found in the alternates # file. If None, it defaults to the GitDB ObjectDBCls = None - + def __init__(self, ref_file): super(ReferenceDB, self).__init__() self._ref_file = ref_file - + def _set_cache_(self, attr): if attr == '_dbs': self._dbs = list() @@ -28,26 +30,27 @@ def _set_cache_(self, attr): else: super(ReferenceDB, self)._set_cache_(attr) # END handle attrs - + def _update_dbs_from_ref_file(self): dbcls = self.ObjectDBCls if dbcls is None: # late import - from git import GitDB + from .git import GitDB dbcls = GitDB # END get db type - + # try to get as many as possible, don't fail if some are unavailable ref_paths = list() try: - ref_paths = [l.strip() for l in open(self._ref_file, 'r').readlines()] + ref_paths = [l.strip() + for l in open(self._ref_file, 'r').readlines()] except (OSError, IOError): pass # END handle alternates - + ref_paths_set = set(ref_paths) cur_ref_paths_set = set(db.root_path() for db in self._dbs) - + # remove existing for path in (cur_ref_paths_set - ref_paths_set): for i, db in enumerate(self._dbs[:]): @@ -56,10 +59,13 @@ def _update_dbs_from_ref_file(self): continue # END del matching db # END for each path to remove - + # add new # sort them to maintain order - added_paths = sorted(ref_paths_set - cur_ref_paths_set, key=lambda p: ref_paths.index(p)) + added_paths = sorted( + ref_paths_set - + cur_ref_paths_set, + key=lambda p: ref_paths.index(p)) for path in added_paths: try: db = dbcls(path) @@ -68,11 +74,11 @@ def _update_dbs_from_ref_file(self): db.databases() # END verification self._dbs.append(db) - except Exception, e: + except Exception as e: # ignore invalid paths or issues pass # END for each path to add - + def update_cache(self, force=False): # re-read alternates and update databases self._update_dbs_from_ref_file() diff --git a/gitdb/exc.py b/gitdb/exc.py index 7180fb5..a375e99 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -3,30 +3,44 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with common exceptions""" -from util import to_hex_sha +from .util import to_hex_sha + class ODBError(Exception): + """All errors thrown by the object database""" - + + class InvalidDBRoot(ODBError): + """Thrown if an object database cannot be initialized at the given path""" - + + class BadObject(ODBError): - """The object with the given SHA does not exist. Instantiate with the + + """The object with the given SHA does not exist. Instantiate with the failed sha""" - + def __str__(self): return "BadObject: %s" % to_hex_sha(self.args[0]) - + + class ParseError(ODBError): + """Thrown if the parsing of a file failed due to an invalid format""" + class AmbiguousObjectName(ODBError): + """Thrown if a possibly shortened name does not uniquely represent a single object in the database""" + class BadObjectType(ODBError): + """The object had an unsupported type""" + class UnsupportedOperation(ODBError): + """Thrown if the given operation cannot be supported by the object database""" diff --git a/gitdb/fun.py b/gitdb/fun.py index c1e73e8..d782037 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -6,11 +6,12 @@ Keeping this code separate from the beginning makes it easier to out-source it into c later, if required""" -from exc import ( +from .exc import ( BadObjectType - ) +) -from util import zlib +from .util import zlib +from functools import reduce decompressobj = zlib.decompressobj import mmap @@ -23,32 +24,42 @@ REF_DELTA = 7 delta_types = (OFS_DELTA, REF_DELTA) -type_id_to_type_map = { - 0 : "", # EXT 1 - 1 : "commit", - 2 : "tree", - 3 : "blob", - 4 : "tag", - 5 : "", # EXT 2 - OFS_DELTA : "OFS_DELTA", # OFFSET DELTA - REF_DELTA : "REF_DELTA" # REFERENCE DELTA - } +type_id_to_type_map = { + 0: "", # EXT 1 + 1: "commit", + 2: "tree", + 3: "blob", + 4: "tag", + 5: "", # EXT 2 + OFS_DELTA: "OFS_DELTA", # OFFSET DELTA + REF_DELTA: "REF_DELTA" # REFERENCE DELTA +} type_to_type_id_map = dict( - commit=1, - tree=2, - blob=3, - tag=4, - OFS_DELTA=OFS_DELTA, - REF_DELTA=REF_DELTA - ) + commit=1, + tree=2, + blob=3, + tag=4, + OFS_DELTA=OFS_DELTA, + REF_DELTA=REF_DELTA +) # used when dealing with larger streams -chunk_size = 1000*mmap.PAGESIZE +chunk_size = 1000 * mmap.PAGESIZE -__all__ = ('is_loose_object', 'loose_object_header_info', 'msb_size', 'pack_object_header_info', - 'write_object', 'loose_object_header', 'stream_copy', 'apply_delta_data', - 'is_equal_canonical_sha', 'connect_deltas', 'DeltaChunkList', 'create_pack_object_header') +__all__ = ( + 'is_loose_object', + 'loose_object_header_info', + 'msb_size', + 'pack_object_header_info', + 'write_object', + 'loose_object_header', + 'stream_copy', + 'apply_delta_data', + 'is_equal_canonical_sha', + 'connect_deltas', + 'DeltaChunkList', + 'create_pack_object_header') #{ Structures @@ -59,11 +70,12 @@ def _set_delta_rbound(d, size): to our size :return: d""" d.ts = size - + # NOTE: data is truncated automatically when applying the delta # MUST NOT DO THIS HERE return d - + + def _move_delta_lbound(d, bytes): """Move the delta by the given amount of bytes, reducing its size so that its right bound stays static @@ -71,19 +83,21 @@ def _move_delta_lbound(d, bytes): :return: d""" if bytes == 0: return - + d.to += bytes d.so += bytes d.ts -= bytes if d.data is not None: d.data = d.data[bytes:] # END handle data - + return d - + + def delta_duplicate(src): return DeltaChunk(src.to, src.ts, src.so, src.data) - + + def delta_chunk_apply(dc, bbuf, write): """Apply own data to the target buffer :param bbuf: buffer providing source bytes for copy operations @@ -94,7 +108,8 @@ def delta_chunk_apply(dc, bbuf, write): else: # APPEND DATA # whats faster: if + 4 function calls or just a write with a slice ? - # Considering data can be larger than 127 bytes now, it should be worth it + # Considering data can be larger than 127 bytes now, it should be worth + # it if dc.ts < len(dc.data): write(dc.data[:dc.ts]) else: @@ -104,16 +119,20 @@ def delta_chunk_apply(dc, bbuf, write): class DeltaChunk(object): + """Represents a piece of a delta, it can either add new data, or copy existing one from a source buffer""" __slots__ = ( - 'to', # start offset in the target buffer in bytes - 'ts', # size of this chunk in the target buffer in bytes - 'so', # start offset in the source buffer in bytes or None - 'data', # chunk of bytes to be added to the target buffer, - # DeltaChunkList to use as base, or None - ) - + 'to', # start offset in the target buffer in bytes + # size of this chunk in the target buffer in bytes + 'ts', + # start offset in the source buffer in bytes or None + 'so', + # chunk of bytes to be added to the target buffer, + 'data', + # DeltaChunkList to use as base, or None + ) + def __init__(self, to, ts, so, data): self.to = to self.ts = ts @@ -121,23 +140,25 @@ def __init__(self, to, ts, so, data): self.data = data def __repr__(self): - return "DeltaChunk(%i, %i, %s, %s)" % (self.to, self.ts, self.so, self.data or "") - + return "DeltaChunk(%i, %i, %s, %s)" % ( + self.to, self.ts, self.so, self.data or "") + #{ Interface - + def rbound(self): return self.to + self.ts - + def has_data(self): """:return: True if the instance has data to add to the target stream""" return self.data is not None - + #} END interface + def _closest_index(dcl, absofs): """:return: index at which the given absofs should be inserted. The index points to the DeltaChunk with a target buffer absofs that equals or is greater than - absofs. + absofs. **Note:** global method for performance only, it belongs to DeltaChunkList""" lo = 0 hi = len(dcl) @@ -152,8 +173,9 @@ def _closest_index(dcl, absofs): lo = mid + 1 # END handle bound # END for each delta absofs - return len(dcl)-1 - + return len(dcl) - 1 + + def delta_list_apply(dcl, bbuf, write): """Apply the chain's changes and write the final result using the passed write function. @@ -165,15 +187,16 @@ def delta_list_apply(dcl, bbuf, write): delta_chunk_apply(dc, bbuf, write) # END for each dc + def delta_list_slice(dcl, absofs, size, ndcl): - """:return: Subsection of this list at the given absolute offset, with the given + """:return: Subsection of this list at the given absolute offset, with the given size in bytes. :return: None""" cdi = _closest_index(dcl, absofs) # delta start index cd = dcl[cdi] slen = len(dcl) - lappend = ndcl.append - + lappend = ndcl.append + if cd.to != absofs: tcd = DeltaChunk(cd.to, cd.ts, cd.so, cd.data) _move_delta_lbound(tcd, absofs - cd.to) @@ -182,7 +205,7 @@ def delta_list_slice(dcl, absofs, size, ndcl): size -= tcd.ts cdi += 1 # END lbound overlap handling - + while cdi < slen and size: # are we larger than the current block cd = dcl[cdi] @@ -198,38 +221,39 @@ def delta_list_slice(dcl, absofs, size, ndcl): # END hadle size cdi += 1 # END for each chunk - - + + class DeltaChunkList(list): + """List with special functionality to deal with DeltaChunks. There are two types of lists we represent. The one was created bottom-up, working - towards the latest delta, the other kind was created top-down, working from the - latest delta down to the earliest ancestor. This attribute is queryable + towards the latest delta, the other kind was created top-down, working from the + latest delta down to the earliest ancestor. This attribute is queryable after all processing with is_reversed.""" - + __slots__ = tuple() - + def rbound(self): """:return: rightmost extend in bytes, absolute""" if len(self) == 0: return 0 return self[-1].rbound() - + def lbound(self): """:return: leftmost byte at which this chunklist starts""" if len(self) == 0: return 0 return self[0].to - + def size(self): """:return: size of bytes as measured by our delta chunks""" return self.rbound() - self.lbound() - + def apply(self, bbuf, write): """Only used by public clients, internally we only use the global routines for performance""" return delta_list_apply(self, bbuf, write) - + def compress(self): """Alter the list to reduce the amount of nodes. Currently we concatenate add-chunks @@ -239,41 +263,45 @@ def compress(self): return self i = 0 slen_orig = slen - + first_data_index = None while i < slen: dc = self[i] i += 1 if dc.data is None: - if first_data_index is not None and i-2-first_data_index > 1: - #if first_data_index is not None: + if first_data_index is not None and i - \ + 2 - first_data_index > 1: + # if first_data_index is not None: nd = StringIO() # new data - so = self[first_data_index].to # start offset in target buffer - for x in xrange(first_data_index, i-1): + # start offset in target buffer + so = self[first_data_index].to + for x in xrange(first_data_index, i - 1): xdc = self[x] nd.write(xdc.data[:xdc.ts]) # END collect data - - del(self[first_data_index:i-1]) + + del(self[first_data_index:i - 1]) buf = nd.getvalue() - self.insert(first_data_index, DeltaChunk(so, len(buf), 0, buf)) - + self.insert( + first_data_index, DeltaChunk(so, len(buf), 0, buf)) + slen = len(self) i = first_data_index + 1 - + # END concatenate data first_data_index = None continue # END skip non-data chunks - + if first_data_index is None: - first_data_index = i-1 + first_data_index = i - 1 # END iterate list - - #if slen_orig != len(self): - # print "INFO: Reduced delta list len to %f %% of former size" % ((float(len(self)) / slen_orig) * 100) + + # if slen_orig != len(self): + # print "INFO: Reduced delta list len to %f %% of former size" % + # ((float(len(self)) / slen_orig) * 100) return self - + def check_integrity(self, target_size=-1): """Verify the list has non-overlapping chunks only, and the total size matches target_size @@ -281,41 +309,43 @@ def check_integrity(self, target_size=-1): :raise AssertionError: if the size doen't match""" if target_size > -1: assert self[-1].rbound() == target_size - assert reduce(lambda x,y: x+y, (d.ts for d in self), 0) == target_size + assert reduce( + lambda x, y: x + y, (d.ts for d in self), 0) == target_size # END target size verification - + if len(self) < 2: return - + # check data for dc in self: assert dc.ts > 0 if dc.has_data(): assert len(dc.data) >= dc.ts # END for each dc - - left = islice(self, 0, len(self)-1) + + left = islice(self, 0, len(self) - 1) right = iter(self) right.next() - # this is very pythonic - we might have just use index based access here, + # this is very pythonic - we might have just use index based access here, # but this could actually be faster - for lft,rgt in izip(left, right): + for lft, rgt in izip(left, right): assert lft.rbound() == rgt.to assert lft.to + lft.ts == rgt.to # END for each pair - + class TopdownDeltaChunkList(DeltaChunkList): - """Represents a list which is generated by feeding its ancestor streams one by + + """Represents a list which is generated by feeding its ancestor streams one by one""" - __slots__ = tuple() - + __slots__ = tuple() + def connect_with_next_base(self, bdcl): """Connect this chain with the next level of our base delta chunklist. The goal in this game is to mark as many of our chunks rigid, hence they - cannot be changed by any of the upcoming bases anymore. Once all our + cannot be changed by any of the upcoming bases anymore. Once all our chunks are marked like that, we can stop all processing - :param bdcl: data chunk list being one of our bases. They must be fed in + :param bdcl: data chunk list being one of our bases. They must be fed in consequtively and in order, towards the earliest ancestor delta :return: True if processing was done. Use it to abort processing of remaining streams if False is returned""" @@ -326,13 +356,14 @@ def connect_with_next_base(self, bdcl): while dci < slen: dc = self[dci] dci += 1 - - # all add-chunks which are already topmost don't need additional processing + + # all add-chunks which are already topmost don't need additional + # processing if dc.data is not None: nfc += 1 continue # END skip add chunks - + # copy chunks # integrate the portion of the base list into ourselves. Lists # dont support efficient insertion ( just one at a time ), but for now @@ -341,37 +372,37 @@ def connect_with_next_base(self, bdcl): # ourselves in order to reduce the amount of insertions ... del(ccl[:]) delta_list_slice(bdcl, dc.so, dc.ts, ccl) - + # move the target bounds into place to match with our chunk ofs = dc.to - dc.so for cdc in ccl: cdc.to += ofs # END update target bounds - + if len(ccl) == 1: - self[dci-1] = ccl[0] + self[dci - 1] = ccl[0] else: # maybe try to compute the expenses here, and pick the right algorithm # It would normally be faster than copying everything physically though # TODO: Use a deque here, and decide by the index whether to extend # or extend left ! post_dci = self[dci:] - del(self[dci-1:]) # include deletion of dc + del(self[dci - 1:]) # include deletion of dc self.extend(ccl) self.extend(post_dci) - + slen = len(self) - dci += len(ccl)-1 # deleted dc, added rest - + dci += len(ccl) - 1 # deleted dc, added rest + # END handle chunk replacement # END for each chunk - + if nfc == slen: return False # END handle completeness return True - - + + #} END structures #{ Routines @@ -384,16 +415,18 @@ def is_loose_object(m): word = (b0 << 8) + b1 return b0 == 0x78 and (word % 31) == 0 + def loose_object_header_info(m): """ - :return: tuple(type_string, uncompressed_size_in_bytes) the type string of the + :return: tuple(type_string, uncompressed_size_in_bytes) the type string of the object as well as its uncompressed size in bytes. :param m: memory map from which to read the compressed object data""" decompress_size = 8192 # is used in cgit as well hdr = decompressobj().decompress(m, decompress_size) type_name, size = hdr[:hdr.find("\0")].split(" ") return type_name, int(size) - + + def pack_object_header_info(data): """ :return: tuple(type_id, uncompressed_size_in_bytes, byte_offset) @@ -413,15 +446,16 @@ def pack_object_header_info(data): # END character loop return (type_id, size, i) + def create_pack_object_header(obj_type, obj_size): """ :return: string defining the pack header comprised of the object type and its incompressed size in bytes - + :param obj_type: pack type_id of the object :param obj_size: uncompressed size in bytes of the following object stream""" c = 0 # 1 byte - hdr = str() # output string + hdr = str() # output string c = (obj_type << 4) | (obj_size & 0xf) obj_size >>= 4 @@ -429,21 +463,22 @@ def create_pack_object_header(obj_type, obj_size): hdr += chr(c | 0x80) c = obj_size & 0x7f obj_size >>= 7 - #END until size is consumed + # END until size is consumed hdr += chr(c) return hdr - + + def msb_size(data, offset=0): """ - :return: tuple(read_bytes, size) read the msb size from the given random + :return: tuple(read_bytes, size) read the msb size from the given random access data starting at the given byte offset""" size = 0 i = 0 l = len(data) hit_msb = False while i < l: - c = ord(data[i+offset]) - size |= (c & 0x7f) << i*7 + c = ord(data[i + offset]) + size |= (c & 0x7f) << i * 7 i += 1 if not c & 0x80: hit_msb = True @@ -451,20 +486,23 @@ def msb_size(data, offset=0): # END check msb bit # END while in range if not hit_msb: - raise AssertionError("Could not find terminating MSB byte in data stream") - return i+offset, size - + raise AssertionError( + "Could not find terminating MSB byte in data stream") + return i + offset, size + + def loose_object_header(type, size): """ :return: string representing the loose object header, which is immediately followed by the content stream of size 'size'""" return "%s %i\0" % (type, size) - + + def write_object(type, size, read, write, chunk_size=chunk_size): """ - Write the object as identified by type, size and source_stream into the + Write the object as identified by type, size and source_stream into the target_stream - + :param type: type string of the object :param size: amount of bytes to write from source_stream :param read: read method of a stream providing the content data @@ -473,26 +511,27 @@ def write_object(type, size, read, write, chunk_size=chunk_size): the routine exits, even if an error is thrown :return: The actual amount of bytes written to stream, which includes the header and a trailing newline""" tbw = 0 # total num bytes written - + # WRITE HEADER: type SP size NULL tbw += write(loose_object_header(type, size)) tbw += stream_copy(read, write, size, chunk_size) - + return tbw + def stream_copy(read, write, size, chunk_size): """ - Copy a stream up to size bytes using the provided read and write methods, + Copy a stream up to size bytes using the provided read and write methods, in chunks of chunk_size - + **Note:** its much like stream_copy utility, but operates just using methods""" dbw = 0 # num data bytes written - + # WRITE ALL DATA UP TO SIZE while True: - cs = min(chunk_size, size-dbw) + cs = min(chunk_size, size - dbw) # NOTE: not all write methods return the amount of written bytes, like - # mmap.write. Its bad, but we just deal with it ... perhaps its not + # mmap.write. Its bad, but we just deal with it ... perhaps its not # even less efficient # data_len = write(read(cs)) # dbw += data_len @@ -505,27 +544,28 @@ def stream_copy(read, write, size, chunk_size): # END check for stream end # END duplicate data return dbw - + + def connect_deltas(dstreams): """ Read the condensed delta chunk information from dstream and merge its information into a list of existing delta chunks - + :param dstreams: iterable of delta stream objects, the delta to be applied last comes first, then all its ancestors in order :return: DeltaChunkList, containing all operations to apply""" tdcl = None # topmost dcl - + dcl = tdcl = TopdownDeltaChunkList() for dsi, ds in enumerate(dstreams): # print "Stream", dsi db = ds.read() delta_buf_size = ds.size - + # read header i, base_size = msb_size(db) i, target_size = msb_size(db, i) - + # interpret opcodes tbw = 0 # amount of target bytes written while i < delta_buf_size: @@ -554,52 +594,53 @@ def connect_deltas(dstreams): if (c & 0x40): cp_size |= (ord(db[i]) << 16) i += 1 - - if not cp_size: + + if not cp_size: cp_size = 0x10000 - + rbound = cp_off + cp_size if (rbound < cp_size or - rbound > base_size): + rbound > base_size): break - + dcl.append(DeltaChunk(tbw, cp_size, cp_off, None)) tbw += cp_size elif c: # NOTE: in C, the data chunks should probably be concatenated here. # In python, we do it as a post-process - dcl.append(DeltaChunk(tbw, c, 0, db[i:i+c])) + dcl.append(DeltaChunk(tbw, c, 0, db[i:i + c])) i += c tbw += c else: raise ValueError("unexpected delta opcode 0") # END handle command byte # END while processing delta data - + dcl.compress() - + # merge the lists ! if dsi > 0: if not tdcl.connect_with_next_base(dcl): break # END handle merge - + # prepare next base dcl = DeltaChunkList() # END for each delta stream - + return tdcl - + + def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): """ Apply data from a delta buffer using a source buffer to the target file - + :param src_buf: random access data from which the delta was created :param src_buf_size: size of the source buffer in bytes :param delta_buf_size: size fo the delta buffer in bytes :param delta_buf: random access delta data :param write: write method taking a chunk of bytes - + **Note:** transcribed to python from the similar routine in patch-delta.c""" i = 0 db = delta_buf @@ -629,44 +670,44 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): if (c & 0x40): cp_size |= (ord(db[i]) << 16) i += 1 - - if not cp_size: + + if not cp_size: cp_size = 0x10000 - + rbound = cp_off + cp_size if (rbound < cp_size or - rbound > src_buf_size): + rbound > src_buf_size): break write(buffer(src_buf, cp_off, cp_size)) elif c: - write(db[i:i+c]) + write(db[i:i + c]) i += c else: raise ValueError("unexpected delta opcode 0") # END handle command byte # END while processing delta data - + # yes, lets use the exact same error message that git uses :) assert i == delta_buf_size, "delta replay has gone wild" - - + + def is_equal_canonical_sha(canonical_length, match, sha1): """ :return: True if the given lhs and rhs 20 byte binary shas - The comparison will take the canonical_length of the match sha into account, + The comparison will take the canonical_length of the match sha into account, hence the comparison will only use the last 4 bytes for uneven canonical representations :param match: less than 20 byte sha :param sha1: 20 byte sha""" - binary_length = canonical_length/2 + binary_length = canonical_length / 2 if match[:binary_length] != sha1[:binary_length]: return False - + if canonical_length - binary_length and \ - (ord(match[-1]) ^ ord(sha1[len(match)-1])) & 0xf0: + (ord(match[-1]) ^ ord(sha1[len(match) - 1])) & 0xf0: return False # END handle uneven canonnical length return True - + #} END routines diff --git a/gitdb/pack.py b/gitdb/pack.py index 48121f0..03e5080 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -4,31 +4,31 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Contains PackIndexFile and PackFile implementations""" from gitdb.exc import ( - BadObject, - UnsupportedOperation, - ParseError - ) -from util import ( - zlib, - mman, - LazyMixin, - unpack_from, - bin_to_hex, - ) - -from fun import ( - create_pack_object_header, - pack_object_header_info, - is_equal_canonical_sha, - type_id_to_type_map, - write_object, - stream_copy, - chunk_size, - delta_types, - OFS_DELTA, - REF_DELTA, - msb_size - ) + BadObject, + UnsupportedOperation, + ParseError +) +from .util import ( + zlib, + mman, + LazyMixin, + unpack_from, + bin_to_hex, +) + +from .fun import ( + create_pack_object_header, + pack_object_header_info, + is_equal_canonical_sha, + type_id_to_type_map, + write_object, + stream_copy, + chunk_size, + delta_types, + OFS_DELTA, + REF_DELTA, + msb_size +) try: from _perf import PackIndexFile_sha_to_index @@ -36,27 +36,27 @@ pass # END try c module -from base import ( # Amazing ! - OInfo, - OStream, - OPackInfo, - OPackStream, - ODeltaStream, - ODeltaPackInfo, - ODeltaPackStream, - ) -from stream import ( - DecompressMemMapReader, - DeltaApplyReader, - Sha1Writer, - NullStream, - FlexibleSha1Writer - ) +from .base import ( # Amazing ! + OInfo, + OStream, + OPackInfo, + OPackStream, + ODeltaStream, + ODeltaPackInfo, + ODeltaPackStream, +) +from .stream import ( + DecompressMemMapReader, + DeltaApplyReader, + Sha1Writer, + NullStream, + FlexibleSha1Writer +) from struct import ( - pack, - unpack, - ) + pack, + unpack, +) from binascii import crc32 @@ -68,10 +68,8 @@ __all__ = ('PackIndexFile', 'PackFile', 'PackEntity') - - -#{ Utilities +#{ Utilities def pack_object_at(cursor, offset, as_stream): """ @@ -81,13 +79,14 @@ def pack_object_at(cursor, offset, as_stream): data to be read decompressed. :param data: random accessable data containing all required information :parma offset: offset in to the data at which the object information is located - :param as_stream: if True, a stream object will be returned that can read + :param as_stream: if True, a stream object will be returned that can read the data, otherwise you receive an info object only""" data = cursor.use_region(offset).buffer() type_id, uncomp_size, data_rela_offset = pack_object_header_info(data) - total_rela_offset = None # set later, actual offset until data stream begins + # set later, actual offset until data stream begins + total_rela_offset = None delta_info = None - + # OFFSET DELTA if type_id == OFS_DELTA: i = data_rela_offset @@ -104,29 +103,34 @@ def pack_object_at(cursor, offset, as_stream): total_rela_offset = i # REF DELTA elif type_id == REF_DELTA: - total_rela_offset = data_rela_offset+20 + total_rela_offset = data_rela_offset + 20 delta_info = data[data_rela_offset:total_rela_offset] # BASE OBJECT else: # assume its a base object total_rela_offset = data_rela_offset # END handle type id - + abs_data_offset = offset + total_rela_offset if as_stream: - stream = DecompressMemMapReader(buffer(data, total_rela_offset), False, uncomp_size) + stream = DecompressMemMapReader( + buffer(data, total_rela_offset), False, uncomp_size) if delta_info is None: - return abs_data_offset, OPackStream(offset, type_id, uncomp_size, stream) + return abs_data_offset, OPackStream( + offset, type_id, uncomp_size, stream) else: - return abs_data_offset, ODeltaPackStream(offset, type_id, uncomp_size, delta_info, stream) + return abs_data_offset, ODeltaPackStream( + offset, type_id, uncomp_size, delta_info, stream) else: if delta_info is None: return abs_data_offset, OPackInfo(offset, type_id, uncomp_size) else: - return abs_data_offset, ODeltaPackInfo(offset, type_id, uncomp_size, delta_info) + return abs_data_offset, ODeltaPackInfo( + offset, type_id, uncomp_size, delta_info) # END handle info # END handle stream + def write_stream_to_pack(read, write, zstream, base_crc=None): """Copy a stream as read from read function, zip it, and write the result. Count the number of written bytes and return it @@ -140,30 +144,30 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): crc = 0 if want_crc: crc = base_crc - #END initialize crc - + # END initialize crc + while True: chunk = read(chunk_size) br += len(chunk) compressed = zstream.compress(chunk) bw += len(compressed) write(compressed) # cannot assume return value - + if want_crc: crc = crc32(compressed, crc) - #END handle crc - + # END handle crc + if len(chunk) != chunk_size: break - #END copy loop - + # END copy loop + compressed = zstream.flush() bw += len(compressed) write(compressed) if want_crc: crc = crc32(compressed, crc) - #END handle crc - + # END handle crc + return (br, bw, crc) @@ -171,83 +175,85 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): class IndexWriter(object): + """Utility to cache index information, allowing to write all information later in one go to the given stream **Note:** currently only writes v2 indices""" __slots__ = '_objs' - + def __init__(self): self._objs = list() - + def append(self, binsha, crc, offset): """Append one piece of object information""" self._objs.append((binsha, crc, offset)) - + def write(self, pack_sha, write): """Write the index file using the given write method :param pack_sha: binary sha over the whole pack that we index :return: sha1 binary sha over all index file contents""" # sort for sha1 hash self._objs.sort(key=lambda o: o[0]) - + sha_writer = FlexibleSha1Writer(write) sha_write = sha_writer.write sha_write(PackIndexFile.index_v2_signature) sha_write(pack(">L", PackIndexFile.index_version_default)) - + # fanout - tmplist = list((0,)*256) # fanout or list with 64 bit offsets + # fanout or list with 64 bit offsets + tmplist = list((0,) * 256) for t in self._objs: tmplist[ord(t[0][0])] += 1 - #END prepare fanout + # END prepare fanout for i in xrange(255): v = tmplist[i] sha_write(pack('>L', v)) - tmplist[i+1] += v - #END write each fanout entry + tmplist[i + 1] += v + # END write each fanout entry sha_write(pack('>L', tmplist[255])) - + # sha1 ordered # save calls, that is push them into c sha_write(''.join(t[0] for t in self._objs)) - + # crc32 for t in self._objs: - sha_write(pack('>L', t[1]&0xffffffff)) - #END for each crc - + sha_write(pack('>L', t[1] & 0xffffffff)) + # END for each crc + tmplist = list() # offset 32 for t in self._objs: ofs = t[2] if ofs > 0x7fffffff: tmplist.append(ofs) - ofs = 0x80000000 + len(tmplist)-1 - #END hande 64 bit offsets - sha_write(pack('>L', ofs&0xffffffff)) - #END for each offset - + ofs = 0x80000000 + len(tmplist) - 1 + # END hande 64 bit offsets + sha_write(pack('>L', ofs & 0xffffffff)) + # END for each offset + # offset 64 for ofs in tmplist: sha_write(pack(">Q", ofs)) - #END for each offset - + # END for each offset + # trailer assert(len(pack_sha) == 20) sha_write(pack_sha) sha = sha_writer.sha(as_hex=False) write(sha) return sha - - + class PackIndexFile(LazyMixin): + """A pack index provides offsets into the corresponding pack, allowing to find locations for offsets faster.""" - + # Dont use slots as we dynamically bind functions for each version, need a dict for this # The slots you see here are just to keep track of our instance variables - # __slots__ = ('_indexpath', '_fanout_table', '_cursor', '_version', + # __slots__ = ('_indexpath', '_fanout_table', '_cursor', '_version', # '_sha_list_offset', '_crc_list_offset', '_pack_offset', '_pack_64_offset') # used in v2 indices @@ -258,7 +264,7 @@ class PackIndexFile(LazyMixin): def __init__(self, indexpath): super(PackIndexFile, self).__init__() self._indexpath = indexpath - + def _set_cache_(self, attr): if attr == "_packfile_checksum": self._packfile_checksum = self._cursor.map()[-40:-20] @@ -270,138 +276,151 @@ def _set_cache_(self, attr): # alternate for instance self._cursor = mman.make_cursor(self._indexpath).use_region() # We will assume that the index will always fully fit into memory ! - if mman.window_size() > 0 and self._cursor.file_size() > mman.window_size(): - raise AssertionError("The index file at %s is too large to fit into a mapped window (%i > %i). This is a limitation of the implementation" % (self._indexpath, self._cursor.file_size(), mman.window_size())) - #END assert window size + if mman.window_size() > 0 and self._cursor.file_size( + ) > mman.window_size(): + raise AssertionError( + "The index file at %s is too large to fit into a mapped window (%i > %i). This is a limitation of the implementation" % + (self._indexpath, self._cursor.file_size(), mman.window_size())) + # END assert window size else: # now its time to initialize everything - if we are here, someone wants # to access the fanout table or related properties - + # CHECK VERSION mmap = self._cursor.map() self._version = (mmap[:4] == self.index_v2_signature and 2) or 1 if self._version == 2: - version_id = unpack_from(">L", mmap, 4)[0] + version_id = unpack_from(">L", mmap, 4)[0] assert version_id == self._version, "Unsupported index version: %i" % version_id # END assert version - + # SETUP FUNCTIONS # setup our functions according to the actual version for fname in ('entry', 'offset', 'sha', 'crc'): - setattr(self, fname, getattr(self, "_%s_v%i" % (fname, self._version))) + setattr( + self, fname, getattr( + self, "_%s_v%i" % + (fname, self._version))) # END for each function to initialize - - + # INITIALIZE DATA # byte offset is 8 if version is 2, 0 otherwise self._initialize() # END handle attributes - #{ Access V1 - + def _entry_v1(self, i): """:return: tuple(offset, binsha, 0)""" - return unpack_from(">L20s", self._cursor.map(), 1024 + i*24) + (0, ) - + return unpack_from(">L20s", self._cursor.map(), 1024 + i * 24) + (0, ) + def _offset_v1(self, i): """see ``_offset_v2``""" - return unpack_from(">L", self._cursor.map(), 1024 + i*24)[0] - + return unpack_from(">L", self._cursor.map(), 1024 + i * 24)[0] + def _sha_v1(self, i): """see ``_sha_v2``""" - base = 1024 + (i*24)+4 - return self._cursor.map()[base:base+20] - + base = 1024 + (i * 24) + 4 + return self._cursor.map()[base:base + 20] + def _crc_v1(self, i): """unsupported""" return 0 - + #} END access V1 - + #{ Access V2 def _entry_v2(self, i): """:return: tuple(offset, binsha, crc)""" return (self._offset_v2(i), self._sha_v2(i), self._crc_v2(i)) - + def _offset_v2(self, i): - """:return: 32 or 64 byte offset into pack files. 64 byte offsets will only + """:return: 32 or 64 byte offset into pack files. 64 byte offsets will only be returned if the pack is larger than 4 GiB, or 2^32""" - offset = unpack_from(">L", self._cursor.map(), self._pack_offset + i * 4)[0] - + offset = unpack_from( + ">L", self._cursor.map(), self._pack_offset + i * 4)[0] + # if the high-bit is set, this indicates that we have to lookup the offset # in the 64 bit region of the file. The current offset ( lower 31 bits ) # are the index into it if offset & 0x80000000: - offset = unpack_from(">Q", self._cursor.map(), self._pack_64_offset + (offset & ~0x80000000) * 8)[0] + offset = unpack_from( + ">Q", self._cursor.map(), self._pack_64_offset + (offset & ~0x80000000) * 8)[0] # END handle 64 bit offset - + return offset - + def _sha_v2(self, i): """:return: sha at the given index of this file index instance""" base = self._sha_list_offset + i * 20 - return self._cursor.map()[base:base+20] - + return self._cursor.map()[base:base + 20] + def _crc_v2(self, i): """:return: 4 bytes crc for the object at index i""" - return unpack_from(">L", self._cursor.map(), self._crc_list_offset + i * 4)[0] - + return unpack_from( + ">L", self._cursor.map(), self._crc_list_offset + i * 4)[0] + #} END access V2 - + #{ Initialization - + def _initialize(self): """initialize base data""" self._fanout_table = self._read_fanout((self._version == 2) * 8) - + if self._version == 2: self._crc_list_offset = self._sha_list_offset + self.size() * 20 self._pack_offset = self._crc_list_offset + self.size() * 4 self._pack_64_offset = self._pack_offset + self.size() * 4 # END setup base - + def _read_fanout(self, byte_offset): """Generate a fanout table from our data""" d = self._cursor.map() out = list() append = out.append for i in range(256): - append(unpack_from('>L', d, byte_offset + i*4)[0]) + append(unpack_from('>L', d, byte_offset + i * 4)[0]) # END for each entry return out - + #} END initialization - + #{ Properties def version(self): return self._version - + def size(self): """:return: amount of objects referred to by this index""" return self._fanout_table[255] - + def path(self): """:return: path to the packindexfile""" return self._indexpath - + def packfile_checksum(self): """:return: 20 byte sha representing the sha1 hash of the pack file""" return self._cursor.map()[-40:-20] - + def indexfile_checksum(self): """:return: 20 byte sha representing the sha1 hash of this index file""" return self._cursor.map()[-20:] - + def offsets(self): """:return: sequence of all offsets in the order in which they were written - + **Note:** return value can be random accessed, but may be immmutable""" if self._version == 2: # read stream to array, convert to tuple - a = array.array('I') # 4 byte unsigned int, long are 8 byte on 64 bit it appears - a.fromstring(buffer(self._cursor.map(), self._pack_offset, self._pack_64_offset - self._pack_offset)) - + # 4 byte unsigned int, long are 8 byte on 64 bit it appears + a = array.array('I') + a.fromstring( + buffer( + self._cursor.map(), + self._pack_offset, + self._pack_64_offset - + self._pack_offset)) + # networkbyteorder to something array likes more if sys.byteorder == 'little': a.byteswap() @@ -409,7 +428,7 @@ def offsets(self): else: return tuple(self.offset(index) for index in xrange(self.size())) # END handle version - + def sha_to_index(self, sha): """ :return: index usable with the ``offset`` or ``entry`` method, or None @@ -419,9 +438,10 @@ def sha_to_index(self, sha): get_sha = self.sha lo = 0 # lower index, the left bound of the bisection if first_byte != 0: - lo = self._fanout_table[first_byte-1] - hi = self._fanout_table[first_byte] # the upper, right bound of the bisection - + lo = self._fanout_table[first_byte - 1] + # the upper, right bound of the bisection + hi = self._fanout_table[first_byte] + # bisect until we have the sha while lo < hi: mid = (lo + hi) / 2 @@ -435,29 +455,30 @@ def sha_to_index(self, sha): # END handle midpoint # END bisect return None - + def partial_sha_to_index(self, partial_bin_sha, canonical_length): """ :return: index as in `sha_to_index` or None if the sha was not found in this index file :param partial_bin_sha: an at least two bytes of a partial binary sha - :param canonical_length: lenght of the original hexadecimal representation of the + :param canonical_length: lenght of the original hexadecimal representation of the given partial binary sha :raise AmbiguousObjectName:""" if len(partial_bin_sha) < 2: raise ValueError("Require at least 2 bytes of partial sha") - + first_byte = ord(partial_bin_sha[0]) get_sha = self.sha lo = 0 # lower index, the left bound of the bisection if first_byte != 0: - lo = self._fanout_table[first_byte-1] - hi = self._fanout_table[first_byte] # the upper, right bound of the bisection - + lo = self._fanout_table[first_byte - 1] + # the upper, right bound of the bisection + hi = self._fanout_table[first_byte] + # fill the partial to full 20 bytes - filled_sha = partial_bin_sha + '\0'*(20 - len(partial_bin_sha)) - - # find lowest + filled_sha = partial_bin_sha + '\0' * (20 - len(partial_bin_sha)) + + # find lowest while lo < hi: mid = (lo + hi) / 2 c = cmp(filled_sha, get_sha(mid)) @@ -471,122 +492,127 @@ def partial_sha_to_index(self, partial_bin_sha, canonical_length): lo = mid + 1 # END handle midpoint # END bisect - + if lo < self.size(): cur_sha = get_sha(lo) - if is_equal_canonical_sha(canonical_length, partial_bin_sha, cur_sha): + if is_equal_canonical_sha( + canonical_length, partial_bin_sha, cur_sha): next_sha = None - if lo+1 < self.size(): - next_sha = get_sha(lo+1) + if lo + 1 < self.size(): + next_sha = get_sha(lo + 1) if next_sha and next_sha == cur_sha: raise AmbiguousObjectName(partial_bin_sha) return lo # END if we have a match # END if we found something return None - + if 'PackIndexFile_sha_to_index' in globals(): - # NOTE: Its just about 25% faster, the major bottleneck might be the attr + # NOTE: Its just about 25% faster, the major bottleneck might be the attr # accesses def sha_to_index(self, sha): return PackIndexFile_sha_to_index(self, sha) - # END redefine heavy-hitter with c version - + # END redefine heavy-hitter with c version + #} END properties - - + + class PackFile(LazyMixin): + """A pack is a file written according to the Version 2 for git packs - + As we currently use memory maps, it could be assumed that the maximum size of - packs therefor is 32 bit on 32 bit systems. On 64 bit systems, this should be + packs therefor is 32 bit on 32 bit systems. On 64 bit systems, this should be fine though. - - **Note:** at some point, this might be implemented using streams as well, or + + **Note:** at some point, this might be implemented using streams as well, or streams are an alternate path in the case memory maps cannot be created - for some reason - one clearly doesn't want to read 10GB at once in that + for some reason - one clearly doesn't want to read 10GB at once in that case""" - + __slots__ = ('_packpath', '_cursor', '_size', '_version') pack_signature = 0x5041434b # 'PACK' pack_version_default = 2 - + # offset into our data at which the first object starts - first_object_offset = 3*4 # header bytes + first_object_offset = 3 * 4 # header bytes footer_size = 20 # final sha - + def __init__(self, packpath): self._packpath = packpath - + def _set_cache_(self, attr): # we fill the whole cache, whichever attribute gets queried first self._cursor = mman.make_cursor(self._packpath).use_region() - + # read the header information - type_id, self._version, self._size = unpack_from(">LLL", self._cursor.map(), 0) - + type_id, self._version, self._size = unpack_from( + ">LLL", self._cursor.map(), 0) + # TODO: figure out whether we should better keep the lock, or maybe # add a .keep file instead ? if type_id != self.pack_signature: raise ParseError("Invalid pack signature: %i" % type_id) - + def _iter_objects(self, start_offset, as_stream=True): """Handle the actual iteration of objects within this pack""" c = self._cursor content_size = c.file_size() - self.footer_size cur_offset = start_offset or self.first_object_offset - + null = NullStream() while cur_offset < content_size: data_offset, ostream = pack_object_at(c, cur_offset, True) # scrub the stream to the end - this decompresses the object, but yields # the amount of compressed bytes we need to get to the next offset - + stream_copy(ostream.read, null.write, ostream.size, chunk_size) - cur_offset += (data_offset - ostream.pack_offset) + ostream.stream.compressed_bytes_read() - - + cur_offset += (data_offset - ostream.pack_offset) + \ + ostream.stream.compressed_bytes_read() + # if a stream is requested, reset it beforehand - # Otherwise return the Stream object directly, its derived from the + # Otherwise return the Stream object directly, its derived from the # info object if as_stream: ostream.stream.seek(0) yield ostream # END until we have read everything - + #{ Pack Information - + def size(self): - """:return: The amount of objects stored in this pack""" + """:return: The amount of objects stored in this pack""" return self._size - + def version(self): """:return: the version of this pack""" return self._version - + def data(self): """ :return: read-only data of this pack. It provides random access and usually is a memory map. :note: This method is unsafe as it returns a window into a file which might be larger than than the actual window size""" - # can use map as we are starting at offset 0. Otherwise we would have to use buffer() + # can use map as we are starting at offset 0. Otherwise we would have + # to use buffer() return self._cursor.use_region().map() - + def checksum(self): """:return: 20 byte sha1 hash on all object sha's contained in this file""" - return self._cursor.use_region(self._cursor.file_size()-20).buffer()[:] - + return self._cursor.use_region( + self._cursor.file_size() - 20).buffer()[:] + def path(self): """:return: path to the packfile""" return self._packpath #} END pack information - + #{ Pack Specific - + def collect_streams(self, offset): """ :return: list of pack streams which are required to build the object - at the given offset. The first entry of the list is the object at offset, + at the given offset. The first entry of the list is the object at offset, the last one is either a full object, or a REF_Delta stream. The latter type needs its reference object to be locked up in an ODB to form a valid delta chain. @@ -601,7 +627,7 @@ def collect_streams(self, offset): offset = ostream.pack_offset - ostream.delta_info else: # the only thing we can lookup are OFFSET deltas. Everything - # else is either an object, or a ref delta, in the latter + # else is either an object, or a ref delta, in the latter # case someone else has to find it break # END handle type @@ -609,55 +635,62 @@ def collect_streams(self, offset): return out #} END pack specific - + #{ Read-Database like Interface - + def info(self, offset): """Retrieve information about the object at the given file-absolute offset - + :param offset: byte offset :return: OPackInfo instance, the actual type differs depending on the type_id attribute""" - return pack_object_at(self._cursor, offset or self.first_object_offset, False)[1] - + return pack_object_at( + self._cursor, offset or self.first_object_offset, False)[1] + def stream(self, offset): """Retrieve an object at the given file-relative offset as stream along with its information - + :param offset: byte offset :return: OPackStream instance, the actual type differs depending on the type_id attribute""" - return pack_object_at(self._cursor, offset or self.first_object_offset, True)[1] - + return pack_object_at( + self._cursor, offset or self.first_object_offset, True)[1] + def stream_iter(self, start_offset=0): """ - :return: iterator yielding OPackStream compatible instances, allowing + :return: iterator yielding OPackStream compatible instances, allowing to access the data in the pack directly. - :param start_offset: offset to the first object to iterate. If 0, iteration + :param start_offset: offset to the first object to iterate. If 0, iteration starts at the very first object in the pack. - + **Note:** Iterating a pack directly is costly as the datastream has to be decompressed to determine the bounds between the objects""" return self._iter_objects(start_offset, as_stream=True) - + #} END Read-Database like Interface - - + + class PackEntity(LazyMixin): - """Combines the PackIndexFile and the PackFile into one, allowing the + + """Combines the PackIndexFile and the PackFile into one, allowing the actual objects to be resolved and iterated""" - - __slots__ = ( '_index', # our index file - '_pack', # our pack file - '_offset_map' # on demand dict mapping one offset to the next consecutive one - ) - + + __slots__ = ('_index', # our index file + '_pack', # our pack file + # on demand dict mapping one offset to the next consecutive + # one + '_offset_map' + ) + IndexFileCls = PackIndexFile PackFileCls = PackFile - + def __init__(self, pack_or_index_path): """Initialize ourselves with the path to the respective pack or index file""" basename, ext = os.path.splitext(pack_or_index_path) - self._index = self.IndexFileCls("%s.idx" % basename) # PackIndexFile instance - self._pack = self.PackFileCls("%s.pack" % basename) # corresponding PackFile instance - + # PackIndexFile instance + self._index = self.IndexFileCls("%s.idx" % basename) + # corresponding PackFile instance + self._pack = self.PackFileCls("%s.pack" % basename) + def _set_cache_(self, attr): # currently this can only be _offset_map # TODO: make this a simple sorted offset array which can be bisected @@ -666,30 +699,30 @@ def _set_cache_(self, attr): offsets_sorted = sorted(self._index.offsets()) last_offset = len(self._pack.data()) - self._pack.footer_size assert offsets_sorted, "Cannot handle empty indices" - + offset_map = None if len(offsets_sorted) == 1: - offset_map = { offsets_sorted[0] : last_offset } + offset_map = {offsets_sorted[0]: last_offset} else: iter_offsets = iter(offsets_sorted) iter_offsets_plus_one = iter(offsets_sorted) iter_offsets_plus_one.next() consecutive = izip(iter_offsets, iter_offsets_plus_one) - + offset_map = dict(consecutive) - + # the last offset is not yet set offset_map[offsets_sorted[-1]] = last_offset # END handle offset amount self._offset_map = offset_map - + def _sha_to_index(self, sha): """:return: index for the given sha, or raise""" index = self._index.sha_to_index(sha) if index is None: raise BadObject(sha) return index - + def _iter_objects(self, as_stream): """Iterate over all objects in our index and yield their OInfo or OStream instences""" _sha = self._index.sha @@ -697,7 +730,7 @@ def _iter_objects(self, as_stream): for index in xrange(self._index.size()): yield _object(_sha(index), as_stream, index) # END for each index - + def _object(self, sha, as_stream, index=-1): """:return: OInfo or OStream object providing information about the given sha :param index: if not -1, its assumed to be the sha's index in the IndexFile""" @@ -708,105 +741,108 @@ def _object(self, sha, as_stream, index=-1): sha = self._index.sha(index) # END assure sha is present ( in output ) offset = self._index.offset(index) - type_id, uncomp_size, data_rela_offset = pack_object_header_info(self._pack._cursor.use_region(offset).buffer()) + type_id, uncomp_size, data_rela_offset = pack_object_header_info( + self._pack._cursor.use_region(offset).buffer()) if as_stream: if type_id not in delta_types: packstream = self._pack.stream(offset) - return OStream(sha, packstream.type, packstream.size, packstream.stream) + return OStream( + sha, packstream.type, packstream.size, packstream.stream) # END handle non-deltas - + # produce a delta stream containing all info - # To prevent it from applying the deltas when querying the size, + # To prevent it from applying the deltas when querying the size, # we extract it from the delta stream ourselves streams = self.collect_streams_at_offset(offset) dstream = DeltaApplyReader.new(streams) - - return ODeltaStream(sha, dstream.type, None, dstream) + + return ODeltaStream(sha, dstream.type, None, dstream) else: if type_id not in delta_types: return OInfo(sha, type_id_to_type_map[type_id], uncomp_size) # END handle non-deltas - + # deltas are a little tougher - unpack the first bytes to obtain # the actual target size, as opposed to the size of the delta data streams = self.collect_streams_at_offset(offset) buf = streams[0].read(512) offset, src_size = msb_size(buf) offset, target_size = msb_size(buf, offset) - + # collect the streams to obtain the actual object type if streams[-1].type_id in delta_types: raise BadObject(sha, "Could not resolve delta object") - return OInfo(sha, streams[-1].type, target_size) + return OInfo(sha, streams[-1].type, target_size) # END handle stream - + #{ Read-Database like Interface - + def info(self, sha): """Retrieve information about the object identified by the given sha - + :param sha: 20 byte sha1 :raise BadObject: :return: OInfo instance, with 20 byte sha""" return self._object(sha, False) - + def stream(self, sha): """Retrieve an object stream along with its information as identified by the given sha - + :param sha: 20 byte sha1 - :raise BadObject: + :raise BadObject: :return: OStream instance, with 20 byte sha""" return self._object(sha, True) def info_at_index(self, index): """As ``info``, but uses a PackIndexFile compatible index to refer to the object""" return self._object(None, False, index) - + def stream_at_index(self, index): - """As ``stream``, but uses a PackIndexFile compatible index to refer to the + """As ``stream``, but uses a PackIndexFile compatible index to refer to the object""" return self._object(None, True, index) - + #} END Read-Database like Interface - - #{ Interface + + #{ Interface def pack(self): """:return: the underlying pack file instance""" return self._pack - + def index(self): """:return: the underlying pack index file instance""" return self._index - + def is_valid_stream(self, sha, use_crc=False): """ Verify that the stream at the given sha is valid. - - :param use_crc: if True, the index' crc is run over the compressed stream of + + :param use_crc: if True, the index' crc is run over the compressed stream of the object, which is much faster than checking the sha1. It is also more prone to unnoticed corruption or manipulation. :param sha: 20 byte sha1 of the object whose stream to verify - whether the compressed stream of the object is valid. If it is - a delta, this only verifies that the delta's data is valid, not the - data of the actual undeltified object, as it depends on more than + whether the compressed stream of the object is valid. If it is + a delta, this only verifies that the delta's data is valid, not the + data of the actual undeltified object, as it depends on more than just this stream. If False, the object will be decompressed and the sha generated. It must match the given sha - + :return: True if the stream is valid :raise UnsupportedOperation: If the index is version 1 only :raise BadObject: sha was not found""" if use_crc: if self._index.version() < 2: - raise UnsupportedOperation("Version 1 indices do not contain crc's, verify by sha instead") + raise UnsupportedOperation( + "Version 1 indices do not contain crc's, verify by sha instead") # END handle index version - + index = self._sha_to_index(sha) offset = self._index.offset(index) next_offset = self._offset_map[offset] crc_value = self._index.crc(index) - + # create the current crc value, on the compressed object data # Read it in chunks, without copying the data crc_update = zlib.crc32 @@ -816,10 +852,11 @@ def is_valid_stream(self, sha, use_crc=False): while cur_pos < next_offset: rbound = min(cur_pos + chunk_size, next_offset) size = rbound - cur_pos - this_crc_value = crc_update(buffer(pack_data, cur_pos, size), this_crc_value) + this_crc_value = crc_update( + buffer(pack_data, cur_pos, size), this_crc_value) cur_pos += size # END window size loop - + # crc returns signed 32 bit numbers, the AND op forces it into unsigned # mode ... wow, sneaky, from dulwich. return (this_crc_value & 0xffffffff) == crc_value @@ -827,8 +864,9 @@ def is_valid_stream(self, sha, use_crc=False): shawriter = Sha1Writer() stream = self._object(sha, as_stream=True) # write a loose object, which is the basis for the sha - write_object(stream.type, stream.size, stream.read, shawriter.write) - + write_object( + stream.type, stream.size, stream.read, shawriter.write) + assert shawriter.sha(as_hex=False) == sha return shawriter.sha(as_hex=False) == sha # END handle crc/sha verification @@ -839,23 +877,24 @@ def info_iter(self): :return: Iterator over all objects in this pack. The iterator yields OInfo instances""" return self._iter_objects(as_stream=False) - + def stream_iter(self): """ :return: iterator over all objects in this pack. The iterator yields OStream instances""" return self._iter_objects(as_stream=True) - + def collect_streams_at_offset(self, offset): """ As the version in the PackFile, but can resolve REF deltas within this pack For more info, see ``collect_streams`` - + :param offset: offset into the pack file at which the object can be found""" streams = self._pack.collect_streams(offset) - + # try to resolve the last one if needed. It is assumed to be either - # a REF delta, or a base object, as OFFSET deltas are resolved by the pack + # a REF delta, or a base object, as OFFSET deltas are resolved by the + # pack if streams[-1].type_id == REF_DELTA: stream = streams[-1] while stream.type_id in delta_types: @@ -866,147 +905,156 @@ def collect_streams_at_offset(self, offset): stream = self._pack.stream(self._index.offset(sindex)) streams.append(stream) else: - # must be another OFS DELTA - this could happen if a REF - # delta we resolve previously points to an OFS delta. Who + # must be another OFS DELTA - this could happen if a REF + # delta we resolve previously points to an OFS delta. Who # would do that ;) ? We can handle it though stream = self._pack.stream(stream.delta_info) streams.append(stream) # END handle ref delta # END resolve ref streams # END resolve streams - + return streams - + def collect_streams(self, sha): """ As ``PackFile.collect_streams``, but takes a sha instead of an offset. Additionally, ref_delta streams will be resolved within this pack. If this is not possible, the stream will be left alone, hence it is adivsed - to check for unresolved ref-deltas and resolve them before attempting to + to check for unresolved ref-deltas and resolve them before attempting to construct a delta stream. - + :param sha: 20 byte sha1 specifying the object whose related streams you want to collect - :return: list of streams, first being the actual object delta, the last being + :return: list of streams, first being the actual object delta, the last being a possibly unresolved base object. :raise BadObject:""" - return self.collect_streams_at_offset(self._index.offset(self._sha_to_index(sha))) - - + return self.collect_streams_at_offset( + self._index.offset(self._sha_to_index(sha))) + @classmethod - def write_pack(cls, object_iter, pack_write, index_write=None, - object_count = None, zlib_compression = zlib.Z_BEST_SPEED): + def write_pack(cls, object_iter, pack_write, index_write=None, + object_count=None, zlib_compression=zlib.Z_BEST_SPEED): """ Create a new pack by putting all objects obtained by the object_iterator into a pack which is written using the pack_write method. The respective index is produced as well if index_write is not Non. - + :param object_iter: iterator yielding odb output objects :param pack_write: function to receive strings to write into the pack stream :param indx_write: if not None, the function writes the index file corresponding to the pack. - :param object_count: if you can provide the amount of objects in your iteration, - this would be the place to put it. Otherwise we have to pre-iterate and store + :param object_count: if you can provide the amount of objects in your iteration, + this would be the place to put it. Otherwise we have to pre-iterate and store all items into a list to get the number, which uses more memory than necessary. :param zlib_compression: the zlib compression level to use :return: tuple(pack_sha, index_binsha) binary sha over all the contents of the pack and over all contents of the index. If index_write was None, index_binsha will be None - + **Note:** The destination of the write functions is up to the user. It could be a socket, or a file for instance - + **Note:** writes only undeltified objects""" objs = object_iter if not object_count: if not isinstance(object_iter, (tuple, list)): objs = list(object_iter) - #END handle list type + # END handle list type object_count = len(objs) - #END handle object - + # END handle object + pack_writer = FlexibleSha1Writer(pack_write) pwrite = pack_writer.write - ofs = 0 # current offset into the pack file + # current offset into the pack file + ofs = 0 index = None wants_index = index_write is not None - + # write header - pwrite(pack('>LLL', PackFile.pack_signature, PackFile.pack_version_default, object_count)) + pwrite(pack('>LLL', PackFile.pack_signature, + PackFile.pack_version_default, object_count)) ofs += 12 - + if wants_index: index = IndexWriter() - #END handle index header - + # END handle index header + actual_count = 0 for obj in objs: actual_count += 1 crc = 0 - + # object header hdr = create_pack_object_header(obj.type_id, obj.size) if index_write: crc = crc32(hdr) else: crc = None - #END handle crc + # END handle crc pwrite(hdr) - + # data stream zstream = zlib.compressobj(zlib_compression) ostream = obj.stream - br, bw, crc = write_stream_to_pack(ostream.read, pwrite, zstream, base_crc = crc) + br, bw, crc = write_stream_to_pack( + ostream.read, pwrite, zstream, base_crc=crc) assert(br == obj.size) if wants_index: index.append(obj.binsha, crc, ofs) - #END handle index - + # END handle index + ofs += len(hdr) + bw if actual_count == object_count: break - #END abort once we are done - #END for each object - + # END abort once we are done + # END for each object + if actual_count != object_count: - raise ValueError("Expected to write %i objects into pack, but received only %i from iterators" % (object_count, actual_count)) - #END count assertion - + raise ValueError( + "Expected to write %i objects into pack, but received only %i from iterators" % + (object_count, actual_count)) + # END count assertion + # write footer - pack_sha = pack_writer.sha(as_hex = False) + pack_sha = pack_writer.sha(as_hex=False) assert len(pack_sha) == 20 pack_write(pack_sha) - ofs += len(pack_sha) # just for completeness ;) - + # just for completeness ;) + ofs += len(pack_sha) + index_sha = None if wants_index: index_sha = index.write(pack_sha, index_write) - #END handle index - + # END handle index + return pack_sha, index_sha - + @classmethod - def create(cls, object_iter, base_dir, object_count = None, zlib_compression = zlib.Z_BEST_SPEED): + def create(cls, object_iter, base_dir, object_count=None, + zlib_compression=zlib.Z_BEST_SPEED): """Create a new on-disk entity comprised of a properly named pack file and a properly named and corresponding index file. The pack contains all OStream objects contained in object iter. :param base_dir: directory which is to contain the files :return: PackEntity instance initialized with the new pack - + **Note:** for more information on the other parameters see the write_pack method""" pack_fd, pack_path = tempfile.mkstemp('', 'pack', base_dir) index_fd, index_path = tempfile.mkstemp('', 'index', base_dir) pack_write = lambda d: os.write(pack_fd, d) index_write = lambda d: os.write(index_fd, d) - - pack_binsha, index_binsha = cls.write_pack(object_iter, pack_write, index_write, object_count, zlib_compression) + + pack_binsha, index_binsha = cls.write_pack( + object_iter, pack_write, index_write, object_count, zlib_compression) os.close(pack_fd) os.close(index_fd) - + fmt = "pack-%s.%s" - new_pack_path = os.path.join(base_dir, fmt % (bin_to_hex(pack_binsha), 'pack')) - new_index_path = os.path.join(base_dir, fmt % (bin_to_hex(pack_binsha), 'idx')) + new_pack_path = os.path.join( + base_dir, fmt % (bin_to_hex(pack_binsha), 'pack')) + new_index_path = os.path.join( + base_dir, fmt % (bin_to_hex(pack_binsha), 'idx')) os.rename(pack_path, new_pack_path) os.rename(index_path, new_index_path) - + return cls(new_pack_path) - - + #} END interface diff --git a/gitdb/stream.py b/gitdb/stream.py index 6441b1e..c11a7f7 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -8,23 +8,23 @@ import mmap import os -from fun import ( - msb_size, - stream_copy, - apply_delta_data, - connect_deltas, - DeltaChunkList, - delta_types - ) - -from util import ( - allocate_memory, - LazyMixin, - make_sha, - write, - close, - zlib - ) +from .fun import ( + msb_size, + stream_copy, + apply_delta_data, + connect_deltas, + DeltaChunkList, + delta_types +) + +from .util import ( + allocate_memory, + LazyMixin, + make_sha, + write, + close, + zlib +) has_perf_mod = False try: @@ -33,35 +33,53 @@ except ImportError: pass -__all__ = ( 'DecompressMemMapReader', 'FDCompressedSha1Writer', 'DeltaApplyReader', - 'Sha1Writer', 'FlexibleSha1Writer', 'ZippedStoreShaWriter', 'FDCompressedSha1Writer', - 'FDStream', 'NullStream') +__all__ = ( + 'DecompressMemMapReader', + 'FDCompressedSha1Writer', + 'DeltaApplyReader', + 'Sha1Writer', + 'FlexibleSha1Writer', + 'ZippedStoreShaWriter', + 'FDCompressedSha1Writer', + 'FDStream', + 'NullStream') #{ RO Streams class DecompressMemMapReader(LazyMixin): - """Reads data in chunks from a memory map and decompresses it. The client sees + + """Reads data in chunks from a memory map and decompresses it. The client sees only the uncompressed data, respective file-like read calls are handling on-demand buffered decompression accordingly - - A constraint on the total size of bytes is activated, simulating + + A constraint on the total size of bytes is activated, simulating a logical file within a possibly larger physical memory area - - To read efficiently, you clearly don't want to read individual bytes, instead, + + To read efficiently, you clearly don't want to read individual bytes, instead, read a few kilobytes at least. - - **Note:** The chunk-size should be carefully selected as it will involve quite a bit - of string copying due to the way the zlib is implemented. Its very wasteful, - hence we try to find a good tradeoff between allocation time and number of + + **Note:** The chunk-size should be carefully selected as it will involve quite a bit + of string copying due to the way the zlib is implemented. Its very wasteful, + hence we try to find a good tradeoff between allocation time and number of times we actually allocate. An own zlib implementation would be good here to better support streamed reading - it would only need to keep the mmap and decompress it into chunks, thats all ... """ - __slots__ = ('_m', '_zip', '_buf', '_buflen', '_br', '_cws', '_cwe', '_s', '_close', - '_cbr', '_phi') - - max_read_size = 512*1024 # currently unused - + __slots__ = ( + '_m', + '_zip', + '_buf', + '_buflen', + '_br', + '_cws', + '_cwe', + '_s', + '_close', + '_cbr', + '_phi') + + max_read_size = 512 * 1024 # currently unused + def __init__(self, m, close_on_deletion, size=None): """Initialize with mmap for stream reading :param m: must be content data - use new if you have object data and no size""" @@ -70,60 +88,67 @@ def __init__(self, m, close_on_deletion, size=None): self._buf = None # buffer of decompressed bytes self._buflen = 0 # length of bytes in buffer if size is not None: - self._s = size # size of uncompressed data to read in total + # size of uncompressed data to read in total + self._s = size self._br = 0 # num uncompressed bytes read - self._cws = 0 # start byte of compression window - self._cwe = 0 # end byte of compression window - self._cbr = 0 # number of compressed bytes read - self._phi = False # is True if we parsed the header info - self._close = close_on_deletion # close the memmap on deletion ? - + # start byte of compression window + self._cws = 0 + # end byte of compression window + self._cwe = 0 + # number of compressed bytes read + self._cbr = 0 + # is True if we parsed the header info + self._phi = False + # close the memmap on deletion ? + self._close = close_on_deletion + def _set_cache_(self, attr): assert attr == '_s' - # only happens for size, which is a marker to indicate we still + # only happens for size, which is a marker to indicate we still # have to parse the header from the stream self._parse_header_info() - + def __del__(self): if self._close: self._m.close() # END handle resource freeing - + def _parse_header_info(self): - """If this stream contains object data, parse the header info and skip the + """If this stream contains object data, parse the header info and skip the stream to a point where each read will yield object content - + :return: parsed type_string, size""" # read header - maxb = 512 # should really be enough, cgit uses 8192 I believe + # should really be enough, cgit uses 8192 I believe + maxb = 512 self._s = maxb hdr = self.read(maxb) hdrend = hdr.find("\0") type, size = hdr[:hdrend].split(" ") size = int(size) self._s = size - + # adjust internal state to match actual header length that we ignore # The buffer will be depleted first on future reads self._br = 0 hdrend += 1 # count terminating \0 self._buf = StringIO(hdr[hdrend:]) self._buflen = len(hdr) - hdrend - + self._phi = True - + return type, size - - #{ Interface - + + #{ Interface + @classmethod def new(self, m, close_on_deletion=False): """Create a new DecompressMemMapReader instance for acting as a read-only stream - This method parses the object header from m and returns the parsed + This method parses the object header from m and returns the parsed type and size, as well as the created stream instance. - + :param m: memory map on which to oparate. It must be object data ( header + contents ) - :param close_on_deletion: if True, the memory map will be closed once we are + :param close_on_deletion: if True, the memory map will be closed once we are being deleted""" inst = DecompressMemMapReader(m, close_on_deletion, 0) type, size = inst._parse_header_info() @@ -131,30 +156,30 @@ def new(self, m, close_on_deletion=False): def data(self): """:return: random access compatible data we are working on""" - return self._m - + return self._m + def compressed_bytes_read(self): """ - :return: number of compressed bytes read. This includes the bytes it + :return: number of compressed bytes read. This includes the bytes it took to decompress the header ( if there was one )""" # ABSTRACT: When decompressing a byte stream, it can be that the first - # x bytes which were requested match the first x bytes in the loosely + # x bytes which were requested match the first x bytes in the loosely # compressed datastream. This is the worst-case assumption that the reader # does, it assumes that it will get at least X bytes from X compressed bytes # in call cases. - # The caveat is that the object, according to our known uncompressed size, + # The caveat is that the object, according to our known uncompressed size, # is already complete, but there are still some bytes left in the compressed # stream that contribute to the amount of compressed bytes. # How can we know that we are truly done, and have read all bytes we need - # to read ? - # Without help, we cannot know, as we need to obtain the status of the + # to read ? + # Without help, we cannot know, as we need to obtain the status of the # decompression. If it is not finished, we need to decompress more data # until it is finished, to yield the actual number of compressed bytes # belonging to the decompressed object - # We are using a custom zlib module for this, if its not present, + # We are using a custom zlib module for this, if its not present, # we try to put in additional bytes up for decompression if feasible # and check for the unused_data. - + # Only scrub the stream forward if we are officially done with the # bytes we were to have. if self._br == self._s and not self._zip.unused_data: @@ -171,45 +196,44 @@ def compressed_bytes_read(self): self.read(mmap.PAGESIZE) # END scrub-loop default zlib # END handle stream scrubbing - + # reset bytes read, just to be sure self._br = self._s # END handle stream scrubbing - + # unused data ends up in the unconsumed tail, which was removed # from the count already return self._cbr - - #} END interface - + + #} END interface + def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): """Allows to reset the stream to restart reading :raise ValueError: If offset and whence are not 0""" if offset != 0 or whence != getattr(os, 'SEEK_SET', 0): raise ValueError("Can only seek to position 0") # END handle offset - + self._zip = zlib.decompressobj() self._br = self._cws = self._cwe = self._cbr = 0 if self._phi: self._phi = False del(self._s) # trigger header parsing on first access # END skip header - + def read(self, size=-1): if size < 1: size = self._s - self._br else: size = min(size, self._s - self._br) # END clamp size - + if size == 0: return str() # END handle depletion - - - # deplete the buffer, then just continue using the decompress object - # which has an own buffer. We just need this to transparently parse the + + # deplete the buffer, then just continue using the decompress object + # which has an own buffer. We just need this to transparently parse the # header from the zlib stream dat = str() if self._buf: @@ -223,26 +247,26 @@ def read(self, size=-1): dat = self._buf.read() # ouch, duplicates data size -= self._buflen self._br += self._buflen - + self._buflen = 0 self._buf = None # END handle buffer len # END handle buffer - + # decompress some data - # Abstract: zlib needs to operate on chunks of our memory map ( which may + # Abstract: zlib needs to operate on chunks of our memory map ( which may # be large ), as it will otherwise and always fill in the 'unconsumed_tail' - # attribute which possible reads our whole map to the end, forcing + # attribute which possible reads our whole map to the end, forcing # everything to be read from disk even though just a portion was requested. - # As this would be a nogo, we workaround it by passing only chunks of data, - # moving the window into the memory map along as we decompress, which keeps + # As this would be a nogo, we workaround it by passing only chunks of data, + # moving the window into the memory map along as we decompress, which keeps # the tail smaller than our chunk-size. This causes 'only' the chunk to be # copied once, and another copy of a part of it when it creates the unconsumed # tail. We have to use it to hand in the appropriate amount of bytes durin g # the next read. tail = self._zip.unconsumed_tail if tail: - # move the window, make it as large as size demands. For code-clarity, + # move the window, make it as large as size demands. For code-clarity, # we just take the chunk from our map again instead of reusing the unconsumed # tail. The latter one would safe some memory copying, but we could end up # with not getting enough data uncompressed, so we had to sort that out as well. @@ -253,18 +277,18 @@ def read(self, size=-1): else: cws = self._cws self._cws = self._cwe - self._cwe = cws + size + self._cwe = cws + size # END handle tail - - - # if window is too small, make it larger so zip can decompress something + + # if window is too small, make it larger so zip can decompress + # something if self._cwe - self._cws < 8: self._cwe = self._cws + 8 # END adjust winsize - - # takes a slice, but doesn't copy the data, it says ... + + # takes a slice, but doesn't copy the data, it says ... indata = buffer(self._m, self._cws, self._cwe - self._cws) - + # get the actual window end to be sure we don't use it for computations self._cwe = self._cws + len(indata) dcompdat = self._zip.decompress(indata, size) @@ -274,85 +298,91 @@ def read(self, size=-1): # if we hit the end of the stream self._cbr += len(indata) - len(self._zip.unconsumed_tail) self._br += len(dcompdat) - + if dat: dcompdat = dat + dcompdat # END prepend our cached data - - # it can happen, depending on the compression, that we get less bytes - # than ordered as it needs the final portion of the data as well. + + # it can happen, depending on the compression, that we get less bytes + # than ordered as it needs the final portion of the data as well. # Recursively resolve that. # Note: dcompdat can be empty even though we still appear to have bytes # to read, if we are called by compressed_bytes_read - it manipulates # us to empty the stream - if dcompdat and (len(dcompdat) - len(dat)) < size and self._br < self._s: - dcompdat += self.read(size-len(dcompdat)) + if dcompdat and ( + len(dcompdat) - len(dat)) < size and self._br < self._s: + dcompdat += self.read(size - len(dcompdat)) # END handle special case return dcompdat - + class DeltaApplyReader(LazyMixin): - """A reader which dynamically applies pack deltas to a base object, keeping the + + """A reader which dynamically applies pack deltas to a base object, keeping the memory demands to a minimum. - - The size of the final object is only obtainable once all deltas have been + + The size of the final object is only obtainable once all deltas have been applied, unless it is retrieved from a pack index. - + The uncompressed Delta has the following layout (MSB being a most significant bit encoded dynamic size): - + * MSB Source Size - the size of the base against which the delta was created * MSB Target Size - the size of the resulting data after the delta was applied * A list of one byte commands (cmd) which are followed by a specific protocol: - + * cmd & 0x80 - copy delta_data[offset:offset+size] - + * Followed by an encoded offset into the delta data * Followed by an encoded size of the chunk to copy - + * cmd & 0x7f - insert - + * insert cmd bytes from the delta buffer into the output stream - + * cmd == 0 - invalid operation ( or error in delta stream ) """ __slots__ = ( - "_bstream", # base stream to which to apply the deltas - "_dstreams", # tuple of delta stream readers - "_mm_target", # memory map of the delta-applied data - "_size", # actual number of bytes in _mm_target - "_br" # number of bytes read - ) - + "_bstream", # base stream to which to apply the deltas + "_dstreams", # tuple of delta stream readers + # memory map of the delta-applied data + "_mm_target", + # actual number of bytes in _mm_target + "_size", + "_br" # number of bytes read + ) + #{ Configuration - k_max_memory_move = 250*1000*1000 + k_max_memory_move = 250 * 1000 * 1000 #} END configuration - + def __init__(self, stream_list): - """Initialize this instance with a list of streams, the first stream being + """Initialize this instance with a list of streams, the first stream being the delta to apply on top of all following deltas, the last stream being the base object onto which to apply the deltas""" - assert len(stream_list) > 1, "Need at least one delta and one base stream" - + assert len( + stream_list) > 1, "Need at least one delta and one base stream" + self._bstream = stream_list[-1] self._dstreams = tuple(stream_list[:-1]) self._br = 0 - + def _set_cache_too_slow_without_c(self, attr): - # the direct algorithm is fastest and most direct if there is only one + # the direct algorithm is fastest and most direct if there is only one # delta. Also, the extra overhead might not be worth it for items smaller - # than X - definitely the case in python, every function call costs + # than X - definitely the case in python, every function call costs # huge amounts of time # if len(self._dstreams) * self._bstream.size < self.k_max_memory_move: if len(self._dstreams) == 1: return self._set_cache_brute_(attr) - - # Aggregate all deltas into one delta in reverse order. Hence we take + + # Aggregate all deltas into one delta in reverse order. Hence we take # the last delta, and reverse-merge its ancestor delta, until we receive # the final delta data stream. - # print "Handling %i delta streams, sizes: %s" % (len(self._dstreams), [ds.size for ds in self._dstreams]) + # print "Handling %i delta streams, sizes: %s" % (len(self._dstreams), + # [ds.size for ds in self._dstreams]) dcl = connect_deltas(self._dstreams) - + # call len directly, as the (optional) c version doesn't implement the sequence # protocol if dcl.rbound() == 0: @@ -360,22 +390,27 @@ def _set_cache_too_slow_without_c(self, attr): self._mm_target = allocate_memory(0) return # END handle empty list - + self._size = dcl.rbound() self._mm_target = allocate_memory(self._size) - + bbuf = allocate_memory(self._bstream.size) - stream_copy(self._bstream.read, bbuf.write, self._bstream.size, 256 * mmap.PAGESIZE) - + stream_copy( + self._bstream.read, + bbuf.write, + self._bstream.size, + 256 * + mmap.PAGESIZE) + # APPLY CHUNKS write = self._mm_target.write dcl.apply(bbuf, write) - + self._mm_target.seek(0) - + def _set_cache_brute_(self, attr): """If we are here, we apply the actual deltas""" - + # TODO: There should be a special case if there is only one stream # Then the default-git algorithm should perform a tad faster, as the # delta is not peaked into, causing less overhead. @@ -385,79 +420,83 @@ def _set_cache_brute_(self, attr): buf = dstream.read(512) # read the header information + X offset, src_size = msb_size(buf) offset, target_size = msb_size(buf, offset) - buffer_info_list.append((buffer(buf, offset), offset, src_size, target_size)) + buffer_info_list.append( + (buffer(buf, offset), offset, src_size, target_size)) max_target_size = max(max_target_size, target_size) # END for each delta stream - + # sanity check - the first delta to apply should have the same source # size as our actual base stream base_size = self._bstream.size target_size = max_target_size - + # if we have more than 1 delta to apply, we will swap buffers, hence we must - # assure that all buffers we use are large enough to hold all the results + # assure that all buffers we use are large enough to hold all the + # results if len(self._dstreams) > 1: base_size = target_size = max(base_size, max_target_size) # END adjust buffer sizes - - + # Allocate private memory map big enough to hold the first base buffer # We need random access to it bbuf = allocate_memory(base_size) - stream_copy(self._bstream.read, bbuf.write, base_size, 256 * mmap.PAGESIZE) - + stream_copy( + self._bstream.read, bbuf.write, base_size, 256 * mmap.PAGESIZE) + # allocate memory map large enough for the largest (intermediate) target - # We will use it as scratch space for all delta ops. If the final + # We will use it as scratch space for all delta ops. If the final # target buffer is smaller than our allocated space, we just use parts # of it upon return. tbuf = allocate_memory(target_size) - - # for each delta to apply, memory map the decompressed delta and + + # for each delta to apply, memory map the decompressed delta and # work on the op-codes to reconstruct everything. # For the actual copying, we use a seek and write pattern of buffer # slices. final_target_size = None - for (dbuf, offset, src_size, target_size), dstream in reversed(zip(buffer_info_list, self._dstreams)): - # allocate a buffer to hold all delta data - fill in the data for + for (dbuf, offset, src_size, target_size), dstream in reversed( + zip(buffer_info_list, self._dstreams)): + # allocate a buffer to hold all delta data - fill in the data for # fast access. We do this as we know that reading individual bytes # from our stream would be slower than necessary ( although possible ) # The dbuf buffer contains commands after the first two MSB sizes, the # offset specifies the amount of bytes read to get the sizes. ddata = allocate_memory(dstream.size - offset) ddata.write(dbuf) - # read the rest from the stream. The size we give is larger than necessary - stream_copy(dstream.read, ddata.write, dstream.size, 256*mmap.PAGESIZE) - - ####################################################################### + # read the rest from the stream. The size we give is larger than + # necessary + stream_copy( + dstream.read, ddata.write, dstream.size, 256 * mmap.PAGESIZE) + + ################################################################### if 'c_apply_delta' in globals(): - c_apply_delta(bbuf, ddata, tbuf); + c_apply_delta(bbuf, ddata, tbuf) else: apply_delta_data(bbuf, src_size, ddata, len(ddata), tbuf.write) - ####################################################################### - - # finally, swap out source and target buffers. The target is now the + ################################################################### + + # finally, swap out source and target buffers. The target is now the # base for the next delta to apply bbuf, tbuf = tbuf, bbuf bbuf.seek(0) tbuf.seek(0) final_target_size = target_size # END for each delta to apply - + # its already seeked to 0, constrain it to the actual size # NOTE: in the end of the loop, it swaps buffers, hence our target buffer # is not tbuf, but bbuf ! self._mm_target = bbuf self._size = final_target_size - - + #{ Configuration if not has_perf_mod: _set_cache_ = _set_cache_brute_ else: _set_cache_ = _set_cache_too_slow_without_c - + #} END configuration - + def read(self, count=0): bl = self._size - self._br # bytes left if count < 1 or count > bl: @@ -467,73 +506,76 @@ def read(self, count=0): data = self._mm_target.read(count) self._br += len(data) return data - + def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): """Allows to reset the stream to restart reading - + :raise ValueError: If offset and whence are not 0""" if offset != 0 or whence != getattr(os, 'SEEK_SET', 0): raise ValueError("Can only seek to position 0") # END handle offset self._br = 0 self._mm_target.seek(0) - - #{ Interface - + + #{ Interface + @classmethod def new(cls, stream_list): """ Convert the given list of streams into a stream which resolves deltas when reading from it. - + :param stream_list: two or more stream objects, first stream is a Delta to the object that you want to resolve, followed by N additional delta streams. The list's last stream must be a non-delta stream. - - :return: Non-Delta OPackStream object whose stream can be used to obtain + + :return: Non-Delta OPackStream object whose stream can be used to obtain the decompressed resolved data :raise ValueError: if the stream list cannot be handled""" if len(stream_list) < 2: raise ValueError("Need at least two streams") # END single object special handling - + if stream_list[-1].type_id in delta_types: - raise ValueError("Cannot resolve deltas if there is no base object stream, last one was type: %s" % stream_list[-1].type) + raise ValueError( + "Cannot resolve deltas if there is no base object stream, last one was type: %s" % + stream_list[ + -1].type) # END check stream - + return cls(stream_list) - + #} END interface - - + #{ OInfo like Interface - + @property def type(self): return self._bstream.type - + @property def type_id(self): return self._bstream.type_id - + @property def size(self): """:return: number of uncompressed bytes in the stream""" return self._size - - #} END oinfo like interface - - + + #} END oinfo like interface + + #} END RO streams #{ W Streams class Sha1Writer(object): + """Simple stream writer which produces a sha whenever you like as it degests everything it is supposed to write""" __slots__ = "sha1" - + def __init__(self): self.sha1 = make_sha() @@ -545,53 +587,56 @@ def write(self, data): self.sha1.update(data) return len(data) - # END stream interface + # END stream interface #{ Interface - - def sha(self, as_hex = False): + + def sha(self, as_hex=False): """:return: sha so far :param as_hex: if True, sha will be hex-encoded, binary otherwise""" if as_hex: return self.sha1.hexdigest() return self.sha1.digest() - - #} END interface + + #} END interface class FlexibleSha1Writer(Sha1Writer): - """Writer producing a sha1 while passing on the written bytes to the given + + """Writer producing a sha1 while passing on the written bytes to the given write function""" __slots__ = 'writer' - + def __init__(self, writer): Sha1Writer.__init__(self) self.writer = writer - + def write(self, data): Sha1Writer.write(self, data) self.writer(data) class ZippedStoreShaWriter(Sha1Writer): + """Remembers everything someone writes to it and generates a sha""" __slots__ = ('buf', 'zip') + def __init__(self): Sha1Writer.__init__(self) self.buf = StringIO() self.zip = zlib.compressobj(zlib.Z_BEST_SPEED) - + def __getattr__(self, attr): return getattr(self.buf, attr) - + def write(self, data): alen = Sha1Writer.write(self, data) self.buf.write(self.zip.compress(data)) return alen - + def close(self): self.buf.write(self.zip.flush()) - + def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): """Seeking currently only supports to rewind written data Multiple writes are not supported""" @@ -599,23 +644,24 @@ def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): raise ValueError("Can only seek to position 0") # END handle offset self.buf.seek(0) - + def getvalue(self): """:return: string value from the current stream position to the end""" return self.buf.getvalue() class FDCompressedSha1Writer(Sha1Writer): - """Digests data written to it, making the sha available, then compress the + + """Digests data written to it, making the sha available, then compress the data and write it to the file descriptor - + **Note:** operates on raw file descriptors **Note:** for this to work, you have to use the close-method of this instance""" __slots__ = ("fd", "sha1", "zip") - + # default exception exc = IOError("Failed to write all bytes to filedescriptor") - + def __init__(self, fd): super(FDCompressedSha1Writer, self).__init__() self.fd = fd @@ -643,52 +689,53 @@ def close(self): class FDStream(object): - """A simple wrapper providing the most basic functions on a file descriptor + + """A simple wrapper providing the most basic functions on a file descriptor with the fileobject interface. Cannot use os.fdopen as the resulting stream takes ownership""" __slots__ = ("_fd", '_pos') + def __init__(self, fd): self._fd = fd self._pos = 0 - + def write(self, data): self._pos += len(data) os.write(self._fd, data) - + def read(self, count=0): if count == 0: count = os.path.getsize(self._filepath) # END handle read everything - + bytes = os.read(self._fd, count) self._pos += len(bytes) return bytes - + def fileno(self): return self._fd - + def tell(self): return self._pos - + def close(self): close(self._fd) class NullStream(object): + """A stream that does nothing but providing a stream interface. Use it like /dev/null""" __slots__ = tuple() - + def read(self, size=0): return '' - + def close(self): pass - + def write(self, data): return len(data) #} END W streams - - diff --git a/gitdb/test/__init__.py b/gitdb/test/__init__.py index f805944..dcf35cc 100644 --- a/gitdb/test/__init__.py +++ b/gitdb/test/__init__.py @@ -5,7 +5,9 @@ import gitdb.util -#{ Initialization +#{ Initialization + + def _init_pool(): """Assure the pool is actually threaded""" size = 2 diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index 62614ee..6e7d04c 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -9,16 +9,16 @@ ZippedStoreShaWriter, fixture_path, TestBase - ) +) from gitdb.stream import Sha1Writer from gitdb.base import ( - IStream, - OStream, - OInfo - ) - + IStream, + OStream, + OInfo +) + from gitdb.exc import BadObject from gitdb.typ import str_blob_type @@ -28,14 +28,16 @@ __all__ = ('TestDBBase', 'with_rw_directory', 'with_packs_rw', 'fixture_path') - + + class TestDBBase(TestBase): + """Base class providing testing routines on databases""" - + # data two_lines = "1234\nhello world" all_data = (two_lines, ) - + def _assert_object_writing_simple(self, db): # write a bunch of objects and query their streams and info null_objs = db.size() @@ -46,23 +48,22 @@ def _assert_object_writing_simple(self, db): new_istream = db.store(istream) assert new_istream is istream assert db.has_object(istream.binsha) - + info = db.info(istream.binsha) assert isinstance(info, OInfo) assert info.type == istream.type and info.size == istream.size - + stream = db.stream(istream.binsha) assert isinstance(stream, OStream) assert stream.binsha == info.binsha and stream.type == info.type assert stream.read() == data # END for each item - + assert db.size() == null_objs + ni shas = list(db.sha_iter()) assert len(shas) == db.size() assert len(shas[0]) == 20 - - + def _assert_object_writing(self, db): """General tests to verify object writing, compatible to ObjectDBW **Note:** requires write access to the database""" @@ -76,25 +77,26 @@ def _assert_object_writing(self, db): ostream = ostreamcls() assert isinstance(ostream, Sha1Writer) # END create ostream - + prev_ostream = db.set_ostream(ostream) - assert type(prev_ostream) in ostreams or prev_ostream in ostreams - + assert type( + prev_ostream) in ostreams or prev_ostream in ostreams + istream = IStream(str_blob_type, len(data), StringIO(data)) - + # store returns same istream instance, with new sha set my_istream = db.store(istream) sha = istream.binsha assert my_istream is istream assert db.has_object(sha) != dry_run - assert len(sha) == 20 - + assert len(sha) == 20 + # verify data - the slow way, we want to run code if not dry_run: info = db.info(sha) assert str_blob_type == info.type assert info.size == len(data) - + ostream = db.stream(sha) assert ostream.read() == data assert ostream.type == str_blob_type @@ -102,57 +104,58 @@ def _assert_object_writing(self, db): else: self.failUnlessRaises(BadObject, db.info, sha) self.failUnlessRaises(BadObject, db.stream, sha) - + # DIRECT STREAM COPY # our data hase been written in object format to the StringIO # we pasesd as output stream. No physical database representation # was created. - # Test direct stream copy of object streams, the result must be + # Test direct stream copy of object streams, the result must be # identical to what we fed in ostream.seek(0) istream.stream = ostream assert istream.binsha is not None prev_sha = istream.binsha - + db.set_ostream(ZippedStoreShaWriter()) db.store(istream) assert istream.binsha == prev_sha new_ostream = db.ostream() - + # note: only works as long our store write uses the same compression # level, which is zip_best assert ostream.getvalue() == new_ostream.getvalue() # END for each data set # END for each dry_run mode - + def _assert_object_writing_async(self, db): """Test generic object writing using asynchronous access""" ni = 5000 + def istream_generator(offset=0, ni=ni): for data_src in xrange(ni): data = str(data_src + offset) yield IStream(str_blob_type, len(data), StringIO(data)) # END for each item # END generator utility - + # for now, we are very trusty here as we expect it to work if it worked # in the single-stream case - + # write objects reader = IteratorReader(istream_generator()) istream_reader = db.store_async(reader) istreams = istream_reader.read() # read all assert istream_reader.task().error() is None assert len(istreams) == ni - + for stream in istreams: assert stream.error is None assert len(stream.binsha) == 20 assert isinstance(stream, IStream) # END assert each stream - + # test has-object-async - we must have all previously added ones - reader = IteratorReader( istream.binsha for istream in istreams ) + reader = IteratorReader(istream.binsha for istream in istreams) hasobject_reader = db.has_object_async(reader) count = 0 for sha, has_object in hasobject_reader: @@ -160,11 +163,11 @@ def istream_generator(offset=0, ni=ni): count += 1 # END for each sha assert count == ni - + # read the objects we have just written - reader = IteratorReader( istream.binsha for istream in istreams ) + reader = IteratorReader(istream.binsha for istream in istreams) ostream_reader = db.stream_async(reader) - + # read items individually to prevent hitting possible sys-limits count = 0 for ostream in ostream_reader: @@ -173,30 +176,30 @@ def istream_generator(offset=0, ni=ni): # END for each ostream assert ostream_reader.task().error() is None assert count == ni - + # get info about our items - reader = IteratorReader( istream.binsha for istream in istreams ) + reader = IteratorReader(istream.binsha for istream in istreams) info_reader = db.info_async(reader) - + count = 0 for oinfo in info_reader: assert isinstance(oinfo, OInfo) count += 1 # END for each oinfo instance assert count == ni - - + # combined read-write using a converter # add 2500 items, and obtain their output streams nni = 2500 reader = IteratorReader(istream_generator(offset=ni, ni=nni)) - istream_to_sha = lambda istreams: [ istream.binsha for istream in istreams ] - + istream_to_sha = lambda istreams: [ + istream.binsha for istream in istreams] + istream_reader = db.store_async(reader) istream_reader.set_post_cb(istream_to_sha) - + ostream_reader = db.stream_async(istream_reader) - + count = 0 # read it individually, otherwise we might run into the ulimit for ostream in ostream_reader: @@ -204,5 +207,3 @@ def istream_generator(offset=0, ni=ni): count += 1 # END for each ostream assert count == nni - - diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index 1ef577a..9046770 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -2,20 +2,21 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from lib import * +from .lib import * from gitdb.exc import BadObject from gitdb.db import GitDB from gitdb.base import OStream, OInfo from gitdb.util import hex_to_bin, bin_to_hex - + + class TestGitDB(TestDBBase): - + def test_reading(self): gdb = GitDB(fixture_path('../../../.git/objects')) - + # we have packs and loose objects, alternates doesn't necessarily exist assert 1 < len(gdb.databases()) < 4 - + # access should be possible gitdb_sha = hex_to_bin("5690fd0d3304f378754b23b098bd7cb5f4aa1976") assert isinstance(gdb.info(gitdb_sha), OInfo) @@ -23,25 +24,27 @@ def test_reading(self): assert gdb.size() > 200 sha_list = list(gdb.sha_iter()) assert len(sha_list) == gdb.size() - - - # This is actually a test for compound functionality, but it doesn't + + # This is actually a test for compound functionality, but it doesn't # have a separate test module # test partial shas # this one as uneven and quite short - assert gdb.partial_to_complete_sha_hex('155b6') == hex_to_bin("155b62a9af0aa7677078331e111d0f7aa6eb4afc") - + assert gdb.partial_to_complete_sha_hex('155b6') == hex_to_bin( + "155b62a9af0aa7677078331e111d0f7aa6eb4afc") + # mix even/uneven hexshas for i, binsha in enumerate(sha_list): - assert gdb.partial_to_complete_sha_hex(bin_to_hex(binsha)[:8-(i%2)]) == binsha + assert gdb.partial_to_complete_sha_hex( + bin_to_hex(binsha)[:8 - (i % 2)]) == binsha # END for each sha - - self.failUnlessRaises(BadObject, gdb.partial_to_complete_sha_hex, "0000") - + + self.failUnlessRaises( + BadObject, gdb.partial_to_complete_sha_hex, "0000") + @with_rw_directory def test_writing(self, path): gdb = GitDB(path) - + # its possible to write objects self._assert_object_writing(gdb) self._assert_object_writing_async(gdb) diff --git a/gitdb/test/db/test_loose.py b/gitdb/test/db/test_loose.py index d7e1d01..42f70de 100644 --- a/gitdb/test/db/test_loose.py +++ b/gitdb/test/db/test_loose.py @@ -2,33 +2,35 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from lib import * +from .lib import * from gitdb.db import LooseObjectDB from gitdb.exc import BadObject from gitdb.util import bin_to_hex - + + class TestLooseDB(TestDBBase): - + @with_rw_directory def test_basics(self, path): ldb = LooseObjectDB(path) - + # write data self._assert_object_writing(ldb) self._assert_object_writing_async(ldb) - + # verify sha iteration and size shas = list(ldb.sha_iter()) assert shas and len(shas[0]) == 20 - + assert len(shas) == ldb.size() - + # verify find short object long_sha = bin_to_hex(shas[-1]) for short_sha in (long_sha[:20], long_sha[:5]): - assert bin_to_hex(ldb.partial_to_complete_sha_hex(short_sha)) == long_sha + assert bin_to_hex( + ldb.partial_to_complete_sha_hex(short_sha)) == long_sha # END for each sha - - self.failUnlessRaises(BadObject, ldb.partial_to_complete_sha_hex, '0000') + + self.failUnlessRaises( + BadObject, ldb.partial_to_complete_sha_hex, '0000') # raises if no object could be foudn - diff --git a/gitdb/test/db/test_mem.py b/gitdb/test/db/test_mem.py index df428e2..0140e99 100644 --- a/gitdb/test/db/test_mem.py +++ b/gitdb/test/db/test_mem.py @@ -2,29 +2,30 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from lib import * +from .lib import * from gitdb.db import ( - MemoryDB, - LooseObjectDB - ) - + MemoryDB, + LooseObjectDB +) + + class TestMemoryDB(TestDBBase): - + @with_rw_directory def test_writing(self, path): mdb = MemoryDB() - + # write data self._assert_object_writing_simple(mdb) - + # test stream copy ldb = LooseObjectDB(path) assert ldb.size() == 0 num_streams_copied = mdb.stream_copy(mdb.sha_iter(), ldb) assert num_streams_copied == mdb.size() - + assert ldb.size() == mdb.size() for sha in mdb.sha_iter(): assert ldb.has_object(sha) - assert ldb.stream(sha).read() == mdb.stream(sha).read() + assert ldb.stream(sha).read() == mdb.stream(sha).read() # END verify objects where copied and are equal diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index f4cb5bb..74ad989 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -2,7 +2,7 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from lib import * +from .lib import * from gitdb.db import PackedDB from gitdb.test.lib import fixture_path @@ -11,46 +11,46 @@ import os import random + class TestPackDB(TestDBBase): - + @with_rw_directory @with_packs_rw def test_writing(self, path): pdb = PackedDB(path) - + # on demand, we init our pack cache num_packs = len(pdb.entities()) assert pdb._st_mtime != 0 - - # test pack directory changed: + + # test pack directory changed: # packs removed - rename a file, should affect the glob pack_path = pdb.entities()[0].pack().path() new_pack_path = pack_path + "renamed" os.rename(pack_path, new_pack_path) - + pdb.update_cache(force=True) assert len(pdb.entities()) == num_packs - 1 - + # packs added os.rename(new_pack_path, pack_path) pdb.update_cache(force=True) assert len(pdb.entities()) == num_packs - + # bang on the cache # access the Entities directly, as there is no iteration interface # yet ( or required for now ) sha_list = list(pdb.sha_iter()) assert len(sha_list) == pdb.size() - + # hit all packs in random order random.shuffle(sha_list) - + for sha in sha_list: info = pdb.info(sha) stream = pdb.stream(sha) # END for each sha to query - - + # test short finding - be a bit more brutal here max_bytes = 19 min_bytes = 2 @@ -58,16 +58,18 @@ def test_writing(self, path): for i, sha in enumerate(sha_list): short_sha = sha[:max((i % max_bytes), min_bytes)] try: - assert pdb.partial_to_complete_sha(short_sha, len(short_sha)*2) == sha + assert pdb.partial_to_complete_sha( + short_sha, len(short_sha) * 2) == sha except AmbiguousObjectName: num_ambiguous += 1 - pass # valid, we can have short objects + pass # valid, we can have short objects # END exception handling # END for each sha to find - + # we should have at least one ambiguous, considering the small sizes - # but in our pack, there is no ambigious ... + # but in our pack, there is no ambigious ... # assert num_ambiguous - + # non-existing - self.failUnlessRaises(BadObject, pdb.partial_to_complete_sha, "\0\0", 4) + self.failUnlessRaises( + BadObject, pdb.partial_to_complete_sha, "\0\0", 4) diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 1637bff..4f930ce 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -2,18 +2,19 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from lib import * +from .lib import * from gitdb.db import ReferenceDB from gitdb.util import ( - NULL_BIN_SHA, - hex_to_bin - ) + NULL_BIN_SHA, + hex_to_bin +) import os - + + class TestReferenceDB(TestDBBase): - + def make_alt_file(self, alt_path, alt_list): """Create an alternates file which contains the given alternates. The list can be empty""" @@ -21,40 +22,38 @@ def make_alt_file(self, alt_path, alt_list): for alt in alt_list: alt_file.write(alt + "\n") alt_file.close() - + @with_rw_directory def test_writing(self, path): - NULL_BIN_SHA = '\0' * 20 - + NULL_BIN_SHA = '\0' * 20 + alt_path = os.path.join(path, 'alternates') rdb = ReferenceDB(alt_path) assert len(rdb.databases()) == 0 assert rdb.size() == 0 assert len(list(rdb.sha_iter())) == 0 - + # try empty, non-existing assert not rdb.has_object(NULL_BIN_SHA) - - + # setup alternate file # add two, one is invalid - own_repo_path = fixture_path('../../../.git/objects') # use own repo + own_repo_path = fixture_path( + '../../../.git/objects') # use own repo self.make_alt_file(alt_path, [own_repo_path, "invalid/path"]) rdb.update_cache() assert len(rdb.databases()) == 1 - + # we should now find a default revision of ours gitdb_sha = hex_to_bin("5690fd0d3304f378754b23b098bd7cb5f4aa1976") assert rdb.has_object(gitdb_sha) - + # remove valid self.make_alt_file(alt_path, ["just/one/invalid/path"]) rdb.update_cache() assert len(rdb.databases()) == 0 - + # add valid self.make_alt_file(alt_path, [own_repo_path]) rdb.update_cache() assert len(rdb.databases()) == 1 - - diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index ac8473a..d9a8d99 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -4,12 +4,12 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Utilities used in ODB testing""" from gitdb import ( - OStream, - ) -from gitdb.stream import ( - Sha1Writer, - ZippedStoreShaWriter - ) + OStream, +) +from gitdb.stream import ( + Sha1Writer, + ZippedStoreShaWriter +) from gitdb.util import zlib @@ -29,16 +29,18 @@ #{ Bases class TestBase(unittest.TestCase): + """Base class for all tests""" - + #} END bases #{ Decorators def with_rw_directory(func): - """Create a temporary directory which can be written to, remove it if the + """Create a temporary directory which can be written to, remove it if the test suceeds, but leave it otherwise to aid additional debugging""" + def wrapper(self): path = tempfile.mktemp(prefix=func.__name__) os.mkdir(path) @@ -47,12 +49,13 @@ def wrapper(self): try: return func(self, path) except Exception: - print >> sys.stderr, "Test %s.%s failed, output is at %r" % (type(self).__name__, func.__name__, path) + print >> sys.stderr, "Test %s.%s failed, output is at %r" % ( + type(self).__name__, func.__name__, path) keep = True raise finally: # Need to collect here to be sure all handles have been closed. It appears - # a windows-only issue. In fact things should be deleted, as well as + # a windows-only issue. In fact things should be deleted, as well as # memory maps closed, once objects go out of scope. For some reason # though this is not the case here unless we collect explicitly. if not keep: @@ -60,20 +63,21 @@ def wrapper(self): shutil.rmtree(path) # END handle exception # END wrapper - + wrapper.__name__ = func.__name__ return wrapper def with_packs_rw(func): - """Function that provides a path into which the packs for testing should be + """Function that provides a path into which the packs for testing should be copied. Will pass on the path to the actual function afterwards""" + def wrapper(self, path): src_pack_glob = fixture_path('packs/*') copy_files_globbed(src_pack_glob, path, hard_link_ok=True) return func(self, path) # END wrapper - + wrapper.__name__ = func.__name__ return wrapper @@ -81,15 +85,17 @@ def wrapper(self, path): #{ Routines + def fixture_path(relapath=''): """:return: absolute path into the fixture directory :param relapath: relative path into the fixtures directory, or '' to obtain the fixture directory itself""" return os.path.join(os.path.dirname(__file__), 'fixtures', relapath) - + + def copy_files_globbed(source_glob, target_dir, hard_link_ok=False): """Copy all files found according to the given source glob into the target directory - :param hard_link_ok: if True, hard links will be created if possible. Otherwise + :param hard_link_ok: if True, hard links will be created if possible. Otherwise the files will be copied""" for src_file in glob.glob(source_glob): if hard_link_ok and hasattr(os, 'link'): @@ -103,7 +109,7 @@ def copy_files_globbed(source_glob, target_dir, hard_link_ok=False): shutil.copy(src_file, target_dir) # END try hard link # END for each file to copy - + def make_bytes(size_in_bytes, randomize=False): """:return: string with given size in bytes @@ -117,11 +123,13 @@ def make_bytes(size_in_bytes, randomize=False): a = array('i', producer) return a.tostring() + def make_object(type, data): """:return: bytes resembling an uncompressed object""" odata = "blob %i\0" % len(data) return odata + data - + + def make_memory_file(size_in_bytes, randomize=False): """:return: tuple(size_of_stream, stream) :param randomize: try to produce a very random stream""" @@ -132,31 +140,33 @@ def make_memory_file(size_in_bytes, randomize=False): #{ Stream Utilities + class DummyStream(object): - def __init__(self): - self.was_read = False - self.bytes = 0 - self.closed = False - - def read(self, size): - self.was_read = True - self.bytes = size - - def close(self): - self.closed = True - - def _assert(self): - assert self.was_read + + def __init__(self): + self.was_read = False + self.bytes = 0 + self.closed = False + + def read(self, size): + self.was_read = True + self.bytes = size + + def close(self): + self.closed = True + + def _assert(self): + assert self.was_read class DeriveTest(OStream): + def __init__(self, sha, type, size, stream, *args, **kwargs): self.myarg = kwargs.pop('myarg') self.args = args - + def _assert(self): assert self.args assert self.myarg #} END stream utilitiess - diff --git a/gitdb/test/performance/lib.py b/gitdb/test/performance/lib.py index 3563fcf..1bf4211 100644 --- a/gitdb/test/performance/lib.py +++ b/gitdb/test/performance/lib.py @@ -20,27 +20,29 @@ def resolve_or_fail(env_var): try: return os.environ[env_var] except KeyError: - raise EnvironmentError("Please set the %r envrionment variable and retry" % env_var) + raise EnvironmentError( + "Please set the %r envrionment variable and retry" % env_var) # END exception handling #} END utilities -#{ Base Classes +#{ Base Classes class TestBigRepoR(TestBase): - """TestCase providing access to readonly 'big' repositories using the following + + """TestCase providing access to readonly 'big' repositories using the following member variables: - + * gitrepopath - + * read-only base path of the git source repository, i.e. .../git/.git""" - + #{ Invariants head_sha_2k = '235d521da60e4699e5bd59ac658b5b48bd76ddca' head_sha_50 = '32347c375250fd470973a5d76185cac718955fd5' - #} END invariants - + #} END invariants + @classmethod def setUpAll(cls): try: @@ -50,5 +52,5 @@ def setUpAll(cls): cls.gitrepopath = resolve_or_fail(k_env_git_repo) assert cls.gitrepopath.endswith('.git') - + #} END base classes diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index 63856e2..bc49951 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -4,8 +4,8 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Performance tests for object store""" from lib import ( - TestBigRepoR - ) + TestBigRepoR +) from gitdb.exc import UnsupportedOperation from gitdb.db.pack import PackedDB @@ -17,18 +17,20 @@ from nose import SkipTest + class TestPackedDBPerformance(TestBigRepoR): - + def test_pack_random_access(self): pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + # sha lookup st = time() sha_list = list(pdb.sha_iter()) elapsed = time() - st ns = len(sha_list) - print >> sys.stderr, "PDB: looked up %i shas by index in %f s ( %f shas/s )" % (ns, elapsed, ns / elapsed) - + print >> sys.stderr, "PDB: looked up %i shas by index in %f s ( %f shas/s )" % ( + ns, elapsed, ns / elapsed) + # sha lookup: best-case and worst case access pdb_pack_info = pdb._pack_info # END shuffle shas @@ -37,13 +39,14 @@ def test_pack_random_access(self): pdb_pack_info(sha) # END for each sha to look up elapsed = time() - st - + # discard cache del(pdb._entities) pdb.entities() - print >> sys.stderr, "PDB: looked up %i sha in %i packs in %f s ( %f shas/s )" % (ns, len(pdb.entities()), elapsed, ns / elapsed) + print >> sys.stderr, "PDB: looked up %i sha in %i packs in %f s ( %f shas/s )" % ( + ns, len(pdb.entities()), elapsed, ns / elapsed) # END for each random mode - + # query info and streams only max_items = 10000 # can wait longer when testing memory for pdb_fun in (pdb.info, pdb.stream): @@ -51,9 +54,10 @@ def test_pack_random_access(self): for sha in sha_list[:max_items]: pdb_fun(sha) elapsed = time() - st - print >> sys.stderr, "PDB: Obtained %i object %s by sha in %f s ( %f items/s )" % (max_items, pdb_fun.__name__.upper(), elapsed, max_items / elapsed) + print >> sys.stderr, "PDB: Obtained %i object %s by sha in %f s ( %f items/s )" % ( + max_items, pdb_fun.__name__.upper(), elapsed, max_items / elapsed) # END for each function - + # retrieve stream and read all max_items = 5000 pdb_stream = pdb.stream @@ -65,12 +69,15 @@ def test_pack_random_access(self): total_size += stream.size elapsed = time() - st total_kib = total_size / 1000 - print >> sys.stderr, "PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (max_items, total_kib, total_kib/elapsed , elapsed, max_items / elapsed) - + print >> sys.stderr, "PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % ( + max_items, total_kib, total_kib / elapsed, elapsed, max_items / elapsed) + def test_correctness(self): - raise SkipTest("Takes too long, enable it if you change the algorithm and want to be sure you decode packs correctly") + raise SkipTest( + "Takes too long, enable it if you change the algorithm and want to be sure you decode packs correctly") pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - # disabled for now as it used to work perfectly, checking big repositories takes a long time + # disabled for now as it used to work perfectly, checking big + # repositories takes a long time print >> sys.stderr, "Endurance run: verify streaming of objects (crc and sha)" for crc in range(2): count = 0 @@ -88,6 +95,6 @@ def test_correctness(self): # END for each index # END for each entity elapsed = time() - st - print >> sys.stderr, "PDB: verified %i objects (crc=%i) in %f s ( %f objects/s )" % (count, crc, elapsed, count / elapsed) + print >> sys.stderr, "PDB: verified %i objects (crc=%i) in %f s ( %f objects/s )" % ( + count, crc, elapsed, count / elapsed) # END for each verify mode - diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index c66e60c..ff0c1b0 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -4,8 +4,8 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Specific test for pack streams only""" from lib import ( - TestBigRepoR - ) + TestBigRepoR +) from gitdb.db.pack import PackedDB from gitdb.stream import NullStream @@ -16,26 +16,29 @@ from time import time from nose import SkipTest + class CountedNullStream(NullStream): __slots__ = '_bw' + def __init__(self): self._bw = 0 - + def bytes_written(self): return self._bw - + def write(self, d): self._bw += NullStream.write(self, d) - + class TestPackStreamingPerformance(TestBigRepoR): - + def test_pack_writing(self): # see how fast we can write a pack from object streams. - # This will not be fast, as we take time for decompressing the streams as well + # This will not be fast, as we take time for decompressing the streams + # as well ostream = CountedNullStream() pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + ni = 5000 count = 0 total_size = 0 @@ -45,21 +48,25 @@ def test_pack_writing(self): pdb.stream(sha) if count == ni: break - #END gather objects for pack-writing + # END gather objects for pack-writing elapsed = time() - st - print >> sys.stderr, "PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % (ni, elapsed, ni / elapsed) - + print >> sys.stderr, "PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % ( + ni, elapsed, ni / elapsed) + st = time() - PackEntity.write_pack((pdb.stream(sha) for sha in pdb.sha_iter()), ostream.write, object_count=ni) + PackEntity.write_pack( + (pdb.stream(sha) for sha in pdb.sha_iter()), + ostream.write, + object_count=ni) elapsed = time() - st total_kb = ostream.bytes_written() / 1000 - print >> sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % (total_kb, elapsed, total_kb/elapsed) - - + print >> sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % ( + total_kb, elapsed, total_kb / elapsed) + def test_stream_reading(self): raise SkipTest() pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + # streaming only, meant for --with-profile runs ni = 5000 count = 0 @@ -75,5 +82,5 @@ def test_stream_reading(self): count += 1 elapsed = time() - st total_kib = total_size / 1000 - print >> sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (ni, total_kib, total_kib/elapsed , elapsed, ni / elapsed) - + print >> sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % ( + ni, total_kib, total_kib / elapsed, elapsed, ni / elapsed) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 010003d..30f54c5 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -8,16 +8,16 @@ from gitdb.base import * from gitdb.stream import * from gitdb.util import ( - pool, - bin_to_hex - ) + pool, + bin_to_hex +) from gitdb.typ import str_blob_type from gitdb.fun import chunk_size -from async import ( - IteratorReader, +from async import ( + IteratorReader, ChannelThreadTask, - ) +) from cStringIO import StringIO from time import time @@ -31,7 +31,7 @@ TestBigRepoR, make_memory_file, with_rw_directory - ) +) #{ Utilities @@ -45,64 +45,67 @@ def read_chunked_stream(stream): # END read stream loop assert total == stream.size return stream - - + + class TestStreamReader(ChannelThreadTask): - """Expects input streams and reads them in chunks. It will read one at a time, + + """Expects input streams and reads them in chunks. It will read one at a time, requireing a queue chunk of size 1""" + def __init__(self, *args): super(TestStreamReader, self).__init__(*args) self.fun = read_chunked_stream self.max_chunksize = 1 - + #} END utilities class TestObjDBPerformance(TestBigRepoR): - - large_data_size_bytes = 1000*1000*50 # some MiB should do it - moderate_data_size_bytes = 1000*1000*1 # just 1 MiB - + + large_data_size_bytes = 1000 * 1000 * 50 # some MiB should do it + moderate_data_size_bytes = 1000 * 1000 * 1 # just 1 MiB + @with_rw_directory def test_large_data_streaming(self, path): ldb = LooseObjectDB(path) string_ios = list() # list of streams we previously created - - # serial mode + + # serial mode for randomize in range(2): desc = (randomize and 'random ') or '' print >> sys.stderr, "Creating %s data ..." % desc st = time() - size, stream = make_memory_file(self.large_data_size_bytes, randomize) + size, stream = make_memory_file( + self.large_data_size_bytes, randomize) elapsed = time() - st print >> sys.stderr, "Done (in %f s)" % elapsed string_ios.append(stream) - - # writing - due to the compression it will seem faster than it is + + # writing - due to the compression it will seem faster than it is st = time() sha = ldb.store(IStream('blob', size, stream)).binsha elapsed_add = time() - st assert ldb.has_object(sha) db_file = ldb.readable_db_object_path(bin_to_hex(sha)) fsize_kib = os.path.getsize(db_file) / 1000 - - + size_kib = size / 1000 - print >> sys.stderr, "Added %i KiB (filesize = %i KiB) of %s data to loose odb in %f s ( %f Write KiB / s)" % (size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add) - + print >> sys.stderr, "Added %i KiB (filesize = %i KiB) of %s data to loose odb in %f s ( %f Write KiB / s)" % ( + size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add) + # reading all at once st = time() ostream = ldb.stream(sha) shadata = ostream.read() elapsed_readall = time() - st - + stream.seek(0) assert shadata == stream.getvalue() - print >> sys.stderr, "Read %i KiB of %s data at once from loose odb in %f s ( %f Read KiB / s)" % (size_kib, desc, elapsed_readall, size_kib / elapsed_readall) - - + print >> sys.stderr, "Read %i KiB of %s data at once from loose odb in %f s ( %f Read KiB / s)" % ( + size_kib, desc, elapsed_readall, size_kib / elapsed_readall) + # reading in chunks of 1 MiB - cs = 512*1000 + cs = 512 * 1000 chunks = list() st = time() ostream = ldb.stream(sha) @@ -113,18 +116,18 @@ def test_large_data_streaming(self, path): break # END read in chunks elapsed_readchunks = time() - st - + stream.seek(0) assert ''.join(chunks) == stream.getvalue() - + cs_kib = cs / 1000 - print >> sys.stderr, "Read %i KiB of %s data in %i KiB chunks from loose odb in %f s ( %f Read KiB / s)" % (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks) - + print >> sys.stderr, "Read %i KiB of %s data in %i KiB chunks from loose odb in %f s ( %f Read KiB / s)" % ( + size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks) + # del db file so we keep something to do os.remove(db_file) # END for each randomization factor - - + # multi-threaded mode # want two, should be supported by most of todays cpus pool.set_size(2) @@ -134,62 +137,66 @@ def test_large_data_streaming(self, path): stream.seek(0) total_kib += len(stream.getvalue()) / 1000 # END rewind - + def istream_iter(): for stream in string_ios: stream.seek(0) yield IStream(str_blob_type, len(stream.getvalue()), stream) # END for each stream # END util - + # write multiple objects at once, involving concurrent compression reader = IteratorReader(istream_iter()) istream_reader = ldb.store_async(reader) istream_reader.task().max_chunksize = 1 - + st = time() istreams = istream_reader.read(nsios) assert len(istreams) == nsios elapsed = time() - st - - print >> sys.stderr, "Threads(%i): Compressed %i KiB of data in loose odb in %f s ( %f Write KiB / s)" % (pool.size(), total_kib, elapsed, total_kib / elapsed) - + + print >> sys.stderr, "Threads(%i): Compressed %i KiB of data in loose odb in %f s ( %f Write KiB / s)" % ( + pool.size(), total_kib, elapsed, total_kib / elapsed) + # decompress multiple at once, by reading them - # chunk size is not important as the stream will not really be decompressed - + # chunk size is not important as the stream will not really be + # decompressed + # until its read - istream_reader = IteratorReader(iter([ i.binsha for i in istreams ])) + istream_reader = IteratorReader(iter([i.binsha for i in istreams])) ostream_reader = ldb.stream_async(istream_reader) - + chunk_task = TestStreamReader(ostream_reader, "chunker", None) output_reader = pool.add_task(chunk_task) output_reader.task().max_chunksize = 1 - + st = time() assert len(output_reader.read(nsios)) == nsios elapsed = time() - st - - print >> sys.stderr, "Threads(%i): Decompressed %i KiB of data in loose odb in %f s ( %f Read KiB / s)" % (pool.size(), total_kib, elapsed, total_kib / elapsed) - - # store the files, and read them back. For the reading, we use a task + + print >> sys.stderr, "Threads(%i): Decompressed %i KiB of data in loose odb in %f s ( %f Read KiB / s)" % ( + pool.size(), total_kib, elapsed, total_kib / elapsed) + + # store the files, and read them back. For the reading, we use a task # as well which is chunked into one item per task. Reading all will - # very quickly result in two threads handling two bytestreams of + # very quickly result in two threads handling two bytestreams of # chained compression/decompression streams reader = IteratorReader(istream_iter()) istream_reader = ldb.store_async(reader) istream_reader.task().max_chunksize = 1 - - istream_to_sha = lambda items: [ i.binsha for i in items ] + + istream_to_sha = lambda items: [i.binsha for i in items] istream_reader.set_post_cb(istream_to_sha) - + ostream_reader = ldb.stream_async(istream_reader) - + chunk_task = TestStreamReader(ostream_reader, "chunker", None) output_reader = pool.add_task(chunk_task) output_reader.max_chunksize = 1 - + st = time() assert len(output_reader.read(nsios)) == nsios elapsed = time() - st - - print >> sys.stderr, "Threads(%i): Compressed and decompressed and read %i KiB of data in loose odb in %f s ( %f Combined KiB / s)" % (pool.size(), total_kib, elapsed, total_kib / elapsed) + + print >> sys.stderr, "Threads(%i): Compressed and decompressed and read %i KiB of data in loose odb in %f s ( %f Combined KiB / s)" % ( + pool.size(), total_kib, elapsed, total_kib / elapsed) diff --git a/gitdb/test/test_base.py b/gitdb/test/test_base.py index d4ce428..43ff89b 100644 --- a/gitdb/test/test_base.py +++ b/gitdb/test/test_base.py @@ -3,50 +3,49 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Test for object db""" -from lib import ( - TestBase, - DummyStream, - DeriveTest, - ) +from .lib import ( + TestBase, + DummyStream, + DeriveTest, +) from gitdb import * from gitdb.util import ( NULL_BIN_SHA - ) +) from gitdb.typ import ( str_blob_type - ) +) class TestBaseTypes(TestBase): - + def test_streams(self): # test info sha = NULL_BIN_SHA s = 20 blob_id = 3 - + info = OInfo(sha, str_blob_type, s) assert info.binsha == sha assert info.type == str_blob_type assert info.type_id == blob_id assert info.size == s - + # test pack info # provides type_id pinfo = OPackInfo(0, blob_id, s) assert pinfo.type == str_blob_type assert pinfo.type_id == blob_id assert pinfo.pack_offset == 0 - + dpinfo = ODeltaPackInfo(0, blob_id, s, sha) assert dpinfo.type == str_blob_type assert dpinfo.type_id == blob_id assert dpinfo.delta_info == sha assert dpinfo.pack_offset == 0 - - + # test ostream stream = DummyStream() ostream = OStream(*(info + (stream, ))) @@ -56,33 +55,33 @@ def test_streams(self): assert stream.bytes == 15 ostream.read(20) assert stream.bytes == 20 - + # test packstream postream = OPackStream(*(pinfo + (stream, ))) assert postream.stream is stream postream.read(10) stream._assert() assert stream.bytes == 10 - + # test deltapackstream dpostream = ODeltaPackStream(*(dpinfo + (stream, ))) dpostream.stream is stream dpostream.read(5) stream._assert() assert stream.bytes == 5 - + # derive with own args - DeriveTest(sha, str_blob_type, s, stream, 'mine',myarg = 3)._assert() - + DeriveTest(sha, str_blob_type, s, stream, 'mine', myarg=3)._assert() + # test istream istream = IStream(str_blob_type, s, stream) - assert istream.binsha == None + assert istream.binsha is None istream.binsha = sha assert istream.binsha == sha - + assert len(istream.binsha) == 20 assert len(istream.hexsha) == 40 - + assert istream.size == s istream.size = s * 2 istream.size == s * 2 @@ -92,7 +91,7 @@ def test_streams(self): assert istream.stream is stream istream.stream = None assert istream.stream is None - + assert istream.error is None istream.error = Exception() assert isinstance(istream.error, Exception) diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index 611ae42..ab38784 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -3,25 +3,26 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with examples from the tutorial section of the docs""" -from lib import * +from .lib import * from gitdb import IStream from gitdb.db import LooseObjectDB from gitdb.util import pool - + from cStringIO import StringIO from async import IteratorReader - + + class TestExamples(TestBase): - + def test_base(self): ldb = LooseObjectDB(fixture_path("../../../.git/objects")) - + for sha1 in ldb.sha_iter(): oinfo = ldb.info(sha1) ostream = ldb.stream(sha1) assert oinfo[:3] == ostream[:3] - + assert len(ostream.read()) == ostream.size assert ldb.has_object(oinfo.binsha) # END for each sha in database @@ -32,33 +33,32 @@ def test_base(self): except UnboundLocalError: pass # END ignore exception if there are no loose objects - + data = "my data" istream = IStream("blob", len(data), StringIO(data)) - + # the object does not yet have a sha assert istream.binsha is None ldb.store(istream) # now the sha is set assert len(istream.binsha) == 20 assert ldb.has_object(istream.binsha) - - + # async operation # Create a reader from an iterator reader = IteratorReader(ldb.sha_iter()) - + # get reader for object streams info_reader = ldb.stream_async(reader) - + # read one info = info_reader.read(1)[0] - + # read all the rest until depletion ostreams = info_reader.read() - + # set the pool to use two threads pool.set_size(2) - + # synchronize the mode of operation pool.set_size(0) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 779155a..96fd8f3 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -3,24 +3,24 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Test everything about packs reading and writing""" -from lib import ( - TestBase, - with_rw_directory, - with_packs_rw, - fixture_path - ) +from .lib import ( + TestBase, + with_rw_directory, + with_packs_rw, + fixture_path +) from gitdb.stream import DeltaApplyReader from gitdb.pack import ( - PackEntity, - PackIndexFile, - PackFile - ) + PackEntity, + PackIndexFile, + PackFile +) from gitdb.base import ( - OInfo, - OStream, - ) + OInfo, + OStream, +) from gitdb.fun import delta_types from gitdb.exc import UnsupportedOperation @@ -38,16 +38,34 @@ def bin_sha_from_filename(filename): return to_bin_sha(os.path.splitext(os.path.basename(filename))[0][5:]) #} END utilities + class TestPack(TestBase): - - packindexfile_v1 = (fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.idx'), 1, 67) - packindexfile_v2 = (fixture_path('packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.idx'), 2, 30) - packindexfile_v2_3_ascii = (fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx'), 2, 42) - packfile_v2_1 = (fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.pack'), 2, packindexfile_v1[2]) - packfile_v2_2 = (fixture_path('packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack'), 2, packindexfile_v2[2]) - packfile_v2_3_ascii = (fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.pack'), 2, packindexfile_v2_3_ascii[2]) - - + + packindexfile_v1 = ( + fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.idx'), + 1, + 67) + packindexfile_v2 = ( + fixture_path('packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.idx'), + 2, + 30) + packindexfile_v2_3_ascii = ( + fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx'), + 2, + 42) + packfile_v2_1 = ( + fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.pack'), + 2, + packindexfile_v1[2]) + packfile_v2_2 = ( + fixture_path('packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack'), + 2, + packindexfile_v2[2]) + packfile_v2_3_ascii = ( + fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.pack'), + 2, + packindexfile_v2_3_ascii[2]) + def _assert_index_file(self, index, version, size): assert index.packfile_checksum() != index.indexfile_checksum() assert len(index.packfile_checksum()) == 20 @@ -55,102 +73,102 @@ def _assert_index_file(self, index, version, size): assert index.version() == version assert index.size() == size assert len(index.offsets()) == size - + # get all data of all objects for oidx in xrange(index.size()): sha = index.sha(oidx) assert oidx == index.sha_to_index(sha) - + entry = index.entry(oidx) assert len(entry) == 3 - + assert entry[0] == index.offset(oidx) assert entry[1] == sha assert entry[2] == index.crc(oidx) - + # verify partial sha - for l in (4,8,11,17,20): - assert index.partial_sha_to_index(sha[:l], l*2) == oidx - + for l in (4, 8, 11, 17, 20): + assert index.partial_sha_to_index(sha[:l], l * 2) == oidx + # END for each object index in indexfile self.failUnlessRaises(ValueError, index.partial_sha_to_index, "\0", 2) - - + def _assert_pack_file(self, pack, version, size): assert pack.version() == 2 assert pack.size() == size assert len(pack.checksum()) == 20 - + num_obj = 0 for obj in pack.stream_iter(): num_obj += 1 info = pack.info(obj.pack_offset) stream = pack.stream(obj.pack_offset) - + assert info.pack_offset == stream.pack_offset assert info.type_id == stream.type_id assert hasattr(stream, 'read') - + # it should be possible to read from both streams assert obj.read() == stream.read() - + streams = pack.collect_streams(obj.pack_offset) assert streams - + # read the stream try: dstream = DeltaApplyReader.new(streams) except ValueError: - # ignore these, old git versions use only ref deltas, + # ignore these, old git versions use only ref deltas, # which we havent resolved ( as we are without an index ) # Also ignore non-delta streams continue # END get deltastream - + # read all data = dstream.read() assert len(data) == dstream.size - + # test seek dstream.seek(0) assert dstream.read() == data - - + # read chunks # NOTE: the current implementation is safe, it basically transfers # all calls to the underlying memory map - + # END for each object assert num_obj == size - - + def test_pack_index(self): # check version 1 and 2 - for indexfile, version, size in (self.packindexfile_v1, self.packindexfile_v2): + for indexfile, version, size in ( + self.packindexfile_v1, self.packindexfile_v2): index = PackIndexFile(indexfile) self._assert_index_file(index, version, size) # END run tests - + def test_pack(self): - # there is this special version 3, but apparently its like 2 ... - for packfile, version, size in (self.packfile_v2_3_ascii, self.packfile_v2_1, self.packfile_v2_2): + # there is this special version 3, but apparently its like 2 ... + for packfile, version, size in ( + self.packfile_v2_3_ascii, self.packfile_v2_1, self.packfile_v2_2): pack = PackFile(packfile) self._assert_pack_file(pack, version, size) # END for each pack to test - + @with_rw_directory def test_pack_entity(self, rw_dir): pack_objs = list() - for packinfo, indexinfo in ( (self.packfile_v2_1, self.packindexfile_v1), - (self.packfile_v2_2, self.packindexfile_v2), - (self.packfile_v2_3_ascii, self.packindexfile_v2_3_ascii)): + for packinfo, indexinfo in ((self.packfile_v2_1, self.packindexfile_v1), + (self.packfile_v2_2, + self.packindexfile_v2), + (self.packfile_v2_3_ascii, self.packindexfile_v2_3_ascii)): packfile, version, size = packinfo indexfile, version, size = indexinfo entity = PackEntity(packfile) assert entity.pack().path() == packfile assert entity.index().path() == indexfile pack_objs.extend(entity.stream_iter()) - + count = 0 for info, stream in izip(entity.info_iter(), entity.stream_iter()): count += 1 @@ -158,10 +176,11 @@ def test_pack_entity(self, rw_dir): assert len(info.binsha) == 20 assert info.type_id == stream.type_id assert info.size == stream.size - - # we return fully resolved items, which is implied by the sha centric access + + # we return fully resolved items, which is implied by the sha + # centric access assert not info.type_id in delta_types - + # try all calls assert len(entity.collect_streams(info.binsha)) oinfo = entity.info(info.binsha) @@ -170,7 +189,7 @@ def test_pack_entity(self, rw_dir): ostream = entity.stream(info.binsha) assert isinstance(ostream, OStream) assert ostream.binsha is not None - + # verify the stream try: assert entity.is_valid_stream(info.binsha, use_crc=True) @@ -180,42 +199,46 @@ def test_pack_entity(self, rw_dir): assert entity.is_valid_stream(info.binsha, use_crc=False) # END for each info, stream tuple assert count == size - + # END for each entity - + # pack writing - write all packs into one # index path can be None pack_path = tempfile.mktemp('', "pack", rw_dir) index_path = tempfile.mktemp('', 'index', rw_dir) iteration = 0 + def rewind_streams(): - for obj in pack_objs: + for obj in pack_objs: obj.stream.seek(0) - #END utility - for ppath, ipath, num_obj in zip((pack_path, )*2, (index_path, None), (len(pack_objs), None)): + # END utility + for ppath, ipath, num_obj in zip( + (pack_path, ) * 2, (index_path, None), (len(pack_objs), None)): pfile = open(ppath, 'wb') iwrite = None if ipath: ifile = open(ipath, 'wb') iwrite = ifile.write - #END handle ip - - # make sure we rewind the streams ... we work on the same objects over and over again - if iteration > 0: + # END handle ip + + # make sure we rewind the streams ... we work on the same objects + # over and over again + if iteration > 0: rewind_streams() - #END rewind streams + # END rewind streams iteration += 1 - - pack_sha, index_sha = PackEntity.write_pack(pack_objs, pfile.write, iwrite, object_count=num_obj) + + pack_sha, index_sha = PackEntity.write_pack( + pack_objs, pfile.write, iwrite, object_count=num_obj) pfile.close() assert os.path.getsize(ppath) > 100 - + # verify pack pf = PackFile(ppath) assert pf.size() == len(pack_objs) assert pf.version() == PackFile.pack_version_default assert pf.checksum() == pack_sha - + # verify index if ipath is not None: ifile.close() @@ -225,9 +248,9 @@ def rewind_streams(): assert idx.packfile_checksum() == pack_sha assert idx.indexfile_checksum() == index_sha assert idx.size() == len(pack_objs) - #END verify files exist - #END for each packpath, indexpath pair - + # END verify files exist + # END for each packpath, indexpath pair + # verify the packs throughly rewind_streams() entity = PackEntity.create(pack_objs, rw_dir) @@ -237,11 +260,10 @@ def rewind_streams(): for use_crc in range(2): assert entity.is_valid_stream(info.binsha, use_crc) # END for each crc mode - #END for each info + # END for each info assert count == len(pack_objs) - - + def test_pack_64(self): # TODO: hex-edit a pack helping us to verify that we can handle 64 byte offsets - # of course without really needing such a huge pack + # of course without really needing such a huge pack raise SkipTest() diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index 6dc2746..ac088a8 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -3,152 +3,159 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Test for object db""" -from lib import ( - TestBase, - DummyStream, - Sha1Writer, - make_bytes, - make_object, - fixture_path - ) +from .lib import ( + TestBase, + DummyStream, + Sha1Writer, + make_bytes, + make_object, + fixture_path +) from gitdb import * from gitdb.util import ( NULL_HEX_SHA, hex_to_bin - ) +) from gitdb.util import zlib from gitdb.typ import ( str_blob_type - ) +) import time import tempfile import os - - class TestStream(TestBase): + """Test stream classes""" - - data_sizes = (15, 10000, 1000*1024+512) - - def _assert_stream_reader(self, stream, cdata, rewind_stream=lambda s: None): - """Make stream tests - the orig_stream is seekable, allowing it to be + + data_sizes = (15, 10000, 1000 * 1024 + 512) + + def _assert_stream_reader( + self, stream, cdata, rewind_stream=lambda s: None): + """Make stream tests - the orig_stream is seekable, allowing it to be rewound and reused :param cdata: the data we expect to read from stream, the contents :param rewind_stream: function called to rewind the stream to make it ready for reuse""" ns = 10 - assert len(cdata) > ns-1, "Data must be larger than %i, was %i" % (ns, len(cdata)) - + assert len(cdata) > ns - \ + 1, "Data must be larger than %i, was %i" % (ns, len(cdata)) + # read in small steps ss = len(cdata) / ns for i in range(ns): data = stream.read(ss) - chunk = cdata[i*ss:(i+1)*ss] + chunk = cdata[i * ss:(i + 1) * ss] assert data == chunk # END for each step rest = stream.read() if rest: assert rest == cdata[-len(rest):] # END handle rest - + if isinstance(stream, DecompressMemMapReader): assert len(stream.data()) == stream.compressed_bytes_read() # END handle special type - + rewind_stream(stream) - + # read everything rdata = stream.read() assert rdata == cdata - + if isinstance(stream, DecompressMemMapReader): assert len(stream.data()) == stream.compressed_bytes_read() # END handle special type - + def test_decompress_reader(self): for close_on_deletion in range(2): for with_size in range(2): for ds in self.data_sizes: cdata = make_bytes(ds, randomize=False) - + # zdata = zipped actual data # cdata = original content data - + # create reader if with_size: # need object data - zdata = zlib.compress(make_object(str_blob_type, cdata)) - type, size, reader = DecompressMemMapReader.new(zdata, close_on_deletion) + zdata = zlib.compress( + make_object(str_blob_type, cdata)) + type, size, reader = DecompressMemMapReader.new( + zdata, close_on_deletion) assert size == len(cdata) assert type == str_blob_type - - # even if we don't set the size, it will be set automatically on first read - test_reader = DecompressMemMapReader(zdata, close_on_deletion=False) + + # even if we don't set the size, it will be set + # automatically on first read + test_reader = DecompressMemMapReader( + zdata, close_on_deletion=False) assert test_reader._s == len(cdata) else: # here we need content data zdata = zlib.compress(cdata) - reader = DecompressMemMapReader(zdata, close_on_deletion, len(cdata)) + reader = DecompressMemMapReader( + zdata, close_on_deletion, len(cdata)) assert reader._s == len(cdata) - # END get reader - - self._assert_stream_reader(reader, cdata, lambda r: r.seek(0)) - + # END get reader + + self._assert_stream_reader( + reader, cdata, lambda r: r.seek(0)) + # put in a dummy stream for closing dummy = DummyStream() reader._m = dummy - + assert not dummy.closed del(reader) assert dummy.closed == close_on_deletion # END for each datasize # END whether size should be used # END whether stream should be closed when deleted - + def test_sha_writer(self): writer = Sha1Writer() assert 2 == writer.write("hi") assert len(writer.sha(as_hex=1)) == 40 assert len(writer.sha(as_hex=0)) == 20 - + # make sure it does something ;) prev_sha = writer.sha() writer.write("hi again") assert writer.sha() != prev_sha - + def test_compressed_writer(self): for ds in self.data_sizes: fd, path = tempfile.mkstemp() ostream = FDCompressedSha1Writer(fd) data = make_bytes(ds, randomize=False) - + # for now, just a single write, code doesn't care about chunking assert len(data) == ostream.write(data) ostream.close() - + # its closed already self.failUnlessRaises(OSError, os.close, fd) - + # read everything back, compare to data we zip - fd = os.open(path, os.O_RDONLY|getattr(os, 'O_BINARY', 0)) + fd = os.open(path, os.O_RDONLY | getattr(os, 'O_BINARY', 0)) written_data = os.read(fd, os.path.getsize(path)) assert len(written_data) == os.path.getsize(path) os.close(fd) assert written_data == zlib.compress(data, 1) # best speed - + os.remove(path) # END for each os - + def test_decompress_reader_special_case(self): odb = LooseObjectDB(fixture_path('objects')) - ostream = odb.stream(hex_to_bin('7bb839852ed5e3a069966281bb08d50012fb309b')) - + ostream = odb.stream( + hex_to_bin('7bb839852ed5e3a069966281bb08d50012fb309b')) + # if there is a bug, we will be missing one byte exactly ! data = ostream.read() assert len(data) == ostream.size - diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index 35f9f44..f029f29 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -6,30 +6,31 @@ import tempfile import os -from lib import TestBase +from .lib import TestBase from gitdb.util import ( - to_hex_sha, - to_bin_sha, - NULL_HEX_SHA, + to_hex_sha, + to_bin_sha, + NULL_HEX_SHA, LockedFD - ) +) + - class TestUtils(TestBase): + def test_basics(self): assert to_hex_sha(NULL_HEX_SHA) == NULL_HEX_SHA assert len(to_bin_sha(NULL_HEX_SHA)) == 20 assert to_hex_sha(to_bin_sha(NULL_HEX_SHA)) == NULL_HEX_SHA - + def _cmp_contents(self, file_path, data): - # raise if data from file at file_path + # raise if data from file at file_path # does not match data string fp = open(file_path, "rb") try: assert fp.read() == data finally: fp.close() - + def test_lockedfd(self): my_file = tempfile.mktemp() orig_data = "hello" @@ -37,62 +38,62 @@ def test_lockedfd(self): my_file_fp = open(my_file, "wb") my_file_fp.write(orig_data) my_file_fp.close() - + try: lfd = LockedFD(my_file) - lockfilepath = lfd._lockfilepath() - + lockfilepath = lfd._lockfilepath() + # cannot end before it was started self.failUnlessRaises(AssertionError, lfd.rollback) self.failUnlessRaises(AssertionError, lfd.commit) - + # open for writing assert not os.path.isfile(lockfilepath) wfd = lfd.open(write=True) assert lfd._fd is wfd assert os.path.isfile(lockfilepath) - + # write data and fail os.write(wfd, new_data) lfd.rollback() assert lfd._fd is None self._cmp_contents(my_file, orig_data) assert not os.path.isfile(lockfilepath) - + # additional call doesnt fail lfd.commit() lfd.rollback() - + # test reading lfd = LockedFD(my_file) rfd = lfd.open(write=False) assert os.read(rfd, len(orig_data)) == orig_data - + assert os.path.isfile(lockfilepath) # deletion rolls back del(lfd) assert not os.path.isfile(lockfilepath) - - + # write data - concurrently lfd = LockedFD(my_file) olfd = LockedFD(my_file) assert not os.path.isfile(lockfilepath) - wfdstream = lfd.open(write=True, stream=True) # this time as stream + # this time as stream + wfdstream = lfd.open(write=True, stream=True) assert os.path.isfile(lockfilepath) # another one fails self.failUnlessRaises(IOError, olfd.open) - + wfdstream.write(new_data) lfd.commit() assert not os.path.isfile(lockfilepath) self._cmp_contents(my_file, new_data) - + # could test automatic _end_writing on destruction finally: os.remove(my_file) # END final cleanup - + # try non-existing file for reading lfd = LockedFD(tempfile.mktemp()) try: @@ -102,4 +103,3 @@ def test_lockedfd(self): else: self.fail("expected OSError") # END handle exceptions - diff --git a/gitdb/typ.py b/gitdb/typ.py index e84dd24..0328287 100644 --- a/gitdb/typ.py +++ b/gitdb/typ.py @@ -4,7 +4,7 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module containing information about types known to the database""" -#{ String types +#{ String types str_blob_type = "blob" str_commit_type = "commit" diff --git a/gitdb/util.py b/gitdb/util.py index 1662b66..99455de 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -24,10 +24,10 @@ from async import ThreadPool from smmap import ( - StaticWindowMapManager, - SlidingWindowMapManager, - SlidingWindowMapBuffer - ) + StaticWindowMapManager, + SlidingWindowMapManager, + SlidingWindowMapBuffer +) # initialize our global memory manager instance # Use it to free cached (and unused) resources. @@ -35,7 +35,7 @@ mman = StaticWindowMapManager() else: mman = SlidingWindowMapManager() -#END handle mman +# END handle mman try: import hashlib @@ -47,6 +47,7 @@ except ImportError: from struct import unpack, calcsize __calcsize_cache = dict() + def unpack_from(fmt, data, offset=0): try: size = __calcsize_cache[fmt] @@ -54,13 +55,13 @@ def unpack_from(fmt, data, offset=0): size = calcsize(fmt) __calcsize_cache[fmt] = size # END exception handling - return unpack(fmt, data[offset : offset + size]) + return unpack(fmt, data[offset: offset + size]) # END own unpack_from implementation #{ Globals -# A pool distributing tasks, initially with zero threads, hence everything +# A pool distributing tasks, initially with zero threads, hence everything # will be handled in the main thread pool = ThreadPool(0) @@ -92,40 +93,43 @@ def unpack_from(fmt, data, offset=0): fsync = os.fsync # constants -NULL_HEX_SHA = "0"*40 -NULL_BIN_SHA = "\0"*20 +NULL_HEX_SHA = "0" * 40 +NULL_BIN_SHA = "\0" * 20 #} END Aliases -#{ compatibility stuff ... +#{ compatibility stuff ... + class _RandomAccessStringIO(object): - """Wrapper to provide required functionality in case memory maps cannot or may + + """Wrapper to provide required functionality in case memory maps cannot or may not be used. This is only really required in python 2.4""" __slots__ = '_sio' - + def __init__(self, buf=''): self._sio = StringIO(buf) - + def __getattr__(self, attr): return getattr(self._sio, attr) - + def __len__(self): return len(self.getvalue()) - + def __getitem__(self, i): return self.getvalue()[i] - + def __getslice__(self, start, end): return self.getvalue()[start:end] - + #} END compatibility stuff ... #{ Routines + def make_sha(source=''): """A python2.4 workaround for the sha/hashlib module fiasco - + **Note** From the dulwich project """ try: return hashlib.sha1(source) @@ -133,30 +137,31 @@ def make_sha(source=''): sha1 = sha.sha(source) return sha1 + def allocate_memory(size): """:return: a file-protocol accessible memory block of the given size""" if size == 0: return _RandomAccessStringIO('') # END handle empty chunks gracefully - + try: return mmap.mmap(-1, size) # read-write by default except EnvironmentError: # setup real memory instead # this of course may fail if the amount of memory is not available in - # one chunk - would only be the case in python 2.4, being more likely on + # one chunk - would only be the case in python 2.4, being more likely on # 32 bit systems. - return _RandomAccessStringIO("\0"*size) + return _RandomAccessStringIO("\0" * size) # END handle memory allocation - + def file_contents_ro(fd, stream=False, allow_mmap=True): """:return: read-only contents of the file represented by the file descriptor fd - + :param fd: file descriptor opened for reading :param stream: if False, random access is provided, otherwise the stream interface is provided. - :param allow_mmap: if True, its allowed to map the contents into memory, which + :param allow_mmap: if True, its allowed to map the contents into memory, which allows large files to be handled and accessed efficiently. The file-descriptor will change its position if this is False""" try: @@ -166,49 +171,55 @@ def file_contents_ro(fd, stream=False, allow_mmap=True): return mmap.mmap(fd, 0, access=mmap.ACCESS_READ) except EnvironmentError: # python 2.4 issue, 0 wants to be the actual size - return mmap.mmap(fd, os.fstat(fd).st_size, access=mmap.ACCESS_READ) + return mmap.mmap( + fd, os.fstat(fd).st_size, access=mmap.ACCESS_READ) # END handle python 2.4 except OSError: pass # END exception handling - + # read manully contents = os.read(fd, os.fstat(fd).st_size) if stream: return _RandomAccessStringIO(contents) return contents - -def file_contents_ro_filepath(filepath, stream=False, allow_mmap=True, flags=0): + + +def file_contents_ro_filepath( + filepath, stream=False, allow_mmap=True, flags=0): """Get the file contents at filepath as fast as possible - + :return: random access compatible memory of the given filepath :param stream: see ``file_contents_ro`` :param allow_mmap: see ``file_contents_ro`` :param flags: additional flags to pass to os.open :raise OSError: If the file could not be opened - - **Note** for now we don't try to use O_NOATIME directly as the right value needs to be - shared per database in fact. It only makes a real difference for loose object + + **Note** for now we don't try to use O_NOATIME directly as the right value needs to be + shared per database in fact. It only makes a real difference for loose object databases anyway, and they use it with the help of the ``flags`` parameter""" - fd = os.open(filepath, os.O_RDONLY|getattr(os, 'O_BINARY', 0)|flags) + fd = os.open(filepath, os.O_RDONLY | getattr(os, 'O_BINARY', 0) | flags) try: return file_contents_ro(fd, stream, allow_mmap) finally: close(fd) # END assure file is closed - + + def sliding_ro_buffer(filepath, flags=0): """ :return: a buffer compatible object which uses our mapped memory manager internally ready to read the whole given filepath""" return SlidingWindowMapBuffer(mman.make_cursor(filepath), flags=flags) - + + def to_hex_sha(sha): """:return: hexified version of sha""" if len(sha) == 40: return sha return bin_to_hex(sha) - + + def to_bin_sha(sha): if len(sha) == 20: return sha @@ -221,18 +232,19 @@ def to_bin_sha(sha): #{ Utilities class LazyMixin(object): + """ Base class providing an interface to lazily retrieve attribute values upon first access. If slots are used, memory will only be reserved once the attribute is actually accessed and retrieved the first time. All future accesses will return the cached value as stored in the Instance's dict or slot. """ - + __slots__ = tuple() - + def __getattr__(self, attr): """ - Whenever an attribute is requested that we do not know, we allow it + Whenever an attribute is requested that we do not know, we allow it to be created and set. Next time the same attribute is reqeusted, it is simply returned from our dict/slots. """ self._set_cache_(attr) @@ -241,79 +253,81 @@ def __getattr__(self, attr): def _set_cache_(self, attr): """ - This method should be overridden in the derived class. + This method should be overridden in the derived class. It should check whether the attribute named by attr can be created and cached. Do nothing if you do not know the attribute or call your subclass - - The derived class may create as many additional attributes as it deems - necessary in case a git command returns more information than represented + + The derived class may create as many additional attributes as it deems + necessary in case a git command returns more information than represented in the single attribute.""" pass - + class LockedFD(object): + """ This class facilitates a safe read and write operation to a file on disk. - If we write to 'file', we obtain a lock file at 'file.lock' and write to - that instead. If we succeed, the lock file will be renamed to overwrite + If we write to 'file', we obtain a lock file at 'file.lock' and write to + that instead. If we succeed, the lock file will be renamed to overwrite the original file. - - When reading, we obtain a lock file, but to prevent other writers from + + When reading, we obtain a lock file, but to prevent other writers from succeeding while we are reading the file. - - This type handles error correctly in that it will assure a consistent state + + This type handles error correctly in that it will assure a consistent state on destruction. - + **note** with this setup, parallel reading is not possible""" __slots__ = ("_filepath", '_fd', '_write') - + def __init__(self, filepath): """Initialize an instance with the givne filepath""" self._filepath = filepath self._fd = None self._write = None # if True, we write a file - + def __del__(self): # will do nothing if the file descriptor is already closed if self._fd is not None: self.rollback() - + def _lockfilepath(self): return "%s.lock" % self._filepath - + def open(self, write=False, stream=False): """ Open the file descriptor for reading or writing, both in binary mode. - + :param write: if True, the file descriptor will be opened for writing. Other wise it will be opened read-only. - :param stream: if True, the file descriptor will be wrapped into a simple stream + :param stream: if True, the file descriptor will be wrapped into a simple stream object which supports only reading or writing :return: fd to read from or write to. It is still maintained by this instance and must not be closed directly :raise IOError: if the lock could not be retrieved :raise OSError: If the actual file could not be opened for reading - + **note** must only be called once""" if self._write is not None: raise AssertionError("Called %s multiple times" % self.open) - + self._write = write - + # try to open the lock file binary = getattr(os, 'O_BINARY', 0) - lockmode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | binary + lockmode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | binary try: - fd = os.open(self._lockfilepath(), lockmode, 0600) + fd = os.open(self._lockfilepath(), lockmode, 0o600) if not write: os.close(fd) else: self._fd = fd # END handle file descriptor except OSError: - raise IOError("Lock at %r could not be obtained" % self._lockfilepath()) + raise IOError("Lock at %r could not be obtained" % + self._lockfilepath()) # END handle lock retrieval - + # open actual file if required if self._fd is None: # we could specify exlusive here, as we obtained the lock anyway @@ -325,41 +339,42 @@ def open(self, write=False, stream=False): raise # END handle lockfile # END open descriptor for reading - + if stream: # need delayed import - from stream import FDStream + from .stream import FDStream return FDStream(self._fd) else: return self._fd # END handle stream - + def commit(self): - """When done writing, call this function to commit your changes into the - actual file. + """When done writing, call this function to commit your changes into the + actual file. The file descriptor will be closed, and the lockfile handled. - + **Note** can be called multiple times""" self._end_writing(successful=True) - + def rollback(self): - """Abort your operation without any changes. The file descriptor will be + """Abort your operation without any changes. The file descriptor will be closed, and the lock released. - + **Note** can be called multiple times""" self._end_writing(successful=False) - + def _end_writing(self, successful=True): """Handle the lock according to the write mode """ if self._write is None: - raise AssertionError("Cannot end operation if it wasn't started yet") - + raise AssertionError( + "Cannot end operation if it wasn't started yet") + if self._fd is None: return - + os.close(self._fd) self._fd = None - + lockfile = self._lockfilepath() if self._write and successful: # on windows, rename does not silently overwrite the existing one @@ -369,11 +384,11 @@ def _end_writing(self, successful=True): # END remove if exists # END win32 special handling os.rename(lockfile, self._filepath) - + # assure others can at least read the file - the tmpfile left it at rw-- # We may also write that file, on windows that boils down to a remove- # protection as well - chmod(self._filepath, 0644) + chmod(self._filepath, 0o644) else: # just delete the file so far, we failed os.remove(lockfile) diff --git a/setup.py b/setup.py index 07f810b..90d7f72 100755 --- a/setup.py +++ b/setup.py @@ -1,94 +1,114 @@ #!/usr/bin/env python -from distutils.core import setup, Extension +from distutils.core import setup, Extension from distutils.command.build_py import build_py from distutils.command.build_ext import build_ext -import os, sys +import os +import sys -# wow, this is a mixed bag ... I am pretty upset about all of this ... +# wow, this is a mixed bag ... I am pretty upset about all of this ... setuptools_build_py_module = None try: - # don't pull it in if we don't have to - if 'setuptools' in sys.modules: - import setuptools.command.build_py as setuptools_build_py_module - from setuptools.command.build_ext import build_ext + # don't pull it in if we don't have to + if 'setuptools' in sys.modules: + import setuptools.command.build_py as setuptools_build_py_module + from setuptools.command.build_ext import build_ext except ImportError: - pass + pass + class build_ext_nofail(build_ext): - """Doesn't fail when build our optional extensions""" - def run(self): - try: - build_ext.run(self) - except Exception: - print("Ignored failure when building extensions, pure python modules will be used instead") - # END ignore errors - + + """Doesn't fail when build our optional extensions""" + + def run(self): + try: + build_ext.run(self) + except Exception: + print( + "Ignored failure when building extensions, pure python modules will be used instead") + # END ignore errors + def get_data_files(self): - """Can you feel the pain ? So, in python2.5 and python2.4 coming with maya, - the line dealing with the ``plen`` has a bug which causes it to truncate too much. - It is fixed in the system interpreters as they receive patches, and shows how - bad it is if something doesn't have proper unittests. - The code here is a plain copy of the python2.6 version which works for all. - - Generate list of '(package,src_dir,build_dir,filenames)' tuples""" - data = [] - if not self.packages: - return data - - # this one is just for the setup tools ! They don't iniitlialize this variable - # when they should, but do it on demand using this method.Its crazy - if hasattr(self, 'analyze_manifest'): - self.analyze_manifest() - # END handle setuptools ... - - for package in self.packages: - # Locate package source directory - src_dir = self.get_package_dir(package) - - # Compute package build directory - build_dir = os.path.join(*([self.build_lib] + package.split('.'))) - - # Length of path to strip from found files - plen = 0 - if src_dir: - plen = len(src_dir)+1 - - # Strip directory from globbed filenames - filenames = [ - file[plen:] for file in self.find_data_files(package, src_dir) - ] - data.append((package, src_dir, build_dir, filenames)) - return data - + """Can you feel the pain ? So, in python2.5 and python2.4 coming with maya, + the line dealing with the ``plen`` has a bug which causes it to truncate too much. + It is fixed in the system interpreters as they receive patches, and shows how + bad it is if something doesn't have proper unittests. + The code here is a plain copy of the python2.6 version which works for all. + + Generate list of '(package,src_dir,build_dir,filenames)' tuples""" + data = [] + if not self.packages: + return data + + # this one is just for the setup tools ! They don't iniitlialize this variable + # when they should, but do it on demand using this method.Its crazy + if hasattr(self, 'analyze_manifest'): + self.analyze_manifest() + # END handle setuptools ... + + for package in self.packages: + # Locate package source directory + src_dir = self.get_package_dir(package) + + # Compute package build directory + build_dir = os.path.join(*([self.build_lib] + package.split('.'))) + + # Length of path to strip from found files + plen = 0 + if src_dir: + plen = len(src_dir) + 1 + + # Strip directory from globbed filenames + filenames = [ + file[plen:] for file in self.find_data_files(package, src_dir) + ] + data.append((package, src_dir, build_dir, filenames)) + return data + build_py.get_data_files = get_data_files if setuptools_build_py_module: - setuptools_build_py_module.build_py._get_data_files = get_data_files + setuptools_build_py_module.build_py._get_data_files = get_data_files # END apply setuptools patch too # NOTE: This is currently duplicated from the gitdb.__init__ module, as we cannot # satisfy the dependencies at installation time, unfortunately, due to inherent limitations -# of distutils, which cannot install the prerequesites of a package before the acutal package. +# of distutils, which cannot install the prerequesites of a package before +# the acutal package. __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" version_info = (0, 5, 4) __version__ = '.'.join(str(i) for i in version_info) -setup(cmdclass={'build_ext':build_ext_nofail}, - name = "gitdb", - version = __version__, - description = "Git Object Database", - author = __author__, - author_email = __contact__, - url = __homepage__, - packages = ('gitdb', 'gitdb.db'), - package_dir = {'gitdb':'gitdb'}, - ext_modules=[Extension('gitdb._perf', ['gitdb/_fun.c', 'gitdb/_delta_apply.c'], include_dirs=['gitdb'])], - license = "BSD License", - zip_safe=False, - requires=('async (>=0.6.1)', 'smmap (>=0.8.0)'), - install_requires=('async >= 0.6.1', 'smmap >= 0.8.0'), - long_description = """GitDB is a pure-Python git object database""" - ) +setup( + cmdclass={ + 'build_ext': build_ext_nofail}, + name="gitdb", + version=__version__, + description="Git Object Database", + author=__author__, + author_email=__contact__, + url=__homepage__, + packages=( + 'gitdb', + 'gitdb.db'), + package_dir = { + 'gitdb': 'gitdb'}, + ext_modules=[ + Extension( + 'gitdb._perf', + [ + 'gitdb/_fun.c', + 'gitdb/_delta_apply.c'], + include_dirs=['gitdb'])], + license = "BSD License", + zip_safe=False, + requires=( + 'async (>=0.6.1)', + 'smmap (>=0.8.0)'), + install_requires=( + 'async >= 0.6.1', + 'smmap >= 0.8.0'), + long_description = """GitDB is a pure-Python git object database""" ) From a1e844cf1779c566264bb13f15a08c8ae33c828f Mon Sep 17 00:00:00 2001 From: Darragh Bailey Date: Thu, 6 Nov 2014 16:34:37 +0000 Subject: [PATCH 2/3] Manual pep8/pyflake error fixes Fix multiple issues about unused imports without breaking tests where assumptions are made on exporting imports to dependent modules. Remove unused variables and correct renaming whitespace issues. --- gitdb/__init__.py | 4 +- gitdb/base.py | 49 +++++++------- gitdb/db/base.py | 25 ++++---- gitdb/db/git.py | 11 +--- gitdb/db/loose.py | 11 +--- gitdb/db/mem.py | 4 +- gitdb/db/pack.py | 15 ++--- gitdb/db/ref.py | 3 +- gitdb/fun.py | 32 ++++------ gitdb/pack.py | 64 +++++++++---------- gitdb/stream.py | 44 ++++++------- gitdb/test/__init__.py | 4 +- gitdb/test/db/lib.py | 3 +- gitdb/test/db/test_pack.py | 5 +- gitdb/test/db/test_ref.py | 1 - gitdb/test/lib.py | 22 +++---- gitdb/test/performance/lib.py | 18 +++--- gitdb/test/performance/test_pack.py | 1 - gitdb/test/performance/test_pack_streaming.py | 1 - gitdb/test/performance/test_stream.py | 8 +-- gitdb/test/test_example.py | 4 +- gitdb/test/test_pack.py | 10 ++- gitdb/test/test_stream.py | 7 +- gitdb/typ.py | 4 +- gitdb/util.py | 20 +++--- setup.py | 5 +- 26 files changed, 163 insertions(+), 212 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 97e2aed..5cf41af 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -7,7 +7,7 @@ import sys import os -#{ Initialization +# { Initialization def _init_externals(): @@ -24,7 +24,7 @@ def _init_externals(): # END verify import # END handel imports -#} END initialization +# } END initialization _init_externals() diff --git a/gitdb/base.py b/gitdb/base.py index b8713bf..bbb7493 100644 --- a/gitdb/base.py +++ b/gitdb/base.py @@ -3,10 +3,7 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with basic data structures - they are designed to be lightweight and fast""" -from .util import ( - bin_to_hex, - zlib -) +from .util import bin_to_hex from .fun import ( type_id_to_type_map, @@ -17,7 +14,7 @@ 'OStream', 'OPackStream', 'ODeltaPackStream', 'IStream', 'InvalidOInfo', 'InvalidOStream') -#{ ODB Bases +# { ODB Bases class OInfo(tuple): @@ -41,7 +38,7 @@ def __new__(cls, sha, type, size): def __init__(self, *args): tuple.__init__(self) - #{ Interface + # { Interface @property def binsha(self): """:return: our sha as binary, 20 bytes""" @@ -63,7 +60,7 @@ def type_id(self): @property def size(self): return self[2] - #} END interface + # } END interface class OPackInfo(tuple): @@ -82,7 +79,7 @@ def __new__(cls, packoffset, type, size): def __init__(self, *args): tuple.__init__(self) - #{ Interface + # { Interface @property def pack_offset(self): @@ -100,7 +97,7 @@ def type_id(self): def size(self): return self[2] - #} END interface + # } END interface class ODeltaPackInfo(OPackInfo): @@ -114,11 +111,11 @@ class ODeltaPackInfo(OPackInfo): def __new__(cls, packoffset, type, size, delta_info): return tuple.__new__(cls, (packoffset, type, size, delta_info)) - #{ Interface + # { Interface @property def delta_info(self): return self[3] - #} END interface + # } END interface class OStream(OInfo): @@ -135,7 +132,7 @@ def __new__(cls, sha, type, size, stream, *args, **kwargs): def __init__(self, *args, **kwargs): tuple.__init__(self) - #{ Stream Reader Interface + # { Stream Reader Interface def read(self, size=-1): return self[3].read(size) @@ -144,7 +141,7 @@ def read(self, size=-1): def stream(self): return self[3] - #} END stream reader interface + # } END stream reader interface class ODeltaStream(OStream): @@ -155,13 +152,13 @@ def __new__(cls, sha, type, size, stream, *args, **kwargs): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (sha, type, size, stream)) - #{ Stream Reader Interface + # { Stream Reader Interface @property def size(self): return self[3].size - #} END stream reader interface + # } END stream reader interface class OPackStream(OPackInfo): @@ -174,14 +171,14 @@ def __new__(cls, packoffset, type, size, stream, *args): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (packoffset, type, size, stream)) - #{ Stream Reader Interface + # { Stream Reader Interface def read(self, size=-1): return self[3].read(size) @property def stream(self): return self[3] - #} END stream reader interface + # } END stream reader interface class ODeltaPackStream(ODeltaPackInfo): @@ -192,14 +189,14 @@ class ODeltaPackStream(ODeltaPackInfo): def __new__(cls, packoffset, type, size, delta_info, stream): return tuple.__new__(cls, (packoffset, type, size, delta_info, stream)) - #{ Stream Reader Interface + # { Stream Reader Interface def read(self, size=-1): return self[4].read(size) @property def stream(self): return self[4] - #} END stream reader interface + # } END stream reader interface class IStream(list): @@ -219,7 +216,7 @@ def __new__(cls, type, size, stream, sha=None): def __init__(self, type, size, stream, sha=None): list.__init__(self, (sha, type, size, stream, None)) - #{ Interface + # { Interface @property def hexsha(self): """:return: our sha, hex encoded, 40 bytes""" @@ -235,18 +232,18 @@ def _set_error(self, exc): error = property(_error, _set_error) - #} END interface + # } END interface - #{ Stream Reader Interface + # { Stream Reader Interface def read(self, size=-1): """Implements a simple stream reader interface, passing the read call on to our internal stream""" return self[3].read(size) - #} END stream reader interface + # } END stream reader interface - #{ interface + # { interface def _set_binsha(self, binsha): self[0] = binsha @@ -280,7 +277,7 @@ def _set_stream(self, stream): stream = property(_stream, _set_stream) - #} END odb info interface + # } END odb info interface class InvalidOInfo(tuple): @@ -315,4 +312,4 @@ class InvalidOStream(InvalidOInfo): """Carries information about an invalid ODB stream""" __slots__ = tuple() -#} END ODB Bases +# } END ODB Bases diff --git a/gitdb/db/base.py b/gitdb/db/base.py index bc866a9..9eb553d 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -34,7 +34,7 @@ class ObjectDBR(object): def __contains__(self, sha): return self.has_obj - #{ Query Interface + # { Query Interface def has_object(self, sha): """ :return: True if the object identified by the given 20 bytes @@ -50,7 +50,7 @@ def has_object_async(self, reader): task = ChannelThreadTask( reader, str( self.has_object_async), lambda sha: ( - sha, self.has_object(sha))) + sha, self.has_object(sha))) return pool.add_task(task) def info(self, sha): @@ -93,7 +93,7 @@ def sha_iter(self): """Return iterator yielding 20 byte shas for all objects in this data base""" raise NotImplementedError() - #} END query interface + # } END query interface class ObjectDBW(object): @@ -103,7 +103,7 @@ class ObjectDBW(object): def __init__(self, *args, **kwargs): self._ostream = None - #{ Edit Interface + # { Edit Interface def set_ostream(self, stream): """ Adjusts the stream to which all data should be sent when storing new objects @@ -154,7 +154,7 @@ def store_async(self, reader): task = ChannelThreadTask(reader, str(self.store_async), self.store) return pool.add_task(task) - #} END edit interface + # } END edit interface class FileDBBase(object): @@ -172,7 +172,7 @@ def __init__(self, root_path): super(FileDBBase, self).__init__() self._root_path = root_path - #{ Interface + # { Interface def root_path(self): """:return: path at which this db operates""" return self._root_path @@ -182,14 +182,14 @@ def db_path(self, rela_path): :return: the given relative path relative to our database root, allowing to pontentially access datafiles""" return join(self._root_path, rela_path) - #} END interface + # } END interface class CachingDB(object): """A database which uses caches to speed-up access""" - #{ Interface + # { Interface def update_cache(self, force=False): """ Call this method if the underlying data changed to trigger an update @@ -206,7 +206,6 @@ def _databases_recursive(database, output): """Fill output list with database from db, in order. Deals with Loose, Packed and compound databases.""" if isinstance(database, CompoundDB): - compounds = list() dbs = database.databases() output.extend(db for db in dbs if not isinstance(db, CompoundDB)) for cdb in (db for db in dbs if isinstance(db, CompoundDB)): @@ -249,7 +248,7 @@ def _db_query(self, sha): # END for each database raise BadObject(sha) - #{ ObjectDBR interface + # { ObjectDBR interface def has_object(self, sha): try: @@ -272,9 +271,9 @@ def size(self): def sha_iter(self): return chain(*(db.sha_iter() for db in self._dbs)) - #} END object DBR Interface + # } END object DBR Interface - #{ Interface + # { Interface def databases(self): """:return: tuple of database instances we use for lookups""" @@ -330,4 +329,4 @@ def partial_to_complete_sha_hex(self, partial_hexsha): raise BadObject(partial_binsha) return candidate - #} END interface + # } END interface diff --git a/gitdb/db/git.py b/gitdb/db/git.py index 57a6631..f573f31 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -12,12 +12,7 @@ from .pack import PackedDB from .ref import ReferenceDB -from gitdb.util import LazyMixin -from gitdb.exc import ( - InvalidDBRoot, - BadObject, - AmbiguousObjectName -) +from gitdb.exc import InvalidDBRoot import os __all__ = ('GitDB', ) @@ -72,7 +67,7 @@ def _set_cache_(self, attr): super(GitDB, self)._set_cache_(attr) # END handle attrs - #{ ObjectDBW interface + # { ObjectDBW interface def store(self, istream): return self._loose_db.store(istream) @@ -83,4 +78,4 @@ def ostream(self): def set_ostream(self, ostream): return self._loose_db.set_ostream(ostream) - #} END objectdbw interface + # } END objectdbw interface diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 900092a..3257f7b 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -10,7 +10,6 @@ from gitdb.exc import ( - InvalidDBRoot, BadObject, AmbiguousObjectName ) @@ -52,7 +51,6 @@ ) import tempfile -import mmap import sys import os @@ -82,7 +80,7 @@ def __init__(self, root_path): # is why it is per instance self._fd_open_flags = getattr(os, 'O_NOATIME', 0) - #{ Interface + # { Interface def object_path(self, hexsha): """ :return: path at which the object with the given hexsha would be stored, @@ -124,7 +122,7 @@ def partial_to_complete_sha_hex(self, partial_hexsha): raise BadObject(partial_hexsha) return candidate - #} END interface + # } END interface def _map_loose_object(self, sha): """ @@ -147,11 +145,6 @@ def _map_loose_object(self, sha): raise BadObject(sha) # END handle error # END exception handling - try: - return mmap.mmap(fd, 0, access=mmap.ACCESS_READ) - finally: - os.close(fd) - # END assure file is closed def set_ostream(self, stream): """:raise TypeError: if the stream does not support the Sha1Writer interface""" diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 69a523c..a076f7d 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -92,7 +92,7 @@ def size(self): def sha_iter(self): return self._cache.iterkeys() - #{ Interface + # { Interface def stream_copy(self, sha_iter, odb): """Copy the streams as identified by sha's yielded by sha_iter into the given odb The streams will be copied directly @@ -114,4 +114,4 @@ def stream_copy(self, sha_iter, odb): count += 1 # END for each sha return count - #} END interface + # } END interface diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 81b7cc0..66a07c0 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -25,7 +25,7 @@ __all__ = ('PackedDB', ) -#{ Utilities +# { Utilities class PackedDB(FileDBBase, ObjectDBR, CachingDB, LazyMixin): @@ -84,7 +84,7 @@ def _pack_info(self, sha): # and leave it to the super-caller to trigger that raise BadObject(sha) - #{ Object DB Read + # { Object DB Read def has_object(self, sha): try: @@ -103,7 +103,6 @@ def stream(self, sha): return entity.stream_at_index(index) def sha_iter(self): - sha_list = list() for entity in self.entities(): index = entity.index() sha_by_index = index.sha @@ -116,9 +115,9 @@ def size(self): sizes = [item[1].index().size() for item in self._entities] return reduce(lambda x, y: x + y, sizes, 0) - #} END object db read + # } END object db read - #{ object db write + # { object db write def store(self, istream): """Storing individual objects is not feasible as a pack is designed to @@ -130,9 +129,9 @@ def store_async(self, reader): # TODO: add ObjectDBRW before implementing this raise NotImplementedError() - #} END object db write + # } END object db write - #{ Interface + # { Interface def update_cache(self, force=False): """ @@ -211,4 +210,4 @@ def partial_to_complete_sha(self, partial_binsha, canonical_length): # still not found ? raise BadObject(partial_binsha) - #} END interface + # } END interface diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index df214af..34cbfeb 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -6,7 +6,6 @@ CompoundDB, ) -import os __all__ = ('ReferenceDB', ) @@ -74,7 +73,7 @@ def _update_dbs_from_ref_file(self): db.databases() # END verification self._dbs.append(db) - except Exception as e: + except Exception: # ignore invalid paths or issues pass # END for each path to add diff --git a/gitdb/fun.py b/gitdb/fun.py index d782037..3d781ec 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -6,10 +6,6 @@ Keeping this code separate from the beginning makes it easier to out-source it into c later, if required""" -from .exc import ( - BadObjectType -) - from .util import zlib from functools import reduce decompressobj = zlib.decompressobj @@ -62,7 +58,7 @@ 'create_pack_object_header') -#{ Structures +# { Structures def _set_delta_rbound(d, size): """Truncate the given delta to the given size @@ -143,7 +139,7 @@ def __repr__(self): return "DeltaChunk(%i, %i, %s, %s)" % ( self.to, self.ts, self.so, self.data or "") - #{ Interface + # { Interface def rbound(self): return self.to + self.ts @@ -152,7 +148,7 @@ def has_data(self): """:return: True if the instance has data to add to the target stream""" return self.data is not None - #} END interface + # } END interface def _closest_index(dcl, absofs): @@ -262,7 +258,6 @@ def compress(self): if slen < 2: return self i = 0 - slen_orig = slen first_data_index = None while i < slen: @@ -403,9 +398,9 @@ def connect_with_next_base(self, bdcl): return True -#} END structures +# } END structures -#{ Routines +# { Routines def is_loose_object(m): """ @@ -546,7 +541,7 @@ def stream_copy(read, write, size, chunk_size): return dbw -def connect_deltas(dstreams): +def _connect_deltas(dstreams): """ Read the condensed delta chunk information from dstream and merge its information into a list of existing delta chunks @@ -630,6 +625,12 @@ def connect_deltas(dstreams): return tdcl +try: + # raise ImportError; # DEBUG + from _perf import connect_deltas +except ImportError: + connect_deltas = _connect_deltas + def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): """ @@ -708,11 +709,4 @@ def is_equal_canonical_sha(canonical_length, match, sha1): # END handle uneven canonnical length return True -#} END routines - - -try: - # raise ImportError; # DEBUG - from _perf import connect_deltas -except ImportError: - pass +# } END routines diff --git a/gitdb/pack.py b/gitdb/pack.py index 03e5080..f558022 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -4,6 +4,7 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Contains PackIndexFile and PackFile implementations""" from gitdb.exc import ( + AmbiguousObjectName, BadObject, UnsupportedOperation, ParseError @@ -53,10 +54,7 @@ FlexibleSha1Writer ) -from struct import ( - pack, - unpack, -) +from struct import pack from binascii import crc32 @@ -69,7 +67,7 @@ __all__ = ('PackIndexFile', 'PackFile', 'PackEntity') -#{ Utilities +# { Utilities def pack_object_at(cursor, offset, as_stream): """ @@ -171,7 +169,7 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): return (br, bw, crc) -#} END utilities +# } END utilities class IndexWriter(object): @@ -308,7 +306,7 @@ def _set_cache_(self, attr): self._initialize() # END handle attributes - #{ Access V1 + # { Access V1 def _entry_v1(self, i): """:return: tuple(offset, binsha, 0)""" @@ -327,9 +325,9 @@ def _crc_v1(self, i): """unsupported""" return 0 - #} END access V1 + # } END access V1 - #{ Access V2 + # { Access V2 def _entry_v2(self, i): """:return: tuple(offset, binsha, crc)""" return (self._offset_v2(i), self._sha_v2(i), self._crc_v2(i)) @@ -360,9 +358,9 @@ def _crc_v2(self, i): return unpack_from( ">L", self._cursor.map(), self._crc_list_offset + i * 4)[0] - #} END access V2 + # } END access V2 - #{ Initialization + # { Initialization def _initialize(self): """initialize base data""" @@ -384,9 +382,9 @@ def _read_fanout(self, byte_offset): # END for each entry return out - #} END initialization + # } END initialization - #{ Properties + # { Properties def version(self): return self._version @@ -429,7 +427,7 @@ def offsets(self): return tuple(self.offset(index) for index in xrange(self.size())) # END handle version - def sha_to_index(self, sha): + def _sha_to_index(self, sha): """ :return: index usable with the ``offset`` or ``entry`` method, or None if the sha was not found in this pack index @@ -456,6 +454,15 @@ def sha_to_index(self, sha): # END bisect return None + if 'PackIndexFile_sha_to_index' in globals(): + # NOTE: Its just about 25% faster, the major bottleneck might be the attr + # accesses + def sha_to_index(self, sha): + return PackIndexFile_sha_to_index(self, sha) + else: + sha_to_index = _sha_to_index + # END redefine heavy-hitter with c version + def partial_sha_to_index(self, partial_bin_sha, canonical_length): """ :return: index as in `sha_to_index` or None if the sha was not found in this @@ -507,14 +514,7 @@ def partial_sha_to_index(self, partial_bin_sha, canonical_length): # END if we found something return None - if 'PackIndexFile_sha_to_index' in globals(): - # NOTE: Its just about 25% faster, the major bottleneck might be the attr - # accesses - def sha_to_index(self, sha): - return PackIndexFile_sha_to_index(self, sha) - # END redefine heavy-hitter with c version - - #} END properties + # } END properties class PackFile(LazyMixin): @@ -578,7 +578,7 @@ def _iter_objects(self, start_offset, as_stream=True): yield ostream # END until we have read everything - #{ Pack Information + # { Pack Information def size(self): """:return: The amount of objects stored in this pack""" @@ -605,9 +605,9 @@ def checksum(self): def path(self): """:return: path to the packfile""" return self._packpath - #} END pack information + # } END pack information - #{ Pack Specific + # { Pack Specific def collect_streams(self, offset): """ @@ -634,9 +634,9 @@ def collect_streams(self, offset): # END while chaining streams return out - #} END pack specific + # } END pack specific - #{ Read-Database like Interface + # { Read-Database like Interface def info(self, offset): """Retrieve information about the object at the given file-absolute offset @@ -665,7 +665,7 @@ def stream_iter(self, start_offset=0): to determine the bounds between the objects""" return self._iter_objects(start_offset, as_stream=True) - #} END Read-Database like Interface + # } END Read-Database like Interface class PackEntity(LazyMixin): @@ -775,7 +775,7 @@ def _object(self, sha, as_stream, index=-1): return OInfo(sha, streams[-1].type, target_size) # END handle stream - #{ Read-Database like Interface + # { Read-Database like Interface def info(self, sha): """Retrieve information about the object identified by the given sha @@ -802,9 +802,9 @@ def stream_at_index(self, index): object""" return self._object(None, True, index) - #} END Read-Database like Interface + # } END Read-Database like Interface - #{ Interface + # { Interface def pack(self): """:return: the underlying pack file instance""" @@ -1057,4 +1057,4 @@ def create(cls, object_iter, base_dir, object_count=None, return cls(new_pack_path) - #} END interface + # } END interface diff --git a/gitdb/stream.py b/gitdb/stream.py index c11a7f7..20544ce 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -4,7 +4,6 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php from cStringIO import StringIO -import errno import mmap import os @@ -13,7 +12,6 @@ stream_copy, apply_delta_data, connect_deltas, - DeltaChunkList, delta_types ) @@ -45,7 +43,7 @@ 'NullStream') -#{ RO Streams +# { RO Streams class DecompressMemMapReader(LazyMixin): @@ -139,7 +137,7 @@ def _parse_header_info(self): return type, size - #{ Interface + # { Interface @classmethod def new(self, m, close_on_deletion=False): @@ -205,7 +203,7 @@ def compressed_bytes_read(self): # from the count already return self._cbr - #} END interface + # } END interface def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): """Allows to reset the stream to restart reading @@ -352,9 +350,9 @@ class DeltaApplyReader(LazyMixin): "_br" # number of bytes read ) - #{ Configuration + # { Configuration k_max_memory_move = 250 * 1000 * 1000 - #} END configuration + # } END configuration def __init__(self, stream_list): """Initialize this instance with a list of streams, the first stream being @@ -403,8 +401,8 @@ def _set_cache_too_slow_without_c(self, attr): mmap.PAGESIZE) # APPLY CHUNKS - write = self._mm_target.write - dcl.apply(bbuf, write) + write_func = self._mm_target.write + dcl.apply(bbuf, write_func) self._mm_target.seek(0) @@ -489,13 +487,13 @@ def _set_cache_brute_(self, attr): self._mm_target = bbuf self._size = final_target_size - #{ Configuration + # { Configuration if not has_perf_mod: _set_cache_ = _set_cache_brute_ else: _set_cache_ = _set_cache_too_slow_without_c - #} END configuration + # } END configuration def read(self, count=0): bl = self._size - self._br # bytes left @@ -517,7 +515,7 @@ def seek(self, offset, whence=getattr(os, 'SEEK_SET', 0)): self._br = 0 self._mm_target.seek(0) - #{ Interface + # { Interface @classmethod def new(cls, stream_list): @@ -545,9 +543,9 @@ def new(cls, stream_list): return cls(stream_list) - #} END interface + # } END interface - #{ OInfo like Interface + # { OInfo like Interface @property def type(self): @@ -562,13 +560,13 @@ def size(self): """:return: number of uncompressed bytes in the stream""" return self._size - #} END oinfo like interface + # } END oinfo like interface -#} END RO streams +# } END RO streams -#{ W Streams +# { W Streams class Sha1Writer(object): @@ -579,7 +577,7 @@ class Sha1Writer(object): def __init__(self): self.sha1 = make_sha() - #{ Stream Interface + # { Stream Interface def write(self, data): """:raise IOError: If not all bytes could be written @@ -589,7 +587,7 @@ def write(self, data): # END stream interface - #{ Interface + # { Interface def sha(self, as_hex=False): """:return: sha so far @@ -598,7 +596,7 @@ def sha(self, as_hex=False): return self.sha1.hexdigest() return self.sha1.digest() - #} END interface + # } END interface class FlexibleSha1Writer(Sha1Writer): @@ -667,7 +665,7 @@ def __init__(self, fd): self.fd = fd self.zip = zlib.compressobj(zlib.Z_BEST_SPEED) - #{ Stream Interface + # { Stream Interface def write(self, data): """:raise IOError: If not all bytes could be written @@ -685,7 +683,7 @@ def close(self): raise self.exc return close(self.fd) - #} END stream interface + # } END stream interface class FDStream(object): @@ -738,4 +736,4 @@ def write(self, data): return len(data) -#} END W streams +# } END W streams diff --git a/gitdb/test/__init__.py b/gitdb/test/__init__.py index dcf35cc..d1d36c7 100644 --- a/gitdb/test/__init__.py +++ b/gitdb/test/__init__.py @@ -5,7 +5,7 @@ import gitdb.util -#{ Initialization +# { Initialization def _init_pool(): @@ -15,4 +15,4 @@ def _init_pool(): gitdb.util.pool.set_size(size) -#} END initialization +# } END initialization diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index 6e7d04c..08f27b6 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -6,12 +6,11 @@ from gitdb.test.lib import ( with_rw_directory, with_packs_rw, - ZippedStoreShaWriter, fixture_path, TestBase ) -from gitdb.stream import Sha1Writer +from gitdb.stream import Sha1Writer, ZippedStoreShaWriter from gitdb.base import ( IStream, diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index 74ad989..af2be92 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -4,7 +4,6 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php from .lib import * from gitdb.db import PackedDB -from gitdb.test.lib import fixture_path from gitdb.exc import BadObject, AmbiguousObjectName @@ -47,8 +46,8 @@ def test_writing(self, path): random.shuffle(sha_list) for sha in sha_list: - info = pdb.info(sha) - stream = pdb.stream(sha) + pdb.info(sha) + pdb.stream(sha) # END for each sha to query # test short finding - be a bit more brutal here diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 4f930ce..83ec2c6 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -25,7 +25,6 @@ def make_alt_file(self, alt_path, alt_list): @with_rw_directory def test_writing(self, path): - NULL_BIN_SHA = '\0' * 20 alt_path = os.path.join(path, 'alternates') rdb = ReferenceDB(alt_path) diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index d9a8d99..f2d341b 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -6,12 +6,6 @@ from gitdb import ( OStream, ) -from gitdb.stream import ( - Sha1Writer, - ZippedStoreShaWriter -) - -from gitdb.util import zlib import sys import random @@ -26,16 +20,16 @@ import gc -#{ Bases +# { Bases class TestBase(unittest.TestCase): """Base class for all tests""" -#} END bases +# } END bases -#{ Decorators +# { Decorators def with_rw_directory(func): """Create a temporary directory which can be written to, remove it if the @@ -81,9 +75,9 @@ def wrapper(self, path): wrapper.__name__ = func.__name__ return wrapper -#} END decorators +# } END decorators -#{ Routines +# { Routines def fixture_path(relapath=''): @@ -136,9 +130,9 @@ def make_memory_file(size_in_bytes, randomize=False): d = make_bytes(size_in_bytes, randomize) return len(d), StringIO(d) -#} END routines +# } END routines -#{ Stream Utilities +# { Stream Utilities class DummyStream(object): @@ -169,4 +163,4 @@ def _assert(self): assert self.args assert self.myarg -#} END stream utilitiess +# } END stream utilitiess diff --git a/gitdb/test/performance/lib.py b/gitdb/test/performance/lib.py index 1bf4211..b89c9d4 100644 --- a/gitdb/test/performance/lib.py +++ b/gitdb/test/performance/lib.py @@ -5,16 +5,14 @@ """Contains library functions""" import os from gitdb.test.lib import * -import shutil -import tempfile -#{ Invvariants +# { Invvariants k_env_git_repo = "GITDB_TEST_GIT_REPO_BASE" -#} END invariants +# } END invariants -#{ Utilities +# { Utilities def resolve_or_fail(env_var): """:return: resolved environment variable or raise EnvironmentError""" try: @@ -24,10 +22,10 @@ def resolve_or_fail(env_var): "Please set the %r envrionment variable and retry" % env_var) # END exception handling -#} END utilities +# } END utilities -#{ Base Classes +# { Base Classes class TestBigRepoR(TestBase): @@ -38,10 +36,10 @@ class TestBigRepoR(TestBase): * read-only base path of the git source repository, i.e. .../git/.git""" - #{ Invariants + # { Invariants head_sha_2k = '235d521da60e4699e5bd59ac658b5b48bd76ddca' head_sha_50 = '32347c375250fd470973a5d76185cac718955fd5' - #} END invariants + # } END invariants @classmethod def setUpAll(cls): @@ -53,4 +51,4 @@ def setUpAll(cls): assert cls.gitrepopath.endswith('.git') -#} END base classes +# } END base classes diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index bc49951..8422908 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -13,7 +13,6 @@ import sys import os from time import time -import random from nose import SkipTest diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index ff0c1b0..93e9636 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -41,7 +41,6 @@ def test_pack_writing(self): ni = 5000 count = 0 - total_size = 0 st = time() for sha in pdb.sha_iter(): count += 1 diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 30f54c5..dafbc9c 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -3,7 +3,6 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Performance data streaming performance""" -from lib import TestBigRepoR from gitdb.db import * from gitdb.base import * from gitdb.stream import * @@ -19,12 +18,9 @@ ChannelThreadTask, ) -from cStringIO import StringIO from time import time import os import sys -import stat -import subprocess from lib import ( @@ -34,7 +30,7 @@ ) -#{ Utilities +# { Utilities def read_chunked_stream(stream): total = 0 while True: @@ -58,7 +54,7 @@ def __init__(self, *args): self.max_chunksize = 1 -#} END utilities +# } END utilities class TestObjDBPerformance(TestBigRepoR): diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index ab38784..f5b88b1 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -52,10 +52,10 @@ def test_base(self): info_reader = ldb.stream_async(reader) # read one - info = info_reader.read(1)[0] + info_reader.read(1)[0] # read all the rest until depletion - ostreams = info_reader.read() + info_reader.read() # set the pool to use two threads pool.set_size(2) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 96fd8f3..d9d71e8 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -6,7 +6,6 @@ from .lib import ( TestBase, with_rw_directory, - with_packs_rw, fixture_path ) from gitdb.stream import DeltaApplyReader @@ -25,18 +24,17 @@ from gitdb.fun import delta_types from gitdb.exc import UnsupportedOperation from gitdb.util import to_bin_sha -from itertools import izip, chain +from itertools import izip from nose import SkipTest import os -import sys import tempfile -#{ Utilities +# { Utilities def bin_sha_from_filename(filename): return to_bin_sha(os.path.splitext(os.path.basename(filename))[0][5:]) -#} END utilities +# } END utilities class TestPack(TestBase): @@ -179,7 +177,7 @@ def test_pack_entity(self, rw_dir): # we return fully resolved items, which is implied by the sha # centric access - assert not info.type_id in delta_types + assert info.type_id not in delta_types # try all calls assert len(entity.collect_streams(info.binsha)) diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index ac088a8..5e40d3a 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -6,24 +6,19 @@ from .lib import ( TestBase, DummyStream, - Sha1Writer, make_bytes, make_object, fixture_path ) from gitdb import * -from gitdb.util import ( - NULL_HEX_SHA, - hex_to_bin -) +from gitdb.util import hex_to_bin from gitdb.util import zlib from gitdb.typ import ( str_blob_type ) -import time import tempfile import os diff --git a/gitdb/typ.py b/gitdb/typ.py index 0328287..5aec951 100644 --- a/gitdb/typ.py +++ b/gitdb/typ.py @@ -4,11 +4,11 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module containing information about types known to the database""" -#{ String types +# { String types str_blob_type = "blob" str_commit_type = "commit" str_tree_type = "tree" str_tag_type = "tag" -#} END string types +# } END string types diff --git a/gitdb/util.py b/gitdb/util.py index 99455de..488229a 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -59,16 +59,16 @@ def unpack_from(fmt, data, offset=0): # END own unpack_from implementation -#{ Globals +# { Globals # A pool distributing tasks, initially with zero threads, hence everything # will be handled in the main thread pool = ThreadPool(0) -#} END globals +# } END globals -#{ Aliases +# { Aliases hex_to_bin = binascii.a2b_hex bin_to_hex = binascii.b2a_hex @@ -96,9 +96,9 @@ def unpack_from(fmt, data, offset=0): NULL_HEX_SHA = "0" * 40 NULL_BIN_SHA = "\0" * 20 -#} END Aliases +# } END Aliases -#{ compatibility stuff ... +# { compatibility stuff ... class _RandomAccessStringIO(object): @@ -122,9 +122,9 @@ def __getitem__(self, i): def __getslice__(self, start, end): return self.getvalue()[start:end] -#} END compatibility stuff ... +# } END compatibility stuff ... -#{ Routines +# { Routines def make_sha(source=''): @@ -226,10 +226,10 @@ def to_bin_sha(sha): return hex_to_bin(sha) -#} END routines +# } END routines -#{ Utilities +# { Utilities class LazyMixin(object): @@ -394,4 +394,4 @@ def _end_writing(self, successful=True): os.remove(lockfile) # END successful handling -#} END utilities +# } END utilities diff --git a/setup.py b/setup.py index 90d7f72..59b0056 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python from distutils.core import setup, Extension from distutils.command.build_py import build_py -from distutils.command.build_ext import build_ext import os import sys @@ -13,6 +12,8 @@ if 'setuptools' in sys.modules: import setuptools.command.build_py as setuptools_build_py_module from setuptools.command.build_ext import build_ext + else: + from distutils.command.build_ext import build_ext except ImportError: pass @@ -111,4 +112,4 @@ def get_data_files(self): install_requires=( 'async >= 0.6.1', 'smmap >= 0.8.0'), - long_description = """GitDB is a pure-Python git object database""" ) + long_description = """GitDB is a pure-Python git object database""") From 72f71bdbb020bcb568b6b8cd97b2137c90ccb627 Mon Sep 17 00:00:00 2001 From: Darragh Bailey Date: Thu, 6 Nov 2014 17:00:22 +0000 Subject: [PATCH 3/3] Work towards python3 compatible use of StringIO Add in try/except to catch import errors with cStringIO and use StringIO from the io module instead. Also ensure that where write access is needed, create the StringIO without an initial string. Need to avoid passing an initial buffer value to cStringIO.StringIO() as it will return a read-only StringI object. Instead create the object first with no initial arguments to get a proper StringIO instance and then write the desired buffer to it. --- gitdb/db/mem.py | 7 +++++-- gitdb/fun.py | 5 ++++- gitdb/stream.py | 6 +++++- gitdb/test/db/lib.py | 5 ++++- gitdb/test/lib.py | 7 +++++-- gitdb/test/test_example.py | 7 +++++-- gitdb/util.py | 17 +++++++++-------- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index a076f7d..ec622af 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -23,7 +23,10 @@ DecompressMemMapReader, ) -from cStringIO import StringIO +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO __all__ = ("MemoryDB", ) @@ -107,7 +110,7 @@ def stream_copy(self, sha_iter, odb): ostream = self.stream(sha) # compressed data including header - sio = StringIO(ostream.stream.data()) + sio = StringIO(ostream.stream.data()) # read-only with cStringIO istream = IStream(ostream.type, ostream.size, sio, sha) odb.store(istream) diff --git a/gitdb/fun.py b/gitdb/fun.py index 3d781ec..642686d 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -13,7 +13,10 @@ import mmap from itertools import islice, izip -from cStringIO import StringIO +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO # INVARIANTS OFS_DELTA = 6 diff --git a/gitdb/stream.py b/gitdb/stream.py index 20544ce..782db1d 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -3,7 +3,6 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php -from cStringIO import StringIO import mmap import os @@ -31,6 +30,11 @@ except ImportError: pass +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + __all__ = ( 'DecompressMemMapReader', 'FDCompressedSha1Writer', diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index 08f27b6..dd1484b 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -22,9 +22,12 @@ from gitdb.typ import str_blob_type from async import IteratorReader -from cStringIO import StringIO from struct import pack +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO __all__ = ('TestDBBase', 'with_rw_directory', 'with_packs_rw', 'fixture_path') diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index f2d341b..491536b 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -10,7 +10,6 @@ import sys import random from array import array -from cStringIO import StringIO import glob import unittest @@ -19,6 +18,10 @@ import os import gc +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO # { Bases @@ -128,7 +131,7 @@ def make_memory_file(size_in_bytes, randomize=False): """:return: tuple(size_of_stream, stream) :param randomize: try to produce a very random stream""" d = make_bytes(size_in_bytes, randomize) - return len(d), StringIO(d) + return len(d), StringIO(d) # read-only StringIO with cStringIO # } END routines diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index f5b88b1..91b2755 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -8,10 +8,13 @@ from gitdb.db import LooseObjectDB from gitdb.util import pool -from cStringIO import StringIO - from async import IteratorReader +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + class TestExamples(TestBase): diff --git a/gitdb/util.py b/gitdb/util.py index 488229a..84a984d 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -8,13 +8,10 @@ import sys import errno -from cStringIO import StringIO - -# in py 2.4, StringIO is only StringI, without write support. -# Hence we must use the python implementation for this -if sys.version_info[1] < 5: - from StringIO import StringIO -# END handle python 2.4 +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO try: import async.mod.zlib as zlib @@ -108,7 +105,11 @@ class _RandomAccessStringIO(object): __slots__ = '_sio' def __init__(self, buf=''): - self._sio = StringIO(buf) + # supplying an initial string to cStringIO.StringIO will result + # in a StringI object being returned which is read-only. + # Instead create and then write to it to get the read/write version + self._sio = StringIO() + self._sio.write(buf) def __getattr__(self, attr): return getattr(self._sio, attr)