Merge pull request #46 from hartym/17_positional_options
Positional options (#17)
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
[run]
|
||||
branch = True
|
||||
omit =
|
||||
bonobo/examples/*
|
||||
bonobo/ext/*
|
||||
bonobo/examples/**
|
||||
bonobo/ext/**
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
||||
@ -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/'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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', )),
|
||||
)
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import bonobo
|
||||
|
||||
graph = bonobo.Graph(
|
||||
bonobo.FileReader(path='coffeeshops.txt'),
|
||||
bonobo.FileReader('coffeeshops.txt'),
|
||||
print,
|
||||
)
|
||||
|
||||
|
||||
@ -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__':
|
||||
|
||||
@ -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__':
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class JsonHandler:
|
||||
class JsonHandler():
|
||||
eol = ',\n'
|
||||
prefix, suffix = '[', ']'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
53
tests/util/test_objects.py
Normal file
53
tests/util/test_objects.py
Normal 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)
|
||||
Reference in New Issue
Block a user