Skip to main content

No project description provided

Project description

DrResult

More radical approach to Rust's std::result in Python.

Motivation

I do not want exceptions in my code. Rust has this figured out quite neatly by essentially revolving around two pathways for errors: A possible error condition is either one that has no prospect of being handled -- then the program should terminate -- or it is one that could be handled -- then it has to be handled or explicitly ignored.

This concept is replicated here by using AssertionError to emulate Rust's panic!, mapping all unhandled exceptions to AssertionError and providing a Rust-like result type to signal error conditions that do not need to terminate the program.

Documentation

Concept

At each point in your code, there are exceptions that are considered to be expected and there are exceptions that are considered unexpected.

In general, an unexpected exception will be mapped to AssertionError. No part of DrResult will attempt to handle an AssertionError. And you should leave all exception handling to DrResult, i.e. have no try/except blocks in your code. In any case, you should never catch an AssertionError. An unexpected exception will therefore result in program termination with stack unwinding.

You will need to specify which exceptions are to be expected. There are two modes of operation here: You can explicitly name the exceptions to be expected in a function. Or you can skip that and basically expect all exceptions.

Basically means: By default only Exception is expected, not BaseException. And even of type Exception that are some considered to be never expected:

AttributeError, ImportError, MemoryError, NameError, SyntaxError, SystemError, TypeError

If you do not explicitly expect these, they will be implicitly unexpected. (Obviously, the exact list may be up for debate.)

Basic Usage

@noexcept

If your function knows no errors, you mark it as @noexcept():

from drresult import noexcept

@noexcept()
def sum(a: list) -> int:
    result = 0
    for item in a:
        result += item
    return result

result = func([1, 2, 3])

This will do what you expect it does. But if you screw up...

@noexcept()
def sum(a: list) -> int:
    result = 0
    for item in a:
        result += item
    print(a[7])   # IndexError
    return result

result = func([1, 2, 3])    # AssertionError

... then it will raise an AssertionError preserving the stack trace and the message of the original exception.

This way all unexpected exceptions are normalised to AssertionError.

Please note that a @noexecpt function does not return a result but just the return type itself.

@returns_result() and expects

Marking a function as @returns_result will wrap its return value in an Ok result and exceptions thrown in an Err result. But only those exceptions that are expected. As noted above, if you do not explicitly specify exceptions to expect, most runtime exceptions are expected by default.

@returns_result()
def read_file() -> Result[str]:
    with open('/some/path/that/might/be/invalid') as f:
        return Ok(f.read())

result = read_file()
if result.is_ok():
    print(f'File content: {result.unwrap()}')
else:
    print(f'Error: {str(result.unwrap_err())}')

This will do as you expect.

You can also explicitly specify the exception to expect:

@returns_result(expects=[FileNotFoundError])
def read_file() -> Result[str]:
    with open('/some/path/that/might/be/invalid') as f:
        return Ok(f.read())

result = read_file()
if result.is_ok():
    print(f'File content: {result.unwrap()}')
else:
    print(f'Error: {str(result.unwrap_err())}')

This also will do as you expect.

If fail to specify an exception that is raised as expected...

from drresult import returns_result

@returns_result(expects=[IndexError, KeyError])
def read_file() -> str:
    with open('/this/path/is/invalid') as f:
        return f.read()

result = read_file()    # AssertionError

.. it will be re-raised as AssertionError.

If you are feeling fancy, you can also do pattern matching:

@returns_result(expects=[FileNotFoundError])
def read_file() -> str:
    with open('/this/path/is/invalid') as f:
        return f.read()

result = read_file()
match result:
    case Ok(v):
        print(f'File content: {v}')
    case Err(e):
        print(f'Error: {e}')

And even fancier:

data = [{ 'foo': 'value-1' }, { 'bar': 'value-2' }]

@returns_result(expects=[IndexError, KeyError, RuntimeError])
def retrieve_record_entry_backend(index: int, key: str) -> str:
    if key == 'baz':
        raise RuntimeError('Cannot process baz!')
    return data[index][key]

def retrieve_record_entry(index: int, key: str):
    match retrieve_record_entry_backend(index: int, key: str):
        case Ok(v):
            print(f'Retrieved: {v}')
        case Err(IndexError()):
            print(f'No such record: {index}')
        case Err(KeyError()):
            print(f'No entry `{key}` in record {index}')
        case Err(RuntimeError() as e):
            print(f'Error: {e}')

retrieve_record_entry(2, 'foo')    # No such record: 2
retrieve_record_entry(1, 'foo')    # No entry `foo` in record 1
retrieve_record_entry(1, 'bar')    # Retrieved: value-2
retrieve_record_entry(1, 'baz')    # Error: Cannot process baz!

Implicit conversion to bool

If you are feeling more lazy than fancy, you can do this:

result = Ok('foo')
assert result

result = Err('bar')
assert not result

unwrap_or_raise

You can replicate the behaviour of Rust's ?-operator with unwrap_or_raise:

@returns_result()
def read_json(filename: str) -> str:
    with open(filename) as f:
        return json.loads(f.read())

@returns_result()
def parse_file(filename: str) -> dict:
    content = read_file(filename).unwrap_or_raise()
    if not 'required_key' in content:
        raise KeyError('required_key')
    return content

If the result is not Ok, unwrap_or_raise() will re-raise the contained exception. Obviously, this will lead to an assertion if the contained exception is not expected.

gather_result

When you are interfacing with other modules that use exceptions, you may want to react to certain exceptions being raised. To avoid having to use try/except again, you can transform exceptions from a part of your code to results:

@returns_result()
def parse_json_file(filename: str) -> Result[dict]:
    with gather_result() as result:
        with open(filename) as f:
            result.set(Ok(json.loads(f.read())))
    result = result.get()
    match result:
        case Ok(data):
            return Ok(data)
        case Err(FileNotFoundError()):
            return Ok({})
        case _:
            return result

Similar Projects

For a less extreme approach on Rust's result type, see:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drresult-0.4.0.tar.gz (5.4 kB view details)

Uploaded Source

Built Distribution

drresult-0.4.0-py3-none-any.whl (6.9 kB view details)

Uploaded Python 3

File details

Details for the file drresult-0.4.0.tar.gz.

File metadata

  • Download URL: drresult-0.4.0.tar.gz
  • Upload date:
  • Size: 5.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.10.12 Linux/5.15.0-113-generic

File hashes

Hashes for drresult-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d1b152b4ef9edb93d3f968e0d4ad9e74a150e44215b123cac4217f402e1ba9e6
MD5 406f9fc43e043d63e3ad0773dd55ce59
BLAKE2b-256 6d857ff9a16c17b0afb5b343ed72cb7e12226f8e5648bd0b16bd40503e6ff56a

See more details on using hashes here.

File details

Details for the file drresult-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: drresult-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 6.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.3 CPython/3.10.12 Linux/5.15.0-113-generic

File hashes

Hashes for drresult-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a207d1e686696df244905a03fc1249fb9566fae0a21fe7ce6a7feef932a45b43
MD5 666006bb16567d657c63674d87cf77be
BLAKE2b-256 b72ba0bc6d8d87cba063f2885f949213ad802cd3f0d1ee70b25918a3788ca405

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page