Giter Club home page Giter Club logo

ensure's Introduction

ensure: Literate assertions in Python

ensure is a set of simple assertion helpers that let you write more expressive, literate, concise, and readable Pythonic code for validating conditions. It's inspired by should.js, expect.js, and builds on top of the unittest/JUnit assert helpers.

If you use Python 3, you can use ensure to enforce your function signature annotations: see PEP 3107 and the @ensure_annotations decorator below.

Because ensure is fast, is a standalone library (not part of a test framework), doesn't monkey-patch anything or use DSLs, and doesn't use the assert statement (which is liable to be turned off with the -O flag), it can be used to validate conditions in production code, not just for testing (though it certainly works as a BDD test utility library).

Aside from better looking code, a big reason to use ensure is that it provides more consistent, readable, and informative error messages when things go wrong. See Motivation and Goals for more.

Installation

pip install ensure

Synopsis

from ensure import ensure

ensure(1).is_an(int)
ensure({1: {2: 3}}).equals({1: {2: 3}}).also.contains(1)
ensure({1: "a"}).has_key(1).whose_value.has_length(1)
ensure.each_of([{1: 2}, {3: 4}]).is_a(dict).of(int).to(int)
ensure(int).called_with("1100101", base=2).returns(101)
ensure(dict).called_with(1, 2).raises(TypeError)
check(1).is_a(float).or_raise(Exception, "An error happened: {msg}. See http://example.com for more information.")

In Python 3:

from ensure import ensure_annotations

@ensure_annotations
def f(x: int, y: float) -> float:
    return x+y

See More examples below.

Notes

The ensure module exports the Ensure class and its convenience instance ensure. Instances of the class are callable, and the call will reset the contents that the instance is inspecting, so you can reuse it for many checks (as seen above).

The class raises EnsureError (a subclass of AssertionError) by default.

There are several ways to chain clauses, depending on the grammatical context: .also, .which, and .whose_value are available per examples below.

Raising custom exceptions

You can pass a callable or exception class as the error_factory keyword argument to Ensure(), or you can use the Check class or its convenience instance check(). This class behaves like Ensure, but does not raise errors immediately. It saves them and chains the methods otherwise(), or_raise() and or_call() to the end of the clauses.

from ensure import check

check("w00t").is_an(int).or_raise(Exception)
check(1).is_a(float).or_raise(Exception, "An error happened: {msg}. See http://example.com for more information.")
check("w00t").is_an(int).or_raise(MyException, 1, 2, x=3, y=4)
def build_fancy_exception(original_exception):
    return MyException(original_exception)

check("w00t").is_an(int).otherwise(build_fancy_exception)
check("w00t").is_an(int).or_call(build_fancy_exception, *args, **kwargs)

More examples

ensure({1: {2: 3}}).is_not_equal_to({1: {2: 4}})
ensure(True).does_not_equal(False)
ensure(1).is_in(range(10))
ensure(True).is_a(bool)
ensure(True).is_(True)
ensure(True).is_not(False)
ensure(["train", "boat"]).contains_one_of(["train"])
ensure(range(8)).contains(5)
ensure(["spam"]).contains_none_of(["eggs", "ham"])
ensure("abcdef").contains_some_of("abcxyz")
ensure("abcdef").contains_one_or_more_of("abcxyz")
ensure("abcdef").contains_all_of("acf")
ensure("abcd").contains_only("dcba")
ensure("abc").does_not_contain("xyz")
ensure([1, 2, 3]).contains_no(float)
ensure(1).is_in(range(10))
ensure("z").is_not_in("abc")
ensure(None).is_not_in([])
ensure(dict).has_attribute('__contains__').which.is_callable()
ensure({1: "a", 2: "b", 3: "c"}).has_keys([1, 2])
ensure({1: "a", 2: "b"}).has_only_keys([1, 2])
ensure(1).is_true()
ensure(0).is_false()
ensure(None).is_none()
ensure(1).is_not_none()
ensure("").is_empty()
ensure([1, 2]).is_nonempty().also.has_length(2)
ensure(1.1).is_a(float).which.equals(1.10)
ensure(KeyError()).is_an(Exception)
ensure({x: str(x) for x in range(5)}).is_a_nonempty(dict).of(int).to(str)
ensure({}).is_an_empty(dict)
ensure(None).is_not_a(list)
import re
ensure("abc").matches("A", flags=re.IGNORECASE)
ensure([1, 2, 3]).is_an_iterable_of(int)
ensure([1, 2, 3]).is_a_list_of(int)
ensure({1, 2, 3}).is_a_set_of(int)
ensure({1: 2, 3: 4}).is_a_mapping_of(int).to(int)
ensure({1: 2, 3: 4}).is_a_dict_of(int).to(int)
ensure({1: 2, 3: 4}).is_a(dict).of(int).to(int)
ensure(10**100).is_numeric()
ensure(lambda: 1).is_callable()
ensure("abc").has_length(3)
ensure("abc").has_length(min=3, max=8)
ensure(1).is_greater_than(0)
ensure(1).exceeds(0)
ensure(0).is_less_than(1)
ensure(1).is_greater_than_or_equal_to(1)
ensure(0).is_less_than_or_equal_to(0)
ensure(1).is_positive()
ensure(1.1).is_a_positive(float)
ensure(-1).is_negative()
ensure(-1).is_a_negative(int)
ensure(0).is_nonnegative()
ensure(0).is_a_nonnegative(int)
ensure([1,2,3]).is_sorted()
ensure("{x} {y}".format).called_with(x=1, y=2).equals("1 2")
ensure(int).called_with("1100101", base=2).returns(101)
ensure("{x} {y}".format).with_args(x=1, y=2).is_a(str)
with ensure().raises(ZeroDivisionError):
    1/0
with ensure().raises_regex(NameError, "'w00t' is not defined"):
    w00t

See complete API documentation.

Enforcing function annotations

Use the @ensure_annotations decorator to enforce function signature annotations:

from ensure import ensure_annotations

@ensure_annotations
def f(x: int, y: float) -> float:
    return x+y

f(1, 2.3)
>>> 3.3
f(1, 2)
>>> ensure.EnsureError: Argument y to <function f at 0x109b7c710> does not match annotation type <class 'float'>

Compare this runtime type checking to compile-time checking in Mypy and type hinting in PEP 484/Python 3.5+.

Motivation and goals

Many BDD assertion libraries suffer from an excess of magic, or end up having to construct statements that don't parse as English easily. ensure is deliberately kept simple to avoid succumbing to either issue. The source is easy to read and extend.

Work remains to make error messages raised by ensure even more readable, informative, and consistent. Going forward, ability to introspect exceptions to extract structured error information will be a major development focus. You will be in control of how much information is presented in each error, which context it's thrown from, and what introspection capabilities the exception object will have.

The original use case for ensure is as an I/O validation helper for API endpoints, where the client needs to be sent a very clear message about what went wrong, some structured information (such as an HTTP error code and machine-readable reference to a failing element) may need to be added, and some information may need to be hidden from the client. To further improve on that, we will work on better error translation, marshalling, message formatting, and schema validation helpers.

Authors

  • Andrey Kislyuk
  • Harrison Metzger

Bugs

Please report bugs, issues, feature requests, etc. on GitHub.

License

Licensed under the terms of the Apache License, Version 2.0.

image

image

image

image

ensure's People

Contributors

adamchainz avatar dpercy avatar harrisonmetz avatar keyweeusr avatar kislyuk avatar naiveai avatar wassname avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ensure's Issues

Suggestion: check for a "numeric" string

I've got a program which receives commands through some transport layers as strings (e.g. "!do sleep 3"). It seems that ensure() is the perfect fit for my command classes to verify the passed number of arguments and data types. However, the arguments are provided as strings, so I cannot check for correctness by ensure(123).is_an(int).

Useful test cases:

ensure("123").is_a_numeric_string()
ensure("+123").is_a_numeric_string()
ensure("-123").is_a_numeric_string()
ensure("1.5").is_a_numeric_string()
ensure("-1.5").is_a_numeric_string()

ensure("123abc")  # Raises an exception!
ensure("abc123")  # Raises an exception!

Possible implementation:

def is_numeric_string(str):
    try:
        int(str)
        return True
    except ValueError:
        return False

PS: Is this library still actively developed and updated on PyPi? I see that the latest version on PyPi is from July last year. The last commit date of this repository definitely indicates the opposite.

Return boolean values?

Hey, I've been using ensure as contracts for functions, but it's pretty tricky due to the varying nature of return values. Can we have an option in the Ensure constructor to return a simple boolean True/False value? Or, and I think this is a much better solution because it allows for chaining, simply implement __bool__ for all values returned by predicates to return the actual pass/fail status.

Adjust to upcoming Python 3.8 / fix deprecation warning

When using ensure in unittests run with pytest with Python3.7, the following warning is emitted:

site-packages/ensure/__init__.py:9: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working

So I suppose this should not be hard to fix, but it would be a good idea to do so.

test fails when running with Python 3.12

  File "/home/runner/work/djk-sample/djk-sample/.tox/py3.12-django-4.2-bs4/lib/python3.12/site-packages/ensure/main.py", line 940, in <module>
    ensure_raises_regex = unittest_case.assertRaisesRegexp
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'TestCase' object has no attribute 'assertRaisesRegexp'. Did you mean: 'assertRaisesRegex'?

Custom predicates?

Suppose I want to convert this assertion to ensure:

from path import path
link = path('spam/and/eggs')
assert link.isabs()  # raises AssertionError

I can use with_args or called_with, but then the subject of the assertion isn't link:

ensure(path.isabs).called_with(link).is_true()
# ensure.EnsureError: False is not true

Is there currently a way to pass in an arbitrary callable as a predicate? Something like

ensure(link).satisfies(path.isabs)
# ensure.EnsureError: path('spam/and/eggs') does not satisfy <unbound method path.isabs>

or_raise(Exception, string) encounters a KeyError when string contains {}

When I pass a string containing {} with something in them, to .or_raise as the 2nd param, I encounter a KeyError. I assume this is because something in or_raise/deeper in the call stack that uses .format on the string. Since the string contains curly braces which are interpreted as a placeholder, it tries to substitute something in there which is not available to the .format call.

This is not a major issue for me since I can change the error_message to not contain the curly braces but it would be great to have support for this within the library

Reproduction

>>> from ensure import check
>>> error_message = "mydata: {a:1, b:2}"
>>> check(1).is_greater_than(2).or_raise(Exception, error_message)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/ensure/main.py", line 742, in or_raise
    raise error_factory(message.format(*args, **kwargs))
KeyError: 'a'

Check type and value at the same time

Right now, when checking types with is_a(), the return value is None for a scalar value, and an IterableInspector for an iterable value. In neither case can I then go on to check the value of the subject.

For a scalar:

In [9]: ensure(9).is_an(int).which.equals(9)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-9-c205f4e9cc21> in <module>()
----> 1 ensure(9).is_an(int).which.equals(9)

AttributeError: 'NoneType' object has no attribute 'which'

For an iterable:

In [7]: ensure('foo').is_a(str).which.equals('foo')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-48f991a6e079> in <module>()
----> 1 ensure('foo').is_a(str).which.equals('foo')

AttributeError: 'IterableInspector' object has no attribute 'which'

The examples are trivial, but I find myself wanting to check the type and value frequently as a part of a chain. I'm used to being able to do this with a fluent assertion interface like Chai, and I'd love to see ensure reach that level of fluency.

A good start would be to never return None, returning the wrapped subject instead, and to add which and whose_value properties on all the inspectors which take you back to the wrapped subject again.

Support an is_sorted condition

It would be great if it was possible to do something like:

ensure([1, 2, 3]).is_sorted()
ensure([3, 2, 1]).is_sorted(key=lambda x: -x)

has_key() triggers linters

has_key triggers linters which are guarding against the usage of dict.has_key(), a deprecated interface.

Would it be possible to get a synonym for has_key() which also returns a KeyInspector? If all I care about is the present of the key, I can use contains(), but that returns a ChainInspector, and sometimes I'd like to check the value of the key too without the linter yelling at me. :)

Fails with default arguments

On Debian with Python 3.4.1rc1 and ensure (0.1.8) from pip.

@ensure_annotations
def food(a: str, b: str='earth'):
print("You said %s and %s" % (a, b))

food('hello')

It fails with:
File "/usr/local/lib/python3.4/dist-packages/ensure/init.py", line 633, in wrapper
value = args[arg_pos[arg]]

_pickle.PicklingError: Can't pickle X: it's not the same object as __main__.X

This sample code causes pickle to throw an error with multiprocessing. It doesn't matter how many processes. It's due to this.

from os import getpid
from multiprocessing import Pool
from ensure import ensure_annotations


@ensure_annotations
def test_func(arg):
    print(f'Hello {arg} from {getpid()}!')


if __name__ == '__main__':
    with Pool(processes=1) as pool:
        pool.map(test_func, ['map'])

Output:

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    pool.map(test_func, ['map'])
  File "/usr/lib/python3.7/multiprocessing/pool.py", line 268, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
  File "/usr/lib/python3.7/multiprocessing/pool.py", line 657, in get
    raise self._value
  File "/usr/lib/python3.7/multiprocessing/pool.py", line 431, in _handle_tasks
    put(task)
  File "/usr/lib/python3.7/multiprocessing/connection.py", line 206, in send
    self._send_bytes(_ForkingPickler.dumps(obj))
  File "/usr/lib/python3.7/multiprocessing/reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <function test_func at 0x7fd8df8101e0>: it's not the same object as __main__.test_func

Conditionally allow None

Often when instantiating classes with kwargs, you want to conditionally validate a parameter's type if it isn't None, e.g.

class SomeClass(object):
    def __init__(self, a=None):
        # assert `a` is an int if it isn't None

It'd be nice to facilitate straightforward expression of this with ensure. Maybe something like

check(a).is_none_or_an(int)

or maybe

check(a).is_an(int, allow_none=True)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.