Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type annotations on custom function-based fields #68

Closed
KunstDerFuge opened this issue May 25, 2020 · 8 comments
Closed

Type annotations on custom function-based fields #68

KunstDerFuge opened this issue May 25, 2020 · 8 comments

Comments

@KunstDerFuge
Copy link

Attempting to generate a spec on a model with a custom function field gives a warning:

could not resolve field on model <class 'model_name'> with path "function_name". this is likely a custom field that does some unknown magic. maybe consider annotating the field/property? defaulting to "string".

But, I have annotated the function's return type already. Do I need to do something special in the serializer or view? Using @extend_schema with the parameter definition doesn't help.

I'm using DRF's generics.RetrieveUpdateAPIView.

(Thank you so much for the excellent efforts on this project!)

@KunstDerFuge
Copy link
Author

Sorry, did not notice the documentation. I think this should be covered there.

@tfranzel
Copy link
Owner

Hi and thanks! I revised the documenation 2 days ago, though some things might still be missing. that is probably not in there 😄

  • It's hard to tell from the message alone. this can only happen with serializers.ReadOnlyField
    I was under the assumption that all the cases were covered. It should work with a model fields and also model properties.
  • The error is unrelated to the type hint. that would emit later.
  • should also be unrelated to @extend_schema which has a different focus (views)

afais this looks like a bug. could you provide more details about the serializer field, model path, model function, that is your custom function field? that would help. with that i can take a look.

@tfranzel tfranzel reopened this May 25, 2020
@KunstDerFuge
Copy link
Author

KunstDerFuge commented May 25, 2020

Yes, thank you!

This project is still in the early stages, and all of the related info can be found in this repository.

I'm actually about to remove this function (returning bool) and replace it with another (returning an enum), but here was the code that was causing the issue:

class Evidence(models.Model):
    claim = models.ForeignKey(Claim, related_name='related_evidence', on_delete=models.CASCADE)
    source_of_evidence = models.ForeignKey(Source, related_name='evidence_cited_in', on_delete=models.CASCADE)
    evidence_relationship = models.CharField(choices=EvidenceRelationship.choices, max_length=25)
    description = models.TextField(blank=True)
    submitted_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, blank=True,
                                     related_name='evidence_submitted')

    def is_expert_verified(self) -> bool:
        topic_experts = self.claim.topic.experts.all()
        expert_reviews = [review for review in self.reviews.all() if review.reviewer in topic_experts]
        return any([review.deduced_evidence_relationship == self.evidence_relationship for review in expert_reviews])

I was able to 'fix' this by moving the function into the serializer, instead of defining it on model, using 'serializers.SerializerMethodField()', which silenced the warning and correctly detects the bool type. 🎉

I later decided what I need is a different function that returns an enum. I don't know if I want to clog this issue with a possibly unrelated one, but, I'm finding trouble there as well:
type hint for function "get_expert_consensus_relationship" is unknown. defaulting to string.
I'm not sure how to annotate the new function-based field, 'expert_consensus_relationship', returning EvidenceRelationship, a custom enum. I'm still using SerializerMethodField, and the function definition looks like this:
def get_expert_consensus_relationship(self, obj: Evidence) -> EvidenceRelationship:

The full code can be found here.

@tfranzel
Copy link
Owner

so i had a look. it is "uncommon" to have a function on the model i suppose. usually people make properties out of their model functions. though i found that DRF does recognize this, so i added that functionality. if you would have added the @property to it, it would have worked instantly.

regarding your second issue. i see you are using TextChoices. This is still relatively new to Django and i have not added this yet. good catch! in the meantime this should do it i believe:

@extend_schema_field(serializers.ChoiceField(choices=EvidenceRelationship.choices))
def get_expert_consensus_relationship(self, obj: Evidence):
    return self.get_consensus(obj, expert=True)

let me know if know if thats works for you. Btw, very nice project! something very similar crossed my mind a while ago.

@KunstDerFuge
Copy link
Author

KunstDerFuge commented May 26, 2020

Yes, this worked! I can't thank you enough for the help, or for this excellent project. I am now able to generate a fully working schema, almost without any intervention.

There is one last issue I'm seeing, and this is also a problem in DRF itself (issue #7204). The URLField model generates a schema with a regex pattern that causes an error in Swagger. This may be a problem with the Swagger/OpenAPI spec, though.

Here is an excerpt from my schema which contains the breaking pattern:

        url:
          type: string
          format: uri
          maxLength: 200
          pattern: "^(?:[a-z0-9\\.\\-\\+]*)://(?:[^\\s:@/]+(?::[^\\s:@/]*)?@)?(?:(?:25[0-5]|2[0-4]\\\
            d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}|\\[[0-9a-f:\\\
            .]+\\]|([a-z\xA1-\uFFFF0-9](?:[a-z\xA1-\uFFFF0-9-]{0,61}[a-z\xA1-\uFFFF\
            0-9])?(?:\\.(?!-)[a-z\xA1-\uFFFF0-9-]{1,63}(?<!-))*\\.(?!-)(?:[a-z\xA1\
            -\uFFFF-]{2,63}|xn--[a-z0-9]{1,59})(?<!-)\\.?|localhost))(?::\\d{2,5})?(?:[/?#][^\\\
            s]*)?\\Z"

I believe the documentation would cover the case of simply re-typing this field as a string (which is fine for my use case), but I thought it might be worth pointing out to you anyway.

And thanks for the feedback on my project! I am excited about it, but we'll see if it ever gets off the ground.

@tfranzel
Copy link
Owner

sure, you're welcome! glad it works for you.

i'm aware of that issue and was hoping the DRF guys come up with a good solution 😄. i believe the regex flavor used here does not play nice with what what swagger expects and for that matter what is expected through OpenAPI json schema specification. i thought about replacing the regex but a compliant, good and working url regex is harder than one might think.

indeed you can override this if you like.

i guess its a critical mass kind of app. good luck with it!

@KunstDerFuge
Copy link
Author

Actually, one more note about the EvidenceRelationship schema. I have further updated the serializer to return two different relationships in a response:

    @extend_schema_field(serializers.ChoiceField(choices=EvidenceRelationship.choices))
    def get_expert_consensus_relationship(self, obj: Evidence) -> EvidenceRelationship:
        return self.get_consensus(obj, expert=True)

    @extend_schema_field(serializers.ChoiceField(choices=EvidenceRelationship.choices))
    def get_community_consensus_relationship(self, obj: Evidence) -> EvidenceRelationship:
        return self.get_consensus(obj, expert=False)

This does work, but it's generating two separate enum types in the spec:

    ExpertConsensusRelationshipEnum:
      enum:
      - PROVES
      - SUPPORTS
      - UNRELATED
      - INCONCLUSIVE
      - DISPUTES
      - DISPROVES
      - SPLIT
      type: string
    CommunityConsensusRelationshipEnum:
      enum:
      - PROVES
      - SUPPORTS
      - UNRELATED
      - INCONCLUSIVE
      - DISPUTES
      - DISPROVES
      - SPLIT
      type: string

Is there any way to tell drf-spectacular that these are the same enum?

@tfranzel
Copy link
Owner

moved this to #70 for clarity. close this issue if we are done here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants