diff -Nru python-apt-1.0.0~beta2/apt/cache.py python-apt-1.0.0~beta3/apt/cache.py --- python-apt-1.0.0~beta2/apt/cache.py 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/apt/cache.py 2015-06-17 16:29:02.000000000 +0000 @@ -23,6 +23,7 @@ import fnmatch import os +import warnings import weakref import apt_pkg @@ -78,6 +79,7 @@ self._records = None self._list = None self._callbacks = {} + self._callbacks2 = {} self._weakref = weakref.WeakValueDictionary() self._changes_count = -1 self._sorted_set = None @@ -144,6 +146,10 @@ else: callback() + if name in self._callbacks2: + for callback, args, kwds in self._callbacks2[name]: + callback(self, *args, **kwds) + def open(self, progress=None): """ Open the package cache, after that it can be used like a dictionary @@ -539,12 +545,40 @@ self._run_callbacks("cache_pre_change") def connect(self, name, callback): - """ connect to a signal, currently only used for - cache_{post,pre}_{changed,open} """ + """Connect to a signal. + + .. deprecated:: 1.0 + + Please use connect2() instead, as this function is very + likely to cause a memory leak. + """ + if callback != '_inc_changes_count': + warnings.warn("connect() likely causes a reference" + " cycle, use connect2() instead", RuntimeWarning, 2) if name not in self._callbacks: self._callbacks[name] = [] self._callbacks[name].append(callback) + def connect2(self, name, callback, *args, **kwds): + """Connect to a signal. + + The callback will be passed the cache as an argument, and + any arguments passed to this function. Make sure that, if you + pass a method of a class as your callback, your class does not + contain a reference to the cache. + + Cyclic references to the cache can cause issues if the Cache object + is replaced by a new one, because the cache keeps a lot of objects and + tens of open file descriptors. + + currently only used for cache_{post,pre}_{changed,open}. + + .. versionadded:: 1.0 + """ + if name not in self._callbacks2: + self._callbacks2[name] = [] + self._callbacks2[name].append((callback, args, kwds)) + def actiongroup(self): """Return an `ActionGroup` object for the current cache. @@ -664,6 +698,38 @@ return False +class _FilteredCacheHelper(object): + """Helper class for FilteredCache to break a reference cycle.""" + + def __init__(self, cache): + # Do not keep a reference to the cache, or you have a cycle! + + self._filtered = {} + self._filters = {} + cache.connect2("cache_post_change", self.filter_cache_post_change) + cache.connect2("cache_post_open", self.filter_cache_post_change) + + def _reapply_filter(self, cache): + " internal helper to refilter " + # Do not keep a reference to the cache, or you have a cycle! + self._filtered = {} + for pkg in cache: + for f in self._filters: + if f.apply(pkg): + self._filtered[pkg.name] = 1 + break + + def set_filter(self, filter): + """Set the current active filter.""" + self._filters = [] + self._filters.append(filter) + + def filter_cache_post_change(self, cache): + """Called internally if the cache changes, emit a signal then.""" + # Do not keep a reference to the cache, or you have a cycle! + self._reapply_filter(cache) + + class FilteredCache(object): """ A package cache that is filtered. @@ -675,66 +741,50 @@ self.cache = Cache(progress) else: self.cache = cache - self.cache.connect("cache_post_change", self.filter_cache_post_change) - self.cache.connect("cache_post_open", self.filter_cache_post_change) - self._filtered = {} - self._filters = [] + self._helper = _FilteredCacheHelper(self.cache) def __len__(self): - return len(self._filtered) + return len(self._helper._filtered) def __getitem__(self, key): return self.cache[key] def __iter__(self): - for pkgname in self._filtered: + for pkgname in self._helper._filtered: yield self.cache[pkgname] def keys(self): - return self._filtered.keys() + return self._helper._filtered.keys() def has_key(self, key): - return (key in self._filtered) + return key in self def __contains__(self, key): - return (key in self._filtered) - - def _reapply_filter(self): - " internal helper to refilter " - self._filtered = {} - for pkg in self.cache: - for f in self._filters: - if f.apply(pkg): - self._filtered[pkg.name] = 1 - break + try: + # Normalize package name for multi arch + return self.cache[key].name in self._helper._filtered + except KeyError: + return False def set_filter(self, filter): """Set the current active filter.""" - self._filters = [] - self._filters.append(filter) - #self._reapplyFilter() - # force a cache-change event that will result in a refiltering + self._helper.set_filter(filter) self.cache.cache_post_change() def filter_cache_post_change(self): """Called internally if the cache changes, emit a signal then.""" - #print "filterCachePostChange()" - self._reapply_filter() - -# def connect(self, name, callback): -# self.cache.connect(name, callback) + self._helper.filter_cache_post_change(self.cache) def __getattr__(self, key): """we try to look exactly like a real cache.""" - #print "getattr: %s " % key return getattr(self.cache, key) -def cache_pre_changed(): +def cache_pre_changed(cache): print("cache pre changed") -def cache_post_changed(): +def cache_post_changed(cache): print("cache post changed") @@ -743,8 +793,8 @@ print("Cache self test") apt_pkg.init() cache = Cache(apt.progress.text.OpProgress()) - cache.connect("cache_pre_change", cache_pre_changed) - cache.connect("cache_post_change", cache_post_changed) + cache.connect2("cache_pre_change", cache_pre_changed) + cache.connect2("cache_post_change", cache_post_changed) print(("aptitude" in cache)) pkg = cache["aptitude"] print(pkg.name) @@ -771,8 +821,8 @@ print("Testing filtered cache (argument is old cache)") filtered = FilteredCache(cache) - filtered.cache.connect("cache_pre_change", cache_pre_changed) - filtered.cache.connect("cache_post_change", cache_post_changed) + filtered.cache.connect2("cache_pre_change", cache_pre_changed) + filtered.cache.connect2("cache_post_change", cache_post_changed) filtered.cache.upgrade() filtered.set_filter(MarkedChangesFilter()) print(len(filtered)) @@ -783,8 +833,8 @@ print("Testing filtered cache (no argument)") filtered = FilteredCache(progress=apt.progress.base.OpProgress()) - filtered.cache.connect("cache_pre_change", cache_pre_changed) - filtered.cache.connect("cache_post_change", cache_post_changed) + filtered.cache.connect2("cache_pre_change", cache_pre_changed) + filtered.cache.connect2("cache_post_change", cache_post_changed) filtered.cache.upgrade() filtered.set_filter(MarkedChangesFilter()) print(len(filtered)) diff -Nru python-apt-1.0.0~beta2/data/templates/Ubuntu.mirrors python-apt-1.0.0~beta3/data/templates/Ubuntu.mirrors --- python-apt-1.0.0~beta2/data/templates/Ubuntu.mirrors 2015-06-12 22:42:40.000000000 +0000 +++ python-apt-1.0.0~beta3/data/templates/Ubuntu.mirrors 2015-06-17 16:29:38.000000000 +0000 @@ -203,6 +203,7 @@ http://kartolo.sby.datautama.net.id/ubuntu/ http://kebo.pens.ac.id/ubuntu/ http://mirror.kavalinux.com/ubuntu/ +http://mirror.poliwangi.ac.id/ubuntu/ http://mirror.unej.ac.id/ubuntu/ http://suro.ubaya.ac.id/ubuntu/ #LOC:IE @@ -212,7 +213,6 @@ #LOC:IN ftp://ftp.iitb.ac.in/distributions/ubuntu/archives/ http://ftp.iitm.ac.in/ubuntu/ -http://mirror.cse.iitk.ac.in/ubuntu/ #LOC:IR http://mirror.iranserver.com/ubuntu/ http://ubuntu.asis.io/ @@ -370,7 +370,6 @@ http://mirror01.idc.hinet.net/ubuntu/ http://tw.archive.ubuntu.com/ubuntu/ http://ubuntu.cs.nctu.edu.tw/ubuntu/ -http://ubuntu.stu.edu.tw/ubuntu/ #LOC:TZ http://deb-mirror.habari.co.tz/ubuntu/ http://mirror.aptus.co.tz/pub/ubuntuarchive/ @@ -452,6 +451,7 @@ http://ubuntu.wikimedia.org/ubuntu/ http://ubuntuarchive.mirror.nac.net/ http://us.archive.ubuntu.com/ubuntu/ +http://www.club.cc.cmu.edu/pub/ubuntu/ http://www.gtlib.gatech.edu/pub/ubuntu/ http://www.lug.bu.edu/mirror/ubuntu/ #LOC:UZ diff -Nru python-apt-1.0.0~beta2/debian/changelog python-apt-1.0.0~beta3/debian/changelog --- python-apt-1.0.0~beta2/debian/changelog 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/debian/changelog 2015-06-17 16:29:02.000000000 +0000 @@ -1,3 +1,16 @@ +python-apt (1.0.0~beta3) unstable; urgency=medium + + * tests/test_paths.py: Catch and assert the DeprecationWarning + * setup.py: If no version is in the environment, return None + * doc/source/library/apt_pkg.rst: Fix an example from old API to new API + * apt.cache.FilteredCache: Fix multi-arch package lookups + * apt.Cache: Introduce a connect2() callback connector + * Break the FilteredCache <-> Cache reference cycle + * apt.Cache: Issue a RuntimeWarning in connect() + * doc: whatsnew: Document what's new in beta3 + + -- Julian Andres Klode Wed, 17 Jun 2015 18:28:44 +0200 + python-apt (1.0.0~beta2) unstable; urgency=low * debian/control: Build-Depend on apt (>= 1.0.9.4) for Files2() diff -Nru python-apt-1.0.0~beta2/doc/source/library/apt_pkg.rst python-apt-1.0.0~beta3/doc/source/library/apt_pkg.rst --- python-apt-1.0.0~beta2/doc/source/library/apt_pkg.rst 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/doc/source/library/apt_pkg.rst 2015-06-17 16:29:02.000000000 +0000 @@ -1352,10 +1352,10 @@ Example (shortened):: - cand = depcache.GetCandidateVer(cache['python-apt']) - records.Lookup(cand.FileList[0]) + cand = depcache.get_candidate_ver(cache['python-apt']) + records.lookup(cand.file_list[0]) # Now you can access the record - print records.SourcePkg # == python-apt + print records.source_pkg # == python-apt .. attribute:: filename diff -Nru python-apt-1.0.0~beta2/doc/source/whatsnew/1.0.rst python-apt-1.0.0~beta3/doc/source/whatsnew/1.0.rst --- python-apt-1.0.0~beta2/doc/source/whatsnew/1.0.rst 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/doc/source/whatsnew/1.0.rst 2015-06-17 16:29:02.000000000 +0000 @@ -10,9 +10,14 @@ * A new a :meth:`apt_pkg.TagFile.close` method was added * :class:`apt_pkg.TagFile` is now a context manager -* The high-level cache class, :class:`apt.cache.Cache` now supports package +* The high-level cache class, :class:`apt.cache.Cache` and it's companion + :class:`apt.cache.FilteredCache` now support package names with special architecture qualifiers such as :all and :native. +* The method :meth:`apt.cache.Cache.connect2` allows connecting callbacks on + cache changes that take the cache as their first argument, reducing the + potential for reference cycles. + Deprecated ---------- The following features are deprecated, starting with this release: @@ -21,6 +26,9 @@ * The `files` member of of :class:`apt_pkg.SourceRecords` * The `md5` argument to :class:`apt_pkg.AcquireFile`, it is replaced by the `hash` argument. +* The method :meth:`apt.cache.Cache.connect` has been deprecated. It is + replaced by :meth:`apt.cache.Cache.connect2` which is more flexible and + less prone to reference cycles. Removed ------- @@ -36,6 +44,10 @@ Maintenance ----------- +* The classes :class:`apt.cache.Cache` and :class:`apt.cache.FilteredCache` no + longer store cyclic references to/between them. This fixes a huge issue, + because a cache can have tens of open file descriptors, causing the maximum + of file descriptors to be reached easily. * :mod:`apt_inst` now supports ar and tar archives that are larger than 4 GiB * Various smaller bug fixes diff -Nru python-apt-1.0.0~beta2/setup.py python-apt-1.0.0~beta3/setup.py --- python-apt-1.0.0~beta2/setup.py 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/setup.py 2015-06-17 16:29:02.000000000 +0000 @@ -28,6 +28,9 @@ def get_version(): """Get a PEP 0440 compatible version string""" version = os.environ.get('DEBVER') + if not version: + return version + version = version.replace("~alpha", ".a") version = version.replace("~beta", ".b") version = version.replace("~rc", ".rc") diff -Nru python-apt-1.0.0~beta2/tests/test_paths.py python-apt-1.0.0~beta3/tests/test_paths.py --- python-apt-1.0.0~beta2/tests/test_paths.py 2015-06-12 22:42:01.000000000 +0000 +++ python-apt-1.0.0~beta3/tests/test_paths.py 2015-06-17 16:29:02.000000000 +0000 @@ -4,6 +4,7 @@ import os import shutil import unittest +import warnings import apt_inst import apt_pkg @@ -40,10 +41,19 @@ Ensure that both "md5" and "hash" is supported as keyword for AcquireFile """ - apt_pkg.AcquireFile( - apt_pkg.Acquire(), "http://example.com", - destfile=self.file_bytes, - md5="abcdef") + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") + apt_pkg.AcquireFile( + apt_pkg.Acquire(), "http://example.com", + destfile=self.file_bytes, + md5="abcdef") + + self.assertEqual(len(caught_warnings), 1) + self.assertTrue(issubclass(caught_warnings[0].category, + DeprecationWarning)) + self.assertIn("md5", str(caught_warnings[0].message)) + self.assertIn("hash", str(caught_warnings[0].message)) + apt_pkg.AcquireFile( apt_pkg.Acquire(), "http://example.com", destfile=self.file_bytes,