Merge lp:~mac9416/unwrapt/modularization into lp:unwrapt

Proposed by mac9416
Status: Merged
Merged at revision: 53
Proposed branch: lp:~mac9416/unwrapt/modularization
Merge into: lp:unwrapt
Diff against target: 447 lines (+116/-84)
3 files modified
example.py (+6/-2)
unwrapt/DefinitionBase.py (+5/-2)
unwrapt/definitions/aptdef/__init__.py (+105/-80)
To merge this branch: bzr merge lp:~mac9416/unwrapt/modularization
Reviewer Review Type Date Requested Status
Chris Oliver Approve
Review via email: mp+32608@code.launchpad.net

This proposal has been superseded by a proposal from 2010-08-13.

Description of the change

I tried to modularize a couple of things, and in the process hacked the existing code to pieces. There's still a lot of work to do, so feel free to do the same to mine. :-)

To post a comment you must log in.
Revision history for this message
mac9416 (mac9416) wrote :

lp:unwrapt was out of date. I've updated it to lp:~excid3/keryx/unwrapt. Resubmitting.

Revision history for this message
Chris Oliver (excid3) wrote :

Yeah I did not update it because I wanted to make sure recent changes were tested in my personal branch before pushing. I think it was all good and I forgot to push. :P

Revision history for this message
Chris Oliver (excid3) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'example.py'
--- example.py 2010-08-01 19:34:08 +0000
+++ example.py 2010-08-13 17:34:44 +0000
@@ -29,7 +29,8 @@
29#apt.set_proxy({"http": "http://192.168.1.100:3128"}, "username", "password")29#apt.set_proxy({"http": "http://192.168.1.100:3128"}, "username", "password")
3030
31# Configure the apt client31# Configure the apt client
32apt.set_architecture("amd64")32#apt.set_architecture("amd64")
33apt.set_architecture("i386")
3334
34apt.set_status("/var/lib/dpkg/status")35apt.set_status("/var/lib/dpkg/status")
3536
@@ -51,7 +52,10 @@
5152
52print "%i available packages" % len(apt.get_available_package_names())53print "%i available packages" % len(apt.get_available_package_names())
5354
54#apt.update(directory="/var/lib/apt/lists", download=False)55#FIXME: /var/lib/apt is not the download directory. We should have the ability
56# to individually specify the download directories.
57#apt.set_download_directory("/var/lib/apt")
58#apt.update(download=False)
5559
56apt.update()60apt.update()
5761
5862
=== modified file 'unwrapt/DefinitionBase.py'
--- unwrapt/DefinitionBase.py 2010-08-01 19:34:08 +0000
+++ unwrapt/DefinitionBase.py 2010-08-13 17:34:44 +0000
@@ -90,6 +90,7 @@
90 90
91 - directory is the location of the 91 - directory is the location of the
92 """92 """
93 #FIXME: that docstring.
93 94
94 self.download_directory = os.path.abspath(os.path.expanduser(directory))95 self.download_directory = os.path.abspath(os.path.expanduser(directory))
9596
@@ -163,6 +164,7 @@
163 164
164 pass165 pass
165166
167
166 @callback 168 @callback
167 def update(self, reporthook=None, directory=None, download=True):169 def update(self, reporthook=None, directory=None, download=True):
168 """170 """
@@ -202,6 +204,7 @@
202 204
203 pass205 pass
204 206
207
205 @callback208 @callback
206 def get_latest_binary(self, package):209 def get_latest_binary(self, package):
207 """210 """
@@ -268,7 +271,8 @@
268 271
269 For example:272 For example:
270 273
271 client.mark_package("firefox")274 package = client.get_latest_binary("firefox")
275 client.mark_package(package)
272 """276 """
273 277
274 pass278 pass
@@ -289,7 +293,6 @@
289 For example:293 For example:
290 294
291 client.apply_changes()295 client.apply_changes()
292
293 """296 """
294 297
295 pass298 pass
296299
=== modified file 'unwrapt/definitions/aptdef/__init__.py'
--- unwrapt/definitions/aptdef/__init__.py 2010-08-01 19:34:08 +0000
+++ unwrapt/definitions/aptdef/__init__.py 2010-08-13 17:34:44 +0000
@@ -51,14 +51,12 @@
5151
5252
53#TODO: Move this code to proper library location53#TODO: Move this code to proper library location
54def url_join(first, last):54def url_join(*args):
55 """ Returns full URL """55 """ Returns full URL """
56 if first.endswith('/'):56 # Strip any leading or trailing slashes from the parts.
57 if last.startswith('/'): return first + last[1:]57 args = [x.strip("/") for x in args]
58 else: return first + last58
59 else:59 return "/".join(args)
60 if last.startswith('/'): return first + last
61 else: return first + '/' + last
6260
6361
64#class Repository(Base):62#class Repository(Base):
@@ -108,10 +106,15 @@
108106
109107
110def to_url(repository, architecture, format):108def to_url(repository, architecture, format):
111 return url_join(repository["url"], url_join(architecture, format))109 return url_join(repository["url"], architecture, format)
112110
113111
114def to_filename(directory, url):112def to_filename(directory, url):
113 """
114 Forms a full filename from a directory and url.
115 i.e. Strips the url of the protocol prefix, replaces all slashes with
116 underscores, and appends it to directory.
117 """
115 return os.path.join(directory, url.split("//")[1].replace("/", "_"))118 return os.path.join(directory, url.split("//")[1].replace("/", "_"))
116119
117 120
@@ -147,8 +150,13 @@
147 supported = ["amd64", "armel", "i386", "ia64", "powerpc", "sparc"]150 supported = ["amd64", "armel", "i386", "ia64", "powerpc", "sparc"]
148 status_properties = ["Package", "Version", "Status", "Provides"]151 status_properties = ["Package", "Version", "Status", "Provides"]
149 binary_dependencies = ["Pre-Depends", "Depends", "Recommends"]152 binary_dependencies = ["Pre-Depends", "Depends", "Recommends"]
150 supported_statuses = ["install ok installed", "to be installed", "to be downloaded"]153 supported_statuses = ["install ok installed",
154 "to be downloaded",
155 "dependency to be downloaded",
156 "to be installed",
157 "dependency to be installed"]
151158
159 #FIXME: This seems redundant. Could it be moved to DefinitionBase?
152 def on_set_proxy(self, proxy, username=None, password=None):160 def on_set_proxy(self, proxy, username=None, password=None):
153 self.proxy = {"proxy": proxy,161 self.proxy = {"proxy": proxy,
154 "user": username,162 "user": username,
@@ -184,7 +192,7 @@
184 self.repositories[count]["url"] = url192 self.repositories[count]["url"] = url
185 self.repositories[count]["dist"] = dist193 self.repositories[count]["dist"] = dist
186 self.repositories[count]["section"] = section194 self.repositories[count]["section"] = section
187 self.repositories[count]["url"] = url_join(url, url_join("dists", url_join(dist, section)))195 self.repositories[count]["url"] = url_join(url, "dists", dist, section)
188196
189 count += 1197 count += 1
190198
@@ -218,13 +226,22 @@
218 """226 """
219 This is a missing docstring ZOMG!227 This is a missing docstring ZOMG!
220 """228 """
229
230 if download:
231 self.on_download_lists(reporthook)
232
233 # Read the newly-downloaded lists.
234 self.on_read_lists()
235
236
237 def on_download_lists(self, reporthook=None):
221 238
222 directory = os.path.join(self.download_directory, "lists")239 directory = os.path.join(self.download_directory, "lists")
223240
224 #TODO: This function obviously needs to be split up and modularized :)241 # If the download directory does not exist, create it
242 if not os.path.exists(directory):
243 os.makedirs(directory)
225244
226 # This is a list of files we downloaded and now need to parse
227 downloaded = []
228 for repo in self.__iter_repositories():245 for repo in self.__iter_repositories():
229246
230 # Build the strings247 # Build the strings
@@ -232,31 +249,31 @@
232 filename = to_filename(directory, url)249 filename = to_filename(directory, url)
233 display_name = "Repository => %s / %s" % (repo["dist"], repo["section"])250 display_name = "Repository => %s / %s" % (repo["dist"], repo["section"])
234251
235 # If the download directory does not exist, create it
236 if not os.path.exists(directory):
237 os.makedirs(directory)
238
239 # Download252 # Download
240 #TODO: pass proxy information and catch exceptions253 #TODO: pass proxy information and catch exceptions
241 #TODO: Support bz2 and unarchived Packages files254 #TODO: Support bz2 and unarchived Packages files
242 filename = "%s.gz" % filename255 filename = "%s.gz" % filename
243 if download:256 download_url("%s.gz" % url, filename, display_name, proxy=self.proxy["proxy"], username=self.proxy["user"], password=self.proxy["pass"])
244 download_url("%s.gz" % url, filename, display_name, proxy=self.proxy["proxy"], username=self.proxy["user"], password=self.proxy["pass"])257
245 downloaded.append((repo, filename))258
246 259 def on_read_lists(self):
247 #TODO: Improve this. For now we are just opening local files in 260
248 # unextracted format (what you find in /var/lib/apt/lists) since261 directory = os.path.join(self.download_directory, "lists")
249 # that's an easy way to do things. This won't open the gz files262
250 # that Unwrapt downloads however263 lists = []
251 else: # Files that are pre-downloaded264 for repo in self.__iter_repositories():
252 downloaded.append((repo, filename[:-3]))265
253 266 # Build the strings
267 url = to_url(repo, self.architecture, "Packages")
268 filename = to_filename(directory, url)
269 filename = "%s.gz" % filename # Works only if the index files are gz
270 lists.append((repo, filename))
254 271
255 self.packages = {}272 self.packages = {}
256 273
257 total = len(downloaded)274 total = len(lists)
258 # Now parse each file, extracting as necessary275 # Now parse each file, extracting as necessary
259 for i, value in enumerate(downloaded):276 for i, value in enumerate(lists):
260 repo, filename = value277 repo, filename = value
261278
262 # Display percent read 279 # Display percent read
@@ -264,17 +281,19 @@
264 sys.stdout.write("\rReading package lists... %3i%%" % frac)281 sys.stdout.write("\rReading package lists... %3i%%" % frac)
265 sys.stdout.flush()282 sys.stdout.flush()
266283
267 # Parse packages into dictionary284 # Attempt to open the package list.
268 try:285 try:
269 if filename.endswith(".gz"):286 if filename.endswith(".gz"):
270 f = gzip.open(filename, "rb")287 f = gzip.open(filename, "rb")
271 else:288 else:
272 f = open(filename, "rb") 289 f = open(filename, "rb")
273 290 except: #FIXME: specify exception.
274 self.__parse(repo, f)291 logging.error("\nPackage list does not exist: %s" % filename)
275 f.close()292 continue
276 except:293
277 logging.error("\nPackage list does not exist: %s" % filename)294 # Parse packages into dictionary
295 self.__parse(repo, f)
296 f.close()
278 297
279 #TODO: Insert items into database298 #TODO: Insert items into database
280299
@@ -282,6 +301,7 @@
282 sys.stdout.write("\n")301 sys.stdout.write("\n")
283 302
284 logging.info("%i packages available" % len(self.packages))303 logging.info("%i packages available" % len(self.packages))
304
285305
286 def __parse(self, repo, f):306 def __parse(self, repo, f):
287 """307 """
@@ -328,7 +348,6 @@
328 installed statuses.348 installed statuses.
329 """349 """
330350
331
332 f = open(status, "rb")351 f = open(status, "rb")
333 352
334 self.status = {}353 self.status = {}
@@ -420,7 +439,7 @@
420 return self.packages[package]439 return self.packages[package]
421440
422441
423 def on_mark_package(self, metadata):442 def on_mark_package(self, metadata, dependency=False):
424 """443 """
425 Get a list of dependencies based on package metadata444 Get a list of dependencies based on package metadata
426 """445 """
@@ -431,25 +450,21 @@
431 #TODO: This function obviously needs to be split up and modularized :)450 #TODO: This function obviously needs to be split up and modularized :)
432451
433 # First check if the package is installed already?452 # First check if the package is installed already?
434 if metadata["Package"] in self.status:453 status = self.on_get_package_status(metadata["Package"])
435 raise AttributeError, "Package already set to status: %s" % \454 if status != "not installed":
436 self.status[metadata["Package"]]["Status"]455 raise AttributeError, "Package already set to status: %s" % status
437 456
438 # Mark the package itself457 # Mark the package itself
439 metadata["Status"] = "to be downloaded"458 if not dependency: metadata["Status"] = "to be downloaded"
459 else: metadata["Status"] = "dependency to be downloaded"
440 self.status[metadata["Package"]] = metadata460 self.status[metadata["Package"]] = metadata
441461
442 logging.info("Finding dependencies for %s..." % metadata["Package"])462 logging.info("Finding dependencies for %s..." % metadata["Package"])
443463
444 # Build a string of the necessary sections we need464 depends = self.on_get_package_dependencies(metadata)
445 depends = []
446 for section in self.binary_dependencies:
447 if section in metadata:
448 depends.append(metadata[section])
449 depends = ", ".join(depends)
450 465
451 # Do the dependency calculations466 # Do the dependency calculations
452 for dep in depends.split(", "):467 for dep in depends:
453 468
454 # In case we have some ORs469 # In case we have some ORs
455 options = dep.split(" | ")470 options = dep.split(" | ")
@@ -470,7 +485,7 @@
470 # Test for compatible version just in case485 # Test for compatible version just in case
471 if len(details) > 1:486 if len(details) > 1:
472 comparison = details[1][1:] # strip the '('487 comparison = details[1][1:] # strip the '('
473 version = details [2][:-1] # strip the ')'488 version = details[2][:-1] # strip the ')'
474 489
475 satisfied = DpkgVersion(self.status[name]["Version"]).compare_string(comparison, version)490 satisfied = DpkgVersion(self.status[name]["Version"]).compare_string(comparison, version)
476 491
@@ -492,15 +507,26 @@
492 507
493 # Mark sub-dependencies as well508 # Mark sub-dependencies as well
494 if pkg:509 if pkg:
495 self.on_mark_package(pkg)510 self.on_mark_package(pkg, dependency=True)
496 511
497 512
513 def on_get_package_dependencies(self, metadata):
514
515 # Build a string of the necessary sections we need
516 depends = []
517 for section in self.binary_dependencies:
518 if section in metadata:
519 depends += metadata[section].split(", ")
520
521 return depends
522
523
498 def on_apply_changes(self):524 def on_apply_changes(self):
499 525
500 directory = os.path.join(self.download_directory, "packages")526 directory = os.path.join(self.download_directory, "packages")
501 527
502 # Build the list of package urls to download528 # Build the list of package urls to download
503 downloads = [(key, value["Repository"]["url"].split("dists")[0] + value["Filename"]) for key, value in self.status.items() if value["Status"] == "to be downloaded"]529 downloads = [(key, value["Repository"]["url"].split("dists")[0] + value["Filename"]) for key, value in self.status.items() if value["Status"] in ["to be downloaded", "dependency to be downloaded"]]
504 530
505 #downloads = []531 #downloads = []
506 #for key, value in self.status.items():532 #for key, value in self.status.items():
@@ -517,12 +543,16 @@
517 for key, url in downloads:543 for key, url in downloads:
518 download_url(url, "%s/%s" % (directory, url.rsplit("/", 1)[1]), proxy=self.proxy["proxy"], username=self.proxy["user"], password=self.proxy["pass"])544 download_url(url, "%s/%s" % (directory, url.rsplit("/", 1)[1]), proxy=self.proxy["proxy"], username=self.proxy["user"], password=self.proxy["pass"])
519 # Once it's downloaded, mark this package status to "to be installed"545 # Once it's downloaded, mark this package status to "to be installed"
520 self.status[key]["Status"] = "to be installed"546 # or "dependency to be installed", depending on what it is now.
547 if self.status[key]["Status"] == "to be downloaded":
548 self.status[key]["Status"] = "to be installed"
549 elif self.status[key]["Status"] == "dependency to be downloaded":
550 self.status[key]["Status"] = "dependency to be installed"
521 551
522 552
523 def on_save_changes(self, status):553 def on_save_changes(self, status):
524 554
525 # This will NOT create a staus file to override /var/lib/dpkg/status555 # This will NOT create a status file to override /var/lib/dpkg/status
526 # so DO NOT try to replace the system status file.556 # so DO NOT try to replace the system status file.
527 # YOU HAVE BEEN WARNED557 # YOU HAVE BEEN WARNED
528 558
@@ -541,22 +571,19 @@
541 571
542 572
543 def on_cancel_changes(self, downloads, installs):573 def on_cancel_changes(self, downloads, installs):
544
545 cancellations = []
546 574
547 for key, value in self.status.items():575 for key, value in self.status.items():
548 if downloads and value["Status"] == "to be downloaded" or \576 if downloads and value["Status"] in \
549 installs and value["Status"] == "to be installed":577 ["to be downloaded", "dependency to be downloaded"] or \
550 cancellations.append(key)578 installs and value["Status"] in \
551 579 ["to be installed", "dependency to be installed"]:
552 for key in cancellations:580 del self.status[key]
553 del self.status[key]
554 581
555 582
556 def on_get_changes_size(self):583 def on_get_changes_size(self):
557 584
558 # Build list of packages to be downloaded585 # Build list of packages to be downloaded
559 packages = [(value["Package"], value["Version"]) for key, value in self.status.items() if value["Status"] == "to be downloaded"]586 packages = [(value["Package"], value["Version"]) for key, value in self.status.items() if value["Status"] in ["to be downloaded", "dependency to be downloaded"]]
560587
561 count = 0588 count = 0
562 total = 0 589 total = 0
@@ -582,36 +609,36 @@
582 We will take the approach of installing by copying the lists to609 We will take the approach of installing by copying the lists to
583 /var/lib/apt/lists and the packages to /var/cache/apt/archives and610 /var/lib/apt/lists and the packages to /var/cache/apt/archives and
584 calling apt-get update and then apt-get install on the packages 611 calling apt-get update and then apt-get install on the packages
585 which have the stats of "to be installed". This prevents tampering612 which have the status of "to be installed". This prevents tampering
586 with sources.list and works more or less the exact same if we made613 with sources.list and works more or less the exact same if we made
587 a local repository.614 a local repository.
588 """615 """
589 616
590 if not os.geteuid()==0: 617 if not os.geteuid() == 0:
591 raise PermissionsError, "You may only install as root"618 raise PermissionsError, "You may only install as root"
592 619
593 # Copy lists over620 # Copy lists over
594 try:621 for repo in self.__iter_repositories():
595 for repo in self.__iter_repositories():622 url = to_url(repo, self.architecture, "Packages")
596 url = to_url(repo, self.architecture, "Packages")623 filename = to_filename(os.path.join(self.download_directory, "lists"), url)
597 filename = to_filename(os.path.join(self.download_directory, "lists"), url)
598624
625 try:
599 # Extract the gz626 # Extract the gz
600 g = gzip.open("%s.gz" % filename, "rb")627 g = gzip.open("%s.gz" % filename, "rb")
601 f = open(os.path.join("/var/lib/apt/lists", os.path.basename(filename)), "wb")628 f = open(os.path.join("/var/lib/apt/lists", os.path.basename(filename)), "wb")
602 f.write(g.read())629 f.write(g.read())
603 f.close()630 f.close()
604 g.close()631 g.close()
605 except IOError, e:632 except IOError, e:
606 # We will just ignore this, it only trip out if the user did download=False on update()633 # We will just ignore this, it only trip out if the user did download=False on update()
607 pass634 pass
608635
609 636
610 # Copy packages over637 # Copy packages over
611 for key, value in self.status.items():638 for key, value in self.status.items():
612 if value["Status"] == "to be installed":639 if value["Status"] in ["to be installed", "dependency to be installed"]:
613 pkg_filename = self.get_binary_version(value["Package"], value["Version"])["Filename"].rsplit("/", 1)[1]640 pkg_filename = self.get_binary_version(value["Package"], value["Version"])["Filename"].rsplit("/", 1)[1]
614 filename = os.path.join(self.download_directory, os.path.join("packages", pkg_filename))641 filename = os.path.join(self.download_directory, "packages", pkg_filename)
615 dest = os.path.join("/var/cache/apt/archives", os.path.basename(filename))642 dest = os.path.join("/var/cache/apt/archives", os.path.basename(filename))
616 shutil.copyfile(filename, dest)643 shutil.copyfile(filename, dest)
617644
@@ -619,7 +646,7 @@
619 # Call apt-get install with the packages646 # Call apt-get install with the packages
620 packages = [value["Package"] for key, value in self.status.items() if value["Status"] == "to be installed"]647 packages = [value["Package"] for key, value in self.status.items() if value["Status"] == "to be installed"]
621 648
622 subprocess.call("apt-get update", shell=True)649 subprocess.call("apt-gcache gencaches", shell=True)
623 subprocess.call("apt-get -y install %s" % " ".join(packages), shell=True)650 subprocess.call("apt-get -y install %s" % " ".join(packages), shell=True)
624 651
625 652
@@ -642,5 +669,3 @@
642669
643 670
644 return upgrades671 return upgrades
645
646

Subscribers

People subscribed via source and target branches