Why the sad face? - Łukasz Langa

When you first encounter Black, a few things about it might surprise you. One of the those things might be "sadface dedent", the style in which closing parentheses in function signatures and other block headers are put on its own line. I arrived at this formatting style long before creating the auto-formatter. It’s got a few objective advantages.

Those advantages can be summed up as follows:

Let’s look into each of those in more detail.

All bracket pairs are treated equal

The argument here goes like this: you already close all other bracket pairs in this manner. You do it for multi-line list and dictionary literals:

directives = {
    'function': EQLFunctionDirective,
    'constraint': EQLConstraintDirective,
    'type': EQLTypeDirective,
    'keyword': EQLKeywordDirective,
    'operator': EQLOperatorDirective,
    'synopsis': EQLSynopsisDirective,
    'react-element': EQLReactElement,
    'section-intro-page': EQLSectionIntroPage,
    'struct': EQLStructElement,
}

You do it when calling functions:

if ret is DEFAULT:
    ret = self._get_child_mock(
        _new_parent=self, _new_name='()'
    )
    self.return_value = ret

You do it when importing many names:

from typing import (
    Iterator,
    List,
    Sequence,
    Set,
)

You do it to help with readability of complex boolean expressions:

create_flag = (
    not self.disable_multitouch
    and not self.multitouch_on_demand
)

It follows quite easily that you might want to do the same for blocks:

def getCommonAncestor(self, lnode, rnode, stop):
    if (
        stop in (lnode, rnode) or
        not (
            hasattr(lnode, '_pyflakes_parent') and
            hasattr(rnode, '_pyflakes_parent')
        )
    ):
        return None

Did you notice? All of the code snippets above come from real-world Python projects. None were written by me and none are auto-formatted by Black. The point I’m making here is that this style isn’t some Black-specific eccentricity. And while I came up with it on my own in my programming journey, so did many other developers.

Moving on.

Clearly delimited block header from body

Let’s take this example:

# Before
if (not line or (line[0] == '#') or
     (line[:3] == '"""') or line[:3] == "'''"):
    self.error('Blank or comment')
    return 0

Due to the number of parentheses in the if test, it’s not obvious visually what the closing parenthesis right before the colon pairs with. It would also be impossible to see where the if test ends without awkwardly 1-indenting the second line of the test to hint that this is not part of the body. Can we do better? Let’s see what Black would do:

# After
if (
    not line
    or (line[0] == "#")
    or (line[:3] == '"""')
    or line[:3] == "'''"
):
    self.error("Blank or comment")
    return 

Wow, six lines instead of two? That was some explosion. But look at it again, is it easier to scan visually? Is it easier to see where the test is and where the body is? I think there’s no contest.

Another similar example:

# Before
if (comp != dotdot or (not initial_slashes and not new_comps) or
     (new_comps and new_comps[-1] == dotdot)):
    new_comps.append(comp)

And black-formatted:

# After
if (
    comp != dotdot
    or (not initial_slashes and not new_comps)
    or (new_comps and new_comps[-1] == dotdot)
):
    new_comps.append(comp)

It minimizes diffs

When you put the closing parenthesis in its separate line, you don’t have to remove it from the last element when adding a new one. Consider:

@@ -114,7 +114,8 @@ class MouseMotionEvent(MotionEvent):
             with win.canvas.after:
                 de = (
                     Color(.8, .2, .2, .7),
-                    Ellipse(size=(20, 20), segments=15))
+                    Ellipse(size=(20, 20), segments=15),
+                    Depth(.5))
             self.ud._drawelement = de
         if de is not None:
             self.push()

versus:

@@ -115,6 +115,7 @@ class MouseMotionEvent(MotionEvent):
                 de = (
                     Color(.8, .2, .2, .7),
                     Ellipse(size=(20, 20), segments=15),
+                    Depth(.5),
                 )
             self.ud._drawelement = de
         if de is not None:

This advantage also requires keeping trailing commas at the last element. You can see this in the second example above.

In function signatures it neatly leaves space for a return type annotation

I don’t think anybody can claim that the "sadface dedent" is particularly beautiful when you look at a function signature like this:

class Command(struct.MixedStruct, metaclass=CommandMeta):

    ...

    @classmethod
    def _modaliases_from_ast(
        cls,
        schema,
        astnode,
        context,
    ):
        modaliases = {}
        if isinstance(astnode, qlast.DDLCommand):
            for alias in astnode.aliases:
                if isinstance(alias, qlast.ModuleAliasDecl):
                    modaliases[alias.alias] = alias.module

Functionally the advantage is that it clearly delimits the function signature from its body. But it further makes sense in the presence of type annotations:

class Command(struct.MixedStruct, metaclass=CommandMeta):

    ...

    @classmethod
    def _modaliases_from_ast(
        cls,
        schema: s_schema.Schema,
        astnode: qlast.DDLOperation,
        context: CommandContext,
    ) -> Dict[Optional[str], str]:
        modaliases = {}
        if isinstance(astnode, qlast.DDLCommand):
            for alias in astnode.aliases:
                if isinstance(alias, qlast.ModuleAliasDecl):
                    modaliases[alias.alias] = alias.module

By having the closing signature parenthesis dedented, we have more space for the return annotation and still provide an elegant separator between the signature and the body.

Can’t you just use a custom indentation level?

If you hug the closing parenthesis to the last line of an if test (like in the “Blank or comment” example above), the test and the body will blend visually. This is enough of a problem that flake8 will complain:

E125 continuation line with same indent as next logical line

You can solve it by using a non-standard indentation level for this case but it’s problematic as you have to choose which non-standard level that should be. Smaller than 4 spaces would violate PEP 8, and a double 4-space indent is unnecessarily giving up precious horizontal space for actual logic in the if test, making it more likely to have to be awkwardly split.

Conclusion

Sure, the disadvantage of the "sadface dedent" is that it looks alien on first encounter. It’s weird. And sure, beautiful is better than ugly. But form should follow function. The "sadface dedent" style is objectively better even if it’s subjectively uglier.

Acknowledgements

This has been proof-read by Hynek. Thanks!

Thanks for your interest!
To subscribe, first choose a category on the website. This will give you an Atom feed with the content that interests you.

Since I'm writing about many topics that have sometimes little to do with each other, I think this is a better approach than subscribing to the entire firehose.
::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


订阅 substack 体验古早写作:


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::