Multi Clipboard
Tags: python, automation
Published: 2023-12-15 21:22:41

## Project: Multi Clipboard
Welcome to the second project in this series. You will be building a functional Multiclipboard. Before you begin, let's talk about what a clipboard does.

A clipboard helps you copy and paste, but once you make another copy, it erases the last copy. This is where a Multiclipboard comes into play. It helps you store text with a certain keyword and access as many texts as possible making it possible for you to retrieve a particular text.

To learn more about the project, check out this [repository](https://github.com/praise002/Automating-the-boring-stuff-with-Python-blog-project/tree/master/chapter-8/multiclipboard).

## Prerequisites

- Part 1 of the series
- Optionally, [Python 3.9](https://www.python.org/downloads/release/python-390/) or an earlier version.

Now that you have gone through the README, let's proceed. In this project, you will find exercises at the end of the project, which can involve fixing a bug or organizing the functions. Be sure to check the project files for reference.

## Writing Code

**Step 1**: Create a file called `mcb.py`.

**Step 2**: In this step, we will take a different approach. We want to define the functions and their respective command line arguments.

You can see that we have defined three functions:

1. save_keyword should take a keyword and its content as arguments.
2. load_text should take a keyword as an argument and load its content.
3. list_keywords() does not require any arguments; it simply displays all the keywords.

This approach will help you structure your code effectively.

```
# save_keyword(keyword, content), load_text(keyword), list_keywords()
# Usage: 
# py mcb.py save <keyword> - Saves clipboard to keyword.
# py mcb.py <keyword> - Loads keyword to clipboard.
# py mcb.py list - Loads all keywords to the clipboard.
# py mcb.py clear - clears all keywords
```

**Step 3**: The next step is to decide how to save the keyword and its content. You have three options to choose from, although the README suggests using shelve. Let's briefly explore your options:

1. **Dictionary**: Using a dictionary allows you to store data during program execution, but it won't persist data after closing the program.
2. **Save to a File**: This approach involves saving the dictionary to a file, allowing you to read and write data to and from the file.
3. **Shelve**: [Shelve](https://docs.python.org/3/library/shelve.html) provides a dictionary-like behavior, but it is different in that it acts as a persistent, on-disk storage system. Data saved using shelve remains accessible even after you close the program, making it a suitable choice for this project. You can think of it as a way to store data for the long term.

**Step 4**: Import the Necessary Modules
sys: allows you to interact with the command line.

`import pyperclip, shelve, sys
`

**Step 5**: Create Functions
You have now created five functions, two additions to the ones defined earlier.
Let's look at these additions:

- delete_keyword takes a keyword as input and deletes it along with its associated contents.
- The run function is where the program's logic is written. 

```
def save_keyword_with_content(keyword, content):
    pass

def list_keywords():
    pass

def load_content(keyword):
    pass
   
def delete_keyword(keyword):
    pass
   
def run():
    pass  

if __name__ == '__main__':
    run()

```
It's time to dive in and start putting these functions to work.

**Step 6**: save_keyword_with_content, list_keywords, load_content, delete_keyword

In this step, you have defined the save_keyword_with_content function. It takes a keyword and content as arguments and uses the shelve module to create or open a shelf with the specified prefix. It then stores the content under the given keyword on the shelf.

```
prefix = 'mcb'

def save_keyword_with_content(keyword, content):
     # Open and close a shelf file
    with shelve.open(prefix) as mcb_shelf:
        mcb_shelf[keyword] = content

```
In the list_keywords function, you want to return a list of all the keywords (keys) stored in the shelve, but you need to return this list as a string. Here's the breakdown:

1. mcb_shelf.keys(): This part retrieves all the keys (keywords) from the mcb_shelf object. These keys are initially in a format that is not a string.
2. list(mcb_shelf.keys()): You convert the keys into a list format using list(). Now, you have a list of the keywords.
3. str(list(mcb_shelf.keys())): Lastly, you use str() to convert this list of bytes objects into a string format. This is necessary because your function is expected to return a string. By converting the list to a string, you ensure it can be returned in a human-readable format, as byte objects are not easily human-readable.

```
def list_keywords():
    # Open and close a shelf file
    with shelve.open(prefix) as mcb_shelf:
        # return keys list
        return str(list(mcb_shelf.keys()))
```

You have created the load_content function. It opens the shelf using the prefix and retrieves the content associated with the provided keyword from the shelf.

```
def load_content(keyword):
    with shelve.open(prefix) as mcb_shelf:
        return mcb_shelf[keyword]
```

In this step, you have defined the delete_keyword function. It opens the shelf with the prefix and checks if the keyword exists on the shelf. If it does, it deletes the keyword and its associated content. If the keyword is not found, it prints a message and exits the program.

```
def delete_keyword(keyword):
    with shelve.open(prefix) as mcb_shelf: 
        if keyword in mcb_shelf.keys():
            del mcb_shelf[keyword]
        else:
            print(f'{keyword} not found. It must have been deleted.')
            sys.exit()
```

You must have noticed something in these functions. What is it you noticed?

These functions all repeat the same code for opening the shelf. This repetition is a violation of the DRY (Don't Repeat Yourself) principle, which suggests that code should be reused or abstracted to avoid redundancy. We will address this issue later in your project to make the code more efficient and maintainable. Let's move on for now.
 
**Step 7**: You define the run function, which handles different command-line arguments to interact with your Multi Clipboard. 

```
def run():
    if len(sys.argv) == 3:
        # save clipboard content
        if sys.argv[1].lower() == 'save':
            keyword = sys.argv[2]
            content = pyperclip.paste()
            save_keyword_with_content(keyword, content)
        # delete keyword
        elif sys.argv[1].lower() == 'delete':  
            keyword = sys.argv[2]
            delete_keyword(keyword)
            print(f'{keyword} deleted with contents.')
            print(list_keywords())
       
    # list keywords and load content
    elif len(sys.argv) == 2:
        keywords = list_keywords()
        if sys.argv[1].lower() == 'list':
            pyperclip.copy(keywords)
            print(keywords)
        elif sys.argv[1] in keywords:
            keyword = sys.argv[1]
            content = load_content(keyword)
            pyperclip.copy(content)
            print(content)
           
    else:
        print(f'''Usage: python your_script.py save <keyword>
                python your_script.py <keyword>
                python your_script.py list''')      

```
To test it, you should run your script in a terminal or command prompt, passing the appropriate arguments.

Here is how you can test it:

1. Open a terminal or command prompt.
2. Test the different functionalities with the following commands:

- To save content to a keyword:
- `python your_script.py save my keyword`.
- To delete a keyword:
- `python your_script.py delete mykeyword`
- To list all keywords:
- `python your_script.py list`
- To load content from a specific keyword:
- `python your_script.py mykeyword`

Replace `mykeyword` with an actual keyword, and the script should perform the corresponding action as defined in the run function.

### Exercise 1
Create a function called clear_keywords without any parameters. This function will clear all keywords stored in the mcb_shelf. Additionally, handle errors properly so that if the shelf is empty, it responds with a clear message instead of generating error messages. You can run this function by executing `py mcb.py clear` in the command line.

### Exercise 2
You will realize this code works well but as you write more code you want to write clean and efficient code, minimizing repetition.
In this exercise, the goal is to address the redundancy of opening the shelve function in multiple parts of your code.

Once you are done, the solution can be found in fix_repitition.py in the project directory.

### Exercise 3
In this exercise, we focus on improving the provided code by addressing issues related to repetition.

```
if len(sys.argv) == 3:
        # save clipboard content
        if sys.argv[1].lower() == 'save':
            keyword = sys.argv[2]
            content = pyperclip.paste()
            save_keyword_with_content(keyword, content)
        # delete keyword
        elif sys.argv[1].lower() == 'delete':  
            keyword = sys.argv[2]
            delete_keyword(keyword)
            print(f'{keyword} deleted with contents.')
            print(list_keywords())
```

To improve the code, follow these steps:

- Define the keyword variable in the outer loop, allowing both the save and delete sections to access it.

### Exercise 4
After making the code structure more robust, we have identified another issue with the functionality when saving text. 

Run this on the command line: py mcb.py save text1. Copy several different texts and run them again. What did you notice?

Currently, it overrides the previous content, which may not be desirable. In this exercise, we aim to address this issue.

**Step 1**: To prevent content from being overridden when saving text, you can use an if-else statement to check if the key already exists. If it exists, return a warning message; otherwise, save the new content.

Now, the program works as expected. What if you want to ask the user for confirmation? I know right now you are thinking, well I am the only user of my program. You want to think of a real-world scenario where you have several users.

**Step 2**: If the key already exists and you want to provide the user with an option to replace it, you can add more code to ask the user if they would like to replace the existing content. This step involves integrating user input and conditional logic to determine whether to replace the content.

These improvements ensure that the Multi Clipboard functions more naturally, avoiding accidental content loss and allowing user interaction when necessary.

Once you are done, for a detailed solution, you can refer to the "fix_repitition2.py" file provided in the project directory.

### Exercise 5
There is one more issue we need to address to make our program more robust.

When users want to delete an item, it's common practice to ask for their confirmation to avoid accidental deletions. However, there is a problem with the current code, and we need to identify and correct it.

To resolve this issue, carefully review the code to find any errors that might be causing the problem. Once identified, make the necessary adjustments to ensure that the deletion confirmation process functions correctly.

For a detailed solution and to compare your answer, please refer to the "mcb2.py" file provided in your project directory.

```
def run():
    if len(sys.argv) == 3:
        # save clipboard content
        if sys.argv[1].lower() == 'save':
            keyword = sys.argv[2]
            content = pyperclip.paste()
            save_keyword_with_content(keyword, content)
        # delete keyword
        elif sys.argv[1].lower() == 'delete':  #TODO: CTA: Are you sure you want to
            keyword = sys.argv[2]
            while True:
                user_input = input(are you sure you want to delete {keyword}: ').lower()
                if user_input == 'yes' or 'y':  # fix this issue not working
                    delete_keyword(keyword)
                    print(f'{keyword} deleted with contents.')
                    print(list_keywords())
                    sys.exit()
                elif user_input == 'no' or 'n':  #fix:not working
                    print('Deletion abolished')
                    sys.exit()
```
       
### Exercise 6
In this exercise, you will create a dictionary and practice writing it to a file to persist the data. This exercise provides an opportunity to hone your skills in writing data into files.

To complete this exercise, follow these steps:

1. Instead of using `shelve`, create an empty dictionary for storage. Store the keywords as the key and the content as the value.
2. Each time you create a new keyword, you want to store it and its content in a file to persist the data. Are you using 'w' or 'a'? What encoding will you use? Answer all these questions in your code editor.

When you are done implementing these steps, test your code and submit your repository link in the comments section.

## Conclusion
We have reached the end of this project. It is important to remember that programming is an ever-evolving journey. If you believe some areas could be further improved, don't hesitate to revisit the code. In the world of programming, inspiration can strike at any moment, even days or months later.

Throughout this project, you utilized various Python modules. You used the pyperclip module for text copying and pasting, employed the shelve module for data persistence, and leveraged the sys module for efficient command-line interactions. You have not only achieved functional automation but also had the chance to explore problem-solving.

As you continue on your programming journey, keep in mind that each project is a stepping stone, contributing to your growth as a developer. Embrace the process, keep coding, stay curious, and enjoy problem-solving.

If you have any questions, want to connect, or just fancy a chat, feel free to reach out to me on [LinkedIn](https://www.linkedin.com/in/praise-ifeoluwa-idowu-back-end-developer-django?utm_source=share&utm_campaign=share_via&utm_content=profile&utm_medium=android_app) and [Twitter](https://twitter.com/PraiseID3?s=09).