-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from magniff/feature/rework_tests
Feature/rework tests
- Loading branch information
Showing
12 changed files
with
673 additions
and
330 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[run] | ||
branch = True | ||
source = . | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,19 @@ | ||
language: python | ||
|
||
python: | ||
- "3.3" | ||
- "3.4" | ||
- "3.5" | ||
- 3.3 | ||
- 3.4 | ||
- 3.5 | ||
- 3.6 | ||
- pypy3 | ||
|
||
install: | ||
- pip install codecov | ||
- pip install pytest-cov | ||
- python setup.py develop | ||
script: py.test | ||
after_success: coveralls | ||
|
||
script: py.test --cov=. tests/ | ||
|
||
after_success: | ||
- codecov | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,67 @@ | ||
# Watch 👁 | ||
# Watch ๏_๏ | ||
|
||
[![Build Status](https://api.travis-ci.org/magniff/watch.svg?branch=master)](https://travis-ci.org/magniff/watch) | ||
[![codecov](https://codecov.io/gh/magniff/watch/branch/master/graph/badge.svg)](https://codecov.io/gh/magniff/watch) | ||
|
||
This very basic library I found myself reimplementing over and over again in different projects, so I finaly decided to put an end to such thankless monkey job, duuuuh. Long story short, this piece of code represents a tiny framework aimed to build object's attributes validators. | ||
This very basic library I found myself reimplementing over and over again for different projects, so I finaly decided to put an end to such thankless monkey job, duuuuh. Long story short, this piece of code represents a tiny framework aimed to build object's attributes validators. | ||
|
||
### Motivation | ||
The main goal of that library is to get rid of code like this one: | ||
The main goal of that library is to get rid of pesky validation code: | ||
```python | ||
class MyClass: | ||
def __init__(self, foo, bar): | ||
assert isinstance(foo, (tuple, list)) and all(isinstance(item, int) for item in foo) | ||
assert isinstance(bar, str) | ||
self.this_should_list_of_ints = foo | ||
self.and_this_should_be_string = bar | ||
``` | ||
Usually you heavily rely on types or values of your attributes. | ||
Note, that you should perform these assertions each time you set attributes foo and bar in order to keep your state consistent. | ||
From my point of view it would be way claner to have the validation expressed like this (pseudocode): | ||
```python | ||
class MyClass: | ||
foo = List(Int) | ||
bar = String | ||
from collections.abc import Mapping | ||
|
||
def __init__(self, foo, bar): | ||
self.foo = foo | ||
self.bar = bar | ||
class MyClass: | ||
def __init__(self, mappings): | ||
assert isinstance(mappings, list) | ||
for mapping in mappings: | ||
assert isinstance(mapping, Mapping) | ||
for key, value in mapping.items(): | ||
assert isinstance(key, int) | ||
assert isinstance(value, str) | ||
self.mappings = mappings | ||
``` | ||
If that makes sense to you, have a look on `Watch` library. Here is a little example: | ||
Note, that you should perform these assertions each time you set `mappings` attribute to keep your state consistent. | ||
`watch` provides a much cleaner way to define an attribute validator: | ||
```python | ||
import watch | ||
from watch.builtins import Container, InstanceOf | ||
|
||
class MyClass(watch.WatchMe): | ||
foo = watch.ArrayOf(watch.builtins.InstanceOf(int)) | ||
mappings = Container(InstanceOf(int) >> InstanceOf(str), container=list) | ||
|
||
instance = MyClass() | ||
instance.foo = [10, 20] # allowed | ||
instance.foo = "sup" # will rise AttributeError | ||
def __init__(self, mappings): | ||
self.mappings = mappings | ||
``` | ||
henceforth attribute `foo` of `MyClass` objects owned by `ArrayOf` descriptor. If value doesnt meet requirements of controller, then `complain(self, field_name, value)` method of `MyClass` takes control, by default there is an implementation located in `WatchMe` base class, that simply raises `AttribureError`. | ||
Here `Container` invocation defines a validator for surrounding `list` object and `>>` constructs a validator for a dict like object, that maps ints to strings. Looks straightforward enough, right? | ||
|
||
### Installation | ||
You can clone this repo and perform installation by running `setup.py` script. This code also available in `pypi` by name `watch`, so to get it from there just run `pip install watch`. | ||
If that makes sense to you, have a look on `watch` library. | ||
|
||
### Main validators | ||
Each validator is represented as a class extending base `PredicateController` type, which main method `predicate` get recursively invoked through nested data. Currently the most expressive validators are following. | ||
```python | ||
from watch import Pred, ArrayOf, MappingOf, SomeOf, CombineFrom | ||
``` | ||
lets have a look on them realy fast: | ||
* `Pred` defines a simple function-based validator | ||
```python | ||
class MyClass(WatchMe): | ||
foo = Pred(lambda value: isinstance(value, int) and value > 5) | ||
``` | ||
* `ArrayOf` allows to set a tuple or list of items, that pass some additonal validation | ||
```python | ||
Integer = Pred(lambda item: isinstance(item, int)) | ||
class MyClass(WatchMe): | ||
foo = ArrayOf(Integer) | ||
tar = ArrayOf(ArrayOf(Integer)) | ||
``` | ||
* `MappingOf` allows to set an object that has some notion of `items()` | ||
```python | ||
class MyClass(WatchMe): | ||
# some mapping, which keys allowed to be palindromic strings; valid values are lists | ||
# of even numbers | ||
foo = MappingOf( | ||
keys_type=Pred(lambda item: isinstance(item, str) and item == item[::-1])), | ||
values_type=ArrayOf(Pred(lambda item: isinstance(item, int) and not item % 2)) | ||
) | ||
``` | ||
|
||
* `SomeOf` basicaly represents `or` operator for validators | ||
```python | ||
class MyClass(WatchMe): | ||
foo = SomeOf(ArrayOf(Integer), Pred(...)) | ||
``` | ||
* `CombineFrom` just a sequential validation, it takes value and validates it against Validator0 -> ... -> ValidatorN, and only if every single one is happy about the value validation considered to be complete | ||
```python | ||
String = Pred(lambda item: isinstance(item, str)) | ||
class MyClass(WatchMe): | ||
# only palindromic strings are allowed | ||
foo = CombineFrom(String, Pred(lambda string: string == string[::-1])) | ||
``` | ||
Note that all of them and each validator, presenting in `watch.builtins` are self-validate, thus you can't construct `watch.builtins.InstanceOf` with non-class. | ||
### Installation | ||
You are very welcome to clone this repo and perform installation by running `setup.py` script. This code also available in `pypi` and goes by name `watch`, so to get it from there just run `pip install watch`. | ||
|
||
### Secondary validators | ||
Find more stuff in `watch.builtins`. | ||
### Validators | ||
Actual list of available validators being significantly reworked for a recent release, so stay tuned for this section. | ||
|
||
### Limitations | ||
Note, that the actual validation is based on `__set__` method of attribute descriptor object (see descriptor protocol documentation on python.org web site). Having that said it should be rather clear, that validation of mutable data is (in general) impossible. Condsider following example: | ||
```python | ||
from watch import WatchMe | ||
from watch.builtins import Container, InstanceOf | ||
|
||
class CouldNotBreak(watch.WatchMe): | ||
# only lists or tuples of ints are allowed, right? | ||
attribute = watch.ArrayOf(watch.builtins.InstanceOf(int)) | ||
# only iterables of ints are allowed, right? | ||
attribute = Container(InstanceOf(int)) | ||
|
||
instance = CouldNotBreak() | ||
|
||
# that works, as expected | ||
instance.attribute = [1,2,3] | ||
|
||
# `Watch` is kind of OK with following | ||
# `watch` is kind of OK with following | ||
instance.attribute.append('hello world') | ||
``` | ||
Sure you coud revalidate attribute by simply reseting it, like: | ||
Sure you could revalidate attribute by simply reseting it, just like: | ||
```python | ||
instance.attribute = instance.attribute | ||
``` | ||
But this looks weird indeed. | ||
|
||
|
||
### How to create a custom validator | ||
Even though you can build rather reach validators using only stuff described above, you are welcome to create your own one. The base class of each validator is `watch.PredicateController`, that has method `predicate(value)`, that should return `True` if value fits to object and `False` otherwise. The following example demonstrates how to build validator, that checks whether this value been set earlier: | ||
```python | ||
class Unique(watch.PredicateController): | ||
def __init__(self): | ||
self.already_seen = set() | ||
|
||
def predicate(self, value): | ||
if value in self.already_seen: | ||
return False | ||
|
||
self.already_seen.add(value) | ||
return True | ||
``` | ||
thus | ||
```python | ||
class MyAwesomeClass(watch.WatchMe): | ||
foo = Unique # yes, you dont really need to instantiate your validators | ||
|
||
awesomness = MyAwesomeClass() | ||
>>> awesomness.foo = 1 | ||
>>> # lets do it again, validator should catch this | ||
>>> awesomness.foo = 1 | ||
AttributeError: Cant set attribute 'foo' of object... | ||
``` | ||
|
||
### How to handle an attribute error | ||
You can customize validation failure handler by overriding `complain` method in your class, say: | ||
```python | ||
class MyClass(WatchMe): | ||
# only palindromic strings are allowed | ||
foo = CombineFrom(String, Pred(lambda string: string == string[::-1])) | ||
|
||
def complain(self, attr_name, value): | ||
print(attr_name, value) | ||
``` | ||
this will print attribute name and corresponding value on screen instead of raising error. | ||
|
||
But that looks weird indeed. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
[aliases] | ||
test=py.test | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.