Skip to content

Animenosekai/breakall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

breakall

Logo

Break from multiple loops at once in Python

Why isn't this a thing in Python?

PyPI version Downloads PyPI - Downloads PyPI - Python Version PyPI - Status GitHub - License GitHub Top Language CI Code Coverage Code Size Repo Size Issues

Note

This basically implements the idea behind PEP 3136 – Labeled break and continue.

Index

Getting Started

Prerequisites

You will need Python +3.10 to run this module

# vermin output
Minimum required versions: 3.10
Incompatible versions:     2

Note

The heavy use of ast.unparse makes this incompatible with Python version lower than 3.10

Installation

pip install --upgrade breakall

Basic Usage

You can use breakall by decorating your function with @enable_breakall:

>>> from breakall import enable_breakall
>>>
>>> @enable_breakall
... def main():
...     for i in range(10):
...         for j in range(10):
...             for k in range(10):
...                 if k == 5:
...                     breakall
...                 print(i, j, k)

Alternatively, you can enable breakall for all functions in your module by calling enable_breakall() without arguments:

from breakall import enable_breakall

def main():
    for i in range(10):
        for j in range(10):
            for k in range(10):
                if k == 5:
                    breakall
                print(i, j, k)

# This will enable breakall for all functions in the current scope
enable_breakall()

main()

Or you can use the command-line interface:

python -m breakall script.py
# or
breakall script.py

This will automatically enable breakall for all functions in the file.

Keyword

You can use breakall to break from multiple loops at once.

There is different syntax to determine how many loops to break from.

breakall

The breakall keyword will break from all loops in the current scope.

>>> from breakall import enable_breakall
>>>
>>> @enable_breakall
... def test():
...     for i in range(3):
...         for j in range(3):
...             if j == 1:
...                 breakall
...             print(f"  ({i}, {j})")
...         print(f"  End of i={i}")
...     print("Done")
>>>
>>> test()
  (0, 0)
Done

As you can see, the loops are completely exited without printing the rest of the iterations or "End of i={i}" message.

breakall: n

The breakall: n keyword will break from n loops in the current scope.

>>> from breakall import enable_breakall
>>>
>>> @enable_breakall
... def test():
...     for i in range(3):
...         for j in range(3):
...             for k in range(3):
...                 if k == 1:
...                     breakall: 2
...                 print(f"    ({i}, {j}, {k})")
...             print(f"  End of j={j}")
...         print(f"End of i={i}")
...     print("Done")
>>>
>>> test()
    (0, 0, 0)
End of i=0
    (1, 0, 0)
End of i=1
    (2, 0, 0)
End of i=2
Done

In this example, breakall: 2 breaks from the innermost two loops (the j and k loops), but the outermost i loop continues.

breakall @ n

The breakall @ n keyword will break from all loops up to and including the n-th loop from the inside.

>>> from breakall import enable_breakall
>>>
>>> @enable_breakall
... def test():
...     for i in range(3):  # Loop 1
...         for j in range(3):  # Loop 2
...             for k in range(3):  # Loop 3
...                 if k == 1:
...                     breakall @ 2
...                 print(f"    ({i}, {j}, {k})")
...             print(f"  End of k loop")
...         print(f"End of j loop")
...     print("Done")
>>>
>>> test()
    (0, 0, 0)
    (1, 0, 0)
    (2, 0, 0)
Done

In this example, breakall @ 2 targets loop 2 (the j loop), so it breaks from both loop 3 (k) and loop 2 (j), but loop 1 (i) continues.

Note

The breakall @ n keyword is 1-indexed, where 1 is the innermost loop

Dynamic Loop Breaking

You can also use a variable to determine how many loops to break from:

>>> from breakall import enable_breakall
>>>
>>> @enable_breakall
... def test():
...     n = 2
...     for i in range(3):
...         for j in range(3):
...             for k in range(3):
...                 breakall @ n # breakall: n also works
...                 print("Shouldn't print")
...             print("Shouldn't print")
...         print("Should print")
...     print("Done")
>>>
>>> test()
Should print
Should print
Should print
Done

You can even perform calculations in the loop breaking statement:

>>> from breakall import enable_breakall
>>>
>>>
>>> @enable_breakall
>>> def test():
...     for i in range(3):
...         for j in range(3):
...             for k in range(3):
...                 breakall: 1 + 2
...                 print("Shouldn't print")
...             print("Shouldn't print")
...         print("Shouldn't print")
...     print("Done")
>>>
>>>
>>> test()
Done

Error handling

All errors originating from an invalid use of breakall all inherit BreakAllError.

There are three types of errors:

  • BreakAllEnvironmentError : When the enable_breakall couldn't properly transform the function (keep in mind that enable_breakall needs to have access to the function's source code to transform it)
  • BreakAllSyntaxError : When the breakall statement is used with invalid syntax, such as an invalid loop number or break count
  • BreakAllRuntimeError : When the breakall statement is used with a valid syntax but results in an invalid loop number or break count at runtime, such as when using a variable that is out of range or a calculation that results in an invalid loop number or break count

It even gives you proper error messages

# breakall @ 4
BreakAllSyntaxError: Invalid loop number
File ".../test.py", line 7, in test
                breakall @ 4
                           ^
There are only 3 loops to break up until this point. Note that it is impossible to break to a loop defined later. (test.py)

# breakall: 4
BreakAllSyntaxError: Invalid break count
File ".../test.py", line 7, in test
                breakall: 4
                          ^
There are only 3 loops to break. (test.py)
# When `n` is too small
Traceback (most recent call last):
  File ".../test.py", line 17, in <module>
    test()
  File ".../test.py", line 7, in test
    for i in range(3):
  File ".../breakall/runtime.py", line 170, in destination_from_loop_number
    raise BreakAllRuntimeError(
breakall.exceptions.BreakAllRuntimeError: Invalid loop number
File ".../test.py", line 11, in test
                breakall @ n
                           ^
The loop number must be greater than 0
# When using a calculation that results in an invalid loop number or break count
Traceback (most recent call last):
  File ".../test.py", line 17, in <module>
    test()
  File ".../test.py", line 7, in test
    for i in range(3):
  File ".../breakall/runtime.py", line 88, in destination_from_break_count
    raise BreakAllRuntimeError(
breakall.exceptions.BreakAllRuntimeError: Invalid break count
File ".../test.py", line 11, in test
                breakall: 1 + 3
                          ^^^^^
There are only 3 loops to break.

Linter and Type Checker Configuration

When using breakall, certain linting and type-checking tools may report false positives since they don't understand the breakall statement transformation.

Importing breakall

You can import the breakall statement to make your type checker happy, but it is not necessary for the code to work:

from breakall import breakall

def hello():
    for i in range(n):
        for j in range(m):
            if j == 1:
                breakall
            print(i, j)

Ruff

Add to your module using breakall:

# ruff: noqa: B018, F842

Where:

  • B018 - Useless expression (for bare breakall statements)
  • F842 - Local variable name is assigned to but never used (for breakall: n statements where the loop counter variable is not used)

Pyright

Add to your module using breakall:

# pyright: reportUnusedExpression=false, reportUnusedVariable=false, reportGeneralTypeIssues=false

Where:

  • reportUnusedExpression=false - Allow unused expressions (for breakall statements)
  • reportUnusedVariable=false - Allow unused variables (for breakall: n statements where the loop counter variable is not used)
  • reportGeneralTypeIssues=false - Suppress general type issues that may when using breakall: n statements (n is an integer, not a type)

Command-Line Interface

You can use the breakall module directly from the command line without decorating functions:

python -m breakall script.py

The CLI automatically applies enable_breakall to all functions in the target file:

# my_script.py
def nested_loops():
    for i in range(3):
        for j in range(3):
            if j == 1:
                breakall
            print(f"  ({i}, {j})")
        print(f"  End of i={i}")
    print("Done")

nested_loops()
$ python -m breakall my_script.py
  (0, 0)
Done

Tracing Imported Modules

By default, the CLI only applies enable_breakall to functions in the main file. If you're using breakall in imported modules, use the --trace flag:

python -m breakall script.py --trace

Example: Without --trace

If your imported module contains breakall, it will fail without --trace:

# helper.py
def helper_function():
    for i in range(3):
        for j in range(3):
            if j == 1:
                breakall
            print(f"  ({i}, {j})")
        print(f"  End of i={i}")
    print("Done")
# main.py
from helper import helper_function

helper_function()
$ python -m breakall main.py
  (0, 0)
Traceback (most recent call last):
  File "main.py", line 3, in <module>
    helper_function()
  File "helper.py", line 6, in helper_function
    breakall
NameError: name 'breakall' is not defined

Example: With --trace

Using --trace enables import hooking, so all imported modules support breakall:

$ python -m breakall main.py --trace
  (0, 0)
Done

The --trace flag automatically applies the enable_breakall decorator to all functions in imported modules, allowing them to use the breakall statement.

Saving Transformed Code

You can save the transformed Python code to a file using the --output option:

python -m breakall script.py --output transformed.py

This generates a new file with all breakall statements converted to proper loop-breaking logic.

Deployment

This module is currently in development and might contain bugs.

Feel free to use it in production if you feel like it is suitable for your production even if you may encounter issues.

Contributing

Pull requests are welcome. For major changes, please open an discussion first to discuss what you would like to change.

Please make sure to update the tests as appropriate.

Authors

Disclaimer

This project is not affiliated with the Python Software Foundation.

License

This project is licensed under the MIT License - see the LICENSE file for details

About

Break from multiple loops at once in Python

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages