diff mbox series

[v3,07/16] qapi/expr.py: Add casts in a few select cases

Message ID 20210223003408.964543-8-jsnow@redhat.com (mailing list archive)
State New, archived
Headers show
Series qapi: static typing conversion, pt3 | expand

Commit Message

John Snow Feb. 23, 2021, 12:33 a.m. UTC
Casts are instructions to the type checker only, they aren't "safe" and
should probably be avoided in general. In this case, when we perform
type checking on a nested structure, the type of each field does not
"stick".

We don't need to assert that something is a str if we've already checked
that it is -- use a cast instead for these cases.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

Comments

Markus Armbruster Feb. 24, 2021, 12:32 p.m. UTC | #1
John Snow <jsnow@redhat.com> writes:

> Casts are instructions to the type checker only, they aren't "safe" and
> should probably be avoided in general. In this case, when we perform
> type checking on a nested structure, the type of each field does not
> "stick".
>
> We don't need to assert that something is a str if we've already checked
> that it is -- use a cast instead for these cases.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
> Reviewed-by: Cleber Rosa <crosa@redhat.com>
> ---
>  scripts/qapi/expr.py | 8 ++++----
>  1 file changed, 4 insertions(+), 4 deletions(-)
>
> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
> index afa6bd07769..f45d6be1f4c 100644
> --- a/scripts/qapi/expr.py
> +++ b/scripts/qapi/expr.py
> @@ -15,7 +15,7 @@
>  # See the COPYING file in the top-level directory.
>  
>  import re
> -from typing import MutableMapping, Optional
> +from typing import MutableMapping, Optional, cast
>  
>  from .common import c_name
>  from .error import QAPISemError
> @@ -232,7 +232,7 @@ def check_enum(expr, info):
>  
>  
>  def check_struct(expr, info):
> -    name = expr['struct']
> +    name = cast(str, expr['struct'])  # Asserted in check_exprs
>      members = expr['data']
>  

I believe you need this only so you can declare check_type()'s
allow_dict to be Optional[str].  More busy-work around allow_dict...

I'm tempted to ask for allow_dict: Any and call it a day.

>      check_type(members, info, "'data'", allow_dict=name)
> @@ -240,7 +240,7 @@ def check_struct(expr, info):
>  
>  
>  def check_union(expr, info):
> -    name = expr['union']
> +    name = cast(str, expr['union'])  # Asserted in check_exprs
>      base = expr.get('base')
>      discriminator = expr.get('discriminator')
>      members = expr['data']

Likwewise.

> @@ -337,7 +337,7 @@ def check_exprs(exprs):
>          else:
>              raise QAPISemError(info, "expression is missing metatype")
>  
> -        name = expr[meta]
> +        name = cast(str, expr[meta])  # Asserted right below:

"Checked", not "asserted".

>          check_name_is_str(name, info, "'%s'" % meta)
>          info.set_defn(meta, name)
>          check_defn_name_str(name, info, meta)


Cast before check gives me a queasy feeling.  It's lying to the type
checker.

Hamfisted way to avoid lying:

           check_name_is_str(expr[meta], ...)
           name = cast(str, expr[meta])

Something like

           name = check_name_is_str(name, ...)

might be neater, but then we'd have to find a better function name.
John Snow Feb. 24, 2021, 10:24 p.m. UTC | #2
On 2/24/21 7:32 AM, Markus Armbruster wrote:
> John Snow <jsnow@redhat.com> writes:
> 
>> Casts are instructions to the type checker only, they aren't "safe" and
>> should probably be avoided in general. In this case, when we perform
>> type checking on a nested structure, the type of each field does not
>> "stick".
>>
>> We don't need to assert that something is a str if we've already checked
>> that it is -- use a cast instead for these cases.
>>
>> Signed-off-by: John Snow <jsnow@redhat.com>
>> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
>> Reviewed-by: Cleber Rosa <crosa@redhat.com>
>> ---
>>   scripts/qapi/expr.py | 8 ++++----
>>   1 file changed, 4 insertions(+), 4 deletions(-)
>>
>> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
>> index afa6bd07769..f45d6be1f4c 100644
>> --- a/scripts/qapi/expr.py
>> +++ b/scripts/qapi/expr.py
>> @@ -15,7 +15,7 @@
>>   # See the COPYING file in the top-level directory.
>>   
>>   import re
>> -from typing import MutableMapping, Optional
>> +from typing import MutableMapping, Optional, cast
>>   
>>   from .common import c_name
>>   from .error import QAPISemError
>> @@ -232,7 +232,7 @@ def check_enum(expr, info):
>>   
>>   
>>   def check_struct(expr, info):
>> -    name = expr['struct']
>> +    name = cast(str, expr['struct'])  # Asserted in check_exprs
>>       members = expr['data']
>>   
> 
> I believe you need this only so you can declare check_type()'s
> allow_dict to be Optional[str].  More busy-work around allow_dict...
> 
> I'm tempted to ask for allow_dict: Any and call it a day.
> 

You're right, this is because of the signature for the allow_dict 
argument. Ultimately, the pragma is a collection of strings and we need 
to prove we are looking up a string somewhere or other.

(Or tell the type checker to leave us alone.)

Unfortunately, the "check" in check_exprs falls off almost immediately. 
Working with dictly-typed objects is a little annoying for this reason.

This works for now; but finding a better way to do the pragma checks is 
probably the more correct thing. (And not something I really want to try 
and get through review until we're done typing, I think.)

>>       check_type(members, info, "'data'", allow_dict=name)
>> @@ -240,7 +240,7 @@ def check_struct(expr, info):
>>   
>>   
>>   def check_union(expr, info):
>> -    name = expr['union']
>> +    name = cast(str, expr['union'])  # Asserted in check_exprs
>>       base = expr.get('base')
>>       discriminator = expr.get('discriminator')
>>       members = expr['data']
> 
> Likwewise.
> 
>> @@ -337,7 +337,7 @@ def check_exprs(exprs):
>>           else:
>>               raise QAPISemError(info, "expression is missing metatype")
>>   
>> -        name = expr[meta]
>> +        name = cast(str, expr[meta])  # Asserted right below:
> 
> "Checked", not "asserted".
> 

Oh, yes.

>>           check_name_is_str(name, info, "'%s'" % meta)
>>           info.set_defn(meta, name)
>>           check_defn_name_str(name, info, meta)
> 
> 
> Cast before check gives me a queasy feeling.  It's lying to the type
> checker.
> 
> Hamfisted way to avoid lying:
> 
>             check_name_is_str(expr[meta], ...)
>             name = cast(str, expr[meta])
> 
> Something like
> 
>             name = check_name_is_str(name, ...)
> 
> might be neater, but then we'd have to find a better function name.
> 

OK, I'll look into re-ordering this.
Markus Armbruster Feb. 25, 2021, 12:07 p.m. UTC | #3
John Snow <jsnow@redhat.com> writes:

> On 2/24/21 7:32 AM, Markus Armbruster wrote:
>> John Snow <jsnow@redhat.com> writes:
>> 
>>> Casts are instructions to the type checker only, they aren't "safe" and
>>> should probably be avoided in general. In this case, when we perform
>>> type checking on a nested structure, the type of each field does not
>>> "stick".
>>>
>>> We don't need to assert that something is a str if we've already checked
>>> that it is -- use a cast instead for these cases.
>>>
>>> Signed-off-by: John Snow <jsnow@redhat.com>
>>> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
>>> Reviewed-by: Cleber Rosa <crosa@redhat.com>
>>> ---
>>>   scripts/qapi/expr.py | 8 ++++----
>>>   1 file changed, 4 insertions(+), 4 deletions(-)
>>>
>>> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
>>> index afa6bd07769..f45d6be1f4c 100644
>>> --- a/scripts/qapi/expr.py
>>> +++ b/scripts/qapi/expr.py
>>> @@ -15,7 +15,7 @@
>>>   # See the COPYING file in the top-level directory.
>>>   
>>>   import re
>>> -from typing import MutableMapping, Optional
>>> +from typing import MutableMapping, Optional, cast
>>>   
>>>   from .common import c_name
>>>   from .error import QAPISemError
>>> @@ -232,7 +232,7 @@ def check_enum(expr, info):
>>>   
>>>   
>>>   def check_struct(expr, info):
>>> -    name = expr['struct']
>>> +    name = cast(str, expr['struct'])  # Asserted in check_exprs
>>>       members = expr['data']
>>>   
>> 
>> I believe you need this only so you can declare check_type()'s
>> allow_dict to be Optional[str].  More busy-work around allow_dict...
>> 
>> I'm tempted to ask for allow_dict: Any and call it a day.
>> 
>
> You're right, this is because of the signature for the allow_dict 
> argument. Ultimately, the pragma is a collection of strings and we need 
> to prove we are looking up a string somewhere or other.
>
> (Or tell the type checker to leave us alone.)
>
> Unfortunately, the "check" in check_exprs falls off almost immediately. 

What do you mean by "falls off"?

> Working with dictly-typed objects is a little annoying for this reason.
>
> This works for now; but finding a better way to do the pragma checks is 
> probably the more correct thing. (And not something I really want to try 
> and get through review until we're done typing, I think.)

Yes, there's probably a neater way to do the name case checking (with
pragma-directed exceptions), and yes, we should refrain from looking for
it right now.

>>>       check_type(members, info, "'data'", allow_dict=name)
>>> @@ -240,7 +240,7 @@ def check_struct(expr, info):
>>>   
>>>   
>>>   def check_union(expr, info):
>>> -    name = expr['union']
>>> +    name = cast(str, expr['union'])  # Asserted in check_exprs
>>>       base = expr.get('base')
>>>       discriminator = expr.get('discriminator')
>>>       members = expr['data']
>> 
>> Likwewise.
>> 
>>> @@ -337,7 +337,7 @@ def check_exprs(exprs):
>>>           else:
>>>               raise QAPISemError(info, "expression is missing metatype")
>>>   
>>> -        name = expr[meta]
>>> +        name = cast(str, expr[meta])  # Asserted right below:
>> 
>> "Checked", not "asserted".
>> 
>
> Oh, yes.
>
>>>           check_name_is_str(name, info, "'%s'" % meta)
>>>           info.set_defn(meta, name)
>>>           check_defn_name_str(name, info, meta)
>> 
>> 
>> Cast before check gives me a queasy feeling.  It's lying to the type
>> checker.
>> 
>> Hamfisted way to avoid lying:
>> 
>>             check_name_is_str(expr[meta], ...)
>>             name = cast(str, expr[meta])
>> 
>> Something like
>> 
>>             name = check_name_is_str(name, ...)
>> 
>> might be neater, but then we'd have to find a better function name.
>> 
>
> OK, I'll look into re-ordering this.

Thanks!  To avoid misunderstandings: ham-fisted would do for now.
John Snow Feb. 25, 2021, 10:10 p.m. UTC | #4
On 2/25/21 7:07 AM, Markus Armbruster wrote:
>> Unfortunately, the "check" in check_exprs falls off almost immediately.
> What do you mean by "falls off"?
> 

mypy loses the constraint in its static analysis. i.e. all of the work 
we do in expr.py is almost entirely opaque to mypy.
diff mbox series

Patch

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index afa6bd07769..f45d6be1f4c 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -15,7 +15,7 @@ 
 # See the COPYING file in the top-level directory.
 
 import re
-from typing import MutableMapping, Optional
+from typing import MutableMapping, Optional, cast
 
 from .common import c_name
 from .error import QAPISemError
@@ -232,7 +232,7 @@  def check_enum(expr, info):
 
 
 def check_struct(expr, info):
-    name = expr['struct']
+    name = cast(str, expr['struct'])  # Asserted in check_exprs
     members = expr['data']
 
     check_type(members, info, "'data'", allow_dict=name)
@@ -240,7 +240,7 @@  def check_struct(expr, info):
 
 
 def check_union(expr, info):
-    name = expr['union']
+    name = cast(str, expr['union'])  # Asserted in check_exprs
     base = expr.get('base')
     discriminator = expr.get('discriminator')
     members = expr['data']
@@ -337,7 +337,7 @@  def check_exprs(exprs):
         else:
             raise QAPISemError(info, "expression is missing metatype")
 
-        name = expr[meta]
+        name = cast(str, expr[meta])  # Asserted right below:
         check_name_is_str(name, info, "'%s'" % meta)
         info.set_defn(meta, name)
         check_defn_name_str(name, info, meta)