3
\$\begingroup\$

A Python script for automatically extracting an archive file in a subfolder:

#!/usr/bin/env python3

// sub7z.py

import subprocess
import re
import os, sys, pathlib

class Counter:
    counter = 0
    def getCount(self):
        self.counter = self.counter + 1
        return self.counter


class ZipFile:
    pattern1 = re.compile(r" +Date +Time +Attr +Size +Compressed +Name\n-{4,}[- ]*\n(.*?)\n-{4,}[- ]*", re.DOTALL)
    pattern2 = re.compile(r"\d{4}-\d{2}-\d{2} +\d{2}:\d{2}:\d{2} +\.+ +\d+ +\d+ +(.*?)\n")

    def __init__(self, pFilePath):
        self.filePath = os.path.abspath(pFilePath)
        if not os.path.isfile(self.filePath):
            raise Exception("You must give me a file as input!")
        self.fileParent = os.path.dirname(self.filePath)
        self.fileStem, self.fileExt = os.path.splitext(self.filePath)
        self.zipItems = ""
        self.withSubfolder = True
        self.zipItemsFrom7z()
        self.checkSubfolder()

    def zipItemsFrom7z(self):
        p = subprocess.Popen(["7z", "l", self.filePath], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self.zipItems, err = p.communicate()
        if err:
            raise Exception("Failed to fetch zip file info through 7z.")
        if not self.zipItems:
            raise Exception("Didn't get any output from 7z, check that 7z is working properly.")
        self.zipItems = self.zipItems.decode()
        self.zipItems = self.pattern1.findall(self.zipItems)[0]
        self.zipItems = self.pattern2.findall(self.zipItems)

    def checkSubfolder(self):
        dirnames = [pathlib.Path(i).parts[0] for i in self.zipItems]
        if len(set(dirnames)) > 1:
            self.withSubfolder = False
        else:
            self.subFolder = os.path.join(self.fileParent, dirnames[0])

    def stripPathSep(self, folder):
        while folder.endswith(os.sep):
            folder = folder[:-1]
        return folder

    def makeNumberedFolder(self, folder):
        folder = self.stripPathSep(folder)
        newFolder = folder
        counter = Counter()
        while os.path.exists(newFolder):
            print("This folder exists already: %s" % newFolder)
            newFolder = folder + "%03d" % counter.getCount()
        print("This folder name can be used without conflict: %s" % newFolder)
        return newFolder

    def makeFolderSameNameAsFileStem(self):
        self.folderSameNameAsFileStem =  self.makeNumberedFolder(self.fileStem)
        os.mkdir(self.folderSameNameAsFileStem)


    def setTargetFolder(self):
        if self.withSubfolder:
            subFolderName = os.path.split(self.subFolder)[1]
            print("Archive contains a subfolder: %s." % subFolderName)
            if os.path.exists(self.subFolder):
                print("But this subfolder already exists. \nSo I cannot use this subfolder.")
                print("I will figure out a usable folder name.")
                self.inferFolderName = True
            else:
                print("And this subfolder does not exist yet.")
                print("I will extract archive directly.")
                self.inferFolderName = False
        else:
            print("Archive does not contain a subfolder.")
            print("I will figure out a usable folder name.")
            self.inferFolderName = True

        if self.inferFolderName:
            self.makeFolderSameNameAsFileStem()
            self.targetFolder = self.folderSameNameAsFileStem
        else:
            self.targetFolder = self.fileParent

    def extract(self):
        self.setTargetFolder()
        p = subprocess.Popen(["7z", "-o%s" % self.targetFolder, "x", self.filePath])


if __name__ == "__main__":
    filePath = sys.argv[1]
    if filePath.endswith(".tar.gz"):
        secondPassFilePath = filePath[:-3]
        unzipCmd = "gunzip"
    elif filePath.endswith(".tgz"):
        secondPassFilePath = filePath[:-2] + "ar"
        unzipCmd = "gunzip"
    elif filePath.endswith(".tar.bz2"):
        secondPassFilePath = filePath[:-4]
        unzipCmd = "bunzip2"
    elif filePath.endswith(".tbz2"):
        secondPassFilePath = filePath[:-3] + "ar"
        unzipCmd = "bunzip2"
    else:
        unzipCmd = ""

    if unzipCmd:
        subprocess.call([unzipCmd, filePath])
        filePath = secondPassFilePath

    z = ZipFile(filePath)
    z.extract()
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

You should follow PEP8, the official style guide of Python.

The Counter class

You declared class variable counter, but then you use the instance variable self.counter for increments. While this seems to work, it's confusing and can have unintended effects. For example, after you do Counter.counter = 3, all future counter instances you create will start counting from 3.

The purpose of class variables is to share between multiple instances. Probably this is not your intention. In which case you should initialize the instance variable in the constructor:

class Counter(object):
    def __init__(self):
        self.counter = 0

Also note that modern classes should extend object, like I did above.

The getCount method is not great. The "get" in the name suggests it's a getter, so it's unexpected that it changes state (increments the count). It would be better to rename to increment_and_get.

Finally, when incrementing a count, it's better to use x += 1 instead of x = x + 1.

The ZipFile class

Many methods rely heavily on side effects, expecting some instance variables to be set by other methods. This is fragile design, making it difficult to maintain the class.

For example, checkSubfolder expects zipItems to be set already, done by calling zipItemsFrom7z first. Whenever possible, it's best to have the required input as method parameters. Making methods share variables makes the design fragile, for two reasons:

  1. The dependence between the methods is not obvious
  2. The variable is exposed to other methods too, and might get modified by accident

You can fix this by making zipItemsFrom7z return zipItems instead of storing in the instance, and pass this list to checkSubfolder. That way no other method can see or mess with zipItems, and the relationship between these methods becomes perfectly clear.

A similar issue is checkSubfolder setting self.subFolder. The problem is that this instance variable is not mentioned in the constructor, which makes the class harder to maintain. It's best to see the initialization of all the fields in the constructor, to know all the elements it requires for functioning correctly.

In fact you should never let other methods initialize instance variables. These methods can be called from anywhere, not only from the constructor, and an accidental misuse can corrupt the object. One way to fix it is to change these methods to return values instead of setting instance variables. Another way is to make them inner functions inside the constructor itself.

I suggest to review the entire class, while considering these concepts carefully.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.