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 in one line, or if the inner expression doesn't have any delimiters to
further split on. Otherwise, the parentheses are always added. 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 ### Typing stub files
PEP 484 describes the syntax for type hints in Python. One of the 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) ### 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) * slices are now formatted according to PEP 8 (#178)
* parentheses are now also managed automatically on the right-hand side * 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 STRING_PRIORITY = 12
COMPARATOR_PRIORITY = 10 COMPARATOR_PRIORITY = 10
MATH_PRIORITIES = { MATH_PRIORITIES = {
token.VBAR: 8, token.VBAR: 9,
token.CIRCUMFLEX: 7, token.CIRCUMFLEX: 8,
token.AMPER: 6, token.AMPER: 7,
token.LEFTSHIFT: 5, token.LEFTSHIFT: 6,
token.RIGHTSHIFT: 5, token.RIGHTSHIFT: 6,
token.PLUS: 4, token.PLUS: 5,
token.MINUS: 4, token.MINUS: 5,
token.STAR: 3, token.STAR: 4,
token.SLASH: 3, token.SLASH: 4,
token.DOUBLESLASH: 3, token.DOUBLESLASH: 4,
token.PERCENT: 3, token.PERCENT: 4,
token.AT: 3, token.AT: 4,
token.TILDE: 2, token.TILDE: 3,
token.DOUBLESTAR: 1, token.DOUBLESTAR: 2,
} }
DOT_PRIORITY = 1
@dataclass @dataclass
@ -1729,6 +1730,14 @@ def is_split_before_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
# Don't treat them as a delimiter. # Don't treat them as a delimiter.
return 0 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 ( if (
leaf.type in MATH_OPERATORS leaf.type in MATH_OPERATORS
and leaf.parent and leaf.parent
@ -2128,6 +2137,10 @@ def delimiter_split(line: Line, py36: bool = False) -> Iterator[Line]:
except ValueError: except ValueError:
raise CannotSplit("No delimiters found") 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) current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
lowest_depth = sys.maxsize lowest_depth = sys.maxsize
trailing_comma_safe = True trailing_comma_safe = True

View File

@ -61,28 +61,37 @@ def test_fails_invalid_post_data(
def foo(list_a, list_b): def foo(list_a, list_b):
results = ( 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)) 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. # Another comment about the filtering on is_quux goes here.
.filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True)))
User.created_at.desc() .order_by(User.created_at.desc())
).with_for_update(key_share=True).all() .with_for_update(key_share=True)
.all()
) )
return results return results
def foo2(list_a, list_b): def foo2(list_a, list_b):
# Standalone comment reasonably placed. # Standalone comment reasonably placed.
return User.query.filter(User.foo == "bar").filter( return (
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) User.query.filter(User.foo == "bar")
).filter(User.xyz.is_(None)) .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): def foo3(list_a, list_b):
return ( return (
# Standlone comment but weirdly placed. # 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)) 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]
slice[0:1] slice[0:1]
@@ -124,107 +144,154 @@ @@ -124,107 +144,159 @@
numpy[-(c + 1) :, d] numpy[-(c + 1) :, d]
numpy[:, l[-2]] numpy[:, l[-2]]
numpy[:, ::-1] numpy[:, ::-1]
@ -173,9 +173,14 @@
+what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( +what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
+ vars_to_remove + vars_to_remove
+) +)
+result = session.query(models.Customer.id).filter( +result = (
+ models.Customer.account_id == account_id, models.Customer.email == email_address + session.query(models.Customer.id)
+).order_by(models.Customer.id.asc()).all() + .filter(
+ models.Customer.account_id == account_id, models.Customer.email == email_address
+ )
+ .order_by(models.Customer.id.asc())
+ .all()
+)
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { 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( what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
vars_to_remove vars_to_remove
) )
result = session.query(models.Customer.id).filter( result = (
models.Customer.account_id == account_id, models.Customer.email == email_address session.query(models.Customer.id)
).order_by(models.Customer.id.asc()).all() .filter(
models.Customer.account_id == account_id, models.Customer.email == email_address
)
.order_by(models.Customer.id.asc())
.all()
)
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { mapping = {

View File

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