Merge pull request #46 from hartym/17_positional_options
Positional options (#17)
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
[run]
|
[run]
|
||||||
branch = True
|
branch = True
|
||||||
omit =
|
omit =
|
||||||
bonobo/examples/*
|
bonobo/examples/**
|
||||||
bonobo/ext/*
|
bonobo/ext/**
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# bonobo (see github.com/python-edgy/project)
|
# bonobo (see github.com/python-edgy/project)
|
||||||
|
|
||||||
name = 'bonobo'
|
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'
|
license = 'Apache License, Version 2.0'
|
||||||
|
|
||||||
url = 'https://www.bonobo-project.org/'
|
url = 'https://www.bonobo-project.org/'
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
from bonobo.config.options import Option
|
from bonobo.config.options import Option
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Configurable',
|
||||||
|
'Option',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableMeta(type):
|
class ConfigurableMeta(type):
|
||||||
"""
|
"""
|
||||||
@ -9,6 +14,8 @@ class ConfigurableMeta(type):
|
|||||||
def __init__(cls, what, bases=None, dict=None):
|
def __init__(cls, what, bases=None, dict=None):
|
||||||
super().__init__(what, bases, dict)
|
super().__init__(what, bases, dict)
|
||||||
cls.__options__ = {}
|
cls.__options__ = {}
|
||||||
|
cls.__positional_options__ = []
|
||||||
|
|
||||||
for typ in cls.__mro__:
|
for typ in cls.__mro__:
|
||||||
for name, value in typ.__dict__.items():
|
for name, value in typ.__dict__.items():
|
||||||
if isinstance(value, Option):
|
if isinstance(value, Option):
|
||||||
@ -16,6 +23,8 @@ class ConfigurableMeta(type):
|
|||||||
value.name = name
|
value.name = name
|
||||||
if not name in cls.__options__:
|
if not name in cls.__options__:
|
||||||
cls.__options__[name] = value
|
cls.__options__[name] = value
|
||||||
|
if value.positional:
|
||||||
|
cls.__positional_options__.append(name)
|
||||||
|
|
||||||
|
|
||||||
class Configurable(metaclass=ConfigurableMeta):
|
class Configurable(metaclass=ConfigurableMeta):
|
||||||
@ -25,16 +34,27 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
# initialize option's value dictionary, used by descriptor implementation (see Option).
|
||||||
self.__options_values__ = {}
|
self.__options_values__ = {}
|
||||||
|
|
||||||
|
# compute missing options, given the kwargs.
|
||||||
missing = set()
|
missing = set()
|
||||||
for name, option in type(self).__options__.items():
|
for name, option in type(self).__options__.items():
|
||||||
if option.required and not option.name in kwargs:
|
if option.required and not option.name in kwargs:
|
||||||
missing.add(name)
|
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):
|
if len(missing):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'{}() missing {} required option{}: {}.'.format(
|
'{}() 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())
|
extraneous = set(kwargs.keys()) - set(type(self).__options__.keys())
|
||||||
if len(extraneous):
|
if len(extraneous):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -52,5 +73,6 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# set option values.
|
||||||
for name, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
|
|||||||
@ -5,10 +5,11 @@ class Option:
|
|||||||
"""
|
"""
|
||||||
_creation_counter = 0
|
_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.name = None
|
||||||
self.type = type
|
self.type = type
|
||||||
self.required = required
|
self.required = required
|
||||||
|
self.positional = positional
|
||||||
self.default = default
|
self.default = default
|
||||||
|
|
||||||
# This hack is necessary for python3.5
|
# This hack is necessary for python3.5
|
||||||
@ -24,4 +25,4 @@ class Option:
|
|||||||
return inst.__options_values__[self.name]
|
return inst.__options_values__[self.name]
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
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
|
from bonobo.commands.run import get_default_services
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.CsvReader(path='datasets/coffeeshops.txt'),
|
bonobo.CsvReader('datasets/coffeeshops.txt'),
|
||||||
print,
|
print,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ def get_fields(row):
|
|||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.JsonReader(path='datasets/theaters.json'),
|
bonobo.JsonReader('datasets/theaters.json'),
|
||||||
get_fields,
|
get_fields,
|
||||||
bonobo.PrettyPrint(title_keys=('eq_nom_equipement', )),
|
bonobo.PrettyPrint(title_keys=('eq_nom_equipement', )),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ def skip_comments(line):
|
|||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.FileReader(path='datasets/passwd.txt'),
|
bonobo.FileReader('datasets/passwd.txt'),
|
||||||
skip_comments,
|
skip_comments,
|
||||||
lambda s: s.split(':'),
|
lambda s: s.split(':'),
|
||||||
lambda l: l[0],
|
lambda l: l[0],
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import bonobo
|
import bonobo
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.FileReader(path='coffeeshops.txt'),
|
bonobo.FileReader('coffeeshops.txt'),
|
||||||
print,
|
print,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,9 @@ def split_one(line):
|
|||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.FileReader(path='coffeeshops.txt'),
|
bonobo.FileReader('coffeeshops.txt'),
|
||||||
split_one,
|
split_one,
|
||||||
bonobo.JsonWriter(path='coffeeshops.json'),
|
bonobo.JsonWriter('coffeeshops.json'),
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -16,9 +16,9 @@ class MyJsonWriter(bonobo.JsonWriter):
|
|||||||
|
|
||||||
|
|
||||||
graph = bonobo.Graph(
|
graph = bonobo.Graph(
|
||||||
bonobo.FileReader(path='coffeeshops.txt'),
|
bonobo.FileReader('coffeeshops.txt'),
|
||||||
split_one_to_map,
|
split_one_to_map,
|
||||||
MyJsonWriter(path='coffeeshops.json'),
|
MyJsonWriter('coffeeshops.json'),
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class FileHandler(Configurable):
|
|||||||
fs (str): service name to use for filesystem.
|
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
|
eol = Option(str, default='\n') # type: str
|
||||||
mode = Option(str) # type: str
|
mode = Option(str) # type: str
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class JsonHandler:
|
class JsonHandler():
|
||||||
eol = ',\n'
|
eol = ',\n'
|
||||||
prefix, suffix = '[', ']'
|
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
|
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
|
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.
|
value. Let's see later.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, value, *, type=None):
|
def __init__(self, value, *, type=None):
|
||||||
self.value = value
|
self.value = value
|
||||||
self.type = type
|
self.type = type
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.value)
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.value < other
|
return self.value < other
|
||||||
|
|
||||||
@ -56,6 +60,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __iadd__(self, other):
|
def __iadd__(self, other):
|
||||||
self.value += other
|
self.value += other
|
||||||
|
return self
|
||||||
|
|
||||||
def __sub__(self, other):
|
def __sub__(self, other):
|
||||||
return self.value - other
|
return self.value - other
|
||||||
@ -65,6 +70,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __isub__(self, other):
|
def __isub__(self, other):
|
||||||
self.value -= other
|
self.value -= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __mul__(self, other):
|
def __mul__(self, other):
|
||||||
return self.value * other
|
return self.value * other
|
||||||
@ -74,6 +80,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __imul__(self, other):
|
def __imul__(self, other):
|
||||||
self.value *= other
|
self.value *= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __matmul__(self, other):
|
def __matmul__(self, other):
|
||||||
return self.value @ other
|
return self.value @ other
|
||||||
@ -83,6 +90,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __imatmul__(self, other):
|
def __imatmul__(self, other):
|
||||||
self.value @= other
|
self.value @= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __truediv__(self, other):
|
def __truediv__(self, other):
|
||||||
return self.value / other
|
return self.value / other
|
||||||
@ -92,6 +100,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __itruediv__(self, other):
|
def __itruediv__(self, other):
|
||||||
self.value /= other
|
self.value /= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __floordiv__(self, other):
|
def __floordiv__(self, other):
|
||||||
return self.value // other
|
return self.value // other
|
||||||
@ -101,6 +110,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __ifloordiv__(self, other):
|
def __ifloordiv__(self, other):
|
||||||
self.value //= other
|
self.value //= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __mod__(self, other):
|
def __mod__(self, other):
|
||||||
return self.value % other
|
return self.value % other
|
||||||
@ -110,6 +120,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __imod__(self, other):
|
def __imod__(self, other):
|
||||||
self.value %= other
|
self.value %= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __divmod__(self, other):
|
def __divmod__(self, other):
|
||||||
return divmod(self.value, other)
|
return divmod(self.value, other)
|
||||||
@ -125,6 +136,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __ipow__(self, other):
|
def __ipow__(self, other):
|
||||||
self.value **= other
|
self.value **= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __lshift__(self, other):
|
def __lshift__(self, other):
|
||||||
return self.value << other
|
return self.value << other
|
||||||
@ -134,6 +146,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __ilshift__(self, other):
|
def __ilshift__(self, other):
|
||||||
self.value <<= other
|
self.value <<= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __rshift__(self, other):
|
def __rshift__(self, other):
|
||||||
return self.value >> other
|
return self.value >> other
|
||||||
@ -143,6 +156,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __irshift__(self, other):
|
def __irshift__(self, other):
|
||||||
self.value >>= other
|
self.value >>= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
return self.value & other
|
return self.value & other
|
||||||
@ -152,6 +166,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __iand__(self, other):
|
def __iand__(self, other):
|
||||||
self.value &= other
|
self.value &= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __xor__(self, other):
|
def __xor__(self, other):
|
||||||
return self.value ^ other
|
return self.value ^ other
|
||||||
@ -161,6 +176,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __ixor__(self, other):
|
def __ixor__(self, other):
|
||||||
self.value ^= other
|
self.value ^= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return self.value | other
|
return self.value | other
|
||||||
@ -170,6 +186,7 @@ class ValueHolder:
|
|||||||
|
|
||||||
def __ior__(self, other):
|
def __ior__(self, other):
|
||||||
self.value |= other
|
self.value |= other
|
||||||
|
return self
|
||||||
|
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return -self.value
|
return -self.value
|
||||||
|
|||||||
@ -19,6 +19,12 @@ class MyBetterConfigurable(MyConfigurable):
|
|||||||
required_str = Option(str, required=False, default='kaboom')
|
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():
|
class PrinterInterface():
|
||||||
def print(self, *args):
|
def print(self, *args):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -133,3 +139,7 @@ def test_service_dependency_unavailable():
|
|||||||
o = MyServiceDependantConfigurable(printer='printer2')
|
o = MyServiceDependantConfigurable(printer='printer2')
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
SERVICES.args_for(o)
|
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