Skip to content

Commit

Permalink
Merge pull request #3 from magniff/feature/rework_tests
Browse files Browse the repository at this point in the history
Feature/rework tests
  • Loading branch information
Aleksandr Koshkin committed Sep 27, 2018
2 parents 3f4d98d + 46992d9 commit a904aff
Show file tree
Hide file tree
Showing 12 changed files with 673 additions and 330 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
branch = True
source = .

20 changes: 15 additions & 5 deletions .travis.yml
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

153 changes: 36 additions & 117 deletions README.md
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.
7 changes: 4 additions & 3 deletions benchs/simple.py → benchmarks/simple.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import perf


from watch import WatchMe, ArrayOf, MappingOf
from watch.builtins import InstanceOf
from watch.builtins import InstanceOf, Container, Mapping
from watch import WatchMe


class MyClass(WatchMe):
foo = ArrayOf(MappingOf(InstanceOf(str), InstanceOf(int)))
foo = Container(InstanceOf(str) >> InstanceOf(int))


my_obj = MyClass()
Expand Down Expand Up @@ -36,3 +36,4 @@ def bench_get_missing():
runner.bench_func("__get__", bench_get)
runner.bench_func("__set__", bench_set)
runner.bench_func("__get__missing", bench_get_missing)

1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[aliases]
test=py.test

23 changes: 13 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@

classifiers = [
(
'Programming Language :: Python :: %s' % x
"Programming Language :: Python :: %s" % x
)
for x in '3.1 3.2 3.3 3.4 3.5 3.6 3.7'.split()
for x in "3.1 3.2 3.3 3.4 3.5 3.6 3.7".split()
]


setuptools.setup(
name='watch',
description='Attribute controlling microframework.',
version='0.1.15',
license='MIT license',
platforms=['unix', 'linux', 'osx', 'win32'],
author='magniff',
url='https://github.com/magniff/watch',
name="watch",
description="A stupid monadic fields tracker.",
version="0.2.0",
license="MIT license",
platforms=["unix", "linux", "osx", "win32"],
author="magniff",
url="https://github.com/magniff/watch",
classifiers=classifiers,
packages=['watch'],
packages=[
"watch",
],
zip_safe=False,
)

0 comments on commit a904aff

Please sign in to comment.