Add type annotations for primary key types by model [FC-0117]#534
Add type annotations for primary key types by model [FC-0117]#534bradenmacdonald wants to merge 24 commits intomainfrom
Conversation
|
Thanks for the pull request, @bradenmacdonald! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
|
@ormsbee @kdmccormick Can I get your thoughts on this? It also has implications for OEP-68. |
| # Note: Django-Stubs forces mypy to identify the `.pk` attribute of this model as having 'Any' type (due to our | ||
| # use of a OneToOneField primary key), and this is impossible for us to override, so we prefer to use | ||
| # `.id` which we can control fully. | ||
| # Since Django uses '.pk' internally, we have to make sure it still works, however. So the best we can do is | ||
| # override this with a deprecated marker, so it shows a warning in developer's IDEs like VS Code. |
There was a problem hiding this comment.
Ouch. Very timely to know this given the OEP in flight on identifiers, where we recommend using .pk.
There was a problem hiding this comment.
Yes, as you can read above in the edited description, I actually changed my approach here, as it turns out it's much easier to override the type of .id than .pk in general. So I'm wondering if we should just recommend .id / _id more.
There was a problem hiding this comment.
oof, good find. I've opened a thread here: openedx/openedx-proposals#773 (comment)
There was a problem hiding this comment.
If we standardize on .id, then I presume we can drop this deprecation warning? I think most developers naturally use .id anyway.
There was a problem hiding this comment.
I found that .pk was being used in quite a few places in this codebase, and the deprecation warnings made it easy for me to track them down and replace them with .id.
|
If we want to also introduce typed keys for Unit/Section/Subsection subclasses, that's easy to do: #535 . Would welcome thoughts on this as I'm a bit on the fence. |
| entities for different learners or at different times. | ||
| """ | ||
|
|
||
| PK: TypeAlias = ContainerPK |
There was a problem hiding this comment.
If we standardize on id, I think it'd make sense to call the types Container.ID, ContainerID, etc.
There was a problem hiding this comment.
Sure, updated to Container.ID/ContainerID etc.
|
OK, good news! I found a nicer way to do this. It still requires a custom field type per model, but we can put the definitions entirely within the model class itself, and there is now one global e.g. within the Note: This could be made even more concise, and copy-pastable without changes: ID = NewType("ID", int)
class IDField(TypedBigAutoField[ID]): ...
id = IDField(primary_key=True)
Edit: actually this works great, so I think I prefer the second version now. If you have several types called |
9d0d7c4 to
cd54e54
Compare
This PR has some experimental implementations of #436 as well as moving closer toward the consistent terms preferred in OEP-68.
Second Approach: fully-typed
.id, avoid.pk(Revised from my initial attempt)
PublishableEntity,LearningPackage,Container, andComponentall get a fully-typed.idfield.PublishableEntity.idis of typePublishableEntity.PKwhich is a sub-type ofint.Container.idis of typeContainer.PKwhich is a sub-type ofPublishableEntity.PK.Component.idis of typeComponent.PKwhich is a sub-type ofPublishableEntity.PK.LearningPackage.idis of typeLearningPackage.PKwhich is a sub-type ofint.CatalogCourseandCourseRunget typed..pkon models that inheritPublishableEntityMixin(or any other models that use aOneToOneFieldas theirprimary_key, at least without modifying django-stubs or implementing a mypy plugin. It will always have typeAny, circumventing all type definitions. However, although we cannot override its type, we can mark it asdeprecated. As a result, and for consistency, accessing.pkon any of these will raise a deprecation warning in pytest/console and show a warning in your IDE:CatalogCourse.pkandCourseRun.pk, because (a) they are typed correctly, and (b) mypy crashes when I do 😬.Prior Approach: fully-typed
.pk, avoid.idExpand for pros/cons/details of this earlier approach
In this earlier approach:PublishableEntity's primary key is the.pkfield, and it has typePublishableEntity.PKwhich is a sub-type ofint.some_entity.idwill show a deprecation warning, telling you to usepkinstead. And since this is an actual field calledpk, any DB lookups need to be changed from e.g.entity__id=...toentity__pk=...idand make an alias calledentity_pk, then use deprecation warnings to encourage the use ofentity_pkoveridandpk; this would be more consistent with what I did for containers and components, but it's slightly annoying to typeentity_pkinstead ofpkeverywhere.idand makingpka fully-typed alias is unfortunately not possible, as django-stubs forcibly overrides the type ofpkunless you have a concrete field namedpkas I did here.ContainerandComponentgetContainer.PKandComponent.PKtypes which are sub-types ofPublishableEntity.PKContainerandComponentcannot have correctly-typed.pkaccessors due to django-stubs limitations, so I created a.container_pkand.component_pkaccessor for them:ContainerandComponenthave aOneToOneField(PublishsableEntity, primary_key=True), django-stubs will always override theirpkattribute to have typeAny, and it's impossible to change this without modifying django-stubs or writing our own mypy plugin. But, we can use the@deprecateddecorator to annotate the attribute with a deprecation warning which does show up in the IDE and in the pytest logs wherever.pkis used..pk_?)General Notes
One downside of using deprecation warnings as I have here is that Django itself uses
.pk, so in the case ofContainerandComponent(but notPublishableEntity) where we've marked.pkas deprecated, even if our code doesn't use.pkitself, you'll see warnings like this when running pytest:django/db/models/fields/related.py:812: DeprecationWarning: Use component_pk instead. It is possible to suppress these deprecation warnings, and I have done so (seetox.ini) but it's an extra step required to use this library well and I'm not a big fan of it.