~ubuntu-branches/ubuntu/saucy/python-git/saucy

« back to all changes in this revision

Viewing changes to git/util.py

  • Committer: Package Import Robot
  • Author(s): TANIGUCHI Takaki
  • Date: 2012-04-08 21:12:50 UTC
  • mfrom: (0.2.1) (1.2.1) (4.1.1 sid)
  • Revision ID: package-import@ubuntu.com-20120408211250-4vufz3g2krw0qz32
Tags: 0.3.2~RC1-1
* Team upload.
* New upstream release
* Add myself to Uploaders.
* Bump Standards-Version to 3.9.3.
  + debian/copyright: copyright-format 1.0
* Add python-gitdb to Depends:.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# utils.py
 
2
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
 
3
#
 
4
# This module is part of GitPython and is released under
 
5
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
 
6
 
 
7
import os
 
8
import re
 
9
import sys
 
10
import time
 
11
import stat
 
12
import shutil
 
13
import tempfile
 
14
import platform
 
15
 
 
16
from gitdb.util import (
 
17
                                                        make_sha, 
 
18
                                                        LockedFD, 
 
19
                                                        file_contents_ro, 
 
20
                                                        LazyMixin, 
 
21
                                                        to_hex_sha, 
 
22
                                                        to_bin_sha
 
23
                                                )
 
24
 
 
25
__all__ = ( "stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux", 
 
26
                        "join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList", 
 
27
                        "BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists',
 
28
                        'RemoteProgress', 'rmtree')
 
29
 
 
30
#{ Utility Methods
 
31
 
 
32
def rmtree(path):
 
33
        """Remove the given recursively.
 
34
        :note: we use shutil rmtree but adjust its behaviour to see whether files that
 
35
                couldn't be deleted are read-only. Windows will not remove them in that case"""
 
36
        def onerror(func, path, exc_info):
 
37
                if not os.access(path, os.W_OK):
 
38
                        # Is the error an access error ?
 
39
                        os.chmod(path, stat.S_IWUSR)
 
40
                        func(path)
 
41
                else:
 
42
                        raise
 
43
        # END end onerror
 
44
        return shutil.rmtree(path, False, onerror)
 
45
 
 
46
        
 
47
 
 
48
def stream_copy(source, destination, chunk_size=512*1024):
 
49
        """Copy all data from the source stream into the destination stream in chunks
 
50
        of size chunk_size
 
51
        
 
52
        :return: amount of bytes written"""
 
53
        br = 0
 
54
        while True:
 
55
                chunk = source.read(chunk_size)
 
56
                destination.write(chunk)
 
57
                br += len(chunk)
 
58
                if len(chunk) < chunk_size:
 
59
                        break
 
60
        # END reading output stream
 
61
        return br
 
62
 
 
63
def join_path(a, *p):
 
64
        """Join path tokens together similar to os.path.join, but always use 
 
65
        '/' instead of possibly '\' on windows."""
 
66
        path = a
 
67
        for b in p:
 
68
                if len(b) == 0:
 
69
                        continue
 
70
                if b.startswith('/'):
 
71
                        path += b[1:]
 
72
                elif path == '' or path.endswith('/'):
 
73
                        path +=  b
 
74
                else:
 
75
                        path += '/' + b
 
76
        # END for each path token to add
 
77
        return path
 
78
        
 
79
def to_native_path_windows(path):
 
80
        return path.replace('/','\\')
 
81
        
 
82
def to_native_path_linux(path):
 
83
        return path.replace('\\','/')
 
84
 
 
85
if sys.platform.startswith('win'):
 
86
        to_native_path = to_native_path_windows
 
87
else:
 
88
        # no need for any work on linux
 
89
        def to_native_path_linux(path):
 
90
                return path
 
91
        to_native_path = to_native_path_linux
 
92
 
 
93
def join_path_native(a, *p):
 
94
        """
 
95
        As join path, but makes sure an OS native path is returned. This is only 
 
96
                needed to play it safe on my dear windows and to assure nice paths that only 
 
97
                use '\'"""
 
98
        return to_native_path(join_path(a, *p))
 
99
        
 
100
def assure_directory_exists(path, is_file=False):
 
101
        """Assure that the directory pointed to by path exists.
 
102
        
 
103
        :param is_file: If True, path is assumed to be a file and handled correctly.
 
104
                Otherwise it must be a directory
 
105
        :return: True if the directory was created, False if it already existed"""
 
106
        if is_file:
 
107
                path = os.path.dirname(path)
 
108
        #END handle file 
 
109
        if not os.path.isdir(path):
 
110
                os.makedirs(path)
 
111
                return True
 
112
        return False
 
113
        
 
114
def get_user_id():
 
115
        """:return: string identifying the currently active system user as name@node
 
116
        :note: user can be set with the 'USER' environment variable, usually set on windows"""
 
117
        ukn = 'UNKNOWN'
 
118
        username = os.environ.get('USER', os.environ.get('USERNAME', ukn))
 
119
        if username == ukn and hasattr(os, 'getlogin'):
 
120
                username = os.getlogin()
 
121
        # END get username from login
 
122
        return "%s@%s" % (username, platform.node())
 
123
 
 
124
#} END utilities
 
125
 
 
126
#{ Classes
 
127
 
 
128
class RemoteProgress(object):
 
129
        """
 
130
        Handler providing an interface to parse progress information emitted by git-push
 
131
        and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.
 
132
        """
 
133
        _num_op_codes = 7
 
134
        BEGIN, END, COUNTING, COMPRESSING, WRITING, RECEIVING, RESOLVING = [1 << x for x in range(_num_op_codes)]
 
135
        STAGE_MASK = BEGIN|END
 
136
        OP_MASK = ~STAGE_MASK
 
137
        
 
138
        __slots__ = ("_cur_line", "_seen_ops")
 
139
        re_op_absolute = re.compile("(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
 
140
        re_op_relative = re.compile("(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")
 
141
        
 
142
        def __init__(self):
 
143
                self._seen_ops = list()
 
144
        
 
145
        def _parse_progress_line(self, line):
 
146
                """Parse progress information from the given line as retrieved by git-push
 
147
                or git-fetch
 
148
                
 
149
                :return: list(line, ...) list of lines that could not be processed"""
 
150
                # handle
 
151
                # Counting objects: 4, done. 
 
152
                # Compressing objects:  50% (1/2)       \rCompressing objects: 100% (2/2)       \rCompressing objects: 100% (2/2), done.
 
153
                self._cur_line = line
 
154
                sub_lines = line.split('\r')
 
155
                failed_lines = list()
 
156
                for sline in sub_lines:
 
157
                        # find esacpe characters and cut them away - regex will not work with 
 
158
                        # them as they are non-ascii. As git might expect a tty, it will send them
 
159
                        last_valid_index = None
 
160
                        for i,c in enumerate(reversed(sline)):
 
161
                                if ord(c) < 32:
 
162
                                        # its a slice index
 
163
                                        last_valid_index = -i-1 
 
164
                                # END character was non-ascii
 
165
                        # END for each character in sline
 
166
                        if last_valid_index is not None:
 
167
                                sline = sline[:last_valid_index]
 
168
                        # END cut away invalid part
 
169
                        sline = sline.rstrip()
 
170
                        
 
171
                        cur_count, max_count = None, None
 
172
                        match = self.re_op_relative.match(sline)
 
173
                        if match is None:
 
174
                                match = self.re_op_absolute.match(sline)
 
175
                                
 
176
                        if not match:
 
177
                                self.line_dropped(sline)
 
178
                                failed_lines.append(sline)
 
179
                                continue
 
180
                        # END could not get match
 
181
                        
 
182
                        op_code = 0
 
183
                        remote, op_name, percent, cur_count, max_count, message = match.groups()
 
184
                        
 
185
                        # get operation id
 
186
                        if op_name == "Counting objects":
 
187
                                op_code |= self.COUNTING
 
188
                        elif op_name == "Compressing objects":
 
189
                                op_code |= self.COMPRESSING
 
190
                        elif op_name == "Writing objects":
 
191
                                op_code |= self.WRITING
 
192
                        elif op_name == 'Receiving objects':
 
193
                                op_code |= self.RECEIVING
 
194
                        elif op_name == 'Resolving deltas':
 
195
                                op_code |= self.RESOLVING
 
196
                        else:
 
197
                                # Note: On windows it can happen that partial lines are sent
 
198
                                # Hence we get something like "CompreReceiving objects", which is 
 
199
                                # a blend of "Compressing objects" and "Receiving objects".
 
200
                                # This can't really be prevented, so we drop the line verbosely
 
201
                                # to make sure we get informed in case the process spits out new
 
202
                                # commands at some point.
 
203
                                self.line_dropped(sline)
 
204
                                sys.stderr.write("Operation name %r unknown - skipping line '%s'" % (op_name, sline))
 
205
                                # Note: Don't add this line to the failed lines, as we have to silently
 
206
                                # drop it
 
207
                                return failed_lines
 
208
                        # END handle op code
 
209
                        
 
210
                        # figure out stage
 
211
                        if op_code not in self._seen_ops:
 
212
                                self._seen_ops.append(op_code)
 
213
                                op_code |= self.BEGIN
 
214
                        # END begin opcode
 
215
                        
 
216
                        if message is None:
 
217
                                message = ''
 
218
                        # END message handling
 
219
                        
 
220
                        message = message.strip()
 
221
                        done_token = ', done.'
 
222
                        if message.endswith(done_token):
 
223
                                op_code |= self.END
 
224
                                message = message[:-len(done_token)]
 
225
                        # END end message handling
 
226
                        
 
227
                        self.update(op_code, cur_count, max_count, message)
 
228
                # END for each sub line
 
229
                return failed_lines
 
230
        
 
231
        def line_dropped(self, line):
 
232
                """Called whenever a line could not be understood and was therefore dropped."""
 
233
                pass
 
234
        
 
235
        def update(self, op_code, cur_count, max_count=None, message=''):
 
236
                """Called whenever the progress changes
 
237
                
 
238
                :param op_code:
 
239
                        Integer allowing to be compared against Operation IDs and stage IDs.
 
240
                        
 
241
                        Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation 
 
242
                        ID as well as END. It may be that BEGIN and END are set at once in case only
 
243
                        one progress message was emitted due to the speed of the operation.
 
244
                        Between BEGIN and END, none of these flags will be set
 
245
                        
 
246
                        Operation IDs are all held within the OP_MASK. Only one Operation ID will 
 
247
                        be active per call.
 
248
                :param cur_count: Current absolute count of items
 
249
                        
 
250
                :param max_count:
 
251
                        The maximum count of items we expect. It may be None in case there is 
 
252
                        no maximum number of items or if it is (yet) unknown.
 
253
                
 
254
                :param message:
 
255
                        In case of the 'WRITING' operation, it contains the amount of bytes
 
256
                        transferred. It may possibly be used for other purposes as well.
 
257
                
 
258
                You may read the contents of the current line in self._cur_line"""
 
259
                pass
 
260
 
 
261
 
 
262
class Actor(object):
 
263
        """Actors hold information about a person acting on the repository. They 
 
264
        can be committers and authors or anything with a name and an email as 
 
265
        mentioned in the git log entries."""
 
266
        # PRECOMPILED REGEX
 
267
        name_only_regex = re.compile( r'<(.+)>' )
 
268
        name_email_regex = re.compile( r'(.*) <(.+?)>' )
 
269
        
 
270
        # ENVIRONMENT VARIABLES
 
271
        # read when creating new commits
 
272
        env_author_name = "GIT_AUTHOR_NAME"
 
273
        env_author_email = "GIT_AUTHOR_EMAIL"
 
274
        env_committer_name = "GIT_COMMITTER_NAME"
 
275
        env_committer_email = "GIT_COMMITTER_EMAIL"
 
276
        
 
277
        # CONFIGURATION KEYS
 
278
        conf_name = 'name'
 
279
        conf_email = 'email'
 
280
        
 
281
        __slots__ = ('name', 'email')
 
282
        
 
283
        def __init__(self, name, email):
 
284
                self.name = name
 
285
                self.email = email
 
286
 
 
287
        def __eq__(self, other):
 
288
                return self.name == other.name and self.email == other.email
 
289
                
 
290
        def __ne__(self, other):
 
291
                return not (self == other)
 
292
                
 
293
        def __hash__(self):
 
294
                return hash((self.name, self.email))
 
295
 
 
296
        def __str__(self):
 
297
                return self.name
 
298
 
 
299
        def __repr__(self):
 
300
                return '<git.Actor "%s <%s>">' % (self.name, self.email)
 
301
 
 
302
        @classmethod
 
303
        def _from_string(cls, string):
 
304
                """Create an Actor from a string.
 
305
                :param string: is the string, which is expected to be in regular git format
 
306
 
 
307
                                John Doe <jdoe@example.com>
 
308
                                
 
309
                :return: Actor """
 
310
                m = cls.name_email_regex.search(string)
 
311
                if m:
 
312
                        name, email = m.groups()
 
313
                        return Actor(name, email)
 
314
                else:
 
315
                        m = cls.name_only_regex.search(string)
 
316
                        if m:
 
317
                                return Actor(m.group(1), None)
 
318
                        else:
 
319
                                # assume best and use the whole string as name
 
320
                                return Actor(string, None)
 
321
                        # END special case name
 
322
                # END handle name/email matching
 
323
                
 
324
        @classmethod
 
325
        def _main_actor(cls, env_name, env_email, config_reader=None):
 
326
                actor = Actor('', '')
 
327
                default_email = get_user_id()
 
328
                default_name = default_email.split('@')[0]
 
329
                
 
330
                for attr, evar, cvar, default in (('name', env_name, cls.conf_name, default_name), 
 
331
                                                                                ('email', env_email, cls.conf_email, default_email)):
 
332
                        try:
 
333
                                setattr(actor, attr, os.environ[evar])
 
334
                        except KeyError:
 
335
                                if config_reader is not None:
 
336
                                        setattr(actor, attr, config_reader.get_value('user', cvar, default))
 
337
                                #END config-reader handling
 
338
                                if not getattr(actor, attr):
 
339
                                        setattr(actor, attr, default)
 
340
                        #END handle name
 
341
                #END for each item to retrieve
 
342
                return actor
 
343
                
 
344
                
 
345
        @classmethod
 
346
        def committer(cls, config_reader=None):
 
347
                """
 
348
                :return: Actor instance corresponding to the configured committer. It behaves
 
349
                        similar to the git implementation, such that the environment will override 
 
350
                        configuration values of config_reader. If no value is set at all, it will be
 
351
                        generated
 
352
                :param config_reader: ConfigReader to use to retrieve the values from in case
 
353
                        they are not set in the environment"""
 
354
                return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader)
 
355
                
 
356
        @classmethod
 
357
        def author(cls, config_reader=None):
 
358
                """Same as committer(), but defines the main author. It may be specified in the environment, 
 
359
                but defaults to the committer"""
 
360
                return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader)
 
361
                
 
362
class Stats(object):
 
363
        """
 
364
        Represents stat information as presented by git at the end of a merge. It is 
 
365
        created from the output of a diff operation.
 
366
        
 
367
        ``Example``::
 
368
        
 
369
         c = Commit( sha1 )
 
370
         s = c.stats
 
371
         s.total                 # full-stat-dict
 
372
         s.files                 # dict( filepath : stat-dict )
 
373
         
 
374
        ``stat-dict``
 
375
        
 
376
        A dictionary with the following keys and values::
 
377
         
 
378
          deletions = number of deleted lines as int
 
379
          insertions = number of inserted lines as int
 
380
          lines = total number of lines changed as int, or deletions + insertions
 
381
          
 
382
        ``full-stat-dict``
 
383
        
 
384
        In addition to the items in the stat-dict, it features additional information::
 
385
        
 
386
         files = number of changed files as int"""
 
387
        __slots__ = ("total", "files")
 
388
        
 
389
        def __init__(self, total, files):
 
390
                self.total = total
 
391
                self.files = files
 
392
 
 
393
        @classmethod
 
394
        def _list_from_string(cls, repo, text):
 
395
                """Create a Stat object from output retrieved by git-diff.
 
396
                
 
397
                :return: git.Stat"""
 
398
                hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': dict()}
 
399
                for line in text.splitlines():
 
400
                        (raw_insertions, raw_deletions, filename) = line.split("\t")
 
401
                        insertions = raw_insertions != '-' and int(raw_insertions) or 0
 
402
                        deletions = raw_deletions != '-' and int(raw_deletions) or 0
 
403
                        hsh['total']['insertions'] += insertions
 
404
                        hsh['total']['deletions'] += deletions
 
405
                        hsh['total']['lines'] += insertions + deletions
 
406
                        hsh['total']['files'] += 1
 
407
                        hsh['files'][filename.strip()] = {'insertions': insertions,
 
408
                                                                                          'deletions': deletions,
 
409
                                                                                          'lines': insertions + deletions}
 
410
                return Stats(hsh['total'], hsh['files'])
 
411
 
 
412
 
 
413
class IndexFileSHA1Writer(object):
 
414
        """Wrapper around a file-like object that remembers the SHA1 of 
 
415
        the data written to it. It will write a sha when the stream is closed
 
416
        or if the asked for explicitly usign write_sha.
 
417
        
 
418
        Only useful to the indexfile
 
419
        
 
420
        :note: Based on the dulwich project"""
 
421
        __slots__ = ("f", "sha1")
 
422
        
 
423
        def __init__(self, f):
 
424
                self.f = f
 
425
                self.sha1 = make_sha("")
 
426
 
 
427
        def write(self, data):
 
428
                self.sha1.update(data)
 
429
                return self.f.write(data)
 
430
 
 
431
        def write_sha(self):
 
432
                sha = self.sha1.digest()
 
433
                self.f.write(sha)
 
434
                return sha
 
435
 
 
436
        def close(self):
 
437
                sha = self.write_sha()
 
438
                self.f.close()
 
439
                return sha
 
440
 
 
441
        def tell(self):
 
442
                return self.f.tell()
 
443
 
 
444
 
 
445
class LockFile(object):
 
446
        """Provides methods to obtain, check for, and release a file based lock which 
 
447
        should be used to handle concurrent access to the same file.
 
448
        
 
449
        As we are a utility class to be derived from, we only use protected methods.
 
450
        
 
451
        Locks will automatically be released on destruction"""
 
452
        __slots__ = ("_file_path", "_owns_lock")
 
453
        
 
454
        def __init__(self, file_path):
 
455
                self._file_path = file_path
 
456
                self._owns_lock = False
 
457
        
 
458
        def __del__(self):
 
459
                self._release_lock()
 
460
        
 
461
        def _lock_file_path(self):
 
462
                """:return: Path to lockfile"""
 
463
                return "%s.lock" % (self._file_path)
 
464
        
 
465
        def _has_lock(self):
 
466
                """:return: True if we have a lock and if the lockfile still exists
 
467
                :raise AssertionError: if our lock-file does not exist"""
 
468
                if not self._owns_lock:
 
469
                        return False
 
470
                
 
471
                return True
 
472
                
 
473
        def _obtain_lock_or_raise(self):
 
474
                """Create a lock file as flag for other instances, mark our instance as lock-holder
 
475
                
 
476
                :raise IOError: if a lock was already present or a lock file could not be written"""
 
477
                if self._has_lock():
 
478
                        return 
 
479
                lock_file = self._lock_file_path()
 
480
                if os.path.isfile(lock_file):
 
481
                        raise IOError("Lock for file %r did already exist, delete %r in case the lock is illegal" % (self._file_path, lock_file))
 
482
                        
 
483
                try:
 
484
                        fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0)
 
485
                        os.close(fd)
 
486
                except OSError,e:
 
487
                        raise IOError(str(e))
 
488
                
 
489
                self._owns_lock = True
 
490
                
 
491
        def _obtain_lock(self):
 
492
                """The default implementation will raise if a lock cannot be obtained.
 
493
                Subclasses may override this method to provide a different implementation"""
 
494
                return self._obtain_lock_or_raise()
 
495
                
 
496
        def _release_lock(self):
 
497
                """Release our lock if we have one"""
 
498
                if not self._has_lock():
 
499
                        return
 
500
                        
 
501
                # if someone removed our file beforhand, lets just flag this issue
 
502
                # instead of failing, to make it more usable.
 
503
                lfp = self._lock_file_path()
 
504
                try:
 
505
                        # on bloody windows, the file needs write permissions to be removable.
 
506
                        # Why ... 
 
507
                        if os.name == 'nt':
 
508
                                os.chmod(lfp, 0777)
 
509
                        # END handle win32
 
510
                        os.remove(lfp)
 
511
                except OSError:
 
512
                        pass
 
513
                self._owns_lock = False
 
514
 
 
515
 
 
516
class BlockingLockFile(LockFile):
 
517
        """The lock file will block until a lock could be obtained, or fail after 
 
518
        a specified timeout.
 
519
        
 
520
        :note: If the directory containing the lock was removed, an exception will 
 
521
                be raised during the blocking period, preventing hangs as the lock 
 
522
                can never be obtained."""
 
523
        __slots__ = ("_check_interval", "_max_block_time")
 
524
        def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=sys.maxint):
 
525
                """Configure the instance
 
526
                
 
527
                :parm check_interval_s:
 
528
                        Period of time to sleep until the lock is checked the next time.
 
529
                        By default, it waits a nearly unlimited time
 
530
                
 
531
                :parm max_block_time_s: Maximum amount of seconds we may lock"""
 
532
                super(BlockingLockFile, self).__init__(file_path)
 
533
                self._check_interval = check_interval_s
 
534
                self._max_block_time = max_block_time_s
 
535
                
 
536
        def _obtain_lock(self):
 
537
                """This method blocks until it obtained the lock, or raises IOError if 
 
538
                it ran out of time or if the parent directory was not available anymore.
 
539
                If this method returns, you are guranteed to own the lock"""
 
540
                starttime = time.time()
 
541
                maxtime = starttime + float(self._max_block_time)
 
542
                while True:
 
543
                        try:
 
544
                                super(BlockingLockFile, self)._obtain_lock()
 
545
                        except IOError:
 
546
                                # synity check: if the directory leading to the lockfile is not
 
547
                                # readable anymore, raise an execption
 
548
                                curtime = time.time()
 
549
                                if not os.path.isdir(os.path.dirname(self._lock_file_path())):
 
550
                                        msg = "Directory containing the lockfile %r was not readable anymore after waiting %g seconds" % (self._lock_file_path(), curtime - starttime)
 
551
                                        raise IOError(msg)
 
552
                                # END handle missing directory
 
553
                                
 
554
                                if curtime >= maxtime:
 
555
                                        msg = "Waited %g seconds for lock at %r" % ( maxtime - starttime, self._lock_file_path())
 
556
                                        raise IOError(msg)
 
557
                                # END abort if we wait too long
 
558
                                time.sleep(self._check_interval)
 
559
                        else:
 
560
                                break
 
561
                # END endless loop
 
562
        
 
563
 
 
564
class IterableList(list):
 
565
        """
 
566
        List of iterable objects allowing to query an object by id or by named index::
 
567
         
 
568
         heads = repo.heads
 
569
         heads.master
 
570
         heads['master']
 
571
         heads[0]
 
572
         
 
573
        It requires an id_attribute name to be set which will be queried from its 
 
574
        contained items to have a means for comparison.
 
575
        
 
576
        A prefix can be specified which is to be used in case the id returned by the 
 
577
        items always contains a prefix that does not matter to the user, so it 
 
578
        can be left out."""
 
579
        __slots__ = ('_id_attr', '_prefix')
 
580
        
 
581
        def __new__(cls, id_attr, prefix=''):
 
582
                return super(IterableList,cls).__new__(cls)
 
583
                
 
584
        def __init__(self, id_attr, prefix=''):
 
585
                self._id_attr = id_attr
 
586
                self._prefix = prefix
 
587
                if not isinstance(id_attr, basestring):
 
588
                        raise ValueError("First parameter must be a string identifying the name-property. Extend the list after initialization")
 
589
                # END help debugging !
 
590
                
 
591
        def __contains__(self, attr):
 
592
                # first try identy match for performance
 
593
                rval = list.__contains__(self, attr)
 
594
                if rval:
 
595
                        return rval
 
596
                #END handle match
 
597
                
 
598
                # otherwise make a full name search
 
599
                try:
 
600
                        getattr(self, attr)
 
601
                        return True
 
602
                except (AttributeError, TypeError):
 
603
                        return False
 
604
                #END handle membership
 
605
                
 
606
        def __getattr__(self, attr):
 
607
                attr = self._prefix + attr
 
608
                for item in self:
 
609
                        if getattr(item, self._id_attr) == attr:
 
610
                                return item
 
611
                # END for each item
 
612
                return list.__getattribute__(self, attr)
 
613
                
 
614
        def __getitem__(self, index):
 
615
                if isinstance(index, int):
 
616
                        return list.__getitem__(self,index)
 
617
                
 
618
                try:
 
619
                        return getattr(self, index)
 
620
                except AttributeError:
 
621
                        raise IndexError( "No item found with id %r" % (self._prefix + index) )
 
622
                # END handle getattr
 
623
                        
 
624
        def __delitem__(self, index):
 
625
                delindex = index
 
626
                if not isinstance(index, int):
 
627
                        delindex = -1
 
628
                        name = self._prefix + index
 
629
                        for i, item in enumerate(self):
 
630
                                if getattr(item, self._id_attr) == name:
 
631
                                        delindex = i
 
632
                                        break
 
633
                                #END search index
 
634
                        #END for each item
 
635
                        if delindex == -1:
 
636
                                raise IndexError("Item with name %s not found" % name)
 
637
                        #END handle error
 
638
                #END get index to delete
 
639
                list.__delitem__(self, delindex)
 
640
                
 
641
 
 
642
class Iterable(object):
 
643
        """Defines an interface for iterable items which is to assure a uniform 
 
644
        way to retrieve and iterate items within the git repository"""
 
645
        __slots__ = tuple()
 
646
        _id_attribute_ = "attribute that most suitably identifies your instance"
 
647
        
 
648
        @classmethod
 
649
        def list_items(cls, repo, *args, **kwargs):
 
650
                """
 
651
                Find all items of this type - subclasses can specify args and kwargs differently.
 
652
                If no args are given, subclasses are obliged to return all items if no additional 
 
653
                arguments arg given.
 
654
                
 
655
                :note: Favor the iter_items method as it will
 
656
                
 
657
                :return:list(Item,...) list of item instances"""
 
658
                out_list = IterableList( cls._id_attribute_ )
 
659
                out_list.extend(cls.iter_items(repo, *args, **kwargs))
 
660
                return out_list
 
661
                
 
662
                
 
663
        @classmethod
 
664
        def iter_items(cls, repo, *args, **kwargs):
 
665
                """For more information about the arguments, see list_items
 
666
                :return:  iterator yielding Items"""
 
667
                raise NotImplementedError("To be implemented by Subclass")
 
668
                
 
669
#} END classes