Merge pull request #46 from hartym/17_positional_options

Positional options (#17)
This commit is contained in:
Romain Dorgueil
2017-05-01 07:56:30 -07:00
committed by GitHub
15 changed files with 119 additions and 16 deletions

View File

@ -1,8 +1,8 @@
[run]
branch = True
omit =
bonobo/examples/*
bonobo/ext/*
bonobo/examples/**
bonobo/ext/**
[report]
# Regexes for lines to exclude from consideration

View File

@ -1,7 +1,7 @@
# bonobo (see github.com/python-edgy/project)
name = 'bonobo'
description = 'Bonobo'
description = 'Bonobo, a simple, modern and atomic extract-transform-load toolkit for python 3.5+.'
license = 'Apache License, Version 2.0'
url = 'https://www.bonobo-project.org/'

View File

@ -1,5 +1,10 @@
from bonobo.config.options import Option
__all__ = [
'Configurable',
'Option',
]
class ConfigurableMeta(type):
"""
@ -9,6 +14,8 @@ class ConfigurableMeta(type):
def __init__(cls, what, bases=None, dict=None):
super().__init__(what, bases, dict)
cls.__options__ = {}
cls.__positional_options__ = []
for typ in cls.__mro__:
for name, value in typ.__dict__.items():
if isinstance(value, Option):
@ -16,6 +23,8 @@ class ConfigurableMeta(type):
value.name = name
if not name in cls.__options__:
cls.__options__[name] = value
if value.positional:
cls.__positional_options__.append(name)
class Configurable(metaclass=ConfigurableMeta):
@ -25,16 +34,27 @@ class Configurable(metaclass=ConfigurableMeta):
"""
def __init__(self, **kwargs):
def __init__(self, *args, **kwargs):
super().__init__()
# initialize option's value dictionary, used by descriptor implementation (see Option).
self.__options_values__ = {}
# compute missing options, given the kwargs.
missing = set()
for name, option in type(self).__options__.items():
if option.required and not option.name in kwargs:
missing.add(name)
# transform positional arguments in keyword arguments if possible.
position = 0
for positional_option in self.__positional_options__:
if positional_option in missing:
kwargs[positional_option] = args[position]
position += 1
missing.remove(positional_option)
# complain if there are still missing options.
if len(missing):
raise TypeError(
'{}() missing {} required option{}: {}.'.format(
@ -43,6 +63,7 @@ class Configurable(metaclass=ConfigurableMeta):
)
)
# complain if there is more options than possible.
extraneous = set(kwargs.keys()) - set(type(self).__options__.keys())
if len(extraneous):
raise TypeError(
@ -52,5 +73,6 @@ class Configurable(metaclass=ConfigurableMeta):
)
)
# set option values.
for name, value in kwargs.items():
setattr(self, name, value)

View File

@ -5,10 +5,11 @@ class Option:
"""
_creation_counter = 0
def __init__(self, type=None, *, required=False, default=None):
def __init__(self, type=None, *, required=False, positional=False, default=None):
self.name = None
self.type = type
self.required = required
self.positional = positional
self.default = default
# This hack is necessary for python3.5
@ -24,4 +25,4 @@ class Option:
return inst.__options_values__[self.name]
def __set__(self, inst, value):
inst.__options_values__[self.name] = self.type(value) if self.type else value
inst.__options_values__[self.name] = self.type(value) if self.type else value

View File

@ -2,7 +2,7 @@ import bonobo
from bonobo.commands.run import get_default_services
graph = bonobo.Graph(
bonobo.CsvReader(path='datasets/coffeeshops.txt'),
bonobo.CsvReader('datasets/coffeeshops.txt'),
print,
)

View File

@ -7,7 +7,7 @@ def get_fields(row):
graph = bonobo.Graph(
bonobo.JsonReader(path='datasets/theaters.json'),
bonobo.JsonReader('datasets/theaters.json'),
get_fields,
bonobo.PrettyPrint(title_keys=('eq_nom_equipement', )),
)

View File

@ -8,7 +8,7 @@ def skip_comments(line):
graph = bonobo.Graph(
bonobo.FileReader(path='datasets/passwd.txt'),
bonobo.FileReader('datasets/passwd.txt'),
skip_comments,
lambda s: s.split(':'),
lambda l: l[0],

View File

@ -1,7 +1,7 @@
import bonobo
graph = bonobo.Graph(
bonobo.FileReader(path='coffeeshops.txt'),
bonobo.FileReader('coffeeshops.txt'),
print,
)

View File

@ -6,9 +6,9 @@ def split_one(line):
graph = bonobo.Graph(
bonobo.FileReader(path='coffeeshops.txt'),
bonobo.FileReader('coffeeshops.txt'),
split_one,
bonobo.JsonWriter(path='coffeeshops.json'),
bonobo.JsonWriter('coffeeshops.json'),
)
if __name__ == '__main__':

View File

@ -16,9 +16,9 @@ class MyJsonWriter(bonobo.JsonWriter):
graph = bonobo.Graph(
bonobo.FileReader(path='coffeeshops.txt'),
bonobo.FileReader('coffeeshops.txt'),
split_one_to_map,
MyJsonWriter(path='coffeeshops.json'),
MyJsonWriter('coffeeshops.json'),
)
if __name__ == '__main__':

View File

@ -20,7 +20,7 @@ class FileHandler(Configurable):
fs (str): service name to use for filesystem.
"""
path = Option(str, required=True) # type: str
path = Option(str, required=True, positional=True) # type: str
eol = Option(str, default='\n') # type: str
mode = Option(str) # type: str

View File

@ -8,7 +8,7 @@ __all__ = [
]
class JsonHandler:
class JsonHandler():
eol = ',\n'
prefix, suffix = '[', ']'

View File

@ -24,12 +24,16 @@ class ValueHolder:
For the sake of concistency, all operator methods have been implemented (see https://docs.python.org/3/reference/datamodel.html) or
at least all in a certain category, but it feels like a more correct method should exist, like with a getattr-something on the
value. Let's see later.
"""
def __init__(self, value, *, type=None):
self.value = value
self.type = type
def __repr__(self):
return repr(self.value)
def __lt__(self, other):
return self.value < other
@ -56,6 +60,7 @@ class ValueHolder:
def __iadd__(self, other):
self.value += other
return self
def __sub__(self, other):
return self.value - other
@ -65,6 +70,7 @@ class ValueHolder:
def __isub__(self, other):
self.value -= other
return self
def __mul__(self, other):
return self.value * other
@ -74,6 +80,7 @@ class ValueHolder:
def __imul__(self, other):
self.value *= other
return self
def __matmul__(self, other):
return self.value @ other
@ -83,6 +90,7 @@ class ValueHolder:
def __imatmul__(self, other):
self.value @= other
return self
def __truediv__(self, other):
return self.value / other
@ -92,6 +100,7 @@ class ValueHolder:
def __itruediv__(self, other):
self.value /= other
return self
def __floordiv__(self, other):
return self.value // other
@ -101,6 +110,7 @@ class ValueHolder:
def __ifloordiv__(self, other):
self.value //= other
return self
def __mod__(self, other):
return self.value % other
@ -110,6 +120,7 @@ class ValueHolder:
def __imod__(self, other):
self.value %= other
return self
def __divmod__(self, other):
return divmod(self.value, other)
@ -125,6 +136,7 @@ class ValueHolder:
def __ipow__(self, other):
self.value **= other
return self
def __lshift__(self, other):
return self.value << other
@ -134,6 +146,7 @@ class ValueHolder:
def __ilshift__(self, other):
self.value <<= other
return self
def __rshift__(self, other):
return self.value >> other
@ -143,6 +156,7 @@ class ValueHolder:
def __irshift__(self, other):
self.value >>= other
return self
def __and__(self, other):
return self.value & other
@ -152,6 +166,7 @@ class ValueHolder:
def __iand__(self, other):
self.value &= other
return self
def __xor__(self, other):
return self.value ^ other
@ -161,6 +176,7 @@ class ValueHolder:
def __ixor__(self, other):
self.value ^= other
return self
def __or__(self, other):
return self.value | other
@ -170,6 +186,7 @@ class ValueHolder:
def __ior__(self, other):
self.value |= other
return self
def __neg__(self):
return -self.value

View File

@ -19,6 +19,12 @@ class MyBetterConfigurable(MyConfigurable):
required_str = Option(str, required=False, default='kaboom')
class MyConfigurableUsingPositionalOptions(MyConfigurable):
first = Option(str, required=True, positional=True)
second = Option(str, required=True, positional=True)
third = Option(str, required=False, positional=True)
class PrinterInterface():
def print(self, *args):
raise NotImplementedError()
@ -133,3 +139,7 @@ def test_service_dependency_unavailable():
o = MyServiceDependantConfigurable(printer='printer2')
with pytest.raises(KeyError):
SERVICES.args_for(o)
def test_option_positional():
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')

View File

@ -0,0 +1,53 @@
from bonobo.util.objects import Wrapper, get_name, ValueHolder
class foo:
pass
class bar:
__name__ = 'baz'
def test_get_name():
assert get_name(42) == 'int'
assert get_name('eat at joe.') == 'str'
assert get_name(str) == 'str'
assert get_name(object) == 'object'
assert get_name(get_name) == 'get_name'
assert get_name(foo) == 'foo'
assert get_name(foo()) == 'foo'
assert get_name(bar) == 'bar'
assert get_name(bar()) == 'baz'
def test_wrapper_name():
assert get_name(Wrapper(42)) == 'int'
assert get_name(Wrapper('eat at joe.')) == 'str'
assert get_name(Wrapper(str)) == 'str'
assert get_name(Wrapper(object)) == 'object'
assert get_name(Wrapper(foo)) == 'foo'
assert get_name(Wrapper(foo())) == 'foo'
assert get_name(Wrapper(bar)) == 'bar'
assert get_name(Wrapper(bar())) == 'baz'
assert get_name(Wrapper(get_name)) == 'get_name'
def test_valueholder():
x = ValueHolder(42)
assert x == 42
x += 1
assert x == 43
assert x + 1 == 44
assert x == 43
y = ValueHolder(44)
assert y == 44
y -= 1
assert y == 43
assert y - 1 == 42
assert y == 43
assert y == x
assert y is not x
assert repr(x) == repr(y) == repr(43)