Repository: psf/black

Issue:
Allow reduced number of blank lines for dummy implementations
Hi!

I would like to start a discussion about reducing blank lines in some special cases, I hope this is welcome :)

The [Blank Lines](https://www.python.org/dev/peps/pep-0008/#blank-lines) section in PEP8 states:

> Blank lines may be omitted between a bunch of related one-liners (e.g. a set of dummy implementations).

While this is in the same paragraph as a sentence about functions, I would like to propose a liberal but well defined interpretation of this. It boils down to allowing reducing blank lines for empty classes and functions. This will make code that uses a lot of type definitions or overloads more readable. In the case of overloads this especially makes sense as it will allow overload definitions to be visually grouped with the overloaded function.

My opinion is also that the requirement for classes to take at least two lines and require being surrounded by two blank lines makes these definition _feel_ heavy. Now talking about how code feels might seem misplaced and unscientific mumbo-jumbo, but I think it is important to allow class definitions to feel light when they're used in a Type-driven development style. Some of these usecases for class definitions that would benefit from _feeling_ lighter are custom exceptions, and intersections. Why should a type definition like `A = NewType("A", int)` only require one line while a type definition like `class Intersection(A, B): ...` require six? I argue that making these definitions feel heavy will make developers refrain from using more elegant solutions that utilizes the type system over solutions that feel light but are in reality less expressive and doesn't describe business domains as fully as they could.

1. Allow putting the entire definition on one line if the entire body consists of `pass` or `...` (ellipses).

```python
class CustomError(Exception): ...

def fn(a): ....
```

2. Allow removing blank lines between empty classes and between functions, but require *one*  blank line between an empty class and an empty function. Functions and classes with a docstring as the entire body will also be considered "empty", but not allowed to be defined on a single line.

```python
class CustomError(Exception):
    """Allow documented classes to feel cozy too, as we don't want to discourage docs."""
class OtherError(CustomError): ...

def dummy(a): ...
def other(b): ...
```

3. Non-dummy classes and functions should remain their current set of rules for surrounding blank lines. With the exception of overloads. The rule for overloads could be generalized as to allow functions with the same name to skip blank lines between them, but to require two blank lines surrounding them.

```python
class Error(Exception): ...


class Implementation(Exception):
    """I am not an empty class and require some space"""

    a = 1


def fn(): ...


@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...
def a(arg: Union[int, str, object]) -> Union[int, str]:
    if not isinstance(arg, (int, str)):
        raise TypeError
    return arg
```

While I think this could become well defined, and should before implementation work starts, I'm sure I've missed some edge cases that needs defining. Let's hammer them out *if* this is welcomed as a good idea in the first place, and not shot down ;)

### Current behaviour

Example 2. is currently reformatted by black as (14 lines instead of 6):

```python
class CustomError(Exception):
    """Allow documented classes to feel cozy too, as we don't want to discourage docs."""


class OtherError(CustomError):
    ...


def dummy(a):
    ...


def other(b):
    ...
```

Example 3. is currently formatted like this with black (33 instead of 22). Notice how information is lost here, the original formatting communicates to a reader that the overloads are related to each other:

```python
class Error(Exception):
    ...


class Implementation(Exception):
    """I am not an empty class and require some space"""

    a = 1


def fn():
    ...


@overload
def a(arg: int) -> int:
    ...


@overload
def a(arg: str) -> str:
    ...


@overload
def a(arg: object) -> NoReturn:
    ...


def a(arg: Union[int, str, object]) -> Union[int, str]:
    if not isinstance(arg, (int, str)):
        raise TypeError
    return arg
```



Discussion:
For people coming to this issue looking for a solution for stubs, the `--pyi` flag is what you're looking for. It's designed for typing stubs, telling Black to follow the [typeshed formatting conventions](https://github.com/python/typeshed/blob/master/CONTRIBUTING.md#conventions) instead of Black's own. 
+1 for keeping related overloads visually together
I came here to request exactly this enhancement (point number 3) — in particular, a sequence of `@overload`-annotated functions in front of a real function are, conceptually, part of the type-annotations of that function, and so it makes sense to group them visually with the function instead of setting them off.
Any action on this?
I'm sympathetic to this change, especially for overloads.

One problem will be that linters like flake8 are going to complain about py files with too few blank lines. We don't guarantee flake8 compatibility, of course, but this will mean people have to turn off a few more flake8 rules.
> One problem will be that linters like flake8 are going to complain about py files with too few blank lines. We don't guarantee flake8 compatibility, of course, but this will mean people have to turn off a few more flake8 rules.

Although I also don’t think we should be constrained by Flake8, we could make significant progress here even within that constraint—Flake8 has no complaints about

```python
from typing import NoReturn, Union, overload


def dummy(a): ...
def other(b): ...


@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...


def a(arg: Union[int, str, object]) -> Union[int, str]:
    if not isinstance(arg, (int, str)):
        raise TypeError
    return arg
```

It still expects the two blank lines between the group of overload declarations and the main definition of the same function, but 2*n* + 2 lines for *n* overloads is still much more readable than 5*n*.
I would accept a PR that does the following:

- If the body of a class or function is just `...`, put it on the same line directly after the colon
- If multiple adjacent functions with the same name each have only `...` as their body, remove the blank lines between them. (This would apply to overloads in practice, but avoids hardcoding behavior for the "overload" name.)
Any progress on this?

It's a pretty glaring omission that honestly makes black unusable if you have a lot of overloads.
@shayded-exe I think it's safe to assume that progress will be reported in this thread. If you're unhappy with the state of things, there's usually two things you can do to make change in an open source project:

- Contribute yourself.
- Pay someone else to contribute.

While I'm too am keen to see this happen, comments just asking for updates are not helpful, and unnecessarily pings lots of people's inboxes. 
@antonagestam Sorry, I didn't mean to sound so bitchy (and ping everyone's inboxes three times now).

I really just wanted to express that I'm shocked that more people haven't requested it. Sometimes issues don't get movement unless people are asking for it. I honestly would contribute if I had the time and didn't have other higher priorities.

Anyways, sorry for being annoying :)

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -281,7 +281,9 @@ def visit_match_case(self, node: Node) -> Iterator[Line]:
 
     def visit_suite(self, node: Node) -> Iterator[Line]:
         """Visit a suite."""
-        if self.mode.is_pyi and is_stub_suite(node):
+        if (
+            self.mode.is_pyi or Preview.dummy_implementations in self.mode
+        ) and is_stub_suite(node):
             yield from self.visit(node.children[2])
         else:
             yield from self.visit_default(node)
@@ -296,7 +298,9 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]:
 
         is_suite_like = node.parent and node.parent.type in STATEMENT
         if is_suite_like:
-            if self.mode.is_pyi and is_stub_body(node):
+            if (
+                self.mode.is_pyi or Preview.dummy_implementations in self.mode
+            ) and is_stub_body(node):
                 yield from self.visit_default(node)
             else:
                 yield from self.line(+1)
@@ -305,7 +309,7 @@ def visit_simple_stmt(self, node: Node) -> Iterator[Line]:
 
         else:
             if (
-                not self.mode.is_pyi
+                not (self.mode.is_pyi or Preview.dummy_implementations in self.mode)
                 or not node.parent
                 or not is_stub_suite(node.parent)
             ):
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -165,6 +165,13 @@ def is_def(self) -> bool:
             and second_leaf.value == "def"
         )
 
+    @property
+    def is_stub_def(self) -> bool:
+        """Is this line a function definition with a body consisting only of "..."?"""
+        return self.is_def and self.leaves[-4:] == [Leaf(token.COLON, ":")] + [
+            Leaf(token.DOT, ".") for _ in range(3)
+        ]
+
     @property
     def is_class_paren_empty(self) -> bool:
         """Is this a class with no base classes but using parentheses?
@@ -578,6 +585,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
             first_leaf.prefix = ""
         else:
             before = 0
+
+        user_had_newline = bool(before)
         depth = current_line.depth
 
         previous_def = None
@@ -589,7 +598,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
             if self.mode.is_pyi:
                 if depth and not current_line.is_def and self.previous_line.is_def:
                     # Empty lines between attributes and methods should be preserved.
-                    before = min(1, before)
+                    before = 1 if user_had_newline else 0
                 elif (
                     Preview.blank_line_after_nested_stub_class in self.mode
                     and previous_def.is_class
@@ -624,7 +633,9 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
                     before = 2
 
         if current_line.is_decorator or current_line.is_def or current_line.is_class:
-            return self._maybe_empty_lines_for_class_or_def(current_line, before)
+            return self._maybe_empty_lines_for_class_or_def(
+                current_line, before, user_had_newline
+            )
 
         if (
             self.previous_line
@@ -648,8 +659,8 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
             return 0, 0
         return before, 0
 
-    def _maybe_empty_lines_for_class_or_def(
-        self, current_line: Line, before: int
+    def _maybe_empty_lines_for_class_or_def(  # noqa: C901
+        self, current_line: Line, before: int, user_had_newline: bool
     ) -> Tuple[int, int]:
         if not current_line.is_decorator:
             self.previous_defs.append(current_line)
@@ -715,6 +726,14 @@ def _maybe_empty_lines_for_class_or_def(
                 newlines = 0
         else:
             newlines = 1 if current_line.depth else 2
+            # If a user has left no space after a dummy implementation, don't insert
+            # new lines. This is useful for instance for @overload or Protocols.
+            if (
+                Preview.dummy_implementations in self.mode
+                and self.previous_line.is_stub_def
+                and not user_had_newline
+            ):
+                newlines = 0
         if comment_to_add_newlines is not None:
             previous_block = comment_to_add_newlines.previous_block
             if previous_block is not None:
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -182,6 +182,7 @@ class Preview(Enum):
     skip_magic_trailing_comma_in_subscript = auto()
     wrap_long_dict_values_in_parens = auto()
     wrap_multiple_context_managers_in_parens = auto()
+    dummy_implementations = auto()
 
 
 class Deprecated(UserWarning):


Test patch:
diff --git a/tests/data/miscellaneous/force_py36.py b/tests/data/miscellaneous/force_py36.py
--- a/tests/data/miscellaneous/force_py36.py
+++ b/tests/data/miscellaneous/force_py36.py
@@ -1,6 +1,6 @@
 # The input source must not contain any Py36-specific syntax (e.g. argument type
 # annotations, trailing comma after *rest) or this test becomes invalid.
-def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): ...
+def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, *rest): pass
 # output
 # The input source must not contain any Py36-specific syntax (e.g. argument type
 # annotations, trailing comma after *rest) or this test becomes invalid.
@@ -13,4 +13,4 @@ def long_function_name(
     argument_six,
     *rest,
 ):
-    ...
+    pass
diff --git a/tests/data/preview/comments7.py b/tests/data/preview/comments7.py
--- a/tests/data/preview/comments7.py
+++ b/tests/data/preview/comments7.py
@@ -278,8 +278,7 @@ class C:
     )
     def test_fails_invalid_post_data(
         self, pyramid_config, db_request, post_data, message
-    ):
-        ...
+    ): ...
 
 
 square = Square(4)  # type: Optional[Square]
diff --git a/tests/data/preview/dummy_implementations.py b/tests/data/preview/dummy_implementations.py
new file mode 100644
--- /dev/null
+++ b/tests/data/preview/dummy_implementations.py
@@ -0,0 +1,99 @@
+from typing import NoReturn, Protocol, Union, overload
+
+
+def dummy(a): ...
+def other(b): ...
+
+
+@overload
+def a(arg: int) -> int: ...
+@overload
+def a(arg: str) -> str: ...
+@overload
+def a(arg: object) -> NoReturn: ...
+def a(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+class Proto(Protocol):
+    def foo(self, a: int) -> int:
+        ...
+
+    def bar(self, b: str) -> str: ...
+    def baz(self, c: bytes) -> str:
+        ...
+
+
+def dummy_two():
+    ...
+@dummy
+def dummy_three():
+    ...
+
+def dummy_four():
+    ...
+
+@overload
+def b(arg: int) -> int: ...
+
+@overload
+def b(arg: str) -> str: ...
+@overload
+def b(arg: object) -> NoReturn: ...
+
+def b(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+# output
+
+from typing import NoReturn, Protocol, Union, overload
+
+
+def dummy(a): ...
+def other(b): ...
+
+
+@overload
+def a(arg: int) -> int: ...
+@overload
+def a(arg: str) -> str: ...
+@overload
+def a(arg: object) -> NoReturn: ...
+def a(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg
+
+
+class Proto(Protocol):
+    def foo(self, a: int) -> int: ...
+
+    def bar(self, b: str) -> str: ...
+    def baz(self, c: bytes) -> str: ...
+
+
+def dummy_two(): ...
+@dummy
+def dummy_three(): ...
+
+
+def dummy_four(): ...
+
+
+@overload
+def b(arg: int) -> int: ...
+
+
+@overload
+def b(arg: str) -> str: ...
+@overload
+def b(arg: object) -> NoReturn: ...
+
+
+def b(arg: Union[int, str, object]) -> Union[int, str]:
+    if not isinstance(arg, (int, str)):
+        raise TypeError
+    return arg