diff mbox series

[03/16] qapi/expr.py: constrain incoming expression types

Message ID 20200922211313.4082880-4-jsnow@redhat.com
State New
Headers show
Series qapi: static typing conversion, pt2 | expand

Commit Message

John Snow Sept. 22, 2020, 9:13 p.m. UTC
mypy does not know the types of values stored in Dicts that masquerade
as objects. Help the type checker out by constraining the type.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

Comments

Eduardo Habkost Sept. 23, 2020, 7:42 p.m. UTC | #1
On Tue, Sep 22, 2020 at 05:13:00PM -0400, John Snow wrote:
> mypy does not know the types of values stored in Dicts that masquerade
> as objects. Help the type checker out by constraining the type.
> 
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/expr.py | 25 ++++++++++++++++++++++---
>  1 file changed, 22 insertions(+), 3 deletions(-)
> 
> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
> index 1872a8a3cc..f6b55a87c1 100644
> --- a/scripts/qapi/expr.py
> +++ b/scripts/qapi/expr.py
> @@ -15,9 +15,17 @@
>  # See the COPYING file in the top-level directory.
>  
>  import re
> +from typing import MutableMapping, Optional
>  
>  from .common import c_name
>  from .error import QAPISemError
> +from .parser import QAPIDoc
> +from .source import QAPISourceInfo
> +
> +
> +# Expressions in their raw form are JSON-like structures with arbitrary forms.
> +# Minimally, their top-level form must be a mapping of strings to values.
> +Expression = MutableMapping[str, object]
>  
>  
>  # Names must be letters, numbers, -, and _.  They must start with letter,
> @@ -280,9 +288,20 @@ def check_event(expr, info):
>  
>  def check_exprs(exprs):
>      for expr_elem in exprs:
> -        expr = expr_elem['expr']
> -        info = expr_elem['info']
> -        doc = expr_elem.get('doc')
> +        # Expression
> +        assert isinstance(expr_elem['expr'], dict)
> +        expr: Expression = expr_elem['expr']
> +        for key in expr.keys():
> +            assert isinstance(key, str)

mypy doesn't seem to require the key type asserts, on my testing.

> +
> +        # QAPISourceInfo
> +        assert isinstance(expr_elem['info'], QAPISourceInfo)
> +        info: QAPISourceInfo = expr_elem['info']
> +
> +        # Optional[QAPIDoc]
> +        tmp = expr_elem.get('doc')
> +        assert tmp is None or isinstance(tmp, QAPIDoc)
> +        doc: Optional[QAPIDoc] = tmp

Do you need a separate variable here?  This seems to work too:

        doc = expr_elem.get('doc')
        assert doc is None or isinstance(doc, QAPIDoc)

after the assert, mypy will infer the type of doc to be
Optional[QAPIDoc].

None of this should block a useful 120-patch cleanup series, so:

Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>

>  
>          if 'include' in expr:
>              continue
> -- 
> 2.26.2
>
Cleber Rosa Sept. 25, 2020, 12:05 a.m. UTC | #2
On Wed, Sep 23, 2020 at 03:42:24PM -0400, Eduardo Habkost wrote:
> On Tue, Sep 22, 2020 at 05:13:00PM -0400, John Snow wrote:
> > mypy does not know the types of values stored in Dicts that masquerade
> > as objects. Help the type checker out by constraining the type.
> > 
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  scripts/qapi/expr.py | 25 ++++++++++++++++++++++---
> >  1 file changed, 22 insertions(+), 3 deletions(-)
> > 
> > diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
> > index 1872a8a3cc..f6b55a87c1 100644
> > --- a/scripts/qapi/expr.py
> > +++ b/scripts/qapi/expr.py
> > @@ -15,9 +15,17 @@
> >  # See the COPYING file in the top-level directory.
> >  
> >  import re
> > +from typing import MutableMapping, Optional
> >  
> >  from .common import c_name
> >  from .error import QAPISemError
> > +from .parser import QAPIDoc
> > +from .source import QAPISourceInfo
> > +
> > +
> > +# Expressions in their raw form are JSON-like structures with arbitrary forms.
> > +# Minimally, their top-level form must be a mapping of strings to values.
> > +Expression = MutableMapping[str, object]
> >  
> >  
> >  # Names must be letters, numbers, -, and _.  They must start with letter,
> > @@ -280,9 +288,20 @@ def check_event(expr, info):
> >  
> >  def check_exprs(exprs):
> >      for expr_elem in exprs:
> > -        expr = expr_elem['expr']
> > -        info = expr_elem['info']
> > -        doc = expr_elem.get('doc')
> > +        # Expression
> > +        assert isinstance(expr_elem['expr'], dict)
> > +        expr: Expression = expr_elem['expr']
> > +        for key in expr.keys():
> > +            assert isinstance(key, str)
> 
> mypy doesn't seem to require the key type asserts, on my testing.
>

Do you mean that mypy actually takes notice of the type assert?  And
includes that as source of information for the type check or am I
misinterpreting you?

BTW, what I understood from this assert is that a more specific
type than the MutableMapping is desirable here.  Did I get that
right John?

- Cleber.
Cleber Rosa Sept. 25, 2020, 12:06 a.m. UTC | #3
On Tue, Sep 22, 2020 at 05:13:00PM -0400, John Snow wrote:
> mypy does not know the types of values stored in Dicts that masquerade
> as objects. Help the type checker out by constraining the type.
> 
> Signed-off-by: John Snow <jsnow@redhat.com>

Reviewed-by: Cleber Rosa <crosa@redhat.com>
John Snow Sept. 25, 2020, 12:40 a.m. UTC | #4
On 9/23/20 3:42 PM, Eduardo Habkost wrote:
> On Tue, Sep 22, 2020 at 05:13:00PM -0400, John Snow wrote:
>> mypy does not know the types of values stored in Dicts that masquerade
>> as objects. Help the type checker out by constraining the type.
>>
>> Signed-off-by: John Snow <jsnow@redhat.com>
>> ---
>>   scripts/qapi/expr.py | 25 ++++++++++++++++++++++---
>>   1 file changed, 22 insertions(+), 3 deletions(-)
>>
>> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
>> index 1872a8a3cc..f6b55a87c1 100644
>> --- a/scripts/qapi/expr.py
>> +++ b/scripts/qapi/expr.py
>> @@ -15,9 +15,17 @@
>>   # See the COPYING file in the top-level directory.
>>   
>>   import re
>> +from typing import MutableMapping, Optional
>>   
>>   from .common import c_name
>>   from .error import QAPISemError
>> +from .parser import QAPIDoc
>> +from .source import QAPISourceInfo
>> +
>> +
>> +# Expressions in their raw form are JSON-like structures with arbitrary forms.
>> +# Minimally, their top-level form must be a mapping of strings to values.
>> +Expression = MutableMapping[str, object]
>>   
>>   
>>   # Names must be letters, numbers, -, and _.  They must start with letter,
>> @@ -280,9 +288,20 @@ def check_event(expr, info):
>>   
>>   def check_exprs(exprs):
>>       for expr_elem in exprs:
>> -        expr = expr_elem['expr']
>> -        info = expr_elem['info']
>> -        doc = expr_elem.get('doc')
>> +        # Expression
>> +        assert isinstance(expr_elem['expr'], dict)
>> +        expr: Expression = expr_elem['expr']
>> +        for key in expr.keys():
>> +            assert isinstance(key, str)
> 
> mypy doesn't seem to require the key type asserts, on my testing.
> 

Strictly no. This code is removed somewhere in part 5 when I introduce a 
typed structure to carry this data from the Parser to the Expression 
checker.

(Sometimes, these asserts were for my own sake.)

>> +
>> +        # QAPISourceInfo
>> +        assert isinstance(expr_elem['info'], QAPISourceInfo)
>> +        info: QAPISourceInfo = expr_elem['info']
>> +
>> +        # Optional[QAPIDoc]
>> +        tmp = expr_elem.get('doc')
>> +        assert tmp is None or isinstance(tmp, QAPIDoc)
>> +        doc: Optional[QAPIDoc] = tmp
> 
> Do you need a separate variable here?  This seems to work too:
> 
>          doc = expr_elem.get('doc')
>          assert doc is None or isinstance(doc, QAPIDoc)
> 
> after the assert, mypy will infer the type of doc to be
> Optional[QAPIDoc].
> 

In full honesty, I don't recall... but this code does get replaced by 
the end of this marathon.

> None of this should block a useful 120-patch cleanup series, so:
> 
> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
> 

Thanks!
John Snow Sept. 25, 2020, 12:43 a.m. UTC | #5
On 9/24/20 8:05 PM, Cleber Rosa wrote:
> On Wed, Sep 23, 2020 at 03:42:24PM -0400, Eduardo Habkost wrote:
>> On Tue, Sep 22, 2020 at 05:13:00PM -0400, John Snow wrote:
>>> mypy does not know the types of values stored in Dicts that masquerade
>>> as objects. Help the type checker out by constraining the type.
>>>
>>> Signed-off-by: John Snow <jsnow@redhat.com>
>>> ---
>>>   scripts/qapi/expr.py | 25 ++++++++++++++++++++++---
>>>   1 file changed, 22 insertions(+), 3 deletions(-)
>>>
>>> diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
>>> index 1872a8a3cc..f6b55a87c1 100644
>>> --- a/scripts/qapi/expr.py
>>> +++ b/scripts/qapi/expr.py
>>> @@ -15,9 +15,17 @@
>>>   # See the COPYING file in the top-level directory.
>>>   
>>>   import re
>>> +from typing import MutableMapping, Optional
>>>   
>>>   from .common import c_name
>>>   from .error import QAPISemError
>>> +from .parser import QAPIDoc
>>> +from .source import QAPISourceInfo
>>> +
>>> +
>>> +# Expressions in their raw form are JSON-like structures with arbitrary forms.
>>> +# Minimally, their top-level form must be a mapping of strings to values.
>>> +Expression = MutableMapping[str, object]
>>>   
>>>   
>>>   # Names must be letters, numbers, -, and _.  They must start with letter,
>>> @@ -280,9 +288,20 @@ def check_event(expr, info):
>>>   
>>>   def check_exprs(exprs):
>>>       for expr_elem in exprs:
>>> -        expr = expr_elem['expr']
>>> -        info = expr_elem['info']
>>> -        doc = expr_elem.get('doc')
>>> +        # Expression
>>> +        assert isinstance(expr_elem['expr'], dict)
>>> +        expr: Expression = expr_elem['expr']
>>> +        for key in expr.keys():
>>> +            assert isinstance(key, str)
>>
>> mypy doesn't seem to require the key type asserts, on my testing.
>>
> 
> Do you mean that mypy actually takes notice of the type assert?  And
> includes that as source of information for the type check or am I
> misinterpreting you?
> 
> BTW, what I understood from this assert is that a more specific
> type than the MutableMapping is desirable here.  Did I get that
> right John?
> 

Yes, we do want a more specific type. We'll get one somewhere in part 5 
when parser.py gets a workout.

> - Cleber.
> 

mypy takes notice of assert isinstance(x, FooType) because below this 
line, it is not possible for x to be anything other than a FooType.

You can use this to "downcast" types.

you can use cast() too, but those are "unsafe", in that they don't 
actually check. assert *will* check.

You can also constrain types by doing a simple:

if isinstance(x, FooType):
     x.FooMethod()
diff mbox series

Patch

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 1872a8a3cc..f6b55a87c1 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -15,9 +15,17 @@ 
 # See the COPYING file in the top-level directory.
 
 import re
+from typing import MutableMapping, Optional
 
 from .common import c_name
 from .error import QAPISemError
+from .parser import QAPIDoc
+from .source import QAPISourceInfo
+
+
+# Expressions in their raw form are JSON-like structures with arbitrary forms.
+# Minimally, their top-level form must be a mapping of strings to values.
+Expression = MutableMapping[str, object]
 
 
 # Names must be letters, numbers, -, and _.  They must start with letter,
@@ -280,9 +288,20 @@  def check_event(expr, info):
 
 def check_exprs(exprs):
     for expr_elem in exprs:
-        expr = expr_elem['expr']
-        info = expr_elem['info']
-        doc = expr_elem.get('doc')
+        # Expression
+        assert isinstance(expr_elem['expr'], dict)
+        expr: Expression = expr_elem['expr']
+        for key in expr.keys():
+            assert isinstance(key, str)
+
+        # QAPISourceInfo
+        assert isinstance(expr_elem['info'], QAPISourceInfo)
+        info: QAPISourceInfo = expr_elem['info']
+
+        # Optional[QAPIDoc]
+        tmp = expr_elem.get('doc')
+        assert tmp is None or isinstance(tmp, QAPIDoc)
+        doc: Optional[QAPIDoc] = tmp
 
         if 'include' in expr:
             continue