black icon indicating copy to clipboard operation
black copied to clipboard

Multiple accesses are turned multiline when the last argument of a call has a trailing comma

Open halworsen opened this issue 2 years ago • 3 comments

I'm a little unsure whether to categorize this as a bug or a code style issue, but anyways...

Describe the style change

This is so incredibly specific that it is best explained by example. See this example in the Black playground.

When chaining dict accesses to the result of a function call that uses a single kwarg with a trailing comma, Black will reformat one of the accesses to be multiline, which does not look good.

Examples in the current Black style

For the following code:

# The return of self.q(...) is a dict containing a dict
regions = self.q(Q_REGION_LIST.format(
    innerQuery='id',
))['worldData']['regions']

Black will produce the following code:

regions = self.q(
    Q_REGION_LIST.format(
        innerQuery="id",
    )
)[
    "worldData"
]["regions"]

Desired style

Black should either:

  1. Format the code as if there was no trailing comma after the single kwarg, or
  2. Produce similar code as when using multiple kwargs with trailing commas (or as when there is only 1 dict access)

For the following code:

regions = self.q(Q_REGION_LIST.format(
    innerQuery='id',
))['worldData']['regions']

In the case of 1., Black should produce the following formatted code:

regions = self.q(Q_REGION_LIST.format(innerQuery="id"))["worldData"]["regions"]

And for 2., it should produce the following formatted code:

regions = self.q(
    Q_REGION_LIST.format(
        innerQuery="id",
    )
)["worldData"]["regions"]

Additional context

  • Black version: 23.1.0
  • OS: Windows 10
  • Python version: 3.9.7

This is an out of the box, brand-spanking-new, fresh install of Black from pip.

halworsen avatar Feb 25 '23 20:02 halworsen

(note that you can get behaviour 1 by passing --skip-magic-trailing-comma/-C )

hauntsaninja avatar Feb 26 '23 07:02 hauntsaninja

Doesn't seem to be related to kwargs, but trailing commas on function args in general. Also happens on arbitrary accesses, not just dicts with string keys.

f(a,)[1][2.56]

# Becomes...
f(
    a,
)[
    1
][2.56]

Same with f(a, b,)[1][2.56]:

f(
    a,
    b,
)[
    1
][2.56]

or any amount of arguments as long as the last one has a trailing comma.

Interestingly, if you keep chaining accesses, you can get different patterns for how Black inserts linebreaks into the accesses. Particularly, an odd number of accesses inserts the linebreaks on the even numbered accesses. An even number of accesses inserts them on the odd numbered accesses:

f(a,)['c']['d']['e']

# Becomes...
f(
    a,
)["c"][
    "d"
]["e"]

f(a,)['c']['d']['e']['f']

# Becomes...
f(
    a,
)[
    "c"
]["d"][
    "e"
]["f"]

(note that you can get behaviour 1 by passing --skip-magic-trailing-comma/-C )

This seems to work pretty well, thanks! I find that Black will sometimes still break the accesses across multiple lines in my code, but that seems to be due to line length, so it's not strictly related to this issue even if it doesn't look great...

class WorldMixin:
    ...

    def get_all_expansions(self) -> list[FFLogsExpansion]:
        '''
        Retrieves a list of all expansions supported by FFLogs.

        Returns:
            A list of FFLogsExpansions representing each expansion.
        '''
        expacs = self.q(Q_EXPANSION_LIST.format(
            innerQuery='id',
        ))['worldData']['expansions']

        return [FFLogsExpansion(id=e['id'], client=self) for e in expacs]

# Becomes...
class WorldMixin:
    ...

    def get_all_expansions(self) -> list[FFLogsExpansion]:
        """
        Retrieves a list of all expansions supported by FFLogs.

        Returns:
            A list of FFLogsExpansions representing each expansion.
        """
        expacs = self.q(Q_EXPANSION_LIST.format(innerQuery="id"))["worldData"][
            "expansions"
        ]

        return [FFLogsExpansion(id=e["id"], client=self) for e in expacs]

halworsen avatar Feb 26 '23 13:02 halworsen

Playing around some more, it looks even more general than functions, since this also happens when a trailing comma in a >1 len tuple or >0 len list/dict/set literal causes a line break:

(a,b,)[1][2][3][4]

# becomes:

(
    a,
    b,
)[
    1
][2][
    3
][4]

Also with the same even-odd swapping behavior:

(a,b,)[1][2][3][4][5]

# becomes:

(
    a,
    b,
)[1][
    2
][3][
    4
][5]

MeGaGiGaGon avatar Jan 27 '25 23:01 MeGaGiGaGon