Introduce file attachment states.
Review Request #13238 — Created Aug. 27, 2023 and submitted
Our file attachments can be in many different states, but we don't really have
a solid concept of these states in our codebase so far. We also don't have a
formal way of determining which state a file attachment is in.This change introduces file attachment states and allows us to efficiently
get the state of a file attachment through its review request or draft.This sets the foundation for an upcoming change that improves how we display
file attachments on a review request and how users can manage them.
- Ran unit tests.
- Ran JS unit tests.
- Used in upcoming changes.
Summary | ID |
---|---|
f81a59d3ac1b258a3dffee7a378a3d0856a7a088 |
Description | From | Last Updated |
---|---|---|
Can we make a note to change this to use StrEnum once we're 3.11+? |
david | |
I wonder if it's worth defining a TypeAlias for this (perhaps just alias for List[FileAttachment]). It feels pretty repetitive. |
david | |
Can you add a module docstring while here? |
chipx86 | |
This should probably be a Sequence[FileAttachment], rather than a List[FileAttachment], and I'll go over why. It's a subtle thing, something … |
chipx86 | |
Can you add a module docstring? |
chipx86 | |
Are these meant to be used as labels, or IDs? "State" reads to me like an internal value, but the … |
chipx86 | |
We should designate all these keyword-only arguments. |
chipx86 | |
If the method is on the same class, you can leave off the class name. |
chipx86 | |
Each of these should be typed as Set[Any]. Note the Any is actually correct, as django-stubs types the primary key … |
chipx86 | |
The problem with the or here is that if the caller explicitly passes in an empty list, it'll cause a … |
chipx86 | |
I couldn't find the change.. Where does sort= come from? |
chipx86 | |
These should be typed. |
chipx86 | |
Missing a type. |
chipx86 | |
Missing a type. |
chipx86 | |
Should designate these keyword-only arguments. |
chipx86 | |
I'd say we probably should avoid the **kwargs for this case, since this is going to always want to exactly … |
chipx86 | |
For here and the reference below, the full module path to ReviewRequest must be specified. For this sort of case, … |
chipx86 | |
Missing , optional. |
chipx86 |
- Change Summary:
-
- Added a
FileAttachmentList
TypeAlias.
- Added a
- Commits:
-
Summary ID 43eb67ff83be5fcac4145d659fee56707c516474 5ec38fdc4f63e1a3a5b01abfb98c27521899a27e - Diff:
-
Revision 2 (+2720 -28)
Checks run (2 succeeded)
-
This is great work.
You're going to scroll down and see a giant comment. Don't worry, it's mostly a deep dive on invariance/covariance in typing. But it explains how
List[FileAttachment]
is, non-obviously, the wrong type to use for a list of file attachments in many cases.The rest are, for the most part, small doc-related things and some missing types.
-
-
This should probably be a
Sequence[FileAttachment]
, rather than aList[FileAttachment]
, and I'll go over why. It's a subtle thing, something I've been finding out the hard way about, and will be pointing out more in code reviews.List
,Dict
,Set
,Tuple
, etc. are very specific types that represent these mutable objects and behaviors. They're great for returning when we really want the caller to have all the same behaviors that the function itself has, such as adding/deleting items. But they're less ideal when we want to return results intended just for reading.Instead, we want to consider more general types, like
Sequence
,Mapping
, andIterator
. These describe a subset of the behaviors, the read-only parts.There are a couple benefits to using more general types here:
- It conveys to the caller that the result is immutable/read-only.
- These general types are covariant, whereas the specific types are invariant, and these affect how the types can be used.
Let's dig into what #2 means.
Variance
An invariant type is a specific type, and can only be assigned or passed in where that specific type is expected. A covariant type can be assigned that type or any subtype.
This is easier with examples:
class A: pass class B(A): pass class C(B): pass # Lists are invariant: l1: List[B] = [B(), C()] # OK l2: List[B] = [A()] # Not OK: Supertype assigned l3: List[A] = l1 # Not OK: List[A] and List[B] are invariant l4: List[Optional[B]] = l1 # Not OK: Same reason # Sequences are covariant: s1: Sequence[B] = [B(), C()] # OK s2: Sequence[B] = [A()] # Not OK: Supertype assigned s3: Sequence[A] = s1 # OK: Sequence[A] is covariant, allowing subtype of Sequence[B] s4: Sequence[Optional[B]] = s1 # OK: Same reason # Let's try mixing-and-matching: s5: Sequence[B] = l1 # OK: List[B] is a subtype of covariant Sequence[B] l5: List[B] = s1 # Not OK: Sequence[B] is not a subtype of List[B].
(Note that the variance here is about the whole type, such as
List[B]
, though that can be controlled if you were making your own Generics. Bigger topic.)There's also contra-variance, which is the opposite of covariance (they can use a type or any parent type).
The reason for
List
being invariant is thatl1
is a type that expectsB
or subclasses as items, anyB
or a subclass can be appended to it, and all read-related functions can therefore ensure aB
or subclass. However,l3
expectsA
or subclasses, meaning aSubA
could be appended in, whichList[B]
would not allow. That's why the types are not compatible in thel3 = l1
assignment above.However,
s1
doesn't have this problem. There are no mutable operations exposed on aSequence
(or the other general types I listed), so the guarantee is always that items of the type are going to be returend. So we can dos3 = s1
without a problem.s3
says that all items are aA
or subclass, and since we can't mutate it, andSequence[B]
is aB
or subclass, that fulfills the contract.Same with
Optional[...]
in those types, becauseOptional[B]
meansB | None
, which is a supertype ofB
, likeA
is a supertype ofB
.(Does that make sense? It might not at first. There's a lot to wrap one's head around, and that only gets worse once you dive in further.)
Guidelines
We can follow two simple rules to keep this relatively sane:
- Use the general types (
Sequence
, etc.) when returning or accepting values that are not/should not be modified. - Use the specific types when building the values or returning/accepting values meant to be modified.
In this case...
Now, it probably doesn't make sense to make a
TypeAlias
for every possible value, but it very much depends.djblets.util.typing
definitely has mutable and immutable versions of these types. But depending on context here, we may want aList[FileAttachment]
, aSequence[FileAttachment]
, or anIterator[FileAttachment]
, and maybe even more. And then there'sOptional
and friends.I know David mentioned that the
Optional[List[FileAttachment]]
was getting wordy, though, which is true. It's even more wordy with aSequence
. The problem in either case is that aTypeAlias
here only saves two characters (especially if keeping theOptional[...]
).In some cases, if the type is really module-specific, a private
TypeAlias
defined inTYPE_CHECKING
would be the way to go, but too many modules need this.So it probably makes sense to define a
FileAttachmentSequence
and use that. If it ends up exceeding the line length limits, you can try:def foo( ..., inactive_file_attachments: Optional[ FileAttachmentSequence ] = None, ... ) -> ...:
Like that.
Thanks for reading all this. Good copy/paste fodder that I should probably put in Notion.
-
-
Are these meant to be used as labels, or IDs? "State" reads to me like an internal value, but the contents read like human-readable.
If it's internal state IDs, let's use slug format (lowercase,
-
instead of spaces).If it needs to be shown to users, we'd have to localize, so no enums. In this case, I'd still recommend we use slugs here, and then map that to localized strings elsewhere.
-
-
-
Each of these should be typed as
Set[Any]
.Note the
Any
is actually correct, as django-stubs types the primary key as anAny
. This is because technically, it can be a string, integer, UUID, or something else model/backend-specific. -
The problem with the
or
here is that if the caller explicitly passes in an empty list, it'll cause a query. So some more verbose code's going to ultimately be needed here.Same below.
-
-
-
-
I'd say we probably should avoid the
**kwargs
for this case, since this is going to always want to exactly match the other method.(Alternatively we could just accept
**kwargs
and pass that in, but we'd lose all typing... Currently there's no way to say "the arguments of the function <blah>"). -
For here and the reference below, the full module path to
ReviewRequest
must be specified.For this sort of case, where you want the displayed text to show the
ClassName.method
format for a class in another module, you must do::py:meth:`ReviewRequest.get_file_attachment_state() <reviewboard.reviews.models.review_request.ReviewRequest.get_file_attachment_state>`
Note that:
- The
()
should be on there when explicitly naming this, to match the normal output of:py:meth:
- You can wrap at any
.
within the reference (custom extension ofbeanbag-docutils
).
- The
-
- Commits:
-
Summary ID 5ec38fdc4f63e1a3a5b01abfb98c27521899a27e f81a59d3ac1b258a3dffee7a378a3d0856a7a088 - Diff:
-
Revision 3 (+2776 -34)