1

how to solve the recursion problem when specifying type hints for classes from different files

models1.py

from models2 import Second
@dataclass
class First:
    attribute: Second

models2.py

from models1 import First
@dataclass
class Second:
    attribute: First

In real code, I wanted to split SentInvoice and User models into different files.

class User(models.Model):
    user_id = fields.BigIntField(index=True, unique=True)
    username = fields.CharField(32, unique=True, index=True, null=True)
    first_name = fields.CharField(255, null=True)
    last_name = fields.CharField(255, null=True)
    language = fields.CharField(32, default="ru")
    balance: Balance = fields.OneToOneField("models.Balance", on_delete=fields.CASCADE)
    sent_invoice: fields.OneToOneNullableRelation["SentInvoice"] # here
    registered_user: RegisteredUser

    @classmethod
    async def create(cls: Type[MODEL], **kwargs: Any):
        return await super().create(**kwargs, balance=await Balance.create())


class SentInvoice(models.Model):
    amount = fields.DecimalField(17, 7)
    shop_id = fields.CharField(50)
    order_id = fields.CharField(10, null=True)
    email = fields.CharField(20, null=True)
    currency = fields.CharField(5, default="RUB", description="USD, RUB, EUR, GBP")
    user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", on_delete=fields.CASCADE)  # here
    created_invoice: fields.OneToOneNullableRelation[CreatedInvoice]

    async def send(self) -> CreatedInvoice:
        cryptocloud = config.payment.cryptocloud
        async with aiohttp.ClientSession(headers={"Authorization": f"Token {cryptocloud.api_key}"}) as session:
            async with session.post(cryptocloud.create_url, data=dict(self)) as res:
                created_invoice = await CreatedInvoice.create(**await res.json(), sent_invoice=self)
                return created_invoice
Charls Ken
  • 101
  • 2
  • 7
  • won't these types use infinite memory? Moreover, how would you even make instances? – joel May 09 '22 at 12:56
  • re type hints, you probably want to quote one of them e.g. `"First"` - i don't think forward annotations would work. That said, I don't know if that's possible with them in different modules – joel May 09 '22 at 12:57
  • 1
    that is, to use quoted names I think the names still need to be in scope somewhere in the file. For that, you need the imports, which are circular – joel May 09 '22 at 14:22
  • a mutually recursive type wil need to be defined in the same file. you should also provide a concrete example to demonstrate how it will be used. this goes a long way in helping you understand the implications of what you are asking. – Mulan May 09 '22 at 15:39

1 Answers1

1

You need to use two techniques that are specific to type hinting in python, 1) forward references, and 2) importing types within a TYPE_CHECKING guard (check e.g. this post for a longer explanation of its implications). The first one allows you to reference a type that is not know to the interpreter at runtime, and the latter resolves types in a "type checking context".

Long story short:

models1.py

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models2 import Second

@dataclass
class First:
    attribute: "Second"

models2.py

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models1 import First

@dataclass
class Second:
    attribute: "First"

Executing the files with python3.8 or higher should work without any issues[1], and can work in python3.7 as well with a __futures__ import. Running mypy on the files should work without any issues, too:

$ mypy models1.py models2.py
Success: no issues found in 2 source files 

[1] As comments have pointed out, creating actual instances of your First/Second classes that would also pass a type check is impossible, but I assume that this is a toy example and your real code has, for example, one of the attribues as Optional.

Arne
  • 17,706
  • 5
  • 83
  • 99
  • Thank you, that's what I need. You are right, I have given this only as an example. In real code, I wanted to split several types of models into different files. – Charls Ken May 13 '22 at 22:26