def ast_equal(observed: ast.AST, expected: ast.AST) -> bool:
    """Checks the equality between two ASTs.

    This function checks if `observed` contains at least the same subtree with
    `expected`. If `observed` has some extra branches that `expected` does not cover,
    it is ignored.

    Args:
        observed: An AST to check.
        expected: The expected AST.

    Returns:
        True if observed and expected represent the same AST, False otherwise.
    """
    ignore_keys = {"lineno", "col_offset", "end_lineno", "end_col_offset", "kind"}
    if sys.version_info.minor <= 12:
        ignore_keys.add("type_params")

    try:
        assert type(observed) is type(expected)

        for k, ve in vars(expected).items():
            if k in ignore_keys:
                continue

            vo = getattr(observed, k)  # May cause AttributeError.

            if isinstance(ve, ast.AST):
                assert ast_equal(cast(ast.AST, vo), ve)
            elif isinstance(ve, list):
                vo = cast(list, vo)
                assert len(vo) == len(ve)
                assert all(
                    ast_equal(cast(ast.AST, co), cast(ast.AST, ce))
                    for co, ce in zip(vo, ve)
                )
            else:
                assert type(vo) is type(ve)
                assert vo == ve

    except (AssertionError, AttributeError):
        raise  # raise to debug easier.

    return True
Checks the equality between two ASTs.

    This function checks if `observed` contains at least the same subtree with
    `expected`. If `observed` has some extra branches that `expected` does not cover,
    it is ignored.

    Args:
        observed: An AST to check.
        expected: The expected AST.

    Returns:
        True if observed and expected represent the same AST, False otherwise.
    
src/latexify/test_utils.py