Daily Tips - Which formatter is your favorite? ❤️

Hi there,

Today we are going to try @xumiao’s project. As those of you who have read my Daily Tips series probably know, I’ve been experimenting with some new Python features in my code recently. Then I’ve found that the current version of yapf(0.32.0) can’t handle merge(|) and update(|=) operators, and it’s also a mess when it comes to assignment expressions. So I tried switching the formatter.

Let’s take a brief look at the comparison of several formatters under the default configuration.

dashace

It looks like the autopep8 is the fastest, but the code changes are minimal and won’t even handle some unreasonable manual line breaks.

The black is about the same speed, and it likes to put a comma on the last item in the list.

The yapf took 1.58 seconds to process 94 lines of code on my laptop!

My code is here.

from dash import Dash, html, Output, Input, State, no_update, callback_context
import dash_ace
import time
from flask import jsonify, Flask
from flask_cors import CORS
from autopep8 import fix_code
from black import format_str, FileMode
from yapf.yapflib.yapf_api import FormatCode

server = Flask(__name__)
CORS(server)

app = Dash(__name__, server=server, routes_pathname_prefix="/dash/")

app.layout = html.Div(
    [
        html.Div(
            [
                btn1 := html.Button("autopep8"),
                btn2 := html.Button("black"),
                btn3 := html.Button("yapf"),
                timer := html.Div(
                    style=dict(display="inline-block", marginLeft="45px"),
                ),
            ],
        ),
        dash_ace.DashAceEditor(
            id="input",
            value=r"""
from dash import Dash, dash_table, dcc, html
from dash.dependencies import Input, Output
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')

app = Dash(__name__)

app.layout = html.Div([
    dash_table.DataTable(
        id='datatable-interactivity',
        columns=[
            {"name": i, "id": i, "deletable": True, "selectable": True} for i in df.columns
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode="multi",
        column_selectable="single",
        row_selectable="multi",
        row_deletable=True,
        selected_columns=[],
        selected_rows=[],
        page_action="native",
        page_current= 0,
        page_size= 10,
    ),
    html.Div(id='datatable-interactivity-container')
])

@app.callback(
    Output('datatable-interactivity', 'style_data_conditional'),
    Input('datatable-interactivity', 'selected_columns')
)
def update_styles(selected_columns):
    return [{
        'if': { 'column_id': i },
        'background_color': '#D2F3FF'
    } for i in selected_columns]

@app.callback(
    Output('datatable-interactivity-container', "children"),
    Input('datatable-interactivity', "derived_virtual_data"),
    Input('datatable-interactivity', "derived_virtual_selected_rows"))
def update_graphs(rows, derived_virtual_selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncrasy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)

    colors = ['#7FDBFF' if i in derived_virtual_selected_rows else '#0074D9'
              for i in range(len(dff))]

    return [
        dcc.Graph(
            id=column,
            figure={
                "data": [
                    {
                        "x": dff["country"],
                        "y": dff[column],
                        "type": "bar",
                        "marker": {"color": colors},
                    }
                ],
                "layout": {
                    "xaxis": {"automargin": True},
                    "yaxis": {
                        "automargin": True,
                        "title": {"text": column}
                    },
                    "height": 250,
                    "margin": {"t": 10, "l": 10, "r": 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ["pop", "lifeExp", "gdpPercap"] if column in dff
    ]


if __name__ == '__main__':
    app.run_server(debug=True)
""",
            theme="github",
            mode="python",
            tabSize=2,
            enableBasicAutocompletion=True,
            enableLiveAutocompletion=True,
            autocompleter="/autocompleter?prefix=",
            placeholder="Python code ...",
        ),
    ]
)


@server.route("/autocompleter", methods=["GET"])
def autocompleter():
    return jsonify(
        [{"name": "Completed", "value": "Completed", "score": 100, "meta": "test"}]
    )


@app.callback(
    [Output("input", "value"), Output(timer, "children")],
    [Input(btn1, "n_clicks"), Input(btn2, "n_clicks"), Input(btn3, "n_clicks")],
    State("input", "value"),
    prevent_initial_call=True,
)
def formatter(n1, n2, n3, v):

    if callback_context.triggered[0]["prop_id"].split(".")[0] == btn1.id:
        t0 = time.perf_counter()
        formatted_code = fix_code(v)
    elif callback_context.triggered[0]["prop_id"].split(".")[0] == btn2.id:
        t0 = time.perf_counter()
        formatted_code = format_str(v, mode=FileMode())
    elif callback_context.triggered[0]["prop_id"].split(".")[0] == btn3.id:
        t0 = time.perf_counter()
        formatted_code, changed = FormatCode(v)

    t1 = time.perf_counter()
    return formatted_code, f"It takes {t1-t0:.2f} seconds."


if __name__ == "__main__":
    app.run_server(debug=True)



Which one is your favorite?

  • autopep8
  • black
  • yapf
  • others

0 voters




Hope you like this. XD

Keywords: MQTT, ESP8266, MicroPython, WebSockets

Other Daily Tips series:
Daily Tips - If I have a Bunch of Triggers
Daily Tips - Share the Function
Daily Tips - How many Ways to share Dash in a company?
Daily Tips - Give your Component a Name
Daily Tips - Share Dash to Social Media
Daily Tips - Double-click to open Dash
Daily Tips - What rows of data have I modified?
Daily Tips - Write the Simplest Client-side Callback
Daily Tips - Some simple Expressions
Daily Tips - IoT? Real Real-time Data and Live Update

2 Likes

Adding a trailing comma at the end of a multiline list or function declaration may look weird at first but it’s very much a feature rather than a bug.

When you add new lines to the end of the sequence , this helps minimises the git diff and preserve git blame (as you’re now just adding lines, not changing an existing line). It also reduces the chance of forgetting to add a comma for new items at the end. Sometimes this just generates a syntax error, but when your list items are strings, the values will be implicitly concatenated together, which can lead to rather nasty bugs to track dow.

1 Like