From 8c74d7901fe8de0abd72a182d775b639b4202577 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Langa?= Date: Wed, 16 May 2018 15:09:02 -0700 Subject: [PATCH] Implement fluent interfaces Fixes #67 --- README.md | 22 ++++++++++++++++++++++ black.py | 41 +++++++++++++++++++++++++++-------------- tests/comments4.py | 29 +++++++++++++++++++---------- tests/expression.diff | 13 +++++++++---- tests/expression.py | 11 ++++++++--- tests/function.py | 12 +++++++++--- 6 files changed, 94 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 9f72a67..ed69cb8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/black.py b/black.py index 06bff08..4b5f19e 100644 --- a/black.py +++ b/black.py @@ -626,21 +626,22 @@ LOGIC_PRIORITY = 14 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 diff --git a/tests/comments4.py b/tests/comments4.py index 9529990..944714d 100644 --- a/tests/comments4.py +++ b/tests/comments4.py @@ -61,28 +61,37 @@ class C: 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)) ) diff --git a/tests/expression.diff b/tests/expression.diff index 21209f6..dfca37c 100644 --- a/tests/expression.diff +++ b/tests/expression.diff @@ -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 = { diff --git a/tests/expression.py b/tests/expression.py index 093d259..cc6f399 100644 --- a/tests/expression.py +++ b/tests/expression.py @@ -411,9 +411,14 @@ what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + se 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 = { diff --git a/tests/function.py b/tests/function.py index 4cfc945..4754588 100644 --- a/tests/function.py +++ b/tests/function.py @@ -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(): -- 2.39.2