Implement fluent interfaces

Fixes #67
This commit is contained in:
Łukasz Langa 2018-05-16 15:09:02 -07:00
parent 1dadeef47a
commit 8c74d7901f
6 changed files with 94 additions and 34 deletions

View File

@ -342,6 +342,26 @@ In those cases, parentheses are removed when the entire statement fits
in one line, or if the inner expression doesn't have any delimiters to
further split on. Otherwise, the parentheses are always added.
### Call chains
Some popular APIs, like ORMs, use call chaining. This API style is known
as a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface).
*Black* formats those treating dots that follow a call or an indexing
operation like a very low priority delimiter. It's easier to show the
behavior than to explain it. Look at the example::
```py3
def example(session):
result = (
session.query(models.Customer.id)
.filter(
models.Customer.account_id == account_id,
models.Customer.email == email_address,
)
.order_by(models.Customer.id.asc())
.all()
)
```
### Typing stub files
PEP 484 describes the syntax for type hints in Python. One of the
@ -589,6 +609,8 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
### 18.5a0 (unreleased)
* call chains are now formatted according to the [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67)
* slices are now formatted according to PEP 8 (#178)
* parentheses are now also managed automatically on the right-hand side

View File

@ -626,21 +626,22 @@ def show(cls, code: str) -> None:
STRING_PRIORITY = 12
COMPARATOR_PRIORITY = 10
MATH_PRIORITIES = {
token.VBAR: 8,
token.CIRCUMFLEX: 7,
token.AMPER: 6,
token.LEFTSHIFT: 5,
token.RIGHTSHIFT: 5,
token.PLUS: 4,
token.MINUS: 4,
token.STAR: 3,
token.SLASH: 3,
token.DOUBLESLASH: 3,
token.PERCENT: 3,
token.AT: 3,
token.TILDE: 2,
token.DOUBLESTAR: 1,
token.VBAR: 9,
token.CIRCUMFLEX: 8,
token.AMPER: 7,
token.LEFTSHIFT: 6,
token.RIGHTSHIFT: 6,
token.PLUS: 5,
token.MINUS: 5,
token.STAR: 4,
token.SLASH: 4,
token.DOUBLESLASH: 4,
token.PERCENT: 4,
token.AT: 4,
token.TILDE: 3,
token.DOUBLESTAR: 2,
}
DOT_PRIORITY = 1
@dataclass
@ -1729,6 +1730,14 @@ def is_split_before_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
# Don't treat them as a delimiter.
return 0
if (
leaf.type == token.DOT
and leaf.parent
and leaf.parent.type not in {syms.import_from, syms.dotted_name}
and (previous is None or previous.type != token.NAME)
):
return DOT_PRIORITY
if (
leaf.type in MATH_OPERATORS
and leaf.parent
@ -2128,6 +2137,10 @@ def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
except ValueError:
raise CannotSplit("No delimiters found")
if delimiter_priority == DOT_PRIORITY:
if bt.delimiter_count_with_priority(delimiter_priority) == 1:
raise CannotSplit("Splitting a single attribute from its owner looks wrong")
current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
lowest_depth = sys.maxsize
trailing_comma_safe = True

View File

@ -61,28 +61,37 @@ def test_fails_invalid_post_data(
def foo(list_a, list_b):
results = (
User.query.filter(User.foo == "bar").filter( # Because foo.
User.query.filter(User.foo == "bar")
.filter( # Because foo.
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(User.xyz.is_(None))
)
.filter(User.xyz.is_(None))
# Another comment about the filtering on is_quux goes here.
.filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by(
User.created_at.desc()
).with_for_update(key_share=True).all()
.filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True)))
.order_by(User.created_at.desc())
.with_for_update(key_share=True)
.all()
)
return results
def foo2(list_a, list_b):
# Standalone comment reasonably placed.
return User.query.filter(User.foo == "bar").filter(
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(User.xyz.is_(None))
return (
User.query.filter(User.foo == "bar")
.filter(
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
)
.filter(User.xyz.is_(None))
)
def foo3(list_a, list_b):
return (
# Standlone comment but weirdly placed.
User.query.filter(User.foo == "bar").filter(
User.query.filter(User.foo == "bar")
.filter(
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(User.xyz.is_(None))
)
.filter(User.xyz.is_(None))
)

View File

@ -128,7 +128,7 @@
]
slice[0]
slice[0:1]
@@ -124,107 +144,154 @@
@@ -124,107 +144,159 @@
numpy[-(c + 1) :, d]
numpy[:, l[-2]]
numpy[:, ::-1]
@ -173,9 +173,14 @@
+what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
+ vars_to_remove
+)
+result = session.query(models.Customer.id).filter(
+ models.Customer.account_id == account_id, models.Customer.email == email_address
+).order_by(models.Customer.id.asc()).all()
+result = (
+ session.query(models.Customer.id)
+ .filter(
+ models.Customer.account_id == account_id, models.Customer.email == email_address
+ )
+ .order_by(models.Customer.id.asc())
+ .all()
+)
Ø = set()
authors.łukasz.say_thanks()
mapping = {

View File

@ -411,9 +411,14 @@ async def f():
what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
vars_to_remove
)
result = session.query(models.Customer.id).filter(
models.Customer.account_id == account_id, models.Customer.email == email_address
).order_by(models.Customer.id.asc()).all()
result = (
session.query(models.Customer.id)
.filter(
models.Customer.account_id == account_id, models.Customer.email == email_address
)
.order_by(models.Customer.id.asc())
.all()
)
Ø = set()
authors.łukasz.say_thanks()
mapping = {

View File

@ -167,9 +167,15 @@ def spaces2(result=_core.Value(None)):
def example(session):
result = session.query(models.Customer.id).filter(
models.Customer.account_id == account_id, models.Customer.email == email_address
).order_by(models.Customer.id.asc()).all()
result = (
session.query(models.Customer.id)
.filter(
models.Customer.account_id == account_id,
models.Customer.email == email_address,
)
.order_by(models.Customer.id.asc())
.all()
)
def long_lines():