Rant first, code will be after!
Right now, I’m attempting to create a command line interface module for a generic shop system I can use in any project going forward. I’m using MyPy since I frankly don’t trust myself to keep types straight and all that, so due to the attempted genericness of this project that’s lead to me using some stuff I’ve never used before. The goal is that I can have a store hold and sell whatever items I want as long as they have a .name.
Anyway, while writing the function to remove an item from the user’s cart by checking an entered name (for example, if I have an item with name “sword” in my cart, I would type “remove sword” into the input), I realized I had no way of narrowing down the inputted types so that I could be sure that the ItemClass (the name of my TypeVar) has a .name. Since I had not narrowed this down, attempting to access .name leads to MyPy throwing a fit. I know type-checker driven development is a bad habit, but I really don’t want to have to slap an ugly “Any” on there.
For testing purposes, the class I’m using for ItemClass is simply a hashable dataclass with a .name attribute, but ideally it could be any hashable class with a .name atrribute.
Here is the offending function. Note that this sits inside a Shop class, which has a self.cart attribute which stores ItemClass objects as keys and some associated metadata in a tuple as the value:
def remove_from_cart(self, item_name):
match: list[ItemClass] = [
item.name for item in self.cart.keys() if item.name == item_name
]
if match:
self.cart.pop(match[0])
MyPy throws a fit on the middle line of the list comprehension, because I have not ensured that the type used for ItemClass has a .name. Ideally, I would want to make it so that the type checker will throw an error if I even try to use a class which doesn’t have the required attribute. If at all possible, I’d like to try my absolute best to avoid having to make this check at runtime, just because I’d prefer to be able to have the checker say “Hey, this type you’re attempting to sell-it doesn’t have a .name!” before I’ve even made it to the F5 button.
Thanks in advance,
HobbitJack
EDIT 1:
My Shop class:
class Shop(Generic[ItemClass, PlayerClass]):
def __init__(self, items: dict[ItemClass, tuple[Decimal, int]]):
self.inventory: dict[ItemClass, tuple[Decimal, int]] = items
self.cart: dict[ItemClass, tuple[Decimal, int]] = {}
def inventory_print(self):
for item, metadata in self.inventory.items():
print(f"{item}: {metadata}")
def add_to_cart(self, item: ItemClass, price: Decimal, amount: int):
self.cart[item] = (price, amount)
def remove_from_cart(self, item_name):
match: list[ItemClass] = [
item.name for item in self.cart if item.name == item_name
]
if match:
self.cart.pop(match[0])
def print_inventory(self):
for item, metadata in self.inventory.items():
print(f"{item}: {metadata[1]} * ${metadata[0]:.2f}")
def cart_print(self):
total_price = Decimal(0)
for item, metadata in self.inventory.items():
total_price += metadata[0]
print(f"{item}: {metadata[1]} * ${metadata[0]:.2f}")
print(f"Total price: {total_price:.2f}")
and my test Item class:
@dataclass(frozen=True)
class Item:
name: str
durability: float
with the MyPy error message:
"ItemClass" has no attribute "name"
EDIT 2:
Here goes for an attempt at a simple version of what I’ve got above:
In module 1, simple_test.py, we have a simple dataclass:
from dataclasses import dataclass
@dataclass
class MyDataclass:
my_hello_world: str
In module 2, we attempt to use a TypeVar to print the .my_hello_world of a MyDataclass object:
from typing import TypeVar
import simple_test
MyTypeVar = TypeVar("MyTypeVar")
def hello_print(string_container: MyTypeVar):
print(string_container.my_hello_world)
hello_print(simple_test.MyDataclass("Hello, World!"))
MyPy complains on the print line of hello_print() with the error message "MyTypeVar" has no attribute "my_hello_world".
My goal is that I would be able to narrow down the type being passed into hello_print into one which definitely has a .my_hello_world so that it stops complaining.
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
You can bind ItemClass type variable to the protocol that defines required attribute. It will be compatible with any object that has name which is a string.
from dataclasses import dataclass
from typing import Protocol, TypeVar
class HasName(Protocol):
name: str
ItemClass = TypeVar('ItemClass', bound=HasName)
@dataclass
class MyDataclass:
name: str
def hello_print(string_container: ItemClass):
print(string_container.name)
hello_print(MyDataclass("Hello, World!"))
Now hello_print knows only that string_container has name attribute which is a string. When you pass MyDataClass to it, mypy checks that it does really have string name attribute. If it does, type checking succeeds. If you try to pass
@dataclass
class AnotherDataclass:
foo: str
hello_print(AnotherDataclass('Foo'))
mypy will complain that AnotherDataclass is not compatible with ItemClass. You can try this in playground.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0