From: Markus Armbruster <armbru@redhat.com>
To: John Snow <jsnow@redhat.com>
Cc: qemu-devel <qemu-devel@nongnu.org>,
Peter Maydell <peter.maydell@linaro.org>,
Michael Roth <michael.roth@amd.com>
Subject: Re: [PATCH 12/19] qapi/schema: split "checked" field into "checking" and "checked"
Date: Thu, 11 Jan 2024 10:24:28 +0100 [thread overview]
Message-ID: <87mstc1aur.fsf@pond.sub.org> (raw)
In-Reply-To: <CAFn=p-bvAcej5vgc4N=WtTrQ1B0dUP5SDy6nhs4eADhQATbxRA@mail.gmail.com> (John Snow's message of "Wed, 10 Jan 2024 15:21:03 -0500")
John Snow <jsnow@redhat.com> writes:
> On Wed, Nov 22, 2023, 9:02 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > Differentiate between "actively in the process of checking" and
>> > "checking has completed". This allows us to clean up the types of some
>> > internal fields such as QAPISchemaObjectType's members field which
>> > currently uses "None" as a canary for determining if check has
>> > completed.
>>
>> Certain members become valid only after .check(). Two ways to code
>> that:
>>
>> 1. Assign to such members only in .check(). If you try to use them
>> before .check(), AttributeError. Nice. Drawback: pylint is unhappy,
>> W0201 attribute-defined-outside-init.
>>
>
> Can be overcome by declaring the field in __init__, which satisfies both
> the linter and my developer usability sense (Classes should be easy to have
> their properties enumerated by looking in one well known place.)
>
>
>> 2. Assign None in .__init__(), and the real value in .check(). If you
>> try to use them before .check(), you get None, which hopefully leads to
>> an error. Meh, but pylint is happy.
>>
>> I picked 2. because pylint's warning made me go "when in Rome..."
>>
>
> Yep, this is perfectly cromulent dynamically typed Python. It's not the
> Roman's fault I'm packing us up to go to another empire.
>
>
>> With type hints, we can declare in .__init__(), and assign in .check().
>> Gives me the AttributeError I like, and pylint remains happy. What do
>> you think?
>>
>
> Sounds good to me in general, I already changed this for 2/3 of my other
> uses of @property.
>
> (I'm only reluctant because I don't really like that it's a "lie", but in
> this case, without potentially significant rewrites, it's a reasonable type
> band-aid. Once we're type checked, we can refactor more confidently if we
> so desire, to make certain patterns less prominent or landmine-y.)
The general problem is "attribute value is valid only after a state
transition" (here: .member is valid only after .check()).
We want to catch uses of the attribute before it becomes valid.
We want to keep pylint and mypy happy.
Solutions:
1. Initialize in .__init__() to some invalid value. Set it to the valid
value in .check().
1.a. Pick the "natural" invalid value: None
How to catch: assert attribute value is valid (here: .members is not
None). Easy to forget. Better: when the use will safely choke on
the invalid value (here: elide for uses like for m in .members),
catch is automatic.
Pylint: fine.
Mypy: adding None to the set of values changes the type from T to
Optional[T]. Because of this, mypy commonly can't prove valid uses
are valid. Keeping it happy requires cluttering the code with
assertions and such. Meh.
Note: catching invalid uses is a run time check. Mypy won't.
1.b. Pick an invalid value of type T (here: [])
How to catch: same as 1.a., except automatic catch is rare. Meh.
Pylint: fine.
Mypy: fine.
2. Declare in .__init__() without initializing. Initialize to valid
value in .check()
How to catch: always automatic. Good, me want!
Pylint: fine.
Mypy: fine.
Note: catching invalid uses is a run time check. Mypy won't.
3. Express the state transition in the type system
To catch invalid uses statically with mypy, we need to use different
types before and after the state transition. Feels possible. Also
feels ludicrously overengineered.
May I have 2., please?
>> Splitting .checked feels like a separate change, though. I can't quite
>> see why we need it. Help me out: what problem does it solve?
>>
>
> Mechanically, I wanted to eliminate the Optional type from the members
> field, but you have conditionals in the code that use the presence or
> absence of this field as a query to determine if we had run check or not
> yet.
>
> So I did the stupidest possible thing and added a "checked" variable to
> explicitly represent it.
If 2. complicates the existing "have we .check()ed?" code too much, then
adding such a variable may indeed be useful.
>> > This simplifies the typing from a cumbersome Optional[List[T]] to merely
>> > a List[T], which is more pythonic: it is safe to iterate over an empty
>> > list with "for x in []" whereas with an Optional[List[T]] you have to
>> > rely on the more cumbersome "if L: for x in L: ..."
>>
>> Yes, but when L is None, it's *invalid*, and for i in L *should* fail
>> when L is invalid.
>>
>
> Sure, but it's so invalid that it causes static typing errors.
>
> You can't write "for x in None" in a way that makes mypy happy, None is not
> iterable.
A variable that is declared, but not initialized (2. above) also not
iterable, and it does make mypy happy, doesn't it?
> If you want to preserve the behavior of "iterating an empty collection is
> an Assertion", you need a custom iterator that throws an exception when the
> collection is empty. I can do that, if you'd like, but I think it's
> actually fine to just allow the collection to be empty and to just catch
> the error in check() with either an assertion (oops, something went wrong
> and the list is empty, this should never happen) or a QAPISemError (oops,
> you didn't specify any members, which is illegal.)
>
> I'd prefer to catch this in check and just allow the iterator to permit
> empty iterators at the callsite, knowing it'll never happen.
>
>
>> I think the actual problem is something else. By adding the type hint
>> Optional[List[T]], the valid uses of L become type errors. We really
>> want L to be a List[T]. Doesn't mean we have to (or want to) make uses
>> of invalid L "work".
>>
>
> I didn't think I did allow for invalid uses to work - if the list should
> semantically never be empty, I think it's fine to enforce that in schema.py
> during construction of the schema object and to assume all uses of "for x
> in L: ..." are inherently valid.
>
>
>> > RFC: are we guaranteed to have members here? can we just use "if
>> > members" without adding the new field?
>>
>> I'm afraid I don't understand these questions.
>>
>
> I think you answered this one for me; I was asking if it was ever valid in
> any circumstance to have an empty members list, but I think you've laid out
> in your response that it isn't.
>
> And I think with that knowledge I can simplify this patch, but don't quite
> recall. (On my mobile again, please excuse my apparent laziness.)
Feel excused!
[...]
next prev parent reply other threads:[~2024-01-11 9:25 UTC|newest]
Thread overview: 76+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-11-16 1:43 [PATCH 00/19] qapi: statically type schema.py John Snow
2023-11-16 1:43 ` [PATCH 01/19] qapi/schema: fix QAPISchemaEntity.__repr__() John Snow
2023-11-16 7:01 ` Philippe Mathieu-Daudé
2023-11-16 1:43 ` [PATCH 02/19] qapi/schema: add pylint suppressions John Snow
2023-11-21 12:23 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 03/19] qapi/schema: name QAPISchemaInclude entities John Snow
2023-11-21 13:33 ` Markus Armbruster
2023-11-21 16:22 ` John Snow
2023-11-22 9:37 ` Markus Armbruster
2023-12-13 0:45 ` John Snow
2023-11-16 1:43 ` [PATCH 04/19] qapi/schema: declare type for QAPISchemaObjectTypeMember.type John Snow
2023-11-16 1:43 ` [PATCH 05/19] qapi/schema: make c_type() and json_type() abstract methods John Snow
2023-11-16 7:03 ` Philippe Mathieu-Daudé
2023-11-21 13:36 ` Markus Armbruster
2023-11-21 13:43 ` Daniel P. Berrangé
2023-11-21 16:28 ` John Snow
2023-11-21 16:34 ` Daniel P. Berrangé
2023-11-22 9:50 ` Markus Armbruster
2023-11-22 9:54 ` Daniel P. Berrangé
2023-11-16 1:43 ` [PATCH 06/19] qapi/schema: adjust type narrowing for mypy's benefit John Snow
2023-11-16 7:04 ` Philippe Mathieu-Daudé
2023-11-21 14:09 ` Markus Armbruster
2023-11-21 16:36 ` John Snow
2023-11-22 12:00 ` Markus Armbruster
2023-11-22 18:12 ` John Snow
2023-11-23 11:00 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 07/19] qapi/introspect: assert schema.lookup_type did not fail John Snow
2023-11-21 14:17 ` Markus Armbruster
2023-11-21 16:41 ` John Snow
2023-11-22 9:52 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 08/19] qapi/schema: add static typing and assertions to lookup_type() John Snow
2023-11-21 14:21 ` Markus Armbruster
2023-11-21 16:46 ` John Snow
2023-11-22 12:09 ` Markus Armbruster
2023-11-22 15:55 ` John Snow
2023-11-23 11:04 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 09/19] qapi/schema: assert info is present when necessary John Snow
2023-11-16 7:05 ` Philippe Mathieu-Daudé
2023-11-16 1:43 ` [PATCH 10/19] qapi/schema: make QAPISchemaArrayType.element_type non-Optional John Snow
2023-11-21 14:27 ` Markus Armbruster
2023-11-21 16:51 ` John Snow
2023-11-16 1:43 ` [PATCH 11/19] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type John Snow
2023-11-22 12:59 ` Markus Armbruster
2023-11-22 15:58 ` John Snow
2023-11-23 13:03 ` Markus Armbruster
2024-01-10 19:33 ` John Snow
2024-01-11 9:33 ` Markus Armbruster
2024-01-11 22:24 ` John Snow
2023-11-16 1:43 ` [PATCH 12/19] qapi/schema: split "checked" field into "checking" and "checked" John Snow
2023-11-22 14:02 ` Markus Armbruster
2024-01-10 20:21 ` John Snow
2024-01-11 9:24 ` Markus Armbruster [this message]
2023-11-16 1:43 ` [PATCH 13/19] qapi/schema: fix typing for QAPISchemaVariants.tag_member John Snow
2023-11-22 14:05 ` Markus Armbruster
2023-11-22 16:02 ` John Snow
2024-01-10 1:47 ` John Snow
2024-01-10 7:52 ` Markus Armbruster
2024-01-10 8:35 ` John Snow
2024-01-17 8:19 ` Markus Armbruster
2024-01-17 10:32 ` Markus Armbruster
2024-01-17 10:53 ` Markus Armbruster
2024-02-01 20:54 ` John Snow
2023-11-16 1:43 ` [PATCH 14/19] qapi/schema: assert QAPISchemaVariants are QAPISchemaObjectType John Snow
2023-11-23 13:51 ` Markus Armbruster
2024-01-10 0:42 ` John Snow
2023-11-16 1:43 ` [PATCH 15/19] qapi/parser: demote QAPIExpression to Dict[str, Any] John Snow
2023-11-23 14:12 ` Markus Armbruster
2024-01-10 0:14 ` John Snow
2024-01-10 7:58 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 16/19] qapi/schema: add type hints John Snow
2023-11-24 15:02 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 17/19] qapi/schema: turn on mypy strictness John Snow
2023-11-16 1:43 ` [PATCH 18/19] qapi/schema: remove unnecessary asserts John Snow
2023-11-28 9:22 ` Markus Armbruster
2023-11-16 1:43 ` [PATCH 19/19] qapi/schema: refactor entity lookup helpers John Snow
2023-11-28 12:06 ` Markus Armbruster
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87mstc1aur.fsf@pond.sub.org \
--to=armbru@redhat.com \
--cc=jsnow@redhat.com \
--cc=michael.roth@amd.com \
--cc=peter.maydell@linaro.org \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).