4
\$\begingroup\$

I'm working on creating a batch file-renaming application that renames specifically the way I coded it.

Essentially, these files should be renamed from

  • RootFolder
    • ThisFolder
      • ThisItem
      • [Incorrectly Labeled] ThatItem
    • ThatFolder
      • ThisThatItem
      • ThatOneSillyItem

to

  • RootFolder
    • ThisFolder
      • [ThisFolder] ThisItem
      • [ThisFolder] ThatItem
    • ThatFolder
      • [ThatFolder] ThisThatItem
      • [ThatFolder] ThatOneSillyItem

This is the working code:

"""
    Renames all files in a given directory
"""
from os import path, stat, walk, rename
from os.path import join, sep, isdir
from re import compile, split
from time import time
import codecs


class BatchFileRename:
    """
    Renames all files in a given directory
    """
    def __init__(self, root):
        self._open_file = None
        self._prefix = self._label = ""
        self._root = root
        self._re = compile('\\[[\\w\\s]*\\]?')
        self._index = len(self._root.split(path.sep))
        self._count = 0
        self._size = 0.0

    # #######################################
    # Public methods
    # --------------
    def batch_rename(self):
        """
        Main class driver.
        Renames all files in a given directory (set by constructor)
        """

        # Raises an exception if there is no directory
        if not isdir(self._root):
            raise NotADirectoryError("self._root is empty")

        # Opens the output file to document changes
        with codecs.open('output' + str(time()) + '.txt', 'wb', "utf-8") as self._open_file:

            # Walk through the root folder
            for root, dirs, files in walk(self._root):
                # Sets the label
                self._set_label(root)

                # For each label folder, iterate through the files to rename them
                for name in files:
                    self._add_size(root, name)                                          # Adds the file size to counter
                    new_file = self._rename_file(root, name)                            # Renames the file
                    self._write("\"{0}\" renamed to \"{1}\"".format(name, new_file))    # Writes change to output file
                    self._count += 1                                                    # Counts # of files
                self._write()

            # Documents total files and sizes at the end.
            self._write("Total files: {0}".format(self._count))
            self._write("Total size: {0}".format(self._get_total_size()))

    # #######################################
    # Private methods
    # --------------
    def _add_size(self, root_path, file_name):
        """
        Adds the file size to the counter
        :param root_path: folder the file is in
        :param file_name: file name
        """

        the_path = join(root_path, file_name)
        size = stat(the_path).st_size
        self._size += size

    def _get_total_size(self):
        """
        Returns total size (string)
        :return: Formatted string of total size.
        """

        index = 0
        tier = ["B", "KB", "MB", "GB", "TB"]

        while self._size / 1024 >= 1:
            self._size /= 1024.0
            index += 1

        return "{0:.2f} {1}".format(self._size, tier[index])

    def _rename_file(self, root_path, file_name):
        """
        Renames the given file
        :param root_path: Folder the file is in
        :param file_name: file name
        :return: New file name.
        """

        # If root_path is the root folder,
        # just assign root_path to split_names
        if root_path == self._root:
            split_names = file_name
        # Otherwise,
        # split it based on regex
        else:
            split_names = split(pattern=self._re, string=file_name)

        # Arrange a new file_name and strip the extra whitespaces
        new_file = self._prefix + ' ' + ''.join(split_names).replace('  ', ' ')
        new_file = new_file.strip()

        # Rename the file
        rename(join(root_path, file_name), join(root_path, new_file))
        return new_file

    def _set_label(self, root_path):
        """
        Sets the label
        :param root_path: the existing folder
        """

        dir_name = None if self._root == root_path else root_path.split(sep)[self._index]

        if self._label != dir_name and dir_name is not None:
            self._write()
            self._write("*"*40)
            self._write("Leaving the \"{0}\" folder.".format(self._label))
            self._write("Opened \"{0}\" folder...".format(dir_name))
            self._write("*"*40)
            self._write()

            self._label = dir_name
            self._prefix = "[{0}]".format(self._label) if "[" not in self._label else self._label

    def _write(self, message="", new_line=True):
        """
        Writes the message in the output file
        :param message: The message to write
        :param new_line: New line is necessary
        """

        self._open_file.write(message + ("\n" if new_line or len(message) > 0 else ""))

if __name__ == '__main__':
    br = BatchFileRename(r"Z:\Private")
    br.batch_rename()

If I could get a feedback on this code, that would be awesome!

Thank you!

\$\endgroup\$
0
\$\begingroup\$

You might break this into two lines:

    self._prefix = self._label = ""

I propose a slightly longer class docstring, which ends in a period:

"""Renames all files in a given directory by pushing them one level down."""

It doesn't change regex behavior in this case, but I'd prefer compile(r'foo') to the compile('foo') in your ctor. Then your double backwhacks become single ones. A + is probably more appropriate than *.

Recommend you delete this redundant comment: "# Raises an exception if there is no directory", as the code is clear.

Kudos on leading underscore for methods not exposed as part of public API, that's helpful to the Gentle Reader.

Here's the one glaring issue I noticed:

while self._size / 1024 = 1:

Assignment (=) is different from equality test (==) -- I have trouble believing the while does what you intended.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the helpful suggestions! I changed my code to reflect most of your suggestions. while self._size / 1024 = 1: was supposed to be while self._size / 1024 >= 1:. I just fixed it in my question... I disagree with the quantifier suggestion because there's a possibility that the files do not have any bracketed word groups in their names, which is why I used the * instead of +. \$\endgroup\$ – Sometowngeek Aug 9 '17 at 1:38
  • \$\begingroup\$ Sure, that's cool. I suppose there's an opportunity to flesh out the spec. of what valid inputs look like, or maybe add some unit test inputs which illustrate that. \$\endgroup\$ – J_H Aug 9 '17 at 6:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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