##// END OF EJS Templates
repoview: fix try/except/finally for py2.4
Matt Mackall -
r22174:0cc2db64 default
parent child Browse files
Show More
@@ -1,329 +1,330 b''
1 # repoview.py - Filtered view of a localrepo object
1 # repoview.py - Filtered view of a localrepo object
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import copy
9 import copy
10 import error
10 import error
11 import hashlib
11 import hashlib
12 import phases
12 import phases
13 import util
13 import util
14 import obsolete
14 import obsolete
15 import struct
15 import struct
16 import tags as tagsmod
16 import tags as tagsmod
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18
18
19 def hideablerevs(repo):
19 def hideablerevs(repo):
20 """Revisions candidates to be hidden
20 """Revisions candidates to be hidden
21
21
22 This is a standalone function to help extensions to wrap it."""
22 This is a standalone function to help extensions to wrap it."""
23 return obsolete.getrevs(repo, 'obsolete')
23 return obsolete.getrevs(repo, 'obsolete')
24
24
25 def _getstaticblockers(repo):
25 def _getstaticblockers(repo):
26 """Cacheable revisions blocking hidden changesets from being filtered.
26 """Cacheable revisions blocking hidden changesets from being filtered.
27
27
28 Additional non-cached hidden blockers are computed in _getdynamicblockers.
28 Additional non-cached hidden blockers are computed in _getdynamicblockers.
29 This is a standalone function to help extensions to wrap it."""
29 This is a standalone function to help extensions to wrap it."""
30 assert not repo.changelog.filteredrevs
30 assert not repo.changelog.filteredrevs
31 hideable = hideablerevs(repo)
31 hideable = hideablerevs(repo)
32 blockers = set()
32 blockers = set()
33 if hideable:
33 if hideable:
34 # We use cl to avoid recursive lookup from repo[xxx]
34 # We use cl to avoid recursive lookup from repo[xxx]
35 cl = repo.changelog
35 cl = repo.changelog
36 firsthideable = min(hideable)
36 firsthideable = min(hideable)
37 revs = cl.revs(start=firsthideable)
37 revs = cl.revs(start=firsthideable)
38 tofilter = repo.revs(
38 tofilter = repo.revs(
39 '(%ld) and children(%ld)', list(revs), list(hideable))
39 '(%ld) and children(%ld)', list(revs), list(hideable))
40 blockers.update([r for r in tofilter if r not in hideable])
40 blockers.update([r for r in tofilter if r not in hideable])
41 return blockers
41 return blockers
42
42
43 def _getdynamicblockers(repo):
43 def _getdynamicblockers(repo):
44 """Non-cacheable revisions blocking hidden changesets from being filtered.
44 """Non-cacheable revisions blocking hidden changesets from being filtered.
45
45
46 Get revisions that will block hidden changesets and are likely to change,
46 Get revisions that will block hidden changesets and are likely to change,
47 but unlikely to create hidden blockers. They won't be cached, so be careful
47 but unlikely to create hidden blockers. They won't be cached, so be careful
48 with adding additional computation."""
48 with adding additional computation."""
49
49
50 cl = repo.changelog
50 cl = repo.changelog
51 blockers = set()
51 blockers = set()
52 blockers.update([par.rev() for par in repo[None].parents()])
52 blockers.update([par.rev() for par in repo[None].parents()])
53 blockers.update([cl.rev(bm) for bm in repo._bookmarks.values()])
53 blockers.update([cl.rev(bm) for bm in repo._bookmarks.values()])
54
54
55 tags = {}
55 tags = {}
56 tagsmod.readlocaltags(repo.ui, repo, tags, {})
56 tagsmod.readlocaltags(repo.ui, repo, tags, {})
57 if tags:
57 if tags:
58 rev, nodemap = cl.rev, cl.nodemap
58 rev, nodemap = cl.rev, cl.nodemap
59 blockers.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
59 blockers.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
60 return blockers
60 return blockers
61
61
62 cacheversion = 1
62 cacheversion = 1
63 cachefile = 'cache/hidden'
63 cachefile = 'cache/hidden'
64
64
65 def cachehash(repo, hideable):
65 def cachehash(repo, hideable):
66 """return sha1 hash of repository data to identify a valid cache.
66 """return sha1 hash of repository data to identify a valid cache.
67
67
68 We calculate a sha1 of repo heads and the content of the obsstore and write
68 We calculate a sha1 of repo heads and the content of the obsstore and write
69 it to the cache. Upon reading we can easily validate by checking the hash
69 it to the cache. Upon reading we can easily validate by checking the hash
70 against the stored one and discard the cache in case the hashes don't match.
70 against the stored one and discard the cache in case the hashes don't match.
71 """
71 """
72 h = hashlib.sha1()
72 h = hashlib.sha1()
73 h.update(''.join(repo.heads()))
73 h.update(''.join(repo.heads()))
74 h.update(str(hash(frozenset(hideable))))
74 h.update(str(hash(frozenset(hideable))))
75 return h.digest()
75 return h.digest()
76
76
77 def trywritehiddencache(repo, hideable, hidden):
77 def trywritehiddencache(repo, hideable, hidden):
78 """write cache of hidden changesets to disk
78 """write cache of hidden changesets to disk
79
79
80 Will not write the cache if a wlock cannot be obtained lazily.
80 Will not write the cache if a wlock cannot be obtained lazily.
81 The cache consists of a head of 22byte:
81 The cache consists of a head of 22byte:
82 2 byte version number of the cache
82 2 byte version number of the cache
83 20 byte sha1 to validate the cache
83 20 byte sha1 to validate the cache
84 n*4 byte hidden revs
84 n*4 byte hidden revs
85 """
85 """
86 wlock = fh = None
86 wlock = fh = None
87 try:
87 try:
88 try:
88 wlock = repo.wlock(wait=False)
89 wlock = repo.wlock(wait=False)
89 # write cache to file
90 # write cache to file
90 newhash = cachehash(repo, hideable)
91 newhash = cachehash(repo, hideable)
91 sortedset = sorted(hidden)
92 sortedset = sorted(hidden)
92 data = struct.pack('>%iI' % len(sortedset), *sortedset)
93 data = struct.pack('>%iI' % len(sortedset), *sortedset)
93 fh = repo.vfs.open(cachefile, 'w+b', atomictemp=True)
94 fh = repo.vfs.open(cachefile, 'w+b', atomictemp=True)
94 fh.write(struct.pack(">H", cacheversion))
95 fh.write(struct.pack(">H", cacheversion))
95 fh.write(newhash)
96 fh.write(newhash)
96 fh.write(data)
97 fh.write(data)
97 except (IOError, OSError):
98 except (IOError, OSError):
98 repo.ui.debug('error writing hidden changesets cache')
99 repo.ui.debug('error writing hidden changesets cache')
99 except error.LockHeld:
100 except error.LockHeld:
100 repo.ui.debug('cannot obtain lock to write hidden changesets cache')
101 repo.ui.debug('cannot obtain lock to write hidden changesets cache')
101 finally:
102 finally:
102 if fh:
103 if fh:
103 fh.close()
104 fh.close()
104 if wlock:
105 if wlock:
105 wlock.release()
106 wlock.release()
106
107
107 def tryreadcache(repo, hideable):
108 def tryreadcache(repo, hideable):
108 """read a cache if the cache exists and is valid, otherwise returns None."""
109 """read a cache if the cache exists and is valid, otherwise returns None."""
109 hidden = fh = None
110 hidden = fh = None
110 try:
111 try:
111 if repo.vfs.exists(cachefile):
112 if repo.vfs.exists(cachefile):
112 fh = repo.vfs.open(cachefile, 'rb')
113 fh = repo.vfs.open(cachefile, 'rb')
113 version, = struct.unpack(">H", fh.read(2))
114 version, = struct.unpack(">H", fh.read(2))
114 oldhash = fh.read(20)
115 oldhash = fh.read(20)
115 newhash = cachehash(repo, hideable)
116 newhash = cachehash(repo, hideable)
116 if (cacheversion, oldhash) == (version, newhash):
117 if (cacheversion, oldhash) == (version, newhash):
117 # cache is valid, so we can start reading the hidden revs
118 # cache is valid, so we can start reading the hidden revs
118 data = fh.read()
119 data = fh.read()
119 count = len(data) / 4
120 count = len(data) / 4
120 hidden = frozenset(struct.unpack('>%iI' % count, data))
121 hidden = frozenset(struct.unpack('>%iI' % count, data))
121 return hidden
122 return hidden
122 finally:
123 finally:
123 if fh:
124 if fh:
124 fh.close()
125 fh.close()
125
126
126 def computehidden(repo):
127 def computehidden(repo):
127 """compute the set of hidden revision to filter
128 """compute the set of hidden revision to filter
128
129
129 During most operation hidden should be filtered."""
130 During most operation hidden should be filtered."""
130 assert not repo.changelog.filteredrevs
131 assert not repo.changelog.filteredrevs
131
132
132 hidden = frozenset()
133 hidden = frozenset()
133 hideable = hideablerevs(repo)
134 hideable = hideablerevs(repo)
134 if hideable:
135 if hideable:
135 cl = repo.changelog
136 cl = repo.changelog
136 hidden = tryreadcache(repo, hideable)
137 hidden = tryreadcache(repo, hideable)
137 if hidden is None:
138 if hidden is None:
138 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
139 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
139 hidden = frozenset(r for r in hideable if r not in blocked)
140 hidden = frozenset(r for r in hideable if r not in blocked)
140 trywritehiddencache(repo, hideable, hidden)
141 trywritehiddencache(repo, hideable, hidden)
141 elif repo.ui.configbool('experimental', 'verifyhiddencache', True):
142 elif repo.ui.configbool('experimental', 'verifyhiddencache', True):
142 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
143 blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True)
143 computed = frozenset(r for r in hideable if r not in blocked)
144 computed = frozenset(r for r in hideable if r not in blocked)
144 if computed != hidden:
145 if computed != hidden:
145 trywritehiddencache(repo, hideable, computed)
146 trywritehiddencache(repo, hideable, computed)
146 repo.ui.warn(_('Cache inconsistency detected. Please ' +
147 repo.ui.warn(_('Cache inconsistency detected. Please ' +
147 'open an issue on http://bz.selenic.com.\n'))
148 'open an issue on http://bz.selenic.com.\n'))
148 hidden = computed
149 hidden = computed
149
150
150 # check if we have wd parents, bookmarks or tags pointing to hidden
151 # check if we have wd parents, bookmarks or tags pointing to hidden
151 # changesets and remove those.
152 # changesets and remove those.
152 dynamic = hidden & _getdynamicblockers(repo)
153 dynamic = hidden & _getdynamicblockers(repo)
153 if dynamic:
154 if dynamic:
154 blocked = cl.ancestors(dynamic, inclusive=True)
155 blocked = cl.ancestors(dynamic, inclusive=True)
155 hidden = frozenset(r for r in hidden if r not in blocked)
156 hidden = frozenset(r for r in hidden if r not in blocked)
156 return hidden
157 return hidden
157
158
158 def computeunserved(repo):
159 def computeunserved(repo):
159 """compute the set of revision that should be filtered when used a server
160 """compute the set of revision that should be filtered when used a server
160
161
161 Secret and hidden changeset should not pretend to be here."""
162 Secret and hidden changeset should not pretend to be here."""
162 assert not repo.changelog.filteredrevs
163 assert not repo.changelog.filteredrevs
163 # fast path in simple case to avoid impact of non optimised code
164 # fast path in simple case to avoid impact of non optimised code
164 hiddens = filterrevs(repo, 'visible')
165 hiddens = filterrevs(repo, 'visible')
165 if phases.hassecret(repo):
166 if phases.hassecret(repo):
166 cl = repo.changelog
167 cl = repo.changelog
167 secret = phases.secret
168 secret = phases.secret
168 getphase = repo._phasecache.phase
169 getphase = repo._phasecache.phase
169 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
170 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
170 revs = cl.revs(start=first)
171 revs = cl.revs(start=first)
171 secrets = set(r for r in revs if getphase(repo, r) >= secret)
172 secrets = set(r for r in revs if getphase(repo, r) >= secret)
172 return frozenset(hiddens | secrets)
173 return frozenset(hiddens | secrets)
173 else:
174 else:
174 return hiddens
175 return hiddens
175
176
176 def computemutable(repo):
177 def computemutable(repo):
177 """compute the set of revision that should be filtered when used a server
178 """compute the set of revision that should be filtered when used a server
178
179
179 Secret and hidden changeset should not pretend to be here."""
180 Secret and hidden changeset should not pretend to be here."""
180 assert not repo.changelog.filteredrevs
181 assert not repo.changelog.filteredrevs
181 # fast check to avoid revset call on huge repo
182 # fast check to avoid revset call on huge repo
182 if util.any(repo._phasecache.phaseroots[1:]):
183 if util.any(repo._phasecache.phaseroots[1:]):
183 getphase = repo._phasecache.phase
184 getphase = repo._phasecache.phase
184 maymutable = filterrevs(repo, 'base')
185 maymutable = filterrevs(repo, 'base')
185 return frozenset(r for r in maymutable if getphase(repo, r))
186 return frozenset(r for r in maymutable if getphase(repo, r))
186 return frozenset()
187 return frozenset()
187
188
188 def computeimpactable(repo):
189 def computeimpactable(repo):
189 """Everything impactable by mutable revision
190 """Everything impactable by mutable revision
190
191
191 The immutable filter still have some chance to get invalidated. This will
192 The immutable filter still have some chance to get invalidated. This will
192 happen when:
193 happen when:
193
194
194 - you garbage collect hidden changeset,
195 - you garbage collect hidden changeset,
195 - public phase is moved backward,
196 - public phase is moved backward,
196 - something is changed in the filtering (this could be fixed)
197 - something is changed in the filtering (this could be fixed)
197
198
198 This filter out any mutable changeset and any public changeset that may be
199 This filter out any mutable changeset and any public changeset that may be
199 impacted by something happening to a mutable revision.
200 impacted by something happening to a mutable revision.
200
201
201 This is achieved by filtered everything with a revision number egal or
202 This is achieved by filtered everything with a revision number egal or
202 higher than the first mutable changeset is filtered."""
203 higher than the first mutable changeset is filtered."""
203 assert not repo.changelog.filteredrevs
204 assert not repo.changelog.filteredrevs
204 cl = repo.changelog
205 cl = repo.changelog
205 firstmutable = len(cl)
206 firstmutable = len(cl)
206 for roots in repo._phasecache.phaseroots[1:]:
207 for roots in repo._phasecache.phaseroots[1:]:
207 if roots:
208 if roots:
208 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
209 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
209 # protect from nullrev root
210 # protect from nullrev root
210 firstmutable = max(0, firstmutable)
211 firstmutable = max(0, firstmutable)
211 return frozenset(xrange(firstmutable, len(cl)))
212 return frozenset(xrange(firstmutable, len(cl)))
212
213
213 # function to compute filtered set
214 # function to compute filtered set
214 #
215 #
215 # When adding a new filter you MUST update the table at:
216 # When adding a new filter you MUST update the table at:
216 # mercurial.branchmap.subsettable
217 # mercurial.branchmap.subsettable
217 # Otherwise your filter will have to recompute all its branches cache
218 # Otherwise your filter will have to recompute all its branches cache
218 # from scratch (very slow).
219 # from scratch (very slow).
219 filtertable = {'visible': computehidden,
220 filtertable = {'visible': computehidden,
220 'served': computeunserved,
221 'served': computeunserved,
221 'immutable': computemutable,
222 'immutable': computemutable,
222 'base': computeimpactable}
223 'base': computeimpactable}
223
224
224 def filterrevs(repo, filtername):
225 def filterrevs(repo, filtername):
225 """returns set of filtered revision for this filter name"""
226 """returns set of filtered revision for this filter name"""
226 if filtername not in repo.filteredrevcache:
227 if filtername not in repo.filteredrevcache:
227 func = filtertable[filtername]
228 func = filtertable[filtername]
228 repo.filteredrevcache[filtername] = func(repo.unfiltered())
229 repo.filteredrevcache[filtername] = func(repo.unfiltered())
229 return repo.filteredrevcache[filtername]
230 return repo.filteredrevcache[filtername]
230
231
231 class repoview(object):
232 class repoview(object):
232 """Provide a read/write view of a repo through a filtered changelog
233 """Provide a read/write view of a repo through a filtered changelog
233
234
234 This object is used to access a filtered version of a repository without
235 This object is used to access a filtered version of a repository without
235 altering the original repository object itself. We can not alter the
236 altering the original repository object itself. We can not alter the
236 original object for two main reasons:
237 original object for two main reasons:
237 - It prevents the use of a repo with multiple filters at the same time. In
238 - It prevents the use of a repo with multiple filters at the same time. In
238 particular when multiple threads are involved.
239 particular when multiple threads are involved.
239 - It makes scope of the filtering harder to control.
240 - It makes scope of the filtering harder to control.
240
241
241 This object behaves very closely to the original repository. All attribute
242 This object behaves very closely to the original repository. All attribute
242 operations are done on the original repository:
243 operations are done on the original repository:
243 - An access to `repoview.someattr` actually returns `repo.someattr`,
244 - An access to `repoview.someattr` actually returns `repo.someattr`,
244 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
245 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
245 - A deletion of `repoview.someattr` actually drops `someattr`
246 - A deletion of `repoview.someattr` actually drops `someattr`
246 from `repo.__dict__`.
247 from `repo.__dict__`.
247
248
248 The only exception is the `changelog` property. It is overridden to return
249 The only exception is the `changelog` property. It is overridden to return
249 a (surface) copy of `repo.changelog` with some revisions filtered. The
250 a (surface) copy of `repo.changelog` with some revisions filtered. The
250 `filtername` attribute of the view control the revisions that need to be
251 `filtername` attribute of the view control the revisions that need to be
251 filtered. (the fact the changelog is copied is an implementation detail).
252 filtered. (the fact the changelog is copied is an implementation detail).
252
253
253 Unlike attributes, this object intercepts all method calls. This means that
254 Unlike attributes, this object intercepts all method calls. This means that
254 all methods are run on the `repoview` object with the filtered `changelog`
255 all methods are run on the `repoview` object with the filtered `changelog`
255 property. For this purpose the simple `repoview` class must be mixed with
256 property. For this purpose the simple `repoview` class must be mixed with
256 the actual class of the repository. This ensures that the resulting
257 the actual class of the repository. This ensures that the resulting
257 `repoview` object have the very same methods than the repo object. This
258 `repoview` object have the very same methods than the repo object. This
258 leads to the property below.
259 leads to the property below.
259
260
260 repoview.method() --> repo.__class__.method(repoview)
261 repoview.method() --> repo.__class__.method(repoview)
261
262
262 The inheritance has to be done dynamically because `repo` can be of any
263 The inheritance has to be done dynamically because `repo` can be of any
263 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
264 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
264 """
265 """
265
266
266 def __init__(self, repo, filtername):
267 def __init__(self, repo, filtername):
267 object.__setattr__(self, '_unfilteredrepo', repo)
268 object.__setattr__(self, '_unfilteredrepo', repo)
268 object.__setattr__(self, 'filtername', filtername)
269 object.__setattr__(self, 'filtername', filtername)
269 object.__setattr__(self, '_clcachekey', None)
270 object.__setattr__(self, '_clcachekey', None)
270 object.__setattr__(self, '_clcache', None)
271 object.__setattr__(self, '_clcache', None)
271
272
272 # not a propertycache on purpose we shall implement a proper cache later
273 # not a propertycache on purpose we shall implement a proper cache later
273 @property
274 @property
274 def changelog(self):
275 def changelog(self):
275 """return a filtered version of the changeset
276 """return a filtered version of the changeset
276
277
277 this changelog must not be used for writing"""
278 this changelog must not be used for writing"""
278 # some cache may be implemented later
279 # some cache may be implemented later
279 unfi = self._unfilteredrepo
280 unfi = self._unfilteredrepo
280 unfichangelog = unfi.changelog
281 unfichangelog = unfi.changelog
281 revs = filterrevs(unfi, self.filtername)
282 revs = filterrevs(unfi, self.filtername)
282 cl = self._clcache
283 cl = self._clcache
283 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
284 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
284 if cl is not None:
285 if cl is not None:
285 # we need to check curkey too for some obscure reason.
286 # we need to check curkey too for some obscure reason.
286 # MQ test show a corruption of the underlying repo (in _clcache)
287 # MQ test show a corruption of the underlying repo (in _clcache)
287 # without change in the cachekey.
288 # without change in the cachekey.
288 oldfilter = cl.filteredrevs
289 oldfilter = cl.filteredrevs
289 try:
290 try:
290 cl.filterrevs = () # disable filtering for tip
291 cl.filterrevs = () # disable filtering for tip
291 curkey = (len(cl), cl.tip(), hash(oldfilter))
292 curkey = (len(cl), cl.tip(), hash(oldfilter))
292 finally:
293 finally:
293 cl.filteredrevs = oldfilter
294 cl.filteredrevs = oldfilter
294 if newkey != self._clcachekey or newkey != curkey:
295 if newkey != self._clcachekey or newkey != curkey:
295 cl = None
296 cl = None
296 # could have been made None by the previous if
297 # could have been made None by the previous if
297 if cl is None:
298 if cl is None:
298 cl = copy.copy(unfichangelog)
299 cl = copy.copy(unfichangelog)
299 cl.filteredrevs = revs
300 cl.filteredrevs = revs
300 object.__setattr__(self, '_clcache', cl)
301 object.__setattr__(self, '_clcache', cl)
301 object.__setattr__(self, '_clcachekey', newkey)
302 object.__setattr__(self, '_clcachekey', newkey)
302 return cl
303 return cl
303
304
304 def unfiltered(self):
305 def unfiltered(self):
305 """Return an unfiltered version of a repo"""
306 """Return an unfiltered version of a repo"""
306 return self._unfilteredrepo
307 return self._unfilteredrepo
307
308
308 def filtered(self, name):
309 def filtered(self, name):
309 """Return a filtered version of a repository"""
310 """Return a filtered version of a repository"""
310 if name == self.filtername:
311 if name == self.filtername:
311 return self
312 return self
312 return self.unfiltered().filtered(name)
313 return self.unfiltered().filtered(name)
313
314
314 # everything access are forwarded to the proxied repo
315 # everything access are forwarded to the proxied repo
315 def __getattr__(self, attr):
316 def __getattr__(self, attr):
316 return getattr(self._unfilteredrepo, attr)
317 return getattr(self._unfilteredrepo, attr)
317
318
318 def __setattr__(self, attr, value):
319 def __setattr__(self, attr, value):
319 return setattr(self._unfilteredrepo, attr, value)
320 return setattr(self._unfilteredrepo, attr, value)
320
321
321 def __delattr__(self, attr):
322 def __delattr__(self, attr):
322 return delattr(self._unfilteredrepo, attr)
323 return delattr(self._unfilteredrepo, attr)
323
324
324 # The `requirements` attribute is initialized during __init__. But
325 # The `requirements` attribute is initialized during __init__. But
325 # __getattr__ won't be called as it also exists on the class. We need
326 # __getattr__ won't be called as it also exists on the class. We need
326 # explicit forwarding to main repo here
327 # explicit forwarding to main repo here
327 @property
328 @property
328 def requirements(self):
329 def requirements(self):
329 return self._unfilteredrepo.requirements
330 return self._unfilteredrepo.requirements
General Comments 0
You need to be logged in to leave comments. Login now