Source code for AutomateTheBoringStuff.Ch09.Projects.P3_fillGaps

"""P3_fillGaps.py

Write a program that finds all files with a given prefix, such as spam001.txt,
spam002.txt, and so on, in a single folder and locates any gaps in the numbering
(such as if there is a spam001.txt and spam003.txt but no spam002.txt).

Have the program rename all the later files to close this gap.

Attributes:
    seqRegex (re.compile): Regular expression object used to group numbers in
        filename.
"""

import os, re

seqRegex = re.compile(r"""
                      ^(.*?)    # All text before numbering
                      (\d+)     # One or more numbers
                      (.*?)$    # All text after numbering
                      """, re.VERBOSE)


[docs]def is_sequence(numberlist: list) -> bool: """Is sequence Can take a list returned by :meth:`get_numbers` and determine if it is a sequence based on the property ``list_length == (last_element - first_element + 1)``. Args: numberlist: List containing integers to check for a sequence. Returns: True if list contains a sequence of numbers, False otherwise. """ return len(numberlist) == (numberlist[-1] - numberlist[0] + 1)
[docs]def get_gap(numberlist: list): """Get gap Can take an integer list returned by :meth:`get_numbers` and determine the missing sequence number. Args: numberlist: List of numbers to find a gap in. Returns: Missing number in the sequence or None if there is no gap. """ seqlist = list(range(numberlist[0], numberlist[-1] + 1)) for element in seqlist: if element not in numberlist: return element return None
[docs]def get_filenames(folder: str) -> list: """Get file names Called by :meth:`fill_gaps` to makes a list of all numbered file names in a given folder. Breaks each file into the prefix (before numbering), numbering, and suffix (after numbering). Args: folder: String containing folder path to get file names of. Relative paths are okay. Returns: List of lists of strings with segmented names of files in alphanumerical order. """ folder = os.path.abspath(folder) files = [] for foldername, subfolders, filenames in os.walk(folder): filenames.sort() for filename in filenames: match_object = seqRegex.search(filename) # Skip files without numbering if match_object is None: continue # Get parts of filename prefix = match_object.group(1) number = match_object.group(2) suffix = match_object.group(3) files.append([prefix, number, suffix]) return files
[docs]def get_numbers(files: list) -> list: """Get numbers Can take a list returned by :meth:`get_filenames` and make an integer list of the numerical parts of the file names. Args: files: List of segmented file names. Returns: List of integers from file names in numerical order. """ numlist = [] for file in files: numlist.append(int(file[1])) return numlist
[docs]def fill_gaps(folder: str) -> None: """Fill gaps Fills gaps in file name numbering of a given folder. Args: folder: String containing path of folder to fill filename gaps. Relative paths are okay. Returns: None. Prints applicable error message and renames files. Note: Running in debug mode. Files to be renamed are printed. Uncomment after testing to rename files. """ files = get_filenames(folder) # Get list of numbers used in file names numlist = get_numbers(files) # Check if numbers are in sequence if is_sequence(numlist): print("There are no gaps to fill.") return None # Find gap in sequence gap = get_gap(numlist) # Rename files to close gap for file in files: prefix, number, suffix = file[0], file[1], file[2] if gap <= int(number): # Make file names numlen = len(number) oldfilename = ''.join(file) newfilename = prefix + str(gap).zfill(numlen) + suffix gap += 1 # Get absolute paths workingdir = os.path.abspath(folder) oldfilepath = os.path.join(workingdir, oldfilename) newfilepath = os.path.join(workingdir, newfilename) # Rename file print("Renaming '%s' to '%s'..." % (oldfilename, newfilename)) # DEBUG
#os.rename(oldfilepath, newfilepath) # Uncomment after testing
[docs]def main(): fill_gaps("./testdir")
# If P3_fillGaps.py is run instead of imported, run main(): if __name__ == "__main__": main()