diff mbox series

[v2,09/38] qapi/common.py: Add indent manager

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

Commit Message

John Snow Sept. 22, 2020, 9 p.m. UTC
Code style tools really dislike the use of global keywords, because it
generally involves re-binding the name at runtime which can have strange
effects depending on when and how that global name is referenced in
other modules.

Make a little indent level manager instead.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
 scripts/qapi/visit.py  |  7 +++---
 2 files changed, 38 insertions(+), 20 deletions(-)

Comments

Eduardo Habkost Sept. 22, 2020, 10:22 p.m. UTC | #1
On Tue, Sep 22, 2020 at 05:00:32PM -0400, John Snow wrote:
> Code style tools really dislike the use of global keywords, because it
> generally involves re-binding the name at runtime which can have strange
> effects depending on when and how that global name is referenced in
> other modules.
> 
> Make a little indent level manager instead.
> 
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
>  scripts/qapi/visit.py  |  7 +++---
>  2 files changed, 38 insertions(+), 20 deletions(-)
> 
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index cee63eb95c..e0c5871b10 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -93,33 +93,52 @@ def c_name(name, protect=True):
>  pointer_suffix = ' *' + eatspace
>  
>  
> -def genindent(count):
> -    ret = ''
> -    for _ in range(count):
> -        ret += ' '
> -    return ret
> +class Indentation:
> +    """
> +    Indentation level management.
>  
> +    :param initial: Initial number of spaces, default 0.
> +    """
> +    def __init__(self, initial: int = 0) -> None:
> +        self._level = initial
>  
> -indent_level = 0
> +    def __int__(self) -> int:
> +        return self._level
>  
> +    def __repr__(self) -> str:
> +        return "{}({:d})".format(type(self).__name__, self._level)
>  
> -def push_indent(indent_amount=4):
> -    global indent_level
> -    indent_level += indent_amount
> +    def __str__(self) -> str:
> +        """Return the current indentation as a string of spaces."""
> +        return ' ' * self._level
>  
> +    def __bool__(self) -> bool:
> +        """True when there is a non-zero indentation."""
> +        return bool(self._level)
>  
> -def pop_indent(indent_amount=4):
> -    global indent_level
> -    indent_level -= indent_amount
> +    def increase(self, amount: int = 4) -> int:
> +        """Increase the indentation level by `amount`, default 4."""
> +        self._level += amount
> +        return self._level
> +
> +    def decrease(self, amount: int = 4) -> int:
> +        """Decrease the indentation level by `amount`, default 4."""
> +        if self._level < amount:
> +            raise ArithmeticError(
> +                f"Can't remove {amount:d} spaces from {self!r}")
> +        self._level -= amount
> +        return self._level
> +
> +
> +indent = Indentation()

Personally, I would keep the push_indent(), pop_indent() API, and
introduce an indent() function, to follow the existing API style
of plain functions.

Something like:

  indent_prefixes = []
  def push_indent(amount=4):
      """Add `amount` spaces to indentation prefix"""
      indent_prefixes.push(' '*amount)
  def pop_indent():
      """Revert the most recent push_indent() call"""
      indent_prefixes.pop()
  def indent():
      """Return the current indentation prefix"""
      return ''.join(indent_prefixes)

What you did is still an improvement, though, so:

Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Cleber Rosa Sept. 23, 2020, 2:55 p.m. UTC | #2
On Tue, Sep 22, 2020 at 05:00:32PM -0400, John Snow wrote:
> Code style tools really dislike the use of global keywords, because it
> generally involves re-binding the name at runtime which can have strange
> effects depending on when and how that global name is referenced in
> other modules.
> 
> Make a little indent level manager instead.
> 
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
>  scripts/qapi/visit.py  |  7 +++---
>  2 files changed, 38 insertions(+), 20 deletions(-)
> 
> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
> index cee63eb95c..e0c5871b10 100644
> --- a/scripts/qapi/common.py
> +++ b/scripts/qapi/common.py
> @@ -93,33 +93,52 @@ def c_name(name, protect=True):
>  pointer_suffix = ' *' + eatspace
>  
>  
> -def genindent(count):
> -    ret = ''
> -    for _ in range(count):
> -        ret += ' '
> -    return ret
> +class Indentation:
> +    """
> +    Indentation level management.
>  
> +    :param initial: Initial number of spaces, default 0.
> +    """
> +    def __init__(self, initial: int = 0) -> None:
> +        self._level = initial
>  
> -indent_level = 0
> +    def __int__(self) -> int:
> +        return self._level
>  
> +    def __repr__(self) -> str:
> +        return "{}({:d})".format(type(self).__name__, self._level)
>  
> -def push_indent(indent_amount=4):
> -    global indent_level
> -    indent_level += indent_amount
> +    def __str__(self) -> str:
> +        """Return the current indentation as a string of spaces."""
> +        return ' ' * self._level
>  
> +    def __bool__(self) -> bool:
> +        """True when there is a non-zero indentation."""
> +        return bool(self._level)
>  
> -def pop_indent(indent_amount=4):
> -    global indent_level
> -    indent_level -= indent_amount
> +    def increase(self, amount: int = 4) -> int:
> +        """Increase the indentation level by `amount`, default 4."""
> +        self._level += amount
> +        return self._level
> +

Do you have a use case for returning the level?  If not, I'd go
without it, and add a "level" property instead, as it'd serve more
cases.

Other than that,

Reviewed-by: Cleber Rosa <crosa@redhat.com>
John Snow Sept. 23, 2020, 5:29 p.m. UTC | #3
On 9/22/20 6:22 PM, Eduardo Habkost wrote:
> On Tue, Sep 22, 2020 at 05:00:32PM -0400, John Snow wrote:
>> Code style tools really dislike the use of global keywords, because it
>> generally involves re-binding the name at runtime which can have strange
>> effects depending on when and how that global name is referenced in
>> other modules.
>>
>> Make a little indent level manager instead.
>>
>> Signed-off-by: John Snow <jsnow@redhat.com>
>> ---
>>   scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
>>   scripts/qapi/visit.py  |  7 +++---
>>   2 files changed, 38 insertions(+), 20 deletions(-)
>>
>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>> index cee63eb95c..e0c5871b10 100644
>> --- a/scripts/qapi/common.py
>> +++ b/scripts/qapi/common.py
>> @@ -93,33 +93,52 @@ def c_name(name, protect=True):
>>   pointer_suffix = ' *' + eatspace
>>   
>>   
>> -def genindent(count):
>> -    ret = ''
>> -    for _ in range(count):
>> -        ret += ' '
>> -    return ret
>> +class Indentation:
>> +    """
>> +    Indentation level management.
>>   
>> +    :param initial: Initial number of spaces, default 0.
>> +    """
>> +    def __init__(self, initial: int = 0) -> None:
>> +        self._level = initial
>>   
>> -indent_level = 0
>> +    def __int__(self) -> int:
>> +        return self._level
>>   
>> +    def __repr__(self) -> str:
>> +        return "{}({:d})".format(type(self).__name__, self._level)
>>   
>> -def push_indent(indent_amount=4):
>> -    global indent_level
>> -    indent_level += indent_amount
>> +    def __str__(self) -> str:
>> +        """Return the current indentation as a string of spaces."""
>> +        return ' ' * self._level
>>   
>> +    def __bool__(self) -> bool:
>> +        """True when there is a non-zero indentation."""
>> +        return bool(self._level)
>>   
>> -def pop_indent(indent_amount=4):
>> -    global indent_level
>> -    indent_level -= indent_amount
>> +    def increase(self, amount: int = 4) -> int:
>> +        """Increase the indentation level by `amount`, default 4."""
>> +        self._level += amount
>> +        return self._level
>> +
>> +    def decrease(self, amount: int = 4) -> int:
>> +        """Decrease the indentation level by `amount`, default 4."""
>> +        if self._level < amount:
>> +            raise ArithmeticError(
>> +                f"Can't remove {amount:d} spaces from {self!r}")
>> +        self._level -= amount
>> +        return self._level
>> +
>> +
>> +indent = Indentation()
> 
> Personally, I would keep the push_indent(), pop_indent() API, and
> introduce an indent() function, to follow the existing API style
> of plain functions.
> 
> Something like:
> 
>    indent_prefixes = []
>    def push_indent(amount=4):
>        """Add `amount` spaces to indentation prefix"""
>        indent_prefixes.push(' '*amount)
>    def pop_indent():
>        """Revert the most recent push_indent() call"""
>        indent_prefixes.pop()
>    def indent():
>        """Return the current indentation prefix"""
>        return ''.join(indent_prefixes)
> 
> What you did is still an improvement, though, so:
> 
> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
> 

Yeah, there's only one user right now, so ... I just kinda wanted to get 
rid of the global usage. Maybe if we make the code generator fancier 
we'll find out what form is best.

--js
John Snow Sept. 23, 2020, 5:30 p.m. UTC | #4
On 9/23/20 10:55 AM, Cleber Rosa wrote:
> Do you have a use case for returning the level?  If not, I'd go
> without it, and add a "level" property instead, as it'd serve more
> cases.

__int__ is doing that lifting. I can remove the return.

--js
Markus Armbruster Sept. 25, 2020, 11:51 a.m. UTC | #5
John Snow <jsnow@redhat.com> writes:

> On 9/22/20 6:22 PM, Eduardo Habkost wrote:
>> On Tue, Sep 22, 2020 at 05:00:32PM -0400, John Snow wrote:
>>> Code style tools really dislike the use of global keywords, because it
>>> generally involves re-binding the name at runtime which can have strange
>>> effects depending on when and how that global name is referenced in
>>> other modules.
>>>
>>> Make a little indent level manager instead.
>>>
>>> Signed-off-by: John Snow <jsnow@redhat.com>
>>> ---
>>>   scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
>>>   scripts/qapi/visit.py  |  7 +++---
>>>   2 files changed, 38 insertions(+), 20 deletions(-)
>>>
>>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>>> index cee63eb95c..e0c5871b10 100644
>>> --- a/scripts/qapi/common.py
>>> +++ b/scripts/qapi/common.py
>>> @@ -93,33 +93,52 @@ def c_name(name, protect=True):
>>>   pointer_suffix = ' *' + eatspace
>>>     
>>> -def genindent(count):
>>> -    ret = ''
>>> -    for _ in range(count):
>>> -        ret += ' '
>>> -    return ret
>>> +class Indentation:
>>> +    """
>>> +    Indentation level management.
>>>   +    :param initial: Initial number of spaces, default 0.
>>> +    """
>>> +    def __init__(self, initial: int = 0) -> None:
>>> +        self._level = initial
>>>   -indent_level = 0
>>> +    def __int__(self) -> int:
>>> +        return self._level
>>>   +    def __repr__(self) -> str:
>>> +        return "{}({:d})".format(type(self).__name__, self._level)
>>>   -def push_indent(indent_amount=4):
>>> -    global indent_level
>>> -    indent_level += indent_amount
>>> +    def __str__(self) -> str:
>>> +        """Return the current indentation as a string of spaces."""
>>> +        return ' ' * self._level
>>>   +    def __bool__(self) -> bool:
>>> +        """True when there is a non-zero indentation."""
>>> +        return bool(self._level)
>>>   -def pop_indent(indent_amount=4):
>>> -    global indent_level
>>> -    indent_level -= indent_amount
>>> +    def increase(self, amount: int = 4) -> int:
>>> +        """Increase the indentation level by `amount`, default 4."""
>>> +        self._level += amount
>>> +        return self._level
>>> +
>>> +    def decrease(self, amount: int = 4) -> int:
>>> +        """Decrease the indentation level by `amount`, default 4."""
>>> +        if self._level < amount:
>>> +            raise ArithmeticError(
>>> +                f"Can't remove {amount:d} spaces from {self!r}")
>>> +        self._level -= amount
>>> +        return self._level
>>> +
>>> +
>>> +indent = Indentation()
>> Personally, I would keep the push_indent(), pop_indent() API, and
>> introduce an indent() function, to follow the existing API style
>> of plain functions.
>> Something like:
>>    indent_prefixes = []
>>    def push_indent(amount=4):
>>        """Add `amount` spaces to indentation prefix"""
>>        indent_prefixes.push(' '*amount)
>>    def pop_indent():
>>        """Revert the most recent push_indent() call"""
>>        indent_prefixes.pop()
>>    def indent():
>>        """Return the current indentation prefix"""
>>        return ''.join(indent_prefixes)
>> What you did is still an improvement, though, so:
>> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
>> 
>
> Yeah, there's only one user right now, so ... I just kinda wanted to
> get rid of the global usage. Maybe if we make the code generator
> fancier we'll find out what form is best.

You don't get rid of the global variable, you just change it from
integer to a class.  A class can be handier when generating multiple
things interleaved, because you can have one class instance per thing.

Note that we already have a class instance per thing we generate:
instances of subtypes of QAPIGen.  The thought of moving the indentation
machinery into QAPIGen or or maybe QAPIGenCCode crossed my mind many
moons ago, but I had bigger fish to fry, and then I forgot :)

John, I suggest you don't try to make this pretty just yet.  Do what
needs to be done for the type hint job.  We can make it pretty later.
Markus Armbruster Sept. 25, 2020, 11:55 a.m. UTC | #6
John Snow <jsnow@redhat.com> writes:

> On 9/23/20 10:55 AM, Cleber Rosa wrote:
>> Do you have a use case for returning the level?  If not, I'd go
>> without it, and add a "level" property instead, as it'd serve more
>> cases.
>
> __int__ is doing that lifting. I can remove the return.

I like my functions to return something useful.

Use your judgement.
Eduardo Habkost Sept. 25, 2020, 1:13 p.m. UTC | #7
On Fri, Sep 25, 2020 at 01:51:54PM +0200, Markus Armbruster wrote:
> John Snow <jsnow@redhat.com> writes:
> > On 9/22/20 6:22 PM, Eduardo Habkost wrote:
[...]
> > Yeah, there's only one user right now, so ... I just kinda wanted to
> > get rid of the global usage. Maybe if we make the code generator
> > fancier we'll find out what form is best.
> 
> You don't get rid of the global variable, you just change it from
> integer to a class.  A class can be handier when generating multiple
> things interleaved, because you can have one class instance per thing.

To be fair, John doesn't claim to be getting rid of a global
variable.  He's getting rid of usage of the 'global' keyword to
make linters happier.  There are ways to do that without changing
the code too much, though.

> 
> Note that we already have a class instance per thing we generate:
> instances of subtypes of QAPIGen.  The thought of moving the indentation
> machinery into QAPIGen or or maybe QAPIGenCCode crossed my mind many
> moons ago, but I had bigger fish to fry, and then I forgot :)
> 
> John, I suggest you don't try to make this pretty just yet.  Do what
> needs to be done for the type hint job.  We can make it pretty later.
Markus Armbruster Sept. 25, 2020, 1:47 p.m. UTC | #8
Eduardo Habkost <ehabkost@redhat.com> writes:

> On Fri, Sep 25, 2020 at 01:51:54PM +0200, Markus Armbruster wrote:
>> John Snow <jsnow@redhat.com> writes:
>> > On 9/22/20 6:22 PM, Eduardo Habkost wrote:
> [...]
>> > Yeah, there's only one user right now, so ... I just kinda wanted to
>> > get rid of the global usage. Maybe if we make the code generator
>> > fancier we'll find out what form is best.
>> 
>> You don't get rid of the global variable, you just change it from
>> integer to a class.  A class can be handier when generating multiple
>> things interleaved, because you can have one class instance per thing.
>
> To be fair, John doesn't claim to be getting rid of a global
> variable.  He's getting rid of usage of the 'global' keyword to
> make linters happier.

True.

>                        There are ways to do that without changing
> the code too much, though.

Let's do something easy and cheap, because ...

>> Note that we already have a class instance per thing we generate:
>> instances of subtypes of QAPIGen.  The thought of moving the indentation
>> machinery into QAPIGen or or maybe QAPIGenCCode crossed my mind many
>> moons ago, but I had bigger fish to fry, and then I forgot :)

... we'll probably want to move this stuff into QAPIGen later, and
that's when we should make it pretty.

>> John, I suggest you don't try to make this pretty just yet.  Do what
>> needs to be done for the type hint job.  We can make it pretty later.
John Snow Sept. 25, 2020, 2:42 p.m. UTC | #9
On 9/25/20 7:51 AM, Markus Armbruster wrote:
> John Snow <jsnow@redhat.com> writes:
> 
>> On 9/22/20 6:22 PM, Eduardo Habkost wrote:
>>> On Tue, Sep 22, 2020 at 05:00:32PM -0400, John Snow wrote:
>>>> Code style tools really dislike the use of global keywords, because it
>>>> generally involves re-binding the name at runtime which can have strange
>>>> effects depending on when and how that global name is referenced in
>>>> other modules.
>>>>
>>>> Make a little indent level manager instead.
>>>>
>>>> Signed-off-by: John Snow <jsnow@redhat.com>
>>>> ---
>>>>    scripts/qapi/common.py | 51 +++++++++++++++++++++++++++++-------------
>>>>    scripts/qapi/visit.py  |  7 +++---
>>>>    2 files changed, 38 insertions(+), 20 deletions(-)
>>>>
>>>> diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
>>>> index cee63eb95c..e0c5871b10 100644
>>>> --- a/scripts/qapi/common.py
>>>> +++ b/scripts/qapi/common.py
>>>> @@ -93,33 +93,52 @@ def c_name(name, protect=True):
>>>>    pointer_suffix = ' *' + eatspace
>>>>      
>>>> -def genindent(count):
>>>> -    ret = ''
>>>> -    for _ in range(count):
>>>> -        ret += ' '
>>>> -    return ret
>>>> +class Indentation:
>>>> +    """
>>>> +    Indentation level management.
>>>>    +    :param initial: Initial number of spaces, default 0.
>>>> +    """
>>>> +    def __init__(self, initial: int = 0) -> None:
>>>> +        self._level = initial
>>>>    -indent_level = 0
>>>> +    def __int__(self) -> int:
>>>> +        return self._level
>>>>    +    def __repr__(self) -> str:
>>>> +        return "{}({:d})".format(type(self).__name__, self._level)
>>>>    -def push_indent(indent_amount=4):
>>>> -    global indent_level
>>>> -    indent_level += indent_amount
>>>> +    def __str__(self) -> str:
>>>> +        """Return the current indentation as a string of spaces."""
>>>> +        return ' ' * self._level
>>>>    +    def __bool__(self) -> bool:
>>>> +        """True when there is a non-zero indentation."""
>>>> +        return bool(self._level)
>>>>    -def pop_indent(indent_amount=4):
>>>> -    global indent_level
>>>> -    indent_level -= indent_amount
>>>> +    def increase(self, amount: int = 4) -> int:
>>>> +        """Increase the indentation level by `amount`, default 4."""
>>>> +        self._level += amount
>>>> +        return self._level
>>>> +
>>>> +    def decrease(self, amount: int = 4) -> int:
>>>> +        """Decrease the indentation level by `amount`, default 4."""
>>>> +        if self._level < amount:
>>>> +            raise ArithmeticError(
>>>> +                f"Can't remove {amount:d} spaces from {self!r}")
>>>> +        self._level -= amount
>>>> +        return self._level
>>>> +
>>>> +
>>>> +indent = Indentation()
>>> Personally, I would keep the push_indent(), pop_indent() API, and
>>> introduce an indent() function, to follow the existing API style
>>> of plain functions.
>>> Something like:
>>>     indent_prefixes = []
>>>     def push_indent(amount=4):
>>>         """Add `amount` spaces to indentation prefix"""
>>>         indent_prefixes.push(' '*amount)
>>>     def pop_indent():
>>>         """Revert the most recent push_indent() call"""
>>>         indent_prefixes.pop()
>>>     def indent():
>>>         """Return the current indentation prefix"""
>>>         return ''.join(indent_prefixes)
>>> What you did is still an improvement, though, so:
>>> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
>>>
>>
>> Yeah, there's only one user right now, so ... I just kinda wanted to
>> get rid of the global usage. Maybe if we make the code generator
>> fancier we'll find out what form is best.
> 
> You don't get rid of the global variable, you just change it from
> integer to a class.  A class can be handier when generating multiple
> things interleaved, because you can have one class instance per thing.
> 

The 'global' keyword, not 'global scoped'. The key thing was to remove 
re-binding of the global name, which is now true. We bind to this object 
once and then modify object state.

The global keyword allows us to re-bind the variable at the global scope 
which is the pattern to avoid. Having globally scoped things you don't 
rebind is perfectly fine.

> Note that we already have a class instance per thing we generate:
> instances of subtypes of QAPIGen.  The thought of moving the indentation
> machinery into QAPIGen or or maybe QAPIGenCCode crossed my mind many
> moons ago, but I had bigger fish to fry, and then I forgot :)
> 
> John, I suggest you don't try to make this pretty just yet.  Do what
> needs to be done for the type hint job.  We can make it pretty later.
> 

Mmhmm. I'm fine with with we have here for now. We can try to make 
things pretty later. We have the rust support series to consider, too, 
so I am not going to put the cart before the horse.

--js
John Snow Sept. 25, 2020, 3:41 p.m. UTC | #10
On 9/25/20 7:55 AM, Markus Armbruster wrote:
> John Snow <jsnow@redhat.com> writes:
> 
>> On 9/23/20 10:55 AM, Cleber Rosa wrote:
>>> Do you have a use case for returning the level?  If not, I'd go
>>> without it, and add a "level" property instead, as it'd serve more
>>> cases.
>>
>> __int__ is doing that lifting. I can remove the return.
> 
> I like my functions to return something useful.
> 
> Use your judgement.
> 
> 

Eh, Cleber had a point. Nothing uses it.
(AKA: I already made the edit ...)

Like you say, we'll figure out the truly beautiful way to do code 
generation when we tackle this all together, holistically. What I've got 
works for now (and isn't terribly complex), let's just roll with it 
while we fry the bigger fish.

--js
diff mbox series

Patch

diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index cee63eb95c..e0c5871b10 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -93,33 +93,52 @@  def c_name(name, protect=True):
 pointer_suffix = ' *' + eatspace
 
 
-def genindent(count):
-    ret = ''
-    for _ in range(count):
-        ret += ' '
-    return ret
+class Indentation:
+    """
+    Indentation level management.
 
+    :param initial: Initial number of spaces, default 0.
+    """
+    def __init__(self, initial: int = 0) -> None:
+        self._level = initial
 
-indent_level = 0
+    def __int__(self) -> int:
+        return self._level
 
+    def __repr__(self) -> str:
+        return "{}({:d})".format(type(self).__name__, self._level)
 
-def push_indent(indent_amount=4):
-    global indent_level
-    indent_level += indent_amount
+    def __str__(self) -> str:
+        """Return the current indentation as a string of spaces."""
+        return ' ' * self._level
 
+    def __bool__(self) -> bool:
+        """True when there is a non-zero indentation."""
+        return bool(self._level)
 
-def pop_indent(indent_amount=4):
-    global indent_level
-    indent_level -= indent_amount
+    def increase(self, amount: int = 4) -> int:
+        """Increase the indentation level by `amount`, default 4."""
+        self._level += amount
+        return self._level
+
+    def decrease(self, amount: int = 4) -> int:
+        """Decrease the indentation level by `amount`, default 4."""
+        if self._level < amount:
+            raise ArithmeticError(
+                f"Can't remove {amount:d} spaces from {self!r}")
+        self._level -= amount
+        return self._level
+
+
+indent = Indentation()
 
 
 # Generate @code with @kwds interpolated.
-# Obey indent_level, and strip eatspace.
+# Obey indent, and strip eatspace.
 def cgen(code, **kwds):
     raw = code % kwds
-    if indent_level:
-        indent = genindent(indent_level)
-        raw = re.sub(r'^(?!(#|$))', indent, raw, flags=re.MULTILINE)
+    if indent:
+        raw = re.sub(r'^(?!(#|$))', str(indent), raw, flags=re.MULTILINE)
     return re.sub(re.escape(eatspace) + r' *', '', raw)
 
 
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
index 808410d6f1..4edaee33e3 100644
--- a/scripts/qapi/visit.py
+++ b/scripts/qapi/visit.py
@@ -19,8 +19,7 @@ 
     gen_endif,
     gen_if,
     mcgen,
-    pop_indent,
-    push_indent,
+    indent,
 )
 from .gen import QAPISchemaModularCVisitor, ifcontext
 from .schema import QAPISchemaObjectType
@@ -69,7 +68,7 @@  def gen_visit_object_members(name, base, members, variants):
     if (visit_optional(v, "%(name)s", &obj->has_%(c_name)s)) {
 ''',
                          name=memb.name, c_name=c_name(memb.name))
-            push_indent()
+            indent.increase()
         ret += mcgen('''
     if (!visit_type_%(c_type)s(v, "%(name)s", &obj->%(c_name)s, errp)) {
         return false;
@@ -78,7 +77,7 @@  def gen_visit_object_members(name, base, members, variants):
                      c_type=memb.type.c_name(), name=memb.name,
                      c_name=c_name(memb.name))
         if memb.optional:
-            pop_indent()
+            indent.decrease()
             ret += mcgen('''
     }
 ''')