# Automate the Boring Stuff CH 10 - filling in the gaps

This exercise comes from Automate the Boring Stuff CH 10 - second edition. The exercise is called "filling in the gaps," and the instructions are as follows:

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.

I wrote two versions of this exercise. My first version was shorter than the second version, but the problem was that all the logic was contained in a single function. So I rewrote the code to separate it into multiple functions, and the result is posted here below. I have two major concerns with the logic of this program:

1. I believe that my logic is closer to a functional programming style, but I would like to better understand how I could approach this code from an object-oriented style
2. I am not sure that how I chose to organize the data is the best approach. This is something that I learned from this forum the last time I posted here, and since I am somewhat of a beginner, I'm attempting to develop a better understanding of data structures and how to organize data properly.
#! python3
# fill_gaps.py -- search a single folder, find gaps in numbered files (ex. spam001.txt, spam003.txt)
# and rename later files to fill in the gaps.

import re, os, shutil
from pathlib import Path

def return_match_object(prefix: str) -> re.Match:
# Remove numbers from prefix using a regex, then separate letters and numbers into groups
return re.compile("(\D*)(\d*)").search(prefix)

def check_user_input(folder: str, regex_mo: re.Match) -> str | bool:
if regex_mo is None:
return "Invalid prefix. Prefix contains no letters or numbers."
elif os.path.isdir(folder) == False:
return "Invalid folder. Argument must be a valid folder path."
else:
return False

def create_file_list(regex_mo: re.Match) -> list:
# Search folder for letters and add matches to a list
letters = regex_mo.group(1)
file_list = [i for i in os.listdir(".") if letters in i]
return file_list

def create_number_list(file_list: list, regex_mo: re.Match) -> list:
# search match_list for numbers and add them to a list
number_list = []
for file_name in file_list:
mo = return_match_object(file_name)
number_list.append(int(mo.group(2)))
return number_list

def fill_gaps(
file_list: list,
number_list: list,
folder: str,
number_prefix_length: int,
letters: str,
) -> list:
# Rename later files to fill in gaps
return_list = []
for index, number in enumerate(number_list, 1):
if index not in number_list:
zeroes_to_add = "0" * (number_prefix_length - len(str(index)))
suffix = Path(folder + "\\" + file_list[0]).suffix
shutil.move(
folder + "\\" + file_list[-1],
folder + "\\" + letters + zeroes_to_add + str(index) + suffix,
)
return_list.append(
"renamed "
+ file_list[-1]
+ " to "
+ letters
+ str(index)
+ suffix
)

return return_list

def main() -> None:
while True:
folder = input("Please enter a folder to search for files\n")
prefix = input("Please enter a prefix to search for, such as spam000\n")
match_object = return_match_object(prefix)
letters = match_object.group(1)
number_prefix_length = len(match_object.group(2))
# check for valid user input
if type(check_user_input(folder, match_object)) == str:
continue
else:
# main program execution
os.chdir(folder)
file_list = create_file_list(match_object)
number_list = create_number_list(file_list, match_object)
print(
fill_gaps(file_list, number_list, folder, number_prefix_length, letters)
)
break

if __name__ == "__main__":
main()

• This is procedural programming, not functional programming May 8 at 14:59

I believe that my logic is closer to a functional programming style

It isn't, and new developers often confuse this with procedural programming; yours is the latter.

Otherwise:

• Avoid os and shutil when Path has the same functionality with more convenient representation
• This application does not need regular expressions. Globs are more rudimentary, but fit the job. Internally they use regular expressions anyway, but it's still probably a good idea to use the more constrained interface.
• I don't think that you should exclude prefixes that have no numbers. And I think that your definition of prefix is a little odd. The word "spam" alone should be your prefix.
• check_user_input should not return string-or-bool; it should throw-or-not.
• create_file_list would be well-represented by a generator function.
• You can replace listdir with glob.

## Example

This example code addresses the above and demonstrates an object-oriented renaming class.

from pathlib import Path
from string import digits
from typing import Iterator, Iterable

class RenamingDir:
SUFFIX_PAT = f'[{digits}]'*3 + '.txt'

def __init__(self, directory: Path, prefix: str) -> None:
self.directory = directory
self.prefix = prefix

def old_numbers(self) -> Iterator[int]:
for file in self.directory.glob(self.prefix + self.SUFFIX_PAT):
number = file.stem.removeprefix(self.prefix)
yield int(number)

def format_names(self, numbers: Iterable[int]) -> Iterator[Path]:
for i in numbers:
yield self.directory / f'{self.prefix}{i:03d}.txt'

def __iter__(self) -> Iterable[tuple[
Path,  # original path
Path,  # new path
]]:
numbers = sorted(self.old_numbers())
for start, number in enumerate(numbers, start=1):
if start != number:
return zip(
self.format_names(numbers[start-1:]),
self.format_names(range(start, 1 + len(numbers)))
)
return ()

def rename(self) -> None:
for old, new in self:
old.rename(new)

def main() -> None:
while True:
directory = Path(input("Please enter a directory to search for files: "))
if directory.is_dir():
break

prefix = input("Please enter a prefix to search for, such as spam: ")
RenamingDir(directory, prefix).rename()

if __name__ == '__main__':
main()

• Hi, and thank you for taking the time to respond. I have a couple of follow up questions for you. 1. In your code example, am I to understand that your decision to use a class is the primary thing that makes it an example of OOP? 2. when you say that check_user_input should "throw-or-not," do you mean that such a function should be built for error handling? May 8 at 19:43
• 1. Basically, yes. It's easy to keep this as procedural-only, but you were curious about what OOP looks like, and this is one example. May 8 at 20:17
• 2. You can indeed build an error-handling function if you want. Inner validation functions should not print, and may return True/False but exceptions are generally a better idea. May 8 at 20:19
• Can you help me understand the purpose of the function rename(). It looks like it iterates through key/value pairs with the line for old, new in self:. If the latter is true, is the line old.rename(new) meant to rename the keys old with the values new? I am struggling to understand what is happening with this function, what data it takes in, and what its execution is. May 8 at 23:26
• The class itself is iterable via __iter__. When you say for .. in self, it calls __iter__ which yields an iterable of pairs (old, new). Both elements are Path. Path.rename renames the given path to the new path. May 8 at 23:32