1
\$\begingroup\$

I'm reworking a set of two functions that return a text file name, constructed from a string argument and string variables from a module called inventory. It seems the inventory module served as a sort of constants module, so I've changed it to constants.py and capitalized the variables. The old functions suffer from a large swath of if/elifs on the order of 600 or so lines with too many elifs to count. The original intent of the code was to not only put together the text file name but to also raise value errors if incorrect strings were passed into the function or constants were changed. I figured I could rewrite this into a try except block, but the only way I could get it to work was nest a second try inside the second except clause. Instead of using single string constants, I've stored the constants in a dictionary to validate the string values before assembling the file name. I'm sure this is silly and there's probably a better way to do it, which is why I'm here.

The function was originally written to accept two arguments, a recipe (e.g.; 'Cake', 'Jam', 'Pie') and an optional argument for an add in ingredient called add_in (e.g.;'ChoclateChip', 'Maple', 'Honey'), with the default set to None. Throughout the remainder of the program, two parameters are never passed, only one, with the idea being that if the parameter passed is an add_in, the recipe retrieved would be for 'Cookies'. This doesn't mean that two parameters will never be passed but as long as I am working on this project they won't, or I will switch it to to require both with keywords. For now, I'm using an arbitrary argument just to keep it flexible. If this is a bad idea, I have no problems changing it. Is this something I should be doing? Or should I just punt this and use just a handful of if/elifs and not do any validating/error checking?

Anyhow. Here's my code:

In constants.py

RECIPES = ['Pie', 'Jam', 'Cookies', 'Doughnuts', 'Cake', 'Marmalade']
ADD_INS = ['ChocolateChip', 'Maple', 'Honey', 'Pecan', 'Walnut', 'BrownSugar']

BAKERY_DICT = {'Apple': {'Recipes': RECIPES, 'Add-ins': ADD_INS},
               'Orange': {'Recipes': RECIPES, 'Add-ins': ADD_INS}}
FRUIT = 'Apple'

The get_file_name function:

def get_file_name(*recipe_or_addin):  # formerly get_file_name(recipe, addin=None)

    try:
        # validate the fruit by dictionary key and recipe by index in the dictionary
        constants.BAKERY_DICT[constants.FRUIT]['Recipes'].index(recipe_or_addin[0])
        return 'cook_book/recipe_' + "_".join([constants.FRUIT, recipe_or_addin[0]]) + '.txt'
    
    except KeyError:
        return 'No ' + constants.FRUIT + ' recipes in the bakery'
    
    except ValueError:
        
        try:   #<--------#####I'm not so sure I should do it this way#####
            # validate the add_in in the dictionary
            constants.BAKERY_DICT[constants.FRUIT]['Add-ins'].index(recipe_or_addin[0])
            return 'cook_book/recipe' + "_".join([constants.FRUIT, recipe_or_addin[0]]) + 'Cookies.txt'
        
        except ValueError:
            return 'No ' + recipe_or_addin[0] + ' in the inventory'
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

At the moment, you're using "_".join(...) to join fruit and ingredient/recipe, but it is a constant sized list. It would be more obvious to me to use an f-string, which would look like:

return f'cook_book/recipe_{constants.FRUIT}_{recipe_or_addin[0]}.txt'

This also would make what I suspect is a typo in your "add_in" -> "cookies" code more obvious (i.e. you return cook_book/recipeApple_WalnutCookies.txt for "Walnut" rather than cook_book/recipe*_*Apple_WalnutCookies.txt)


You use a slurped list (*recipe_or_addin) which takes any number of arguments and packs them into a list, and then you only ever check the first element of that list? Is what you want instead a default value?

def get_file_name(recipe_or_addin="Cookies")

Try not to use error handling where you really want an if statement. I believe what you intend is:

def get_file_name(recipe_or_addin):  # formerly get_file_name(recipe, addin=None)

    if recipe_or_addin in constants.RECIPES: 
        ...    
    elif recipe_or_addin in constants.ADD_INS:
        ...    
    else:
        ...

I'm not 100% sure what your ultimate goal is, but this does seem like something where you actually want to pass in fruit, recipe and add_in with defaults, e.g.

def get_file_name(fruit="apple", recipe="cookies", *add_in):
    return f'cook_book/recipe_{fruit}_{recipe}{("_"+"_".join(add_in)) if add_in else ""}.txt'

Possibly with other checks of whether the combinations are valid, or you could use pathlib or the more beginner friendly, but ultimately more verbose os.path to check whether that file exists (and also join paths so your code is system independent, i.e Windows uses \ and *nix use / as path separators) and if it is found return it or otherwise raise an error or print the appropriate warning message.

\$\endgroup\$
7
  • \$\begingroup\$ My goal was to try to keep as much of how the function is used in the existing code but eliminate lines and make things more readable. I used the arbitrary argument list (I think it actually passes as a tuple) and then only use the first element, because nowhere in the program is the function called with two parameters but the original programmer for an unknown reason wanted two parameters to be passed. Also, one of the intents of the original code was to raise exceptions IF something in the inventory (now constants) module changed or if an incorrect parameter was passed. \$\endgroup\$
    – Dan
    Commented Mar 21, 2023 at 1:20
  • \$\begingroup\$ Thank you so much for your review. I do appreciate it very much. \$\endgroup\$
    – Dan
    Commented Mar 21, 2023 at 1:20
  • \$\begingroup\$ If you are refactoring, then refactor. Cleaner, more obvious and maintainable code will help everyone in the long run if you can guarantee that it is more obvious and maintainable. You've already made an API (how things are imported/called/run) breaking change in renaming the module. Why stop there? One thing I didn't note is the bakery_dict (also unhelpful name) as it stands is not terribly helpful and is just an indirection from RECIPES and ADD_INS. What it could be is a list for each fruit of valid combinations, and then when you pass args in, you check that they are all available \$\endgroup\$ Commented Mar 21, 2023 at 7:42
  • \$\begingroup\$ Tuple vs. list in this instance is irrelevant, though you're right. The point is that the *args call makes it seem like I should pass in multiple arguments, not that there is an API conformance, and it doesn't conform to the original API anyway since I can now pass in 5 arguments, but the names have changed, so if I called it as get_file_name(recipe="Cake", addin="Walnut") my code which imports this is broken anyway. \$\endgroup\$ Commented Mar 21, 2023 at 7:46
  • \$\begingroup\$ Bear in mind, however, that I don't know what the original looked like or what it did in full, so I don't know how I would refactor, but it sounds like you're wanting some kind of in validations with sets of valid combinations possibly with dict.get(value, default) to ensure it doesn't KeyError and handle that appropriately. \$\endgroup\$ Commented Mar 21, 2023 at 7:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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