Training a Snake to Clean my Desktop
Tags: python, beginners, automation
Published: 2023-05-17 18:36:39

## The Task
The final project in the [Coding Nomads](https://codingnomads.co/) Python 101 course is to write (1) a Python script that automatically cleans your Desktop by moving all screenshots to a new folder; and (2) writing a blog post about said script.

I began the project by looking at my actual Desktop and thinking about how to tailor the stated task to meet my actual needs. A minimally acceptable solution is to create a new folder and transfer all `.png` images into it, but that would hardly make a dent in the 99 items contained in `C:\Users\MyName\Desktop`. Therefore, my revised plan is to automatically move:
* `.png`s, `.svg`s, and `.gif`s into a new subdirectory, `PICS`; 
* `.txt`s, `.md`s, and `.rtf`s into `NOTES`; 
* `.pdf`s into `PDFS`; and 
* `.c` source files into `CODE`.

<figcaption>Yes, I have outed myself. I am a C developer. You won't find a lick of Python on my cluttered Desktop, yet.</figcaption>

## Setting the Path to my Desktop
If I were coding this tool at work, the path to the Desktop would likely be set via a command line argument. However, we haven't "officially" learned to parse command line arguments with `sys.argv()` or `argparse` yet. Another option would be to prompt for user input, but during a lab exercise, I had a bad experience where I meant to type `C:\Users\MyName\Desktop\TestFolder`, but I missed the `\` key and hit `Enter` early, mistakenly running faulty work-in-progress code against my actual Desktop! (Fortunately, that script crashed after moving a single file to where I didn't want it). In the interest of safety, I will hardcode the path thus:
```
desktop = pathlib.Path(r"C:\Users\MyName\Desktop\TestFolder")
```
Only after I've tested the script against `TestDir` will I shorten the path to run things against my Desktop for real. 

Giving credit where credit is due, [Stack Overflow](https://stackoverflow.com/questions/1347791/unicode-error-unicodeescape-codec-cant-decode-bytes-cannot-open-text-file) straightened me out when my initial code was wrong. The Windows path separator `\` easily collides with Unicode escape sequences, so I added `r` to create a raw string, slightly easier than escaping each path separator.

## Creating the New Directories
Since I want to create a series of new directories, not just one, I iterated through a list of names, joining them to the `desktop` path.
```
for sub_dir in ["CODE", "NOTES", "PDFS", "PICS"]:
    new_dir = desktop.joinpath(sub_dir)
    new_dir.mkdir(exist_ok=True)
```
The `exist_ok` argument should make testing less tedious, since the script won't error out if run in succession without deleting the new folders in between.

## Identifying and Relocating Files
A hefty for loop did all the heavy lifting.
```
# Filter for screenshots only
for each in desktop.iterdir():
    if not each.is_file():
        # Skip directories
        continue

    extension = each.suffix.lower()

    # Create a new path for each file
    if extension in [".c", ".h", ".py"]:
        # put it in CODE
        new_path = desktop / "CODE" / each.name

    elif extension in [".md", ".rtf", ".txt"]:
        # put it in NOTES
        new_path = desktop / "NOTES" / each.name

    elif extension == ".pdf":
        # put it in PDFS
        new_path = desktop / "PDFS" / each.name

    elif extension in [".bmp", ".gif", ".jpg", ".jpeg", ".svg", ".png"]:
        # put it in PICS
        new_path = desktop / "PICS" / each.name

    else:
        continue
```
First, I iterated through every file on the Desktop using `iterdir()`. Second, to recognize file types in a case-insensitive fashion, I converted the suffix of each file to lowercase and tested whether it was found in a list of file extensions belonging to the same category. Third, I set a `new_path` variable with the future location of the file. As a convenience, `pathlib.Path` supports building paths using the `/` operator; I find that more readable than the `joinpath()` method when you have multiple pieces to string together.

```
files_moved = 0

....

# Move the screenshot there
print(f"[!] Moving {each} to {new_path}...")
each.rename(new_path)
files_moved += 1
```

Finally, `each` file from the for loop was moved to `new_path` using the `rename()` method. For my own edification, I added a `files_moved` variable to quantify the work completed by the automation script.

## Running it...
When I changed the path from the test directory to my actual Desktop, the script proudly reported `[+] Successfully moved 41 files!`. This was nice and all, but a quick visual inspection told me that the PDFs stayed put. Notice that unlike the other categories of files, there is only one valid file extension for a PDF. The initial version of my code had a silly logic bug that was syntactically valid: `elif extension == [".pdf"]:`. After quickly fixing that and re-running the script, 16 additional files were relocated.

Now, with my Desktop in a semblance of order, I can tackle individual subdirectories at my leisure and delete any files that do not spark joy.


![Custom KonMari meme that suggests vacation photos spark joy but credit card statements do not](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lp0n6xgyylkefevnnk1f.jpg)

## Complete Code Listing
```
# Write a script that moves all files with the .png extension
# from one folder to another
""" Clean up my Desktop Automagically """

import pathlib

# Find the path to my Desktop
desktop = pathlib.Path(r"C:\Users\MyName\Desktop")

# Create a new folder
for sub_dir in ["CODE", "NOTES", "PDFS", "PICS"]:
    new_dir = desktop.joinpath(sub_dir)
    new_dir.mkdir(exist_ok=True)

files_moved = 0

# Filter for screenshots only
for each in desktop.iterdir():
    if not each.is_file():
        # Skip directories
        continue

    extension = each.suffix.lower()

    # Create a new path for each file
    if extension in [".c", ".h", ".py"]:
        # put it in CODE
        new_path = desktop / "CODE" / each.name

    elif extension in [".md", ".rtf", ".txt"]:
        # put it in NOTES
        new_path = desktop / "NOTES" / each.name

    elif extension == ".pdf":
        # put it in PDFS
        new_path = desktop / "PDFS" / each.name

    elif extension in [".bmp", ".gif", ".jpg", ".jpeg", ".svg", ".png"]:
        # put it in PICS
        new_path = desktop / "PICS" / each.name

    else:
        continue

    # Move the screenshot there
    print(f"[!] Moving {each} to {new_path}...")
    each.rename(new_path)
    files_moved += 1

if files_moved > 0:
    print(f"[+] Successfully moved {files_moved} files!")

```