Cater Waiter TypeError in testcases

Hi, I think I’ve found a bug in a test case. I mean they’re a bit too complicated for me to understand, but I’m getting a crash and it seems to be a crash in test case for the last exercase in SetsTest.test_singleton_ingredients. To make sure, I’ve restored the function to its original state and it only has pass as the body. Log below:

b0rsuk@dziupla:~/warsztat/cwiczenia/exercism/python/cater-waiter$ uv run pytest -o markers=task
========================================================================= test session starts =========================================================================
platform linux -- Python 3.13.5, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/b0rsuk/warsztat/cwiczenia/exercism/python
configfile: pyproject.toml
collected 7 items                                                                                                                                                     

sets_test.py .....F.                                                                                                                                            [100%]

============================================================================== FAILURES ===============================================================================
_________________________________________________________________ SetsTest.test_singleton_ingredients _________________________________________________________________

self = <sets_test.SetsTest testMethod=test_singleton_ingredients>

    @pytest.mark.task(taskno=7)
    def test_singleton_ingredients(self):
        test_data = zip(dishes_and_overlap, singletons)
    
>       for variant, (item, result) in enumerate(test_data, start=1):
                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

sets_test.py:113: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
sets_test_data.py:453: in <genexpr>
    singletons  = (item[0] ^ item[1] for item in zip(ingredients, intersections))
                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

.0 = <list_iterator object at 0x7fedf8cc0940>

>   ingredients = (set.union(*group) for group in ingredients_only)
                   ^^^^^^^^^^^^^^^^^
E   TypeError: unbound method set.union() needs an argument

sets_test_data.py:452: TypeError
======================================================================= short test summary info =======================================================================
FAILED sets_test.py::SetsTest::test_singleton_ingredients - TypeError: unbound method set.union() needs an argument
===================================================================== 1 failed, 6 passed in 0.05s =====================================================================

I thought maybe this fancy new tool I’m using, uv, was interfering with tests and tried running them the old way:

b0rsuk@dziupla:~/warsztat/cwiczenia/exercism/python/cater-waiter$ dpkg -l | grep python3-pytest
ii  python3-pytest                                 8.3.5-2                              all          Simple, powerful testing in Python3

(Yes, I just installed the package and I’m using Debian Linux 13/Trixie)

b0rsuk@dziupla:~/warsztat/cwiczenia/exercism/python/cater-waiter$ python3 --version
Python 3.13.5

But running tests without uv doesn’t help:

b0rsuk@dziupla:~/warsztat/cwiczenia/exercism/python/cater-waiter$ python3 -m pytest -o markers=task
========================================================================= test session starts =========================================================================
platform linux -- Python 3.13.5, pytest-8.3.5, pluggy-1.5.0
rootdir: /home/b0rsuk/warsztat/cwiczenia/exercism/python
configfile: pyproject.toml
plugins: typeguard-4.4.2
collected 7 items                                                                                                                                                     

sets_test.py .....F.                                                                                                                                            [100%]

============================================================================== FAILURES ===============================================================================
_________________________________________________________________ SetsTest.test_singleton_ingredients _________________________________________________________________

self = <sets_test.SetsTest testMethod=test_singleton_ingredients>

    @pytest.mark.task(taskno=7)
    def test_singleton_ingredients(self):
        test_data = zip(dishes_and_overlap, singletons)
    
>       for variant, (item, result) in enumerate(test_data, start=1):

sets_test.py:113: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
sets_test_data.py:453: in <genexpr>
    singletons  = (item[0] ^ item[1] for item in zip(ingredients, intersections))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

.0 = <list_iterator object at 0x7f68ce4013c0>

>   ingredients = (set.union(*group) for group in ingredients_only)
E   TypeError: unbound method set.union() needs an argument

sets_test_data.py:452: TypeError
======================================================================= short test summary info =======================================================================
FAILED sets_test.py::SetsTest::test_singleton_ingredients - TypeError: unbound method set.union() needs an argument
===================================================================== 1 failed, 6 passed in 0.06s ====================================================================

To make it easier for you, I’ve just submitted my partial, unfinished program. The last solution (for singleton_ingredients) is currently commented out.

Could you share your code? It seems like either you’ve modified one of the test files or your code is modifying one of the test values that shouldn’t be modified (or modify-able).

Can’t you see my solution file I uploaded using ./exercise --submit? Anyway, I think you were right. I was modifying the variable passed to the function by using .pop() in a while loop. This is my code. I have since modified it:

"""Functions for compiling dishes and ingredients for a catering company."""


from sets_categories_data import (VEGAN,
                                  VEGETARIAN,
                                  KETO,
                                  PALEO,
                                  OMNIVORE,
                                  ALCOHOLS,
                                  SPECIAL_INGREDIENTS)


def clean_ingredients(dish_name: str, dish_ingredients: list[str]) -> tuple[str, set[str]]:
    """Remove duplicates from `dish_ingredients`.

    Parameters:
        dish_name (str): The name of the dish.
        dish_ingredients (list): The ingredients for the dish.

    Returns:
        tuple: Containing (dish name, ingredient set).

    This function should return a `tuple` with the name of the dish as the first item,
    followed by the de-duped `set` of ingredients as the second item.

    """

    return (dish_name, set(dish_ingredients))


def check_drinks(drink_name: str, drink_ingredients: list) -> str:
    """Append "Cocktail" (alcohol)  or "Mocktail" (no alcohol) to `drink_name`, based on `drink_ingredients`.

    Parameters:
        drink_name (str): Name of the drink.
        drink_ingredients (list): Ingredients in the drink.

    Returns:
        str: `drink_name` appended with "Mocktail" or "Cocktail".

    The function should return the name of the drink followed by "Mocktail" (non-alcoholic) and drink
    name followed by "Cocktail" (includes alcohol).

    """

    last_word = "Cocktail"
    if set(drink_ingredients).isdisjoint(ALCOHOLS):
        last_word = "Mocktail"
    return f"{drink_name} {last_word}"


def categorize_dish(dish_name: str, dish_ingredients: set[str]) -> str:
    """Categorize `dish_name` based on `dish_ingredients`.

    Parameters:
        dish_name (str): The dish to be categorized.
        dish_ingredients (set): The ingredients for the dish.

    Returns:
        str: The dish name appended with ": <CATEGORY>".

    This function should return a string with the `dish name: <CATEGORY>` (which meal category the dish belongs to).
    `<CATEGORY>` can be any one of  (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE).
    All dishes will "fit" into one of the categories imported from `sets_categories_data.py`
    """

    category = "VEGAN"
    if dish_ingredients <= VEGETARIAN:
        category = "VEGETARIAN"
    elif dish_ingredients <= PALEO:
        category = "PALEO"
    elif dish_ingredients <= KETO:
        category = "KETO"
    elif dish_ingredients <= OMNIVORE:
        category = "OMNIVORE"
    return f"{dish_name}: {category}"


def tag_special_ingredients(dish) -> tuple[str, set[str]]:
    """Compare `dish` ingredients to `SPECIAL_INGREDIENTS`.

    Parameters:
        dish (tuple): (dish name, list of dish ingredients).

    Returns:
        tuple: Containing (dish name, dish special ingredients).

    Return the dish name followed by the `set` of ingredients that require a special note on the dish description.
    For the purposes of this exercise, all allergens or special ingredients that need to be tracked are in the
    SPECIAL_INGREDIENTS constant imported from `sets_categories_data.py`.
    """

    name, ingredients = dish
    return (name, set(ingredients) & SPECIAL_INGREDIENTS)


def compile_ingredients(dishes: list[set[str]]) -> set[str]:
    """Create a master list of ingredients.

    Parameters:
        dishes (list): Dish ingredient sets.

    Returns:
        set: Ingredients compiled from `dishes`.

    This function should return a `set` of all ingredients from all listed dishes.
    """

    result = set()
    while dishes:
        result |= set(dishes.pop())
    return result


def separate_appetizers(dishes: list[str], appetizers: list[str]) -> list[str]:
    """Determine which `dishes` are designated `appetizers` and remove them.

    Parameters:
        dishes (list): Group of dish names.
        appetizers (list): Group of appetizer names.

    Returns:
        list: Group of dish names that do not appear on appetizer list.

    The function should return the list of dish names with appetizer names removed.
    Either list could contain duplicates and may require de-duping.
    """

    return list(set(dishes) - set(appetizers))


def singleton_ingredients(dishes, intersection):
    """Find singleton ingredients within the group of dishes (ingredients that only appear once across dishes).

    Parameters:
        dishes (list): Group of ingredient sets.
        intersection (set): Can be one of `<CATEGORY>_INTERSECTIONS` constants imported from `sets_categories_data.py`.

    Returns:
        set: Containing singleton ingredients.

    Each dish is represented by a `set` of its ingredients.

    Each `<CATEGORY>_INTERSECTIONS` is an `intersection` of all dishes in the category. `<CATEGORY>` can be any one of:
        (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE).

    The function should return a `set` of ingredients that only appear in a single dish.
    """

    #result = set()
    #for dish in dishes:
    #    result ^= dish
    #return result - intersection
    pass

The problem was in compile_ingredients. Thank you.
But shouldn’t individual tests be independent from each other?

I think it would be even better if tests detected this type of functions and warned about them. Or if some kind of linter warned about it. This is something that has bitten me a couple of times in the past: I write a function that takes an argument and returns the argument, but also uses the argument as a temporary variable for preparing the result. I think I picked up this habit because lots of Python functions operate on immutable data (numbers, strings, and tuples) so I generally use functions like this: variable = some_function(variable), and I use list_variable = sorted(list_variable) rather than list_variable.sort().
I need to be more aware of this and perhaps adopt some kind of naming convention.

That would likely be the culprit :smile:

@BethanyG Would you be open to me opening a PR to change the tests here so they pass in a copy of the data to protect the test data?

I think I used this kind of (anti)pattern in my prior Python solutions here. I’ll revisit them and modify them to be safer/more explicit.

If you’re not already doing so, you should request code reviews from the mentors! That’s something they might call out.

1 Like

I’m not doing this, because I’m still at the “learning” exercises.

If you’re doing learning exercises, that’s an even better time to get mentoring! Wouldn’t you want to learn even more?