Bowler Documentation

repository·main·Indexed 23 days ago

https://github.com/facebookincubator/bowler

Bowler is a safe, large-scale Python refactoring tool that operates at the syntax tree level using the Concrete Syntax Tree (CST) from lib2to3. It provides a CLI and a fluent Python API to perform complex code modifications through a sequence of selectors, filters, and modifiers, ensuring the resulting output remains valid Python code.

Tokens
10K
Snippets
49
Records
70
Agent score
79%

What's inside Bowler

  1. Overview of Bowler

    main

    Bowler is a refactoring tool for manipulating Python at the syntax tree level. It is designed for safe, large-scale code modifications, ensuring that the resulting code compiles and runs.

    It provides two primary ways to interact with it:

    1. A command line interface.
    2. A fluent Python API for generating complex code modifications.

    Bowler uses a Query API to build refactoring scripts through a series of selectors, filters, and modifiers. You can use built-in tools or provide custom selectors, filters, and modifiers for complex refactorings.

  2. What is Bowler?

    main
    Bowler is a safe refactoring tool designed for modern Python. It is built using syntax trees from the Python standard library, which ensures compatibility across different Python versions. It is intended for building powerful, reusable code modifications (code mods), such as upgrading code to new APIs or fixing broken attributes across large codebases. Bowler uses a fluent query API that allows developers to compose refactoring scripts from reusable components.
  3. How to use Bowler for Python refactoring

    main

    Bowler is a refactoring tool that manipulates Python code at the syntax tree level using the Concrete Syntax Tree (CST) from lib2to3. It allows for safe, large-scale code modifications by ensuring the resulting code remains syntactically correct.

    You can use Bowler via a command-line interface or a fluent Python API. The fluent API allows you to chain selectors, filters, and modifiers to build complex transformations.

    query = (
        Query([<file paths>])
        # rename class Foo to Bar
        .select_class("Foo")
        .rename("Bar")
        # change method buzz(x) to buzzard(x: int)
        .select_method("buzz")
        .rename("buzzard")
        .modify_argument("x", type_annotation="int")
    )
    
    query.diff()  # generate unified diff on stdout
    query.write()  # write changes directly to files
  4. How to construct a Bowler Query

    main

    Queries use a fluent API to build a series of transforms over a set of file paths. A query consists of a sequence of transforms, where each transform includes a selector, optional filters, and one or more modifiers.

    To build a query, follow this pattern:

    1. Initialize: Create a Query object with a list of file paths.
    2. Select: Use a selector to define broad search criteria.
    3. Filter (Optional): Use filters to refine the scope of the modification.
    4. Modify: Use modifiers to apply changes to the matched nodes.
    5. Repeat: Add more transforms by repeating steps 2-4.
    6. Execute: Trigger a terminal action like .diff() or .write() to apply the changes.
  5. How filters work in Bowler

    main

    Filters are intermediate functions used to restrict the set of syntax tree elements matched by selectors before any modifications are applied.

    Key behaviors:

    • Short-circuiting: Matched elements must pass all applied filters to be modified. If an element fails a single filter, it is dropped immediately and subsequent filters are not evaluated for that element.
    • Signature: All custom filter functions must follow this signature:
    def some_filter(node: LN, capture: Capture, filename: Filename) -> bool:
        ...

    Arguments:

    • node: The matched syntax tree element.
    • capture: Sub-elements captured by the selector pattern.
    • filename: The file being considered for modification.
    • Return Value: Return True to keep the element for modification, or False to drop it.
  6. What are Modifiers in Bowler

    main

    Modifiers are functions used to modify, add, remove, or replace syntax tree elements that were matched by selectors after they have passed all filters.

    Modifications can occur anywhere in the syntax tree (above or below the matched element) and can include multiple changes.

    All modifier functions must follow this signature:

    def modifier(node: LN, capture: Capture, filename: Filename) -> Optional[LN]:
        ...

    Arguments

    • node: The matched syntax tree element.
    • capture: Sub-elements captured by the selector pattern.
    • filename: The file being modified.
    • Return Value: The leaf or nodes returned will automatically replace the matched element.
  7. How pattern syntax works for selectors

    main

    Selectors in Bowler use lib2to3 pattern syntax to search the Python syntax tree. Patterns can be nested and include alternate branches using the | operator.

    Key Syntax Rules:

    • Grammar Elements: List the grammar element (e.g., classdef, funcdef) to match it.
    • Nesting: Use angle brackets <...> to define nested match expressions.
    • Wildcards: Use any to match any grammar element, and * to denote zero or more repetitions (e.g., any*).
    • Capturing: Precede an element with a name and an equals sign (e.g., name=NAME) to capture it. These captures are available in the Capture argument of filters and modifiers.
    • Optionality: Use square brackets [...] for optional elements (zero or one).
    • String Literals: When matching specific tokens like ( or ), you must declare them as string literals to differentiate them from syntax like [ or ].
    • Alternation: Use ( expression1 | expression2 ) to match multiple possible arrangements.
    # Example: Match class definitions containing a function definition
    PATTERN = """
        classdef<
            any*
            suite<
                any* funcdef any*
            >
        >
    """
    
    # Example: Complex pattern with captures and optionality
    PATTERN = """
        classdef<
            "class" name=NAME ["(" [ancestors=arglist] ")"] ":"
            suite<
                any*
                funcdef< "def" func_name=NAME any* >
                any*
            >
        >
    """
  8. How Bowler handles Python syntax and formatting

    main

    Bowler is built on top of lib2to3 (specifically using fissix, a backport with improved features). This provides a Concrete Syntax Tree (CST) implementation.

    Because it uses a CST rather than an Abstract Syntax Tree (AST), Bowler can modify the syntax tree while preserving all original formatting and comments. This prevents refactoring operations from destroying valuable code metadata or stylistic information.

    Compatibility Notes:

    • Source Code: Can read and modify source files written for both Python 2 and Python 3 (back to version 2.6).
    • Runtime Requirement: Bowler itself requires Python 3.6 or newer to run.
  9. Use Selectors to find syntax tree patterns

    main

    Selectors are lib2to3 search patterns used to identify nodes in the CST. They can capture child nodes or leaves using named assignments.

    Example pattern to find print function calls and capture their arguments:

    pattern = """
        power< "print"
            trailer< "(" print_args=any* ")" >
        >
    """
    
    (
        Query()
        .select(pattern)
        ...
    )

    In this example, print_args captures everything inside the parentheses as a list.

    pattern = """
        power< "print"
            trailer< "(" print_args=any* ")" >
        >
    """
    
    (
        Query()
        .select(pattern)
        ...
    )
    """
  10. How Bowler queries and transforms work together

    main

    Bowler refactors Python code by building queries against the Concrete Syntax Tree (CST). A query is composed of one or more transforms. Each transform follows a specific lifecycle:

    1. Selector: A pattern used to find specific nodes in the syntax tree (e.g., finding all print calls).
    2. Filters: Functions that inspect the matched nodes and return True to keep them or False to exclude them.
    3. Modifiers: Functions that perform the actual tree transformation (modifying, removing, or inserting nodes).

    Bowler uses a fluent API, allowing you to chain these operations together in a single statement.

    (
        Query()
        .select(pattern)
        .filter(filter_func)
        .modify(modifier_func)
        .execute()
    )
    (
        Query()
        .select(...)
        .modify(...)
        .execute()
    )
  11. How the Bowler Query API works

    main

    Bowler uses a "fluent" Query API, meaning most methods return the Query object itself. This allows you to chain multiple refactoring operations in a single expression without needing to assign the object to a variable at every step.

    Both fluent and imperative styles are supported:

    Fluent style (preferred):

    (
        Query()
        .select_function("foo")
        .rename("bar")
        .diff()
    )

    Imperative style:

    query = Query()
    query.select_function("foo")
    query.rename("bar")
    query.diff()
    (
        Query()
        .select_function("foo")
        .rename("bar")
        .diff()
    )