I've got a bunch of Django template inclusion tags, which take as an argument either a specific instance of a database object or a string/int, which is interpreted as the primary key of that database object. For example...
{% render_product product=obj %}
{% render_product product=42 %}
{% render_product product="42" %}
...all work fine and do the obvious: they render a template fragment with a particular Product instance, fetching it by primary key from the database, if needed. This is how Product and similar classes are defined:
class Product(models.Model):
# standard django model definition goes here
Here's what usually happens in such an inclusion tag:
@register.inclusion_tag("render_product.html")
def render_product(product: Union[Product, str, int] = None) -> dict:
_product = None
if isinstance(product, Product):
_product = product
elif isinstance(product, str) or isinstance(product, int):
try:
_product = Product.objects.get(pk=product)
except (Product.DoesNotExist, ValueError):
pass
return {"product": _product}
Since I've got the same pattern happening in dozens of inclusion tags, I'm trying to refactor it out, so that I've got something like:
@register.inclusion_tag("render_product.html")
def render_product(product: Union[Product, str, int] = None) -> dict:
_product = fetch_object(Product, product)
return {"product": _product}
Here's the fetch_object code:
def fetch_object(cls: Type[Model] = None, obj: Union[Model, str, int] = None):
if isinstance(obj, cls):
return obj
elif isinstance(obj, str) or isinstance(obj, int):
try:
return cls.objects.get(pk=obj)
except (cls.DoesNotExist, ValueError):
pass
return None
My problem is: I have no idea how to specify the return type of that function. Basically it should be something like "instance of any class, which is derived from Model or None". But if I try something like...
def fetch_object(
cls: Type[Model] = None, obj: Union[Model, str, int] = None
) -> Union[Model, None]:
...then PyCharm complains about "unresolved attribute reference" if I access a method on the fetched object, which is Product-specific, not Model-specific.
I'm trying to use more and more type-hinting in my Python code, because it has already saved my butt a few times, but this is one of those cases, where I have no idea what the correct way of doing it would be and my google-fu is failing me.
What is the correct type-hinting for fetch_object?
What you want to do here is make your fetch_object
function a generic function.
That is, rather then just saying that your function accepts any Type[Model]
, capture exactly which kind of model you accept using a type variable, and specify that exact kind is the output. For example:
from typing import TypeVar
# The bound states that T can be bound to Model or any subclass of Model.
# If the bound keyword argument is omitted, we assume the bound is 'object'.
T = TypeVar('T', bound=Model)
def fetch_object(cls: Type[T] = None, obj: Union[T, str, int] = None) -> Optional[T]:
if isinstance(obj, cls):
return obj
elif isinstance(obj, str) or isinstance(obj, int):
try:
return cls.objects.get(pk=obj)
except (cls.DoesNotExist, ValueError):
pass
return None
One minor note on stylistic conventions: I chose to name the typevar T
here for brevity. The other common convention is to name your typevar something like _TModel
or _ModelT
. That is, the underscore to make the variable private, and a longer name for readability.