feat: new alternate syntax and switch to black + isort (yeah, maybe not the best time, but that is done).

This commit is contained in:
Romain Dorgueil
2018-07-29 18:21:56 +01:00
parent 3094e43f9f
commit 89dda0dca6
123 changed files with 1672 additions and 1640 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
from bonobo.commands import entrypoint
if __name__ == '__main__':
if __name__ == "__main__":
entrypoint()

View File

@ -45,14 +45,17 @@ def run(graph, *, plugins=None, services=None, strategy=None):
plugins = plugins or []
from bonobo import settings
settings.check()
if not settings.QUIET.get(): # pragma: no cover
if _is_interactive_console():
import mondrian
mondrian.setup(excepthook=True)
from bonobo.plugins.console import ConsoleOutputPlugin
if ConsoleOutputPlugin not in plugins:
plugins.append(ConsoleOutputPlugin)
@ -61,20 +64,23 @@ def run(graph, *, plugins=None, services=None, strategy=None):
from bonobo.contrib.jupyter import JupyterOutputPlugin
except ImportError:
import logging
logging.warning(
'Failed to load jupyter widget. Easiest way is to install the optional "jupyter" '
'dependencies with «pip install bonobo[jupyter]», but you can also install a specific '
'version by yourself.'
"dependencies with «pip install bonobo[jupyter]», but you can also install a specific "
"version by yourself."
)
else:
if JupyterOutputPlugin not in plugins:
plugins.append(JupyterOutputPlugin)
import logging
logging.getLogger().setLevel(settings.LOGGING_LEVEL.get())
strategy = create_strategy(strategy)
from bonobo.util.errors import sweeten_errors
with sweeten_errors():
return strategy.execute(graph, plugins=plugins, services=services)
@ -83,15 +89,15 @@ def _inspect_as_graph(graph):
return graph._repr_dot_()
_inspect_formats = {'graph': _inspect_as_graph}
_inspect_formats = {"graph": _inspect_as_graph}
@api.register_graph
def inspect(graph, *, plugins=None, services=None, strategy=None, format):
if not format in _inspect_formats:
raise NotImplementedError(
'Output format {} not implemented. Choices are: {}.'.format(
format, ', '.join(sorted(_inspect_formats.keys()))
"Output format {} not implemented. Choices are: {}.".format(
format, ", ".join(sorted(_inspect_formats.keys()))
)
)
print(_inspect_formats[format](graph))
@ -160,20 +166,18 @@ api.register_group(
)
# registry
api.register_group(
create_reader,
create_writer,
)
api.register_group(create_reader, create_writer)
def _is_interactive_console():
import sys
return sys.stdout.isatty()
def _is_jupyter_notebook():
try:
return get_ipython().__class__.__name__ == 'ZMQInteractiveShell'
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
except NameError:
return False
@ -182,7 +186,8 @@ def _is_jupyter_notebook():
def get_examples_path(*pathsegments):
import os
import pathlib
return str(pathlib.Path(os.path.dirname(__file__), 'examples', *pathsegments))
return str(pathlib.Path(os.path.dirname(__file__), "examples", *pathsegments))
@api.register

View File

@ -1 +1 @@
__version__ = '0.6.2'
__version__ = "0.6.2"

View File

@ -19,9 +19,9 @@ def entrypoint(args=None):
logger.setLevel(settings.LOGGING_LEVEL.get())
parser = argparse.ArgumentParser()
parser.add_argument('--debug', '-D', action='store_true')
parser.add_argument("--debug", "-D", action="store_true")
subparsers = parser.add_subparsers(dest='command')
subparsers = parser.add_subparsers(dest="command")
subparsers.required = True
commands = {}
@ -39,23 +39,24 @@ def entrypoint(args=None):
# old school, function based.
commands[ext.name] = ext.plugin(parser)
except Exception:
logger.exception('Error while loading command {}.'.format(ext.name))
logger.exception("Error while loading command {}.".format(ext.name))
from stevedore import ExtensionManager
mgr = ExtensionManager(namespace='bonobo.commands')
mgr = ExtensionManager(namespace="bonobo.commands")
mgr.map(register_extension)
parsed_args = parser.parse_args(args).__dict__
if parsed_args.pop('debug', False):
if parsed_args.pop("debug", False):
settings.DEBUG.set(True)
settings.LOGGING_LEVEL.set(logging.DEBUG)
logger.setLevel(settings.LOGGING_LEVEL.get())
logger.debug('Command: ' + parsed_args['command'] + ' Arguments: ' + repr(parsed_args))
logger.debug("Command: " + parsed_args["command"] + " Arguments: " + repr(parsed_args))
# Get command handler, execute, rince.
command = commands[parsed_args.pop('command')]
command = commands[parsed_args.pop("command")]
command(**parsed_args)
return 0

View File

@ -33,7 +33,7 @@ class BaseCommand:
"""
The actual logic of the command. Subclasses must implement this method.
"""
raise NotImplementedError('Subclasses of BaseCommand must provide a handle() method')
raise NotImplementedError("Subclasses of BaseCommand must provide a handle() method")
class BaseGraphCommand(BaseCommand):
@ -48,8 +48,8 @@ class BaseGraphCommand(BaseCommand):
def add_arguments(self, parser):
# target arguments (cannot provide both).
source_group = parser.add_mutually_exclusive_group(required=self.required)
source_group.add_argument('file', nargs='?', type=str)
source_group.add_argument('-m', dest='mod', type=str)
source_group.add_argument("file", nargs="?", type=str)
source_group.add_argument("-m", dest="mod", type=str)
# add arguments to enforce system environment.
parser = get_argument_parser(parser)
@ -66,7 +66,7 @@ class BaseGraphCommand(BaseCommand):
def do_handle(self, graph, **options):
if not self.handler:
raise RuntimeError('{} has no handler defined.'.format(get_name(self)))
raise RuntimeError("{} has no handler defined.".format(get_name(self)))
return self.handler(graph, **options)
@contextmanager
@ -87,20 +87,20 @@ class BaseGraphCommand(BaseCommand):
sys.argv = [mod]
self._run_module(mod)
else:
raise RuntimeError('No target provided.')
raise RuntimeError("No target provided.")
finally:
sys.argv = _argv
if _graph is None:
raise RuntimeError('Could not find graph.')
raise RuntimeError("Could not find graph.")
yield _graph, _graph_execution_options, options
def _run_path(self, file):
return runpy.run_path(file, run_name='__main__')
return runpy.run_path(file, run_name="__main__")
def _run_module(self, mod):
return runpy.run_module(mod, run_name='__main__')
return runpy.run_module(mod, run_name="__main__")
@contextmanager

View File

@ -6,82 +6,75 @@ from bonobo.util.resolvers import _resolve_options, _resolve_transformations
class ConvertCommand(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('input_filename', help='Input filename.')
parser.add_argument('output_filename', help='Output filename.')
parser.add_argument("input_filename", help="Input filename.")
parser.add_argument("output_filename", help="Output filename.")
parser.add_argument(
'--' + READER,
'-r',
help='Choose the reader factory if it cannot be detected from extension, or if detection is wrong.'
"--" + READER,
"-r",
help="Choose the reader factory if it cannot be detected from extension, or if detection is wrong.",
)
parser.add_argument(
'--' + WRITER,
'-w',
help=
'Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).'
"--" + WRITER,
"-w",
help="Choose the writer factory if it cannot be detected from extension, or if detection is wrong (use - for console pretty print).",
)
parser.add_argument("--limit", "-l", type=int, help="Adds a Limit() after the reader instance.", default=None)
parser.add_argument(
"--transformation",
"-t",
dest="transformation",
action="append",
help="Add a transformation between input and output (can be used multiple times, order is preserved).",
)
parser.add_argument(
'--limit',
'-l',
type=int,
help='Adds a Limit() after the reader instance.',
default=None,
)
parser.add_argument(
'--transformation',
'-t',
dest='transformation',
action='append',
help='Add a transformation between input and output (can be used multiple times, order is preserved).',
)
parser.add_argument(
'--option',
'-O',
dest='option',
action='append',
"--option",
"-O",
dest="option",
action="append",
help='Add a named option to both reader and writer factories (i.e. foo="bar").',
)
parser.add_argument(
'--' + READER + '-option',
'-' + READER[0].upper(),
dest=READER + '_option',
action='append',
help='Add a named option to the reader factory.',
"--" + READER + "-option",
"-" + READER[0].upper(),
dest=READER + "_option",
action="append",
help="Add a named option to the reader factory.",
)
parser.add_argument(
'--' + WRITER + '-option',
'-' + WRITER[0].upper(),
dest=WRITER + '_option',
action='append',
help='Add a named option to the writer factory.',
"--" + WRITER + "-option",
"-" + WRITER[0].upper(),
dest=WRITER + "_option",
action="append",
help="Add a named option to the writer factory.",
)
def handle(
self,
input_filename,
output_filename,
reader=None,
reader_option=None,
writer=None,
writer_option=None,
option=None,
limit=None,
transformation=None,
self,
input_filename,
output_filename,
reader=None,
reader_option=None,
writer=None,
writer_option=None,
option=None,
limit=None,
transformation=None,
):
reader_factory = default_registry.get_reader_factory_for(input_filename, format=reader)
reader_kwargs = _resolve_options((option or []) + (reader_option or []))
if output_filename == '-':
if output_filename == "-":
writer_factory = bonobo.PrettyPrinter
writer_args = ()
else:
writer_factory = default_registry.get_writer_factory_for(output_filename, format=writer)
writer_args = (output_filename, )
writer_args = (output_filename,)
writer_kwargs = _resolve_options((option or []) + (writer_option or []))
transformations = ()
if limit:
transformations += (bonobo.Limit(limit), )
transformations += (bonobo.Limit(limit),)
transformations += _resolve_transformations(transformation)
@ -92,8 +85,4 @@ class ConvertCommand(BaseCommand):
writer_factory(*writer_args, **writer_kwargs),
)
return bonobo.run(
graph, services={
'fs': bonobo.open_fs(),
}
)
return bonobo.run(graph, services={"fs": bonobo.open_fs()})

View File

@ -6,28 +6,28 @@ import requests
import bonobo
from bonobo.commands import BaseCommand
EXAMPLES_BASE_URL = 'https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/'
EXAMPLES_BASE_URL = "https://raw.githubusercontent.com/python-bonobo/bonobo/master/bonobo/examples/"
"""The URL to our git repository, in raw mode."""
class DownloadCommand(BaseCommand):
def handle(self, *, path, **options):
if not path.startswith('examples'):
raise ValueError('Download command currently supports examples only')
examples_path = re.sub('^examples/', '', path)
if not path.startswith("examples"):
raise ValueError("Download command currently supports examples only")
examples_path = re.sub("^examples/", "", path)
output_path = bonobo.get_examples_path(examples_path)
with _open_url(EXAMPLES_BASE_URL + examples_path) as response, open(output_path, 'wb') as fout:
with _open_url(EXAMPLES_BASE_URL + examples_path) as response, open(output_path, "wb") as fout:
for chunk in response.iter_content(io.DEFAULT_BUFFER_SIZE):
fout.write(chunk)
self.logger.info('Download saved to {}'.format(output_path))
self.logger.info("Download saved to {}".format(output_path))
def add_arguments(self, parser):
parser.add_argument('path', help='The relative path of the thing to download.')
parser.add_argument("path", help="The relative path of the thing to download.")
def _open_url(url):
"""Open a HTTP connection to the URL and return a file-like object."""
response = requests.get(url, stream=True)
if response.status_code != 200:
raise IOError('Unable to download {}, HTTP {}'.format(url, response.status_code))
raise IOError("Unable to download {}, HTTP {}".format(url, response.status_code))
return response

View File

@ -1,23 +1,23 @@
from bonobo.commands import BaseCommand
all_examples = (
'clock',
'datasets',
'environ',
'files.csv_handlers',
'files.json_handlers',
'files.pickle_handlers',
'files.text_handlers',
'types',
"clock",
"datasets",
"environ",
"files.csv_handlers",
"files.json_handlers",
"files.pickle_handlers",
"files.text_handlers",
"types",
)
class ExamplesCommand(BaseCommand):
def handle(self):
print('You can run the following examples:')
print("You can run the following examples:")
print()
for example in all_examples:
print(' $ python -m bonobo.examples.{}'.format(example))
print(" $ python -m bonobo.examples.{}".format(example))
print()
def add_arguments(self, parser):

View File

@ -6,67 +6,67 @@ from bonobo.commands import BaseCommand
class InitCommand(BaseCommand):
TEMPLATES = {'bare', 'default'}
TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates')
TEMPLATES = {"bare", "default"}
TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), "templates")
def add_arguments(self, parser):
parser.add_argument('filename')
parser.add_argument('--force', '-f', default=False, action='store_true')
parser.add_argument("filename")
parser.add_argument("--force", "-f", default=False, action="store_true")
target_group = parser.add_mutually_exclusive_group(required=False)
target_group.add_argument('--template', '-t', choices=self.TEMPLATES, default='default')
target_group.add_argument('--package', '-p', action='store_true', default=False)
target_group.add_argument("--template", "-t", choices=self.TEMPLATES, default="default")
target_group.add_argument("--package", "-p", action="store_true", default=False)
def create_file_from_template(self, *, template, filename):
template_name = template
name, ext = os.path.splitext(filename)
if ext != '.py':
if ext != ".py":
raise ValueError('Filenames should end with ".py".')
loader = FileSystemLoader(self.TEMPLATES_PATH)
env = Environment(loader=loader)
template = env.get_template(template_name + '.py-tpl')
template = env.get_template(template_name + ".py-tpl")
with open(filename, 'w+') as f:
with open(filename, "w+") as f:
f.write(template.render(name=name))
self.logger.info('Generated {} using template {!r}.'.format(filename, template_name))
self.logger.info("Generated {} using template {!r}.".format(filename, template_name))
def create_package(self, *, filename):
name, ext = os.path.splitext(filename)
if ext != '':
raise ValueError('Package names should not have an extension.')
if ext != "":
raise ValueError("Package names should not have an extension.")
try:
import medikit.commands
except ImportError as exc:
raise ImportError(
'To initialize a package, you need to install medikit (pip install --upgrade medikit).'
"To initialize a package, you need to install medikit (pip install --upgrade medikit)."
) from exc
package_name = os.path.basename(filename)
medikit.commands.handle_init(
os.path.join(os.getcwd(), filename, 'Projectfile'), name=package_name, requirements=['bonobo']
os.path.join(os.getcwd(), filename, "Projectfile"), name=package_name, requirements=["bonobo"]
)
self.logger.info('Generated "{}" package with medikit.'.format(package_name))
self.create_file_from_template(template='default', filename=os.path.join(filename, package_name, '__main__.py'))
self.create_file_from_template(template="default", filename=os.path.join(filename, package_name, "__main__.py"))
print('Your "{}" package has been created.'.format(package_name))
print()
print('Install it...')
print("Install it...")
print()
print(' pip install --editable {}'.format(filename))
print(" pip install --editable {}".format(filename))
print()
print('Then maybe run the example...')
print("Then maybe run the example...")
print()
print(' python -m {}'.format(package_name))
print(" python -m {}".format(package_name))
print()
print('Enjoy!')
print("Enjoy!")
def handle(self, *, template, filename, package=False, force=False):
if os.path.exists(filename) and not force:
raise FileExistsError('Target filename already exists, use --force to override.')
raise FileExistsError("Target filename already exists, use --force to override.")
if package:
self.create_package(filename=filename)

View File

@ -7,9 +7,9 @@ class InspectCommand(BaseGraphCommand):
def add_arguments(self, parser):
super(InspectCommand, self).add_arguments(parser)
parser.add_argument('--graph', '-g', dest='format', action='store_const', const='graph')
parser.add_argument("--graph", "-g", dest="format", action="store_const", const="graph")
def parse_options(self, **options):
if not options.get('format'):
raise RuntimeError('You must provide a format (try --graph).')
if not options.get("format"):
raise RuntimeError("You must provide a format (try --graph).")
return options

View File

@ -12,13 +12,14 @@ class RunCommand(BaseGraphCommand):
super(RunCommand, self).add_arguments(parser)
verbosity_group = parser.add_mutually_exclusive_group()
verbosity_group.add_argument('--quiet', '-q', action='store_true')
verbosity_group.add_argument('--verbose', '-v', action='store_true')
verbosity_group.add_argument("--quiet", "-q", action="store_true")
verbosity_group.add_argument("--verbose", "-v", action="store_true")
parser.add_argument('--install', '-I', action='store_true')
parser.add_argument("--install", "-I", action="store_true")
def parse_options(self, *, quiet=False, verbose=False, install=False, **options):
from bonobo import settings
settings.QUIET.set_if_true(quiet)
settings.DEBUG.set_if_true(verbose)
self.install = install
@ -28,9 +29,9 @@ class RunCommand(BaseGraphCommand):
# add install logic
if self.install:
if os.path.isdir(file):
requirements = os.path.join(file, 'requirements.txt')
requirements = os.path.join(file, "requirements.txt")
else:
requirements = os.path.join(os.path.dirname(file), 'requirements.txt')
requirements = os.path.join(os.path.dirname(file), "requirements.txt")
_install_requirements(requirements)
return super()._run_path(file)
@ -38,7 +39,7 @@ class RunCommand(BaseGraphCommand):
def _run_module(self, mod):
# install not implemented for a module, not sure it even make sense.
if self.install:
raise RuntimeError('--install behaviour when running a module is not defined.')
raise RuntimeError("--install behaviour when running a module is not defined.")
return super()._run_module(mod)
@ -59,10 +60,11 @@ def _install_requirements(requirements):
import importlib
import pip
pip.main(['install', '-r', requirements])
pip.main(["install", "-r", requirements])
# Some shenanigans to be sure everything is importable after this, especially .egg-link files which
# are referenced in *.pth files and apparently loaded by site.py at some magic bootstrap moment of the
# python interpreter.
pip.utils.pkg_resources = importlib.reload(pip.utils.pkg_resources)
import site
importlib.reload(site)

View File

@ -3,6 +3,7 @@ import bonobo
def get_graph(**options):
graph = bonobo.Graph()
graph.get_cursor() >> ...
return graph

View File

@ -27,7 +27,7 @@ def get_graph(**options):
"""
graph = bonobo.Graph()
graph.add_chain(extract, transform, load)
graph.get_cursor() >> extract >> transform >> load
return graph

View File

@ -9,15 +9,15 @@ def get_versions(*, all=False, quiet=None):
if all:
for name in sorted(bonobo_packages):
if name != 'bonobo':
if name != "bonobo":
try:
mod = __import__(name.replace('-', '_'))
mod = __import__(name.replace("-", "_"))
try:
yield _format_version(mod, name=name, quiet=quiet)
except Exception as exc:
yield '{} ({})'.format(name, exc)
yield "{} ({})".format(name, exc)
except ImportError as exc:
yield '{} is not importable ({}).'.format(name, exc)
yield "{} is not importable ({}).".format(name, exc)
class VersionCommand(BaseCommand):
@ -26,23 +26,24 @@ class VersionCommand(BaseCommand):
print(line)
def add_arguments(self, parser):
parser.add_argument('--all', '-a', action='store_true')
parser.add_argument('--quiet', '-q', action='count')
parser.add_argument("--all", "-a", action="store_true")
parser.add_argument("--quiet", "-q", action="count")
def _format_version(mod, *, name=None, quiet=False):
from bonobo.util.pkgs import bonobo_packages
args = {
'name': name or mod.__name__,
'version': mod.__version__,
'location': bonobo_packages[name or mod.__name__].location
"name": name or mod.__name__,
"version": mod.__version__,
"location": bonobo_packages[name or mod.__name__].location,
}
if not quiet:
return '{name} v.{version} (in {location})'.format(**args)
return "{name} v.{version} (in {location})".format(**args)
if quiet < 2:
return '{name} {version}'.format(**args)
return "{name} {version}".format(**args)
if quiet < 3:
return '{version}'.format(**args)
return "{version}".format(**args)
raise RuntimeError('Hard to be so quiet...')
raise RuntimeError("Hard to be so quiet...")

View File

@ -11,23 +11,23 @@ from bonobo.config.processors import ContextProcessor, use_context, use_context_
from bonobo.config.services import Container, Exclusive, Service, use, create_container
from bonobo.util import deprecated_alias
requires = deprecated_alias('requires', use)
requires = deprecated_alias("requires", use)
# Bonobo's Config API
__all__ = [
'Configurable',
'Container',
'ContextProcessor',
'Exclusive',
'Method',
'Option',
'Service',
'create_container',
'requires',
'transformation_factory',
'use',
'use_context',
'use_context_processor',
'use_no_input',
'use_raw_input',
"Configurable",
"Container",
"ContextProcessor",
"Exclusive",
"Method",
"Option",
"Service",
"create_container",
"requires",
"transformation_factory",
"use",
"use_context",
"use_context_processor",
"use_no_input",
"use_raw_input",
]

View File

@ -1,9 +1,7 @@
from bonobo.errors import AbstractError
from bonobo.util import get_name, iscontextprocessor, isoption, sortedlist
__all__ = [
'Configurable',
]
__all__ = ["Configurable"]
get_creation_counter = lambda v: v._creation_counter
@ -42,12 +40,12 @@ class ConfigurableMeta(type):
for _positional, _counter, _name, _value in cls.__options:
_param = _name
if _value.type:
_param = get_name(_value.type) + ' ' + _param
_param = get_name(_value.type) + " " + _param
prefix = ':param {}: '.format(_param)
for lineno, line in enumerate((_value.__doc__ or '').split('\n')):
_options_doc.append((' ' * len(prefix) if lineno else prefix) + line)
cls.__doc__ = '\n\n'.join(map(str.strip, filter(None, (cls.__doc__, '\n'.join(_options_doc)))))
prefix = ":param {}: ".format(_param)
for lineno, line in enumerate((_value.__doc__ or "").split("\n")):
_options_doc.append((" " * len(prefix) if lineno else prefix) + line)
cls.__doc__ = "\n\n".join(map(str.strip, filter(None, (cls.__doc__, "\n".join(_options_doc)))))
@property
def __options__(cls):
@ -64,10 +62,7 @@ class ConfigurableMeta(type):
return cls.__processors_cache
def __repr__(self):
return ' '.join((
'<Configurable',
super(ConfigurableMeta, self).__repr__().split(' ', 1)[1],
))
return " ".join(("<Configurable", super(ConfigurableMeta, self).__repr__().split(" ", 1)[1]))
try:
@ -154,9 +149,11 @@ class Configurable(metaclass=ConfigurableMeta):
extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set())
if len(extraneous):
raise TypeError(
'{}() got {} unexpected option{}: {}.'.format(
cls.__name__, len(extraneous), 's'
if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
"{}() got {} unexpected option{}: {}.".format(
cls.__name__,
len(extraneous),
"s" if len(extraneous) > 1 else "",
", ".join(map(repr, sorted(extraneous))),
)
)
@ -165,9 +162,11 @@ class Configurable(metaclass=ConfigurableMeta):
if len(missing):
if _final:
raise TypeError(
'{}() missing {} required option{}: {}.'.format(
cls.__name__, len(missing), 's'
if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
"{}() missing {} required option{}: {}.".format(
cls.__name__,
len(missing),
"s" if len(missing) > 1 else "",
", ".join(map(repr, sorted(missing))),
)
)
return PartiallyConfigured(cls, *args, **kwargs)
@ -196,7 +195,7 @@ class Configurable(metaclass=ConfigurableMeta):
break
if name in self._options_values:
raise ValueError('Already got a value for option {}'.format(name))
raise ValueError("Already got a value for option {}".format(name))
setattr(self, name, args[position])
position += 1

View File

@ -6,8 +6,8 @@ def transformation_factory(f):
@functools.wraps(f)
def _transformation_factory(*args, **kwargs):
retval = f(*args, **kwargs)
retval.__name__ = f.__name__ + '({})'.format(
', '.join(itertools.chain(map(repr, args), ('{}={!r}'.format(k, v) for k, v in kwargs.items())))
retval.__name__ = f.__name__ + "({})".format(
", ".join(itertools.chain(map(repr, args), ("{}={!r}".format(k, v) for k, v in kwargs.items())))
)
return retval

View File

@ -66,9 +66,9 @@ class Option:
# Docstring formating
self.__doc__ = __doc__ or None
if self.__doc__:
self.__doc__ = textwrap.dedent(self.__doc__.strip('\n')).strip()
self.__doc__ = textwrap.dedent(self.__doc__.strip("\n")).strip()
if default:
self.__doc__ += '\n\nDefault: {!r}'.format(default)
self.__doc__ += "\n\nDefault: {!r}".format(default)
# This hack is necessary for python3.5
self._creation_counter = Option._creation_counter
@ -88,13 +88,13 @@ class Option:
inst._options_values[self.name] = self.clean(value)
def __repr__(self):
return '<{positional}{typename} {type}{name} default={default!r}{required}>'.format(
return "<{positional}{typename} {type}{name} default={default!r}{required}>".format(
typename=type(self).__name__,
type='({})'.format(self.type) if istype(self.type) else '',
type="({})".format(self.type) if istype(self.type) else "",
name=self.name,
positional='*' if self.positional else '**',
positional="*" if self.positional else "**",
default=self.default,
required=' (required)' if self.required else '',
required=" (required)" if self.required else "",
)
def clean(self, value):
@ -106,15 +106,16 @@ class Option:
class RemovedOption(Option):
def __init__(self, *args, value, **kwargs):
kwargs['required'] = False
kwargs["required"] = False
super(RemovedOption, self).__init__(*args, **kwargs)
self.value = value
def clean(self, value):
if value != self.value:
raise ValueError(
'Removed options cannot change value, {!r} must now be {!r} (and you should remove setting the value explicitely, as it is deprecated and will be removed quite soon.'.
format(self.name, self.value)
"Removed options cannot change value, {!r} must now be {!r} (and you should remove setting the value explicitely, as it is deprecated and will be removed quite soon.".format(
self.name, self.value
)
)
return self.value
@ -129,12 +130,12 @@ class RenamedOption(Option):
def __get__(self, instance, owner):
raise ValueError(
'Trying to get value from renamed option {}, try getting {} instead.'.format(self.name, self.target)
"Trying to get value from renamed option {}, try getting {} instead.".format(self.name, self.target)
)
def clean(self, value):
raise ValueError(
'Trying to set value of renamed option {}, try setting {} instead.'.format(self.name, self.target)
"Trying to set value of renamed option {}, try setting {} instead.".format(self.name, self.target)
)
@ -182,7 +183,7 @@ class Method(Option):
# If a callable is provided as default, then use self as if it was used as a decorator
if default is not None:
if not callable(default):
raise ValueError('Method defaults should be callable, if provided.')
raise ValueError("Method defaults should be callable, if provided.")
self(default)
def __get__(self, inst, type_):
@ -194,17 +195,15 @@ class Method(Option):
def __set__(self, inst, value):
if not callable(value):
raise TypeError(
'Option {!r} ({}) is expecting a callable value, got {!r} object: {!r}.'.format(
self.name,
type(self).__name__,
type(value).__name__, value
"Option {!r} ({}) is expecting a callable value, got {!r} object: {!r}.".format(
self.name, type(self).__name__, type(value).__name__, value
)
)
inst._options_values[self.name] = self.type(value) if self.type else value
def __call__(self, impl):
if self.default:
raise RuntimeError('Can only be used once as a decorator.')
raise RuntimeError("Can only be used once as a decorator.")
self.default = impl
self.required = False
return self

View File

@ -53,7 +53,7 @@ class ContextProcessor(Option):
self.name = self.__name__
def __repr__(self):
return repr(self.func).replace('<function', '<{}'.format(type(self).__name__))
return repr(self.func).replace("<function", "<{}".format(type(self).__name__))
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
@ -74,7 +74,7 @@ class ContextCurrifier:
self.wrapped = wrapped
self.args = args
self.kwargs = kwargs
self.format = getattr(wrapped, '__input_format__', _args)
self.format = getattr(wrapped, "__input_format__", _args)
self._stack, self._stack_values = None, None
def __iter__(self):
@ -91,30 +91,32 @@ class ContextCurrifier:
return bind(*self.args, _input, **self.kwargs)
if self.format is _none:
return bind(*self.args, **self.kwargs)
raise NotImplementedError('Invalid format {!r}.'.format(self.format))
raise NotImplementedError("Invalid format {!r}.".format(self.format))
def __call__(self, _input):
if not callable(self.wrapped):
if isinstance(self.wrapped, Iterable):
return self.__iter__()
raise UnrecoverableTypeError('Uncallable node {}'.format(self.wrapped))
raise UnrecoverableTypeError("Uncallable node {}".format(self.wrapped))
try:
bound = self._bind(_input)
except TypeError as exc:
raise UnrecoverableTypeError((
'Input of {wrapped!r} does not bind to the node signature.\n'
'Args: {args}\n'
'Input: {input}\n'
'Kwargs: {kwargs}\n'
'Signature: {sig}'
).format(
wrapped=self.wrapped, args=self.args, input=_input, kwargs=self.kwargs, sig=signature(self.wrapped)
)) from exc
raise UnrecoverableTypeError(
(
"Input of {wrapped!r} does not bind to the node signature.\n"
"Args: {args}\n"
"Input: {input}\n"
"Kwargs: {kwargs}\n"
"Signature: {sig}"
).format(
wrapped=self.wrapped, args=self.args, input=_input, kwargs=self.kwargs, sig=signature(self.wrapped)
)
) from exc
return self.wrapped(*bound.args, **bound.kwargs)
def setup(self, *context):
if self._stack is not None:
raise RuntimeError('Cannot setup context currification twice.')
raise RuntimeError("Cannot setup context currification twice.")
self._stack, self._stack_values = list(), list()
for processor in resolve_processors(self.wrapped):
@ -136,7 +138,7 @@ class ContextCurrifier:
pass
else:
# No error ? We should have had StopIteration ...
raise RuntimeError('Context processors should not yield more than once.')
raise RuntimeError("Context processors should not yield more than once.")
self._stack, self._stack_values = None, None
@contextmanager
@ -164,7 +166,7 @@ def resolve_processors(mixed):
yield from ()
get_context_processors = deprecated_alias('get_context_processors', resolve_processors)
get_context_processors = deprecated_alias("get_context_processors", resolve_processors)
def use_context(f):
@ -192,11 +194,11 @@ def use_context_processor(context_processor):
def _use_input_format(input_format):
if input_format not in INPUT_FORMATS:
raise ValueError(
'Invalid input format {!r}. Choices: {}'.format(input_format, ', '.join(sorted(INPUT_FORMATS)))
"Invalid input format {!r}. Choices: {}".format(input_format, ", ".join(sorted(INPUT_FORMATS)))
)
def _set_input_format(f):
setattr(f, '__input_format__', input_format)
setattr(f, "__input_format__", input_format)
return f
return _set_input_format

View File

@ -11,7 +11,7 @@ _service_name_re = re.compile(r"^[^\d\W]\w*(:?\.[^\d\W]\w*)*$", re.UNICODE)
def validate_service_name(name):
if not _service_name_re.match(name):
raise ValueError('Invalid service name {!r}.'.format(name))
raise ValueError("Invalid service name {!r}.".format(name))
return name
@ -66,7 +66,7 @@ class Service(Option):
class Container(dict):
def __new__(cls, *args, **kwargs):
if len(args) == 1:
assert not len(kwargs), 'only one usage at a time, my dear.'
assert not len(kwargs), "only one usage at a time, my dear."
if not (args[0]):
return super().__new__(cls)
if isinstance(args[0], cls):
@ -86,7 +86,7 @@ class Container(dict):
if default:
return default
raise MissingServiceImplementationError(
'Cannot resolve service {!r} using provided service collection.'.format(name)
"Cannot resolve service {!r} using provided service collection.".format(name)
)
value = super().get(name)
# XXX this is not documented and can lead to errors.
@ -108,13 +108,15 @@ def create_container(services=None, factory=Container):
"""
container = factory(services) if services else factory()
if not 'fs' in container:
if not "fs" in container:
import bonobo
container.setdefault('fs', bonobo.open_fs())
if not 'http' in container:
container.setdefault("fs", bonobo.open_fs())
if not "http" in container:
import requests
container.setdefault('http', requests)
container.setdefault("http", requests)
return container
@ -137,6 +139,7 @@ class Exclusive(ContextDecorator):
ensure that.
"""
_locks = {}
def __init__(self, wrapped):

View File

@ -23,8 +23,8 @@
from bonobo.structs.tokens import Token
from bonobo.util.envelopes import UnchangedEnvelope
BEGIN = Token('Begin')
END = Token('End')
BEGIN = Token("Begin")
END = Token("End")
NOT_MODIFIED = UnchangedEnvelope()

View File

@ -9,7 +9,4 @@ This module contains all tools for Bonobo and Django to interract nicely.
from .utils import create_or_update
from .commands import ETLCommand
__all__ = [
'ETLCommand',
'create_or_update',
]
__all__ = ["ETLCommand", "create_or_update"]

View File

@ -35,7 +35,7 @@ class ETLCommand(BaseCommand):
def get_graph(self, *args, **options):
def not_implemented():
raise NotImplementedError('You must implement {}.get_graph() method.'.format(self))
raise NotImplementedError("You must implement {}.get_graph() method.".format(self))
return bonobo.Graph(not_implemented)
@ -56,14 +56,14 @@ class ETLCommand(BaseCommand):
graph_coll = self.get_graph(*args, **options)
if not isinstance(graph_coll, GeneratorType):
graph_coll = (graph_coll, )
graph_coll = (graph_coll,)
for i, graph in enumerate(graph_coll):
assert isinstance(graph, bonobo.Graph), 'Invalid graph provided.'
print(term.lightwhite('{}. {}'.format(i + 1, graph.name)))
assert isinstance(graph, bonobo.Graph), "Invalid graph provided."
print(term.lightwhite("{}. {}".format(i + 1, graph.name)))
result = bonobo.run(graph, services=services, strategy=strategy)
results.append(result)
print(term.lightblack(' ... return value: ' + str(result)))
print(term.lightblack(" ... return value: " + str(result)))
print()
return results
@ -71,9 +71,9 @@ class ETLCommand(BaseCommand):
def handle(self, *args, **options):
_stdout_backup, _stderr_backup = self.stdout, self.stderr
self.stdout = OutputWrapper(ConsoleOutputPlugin._stdout, ending=CLEAR_EOL + '\n')
self.stderr = OutputWrapper(ConsoleOutputPlugin._stderr, ending=CLEAR_EOL + '\n')
self.stderr.style_func = lambda x: Fore.LIGHTRED_EX + Back.RED + '!' + Style.RESET_ALL + ' ' + x
self.stdout = OutputWrapper(ConsoleOutputPlugin._stdout, ending=CLEAR_EOL + "\n")
self.stderr = OutputWrapper(ConsoleOutputPlugin._stderr, ending=CLEAR_EOL + "\n")
self.stderr.style_func = lambda x: Fore.LIGHTRED_EX + Back.RED + "!" + Style.RESET_ALL + " " + x
self.run(*args, **kwargs)

View File

@ -9,8 +9,8 @@ from oauth2client import client, tools
from oauth2client.file import Storage
from oauth2client.tools import argparser
HOME_DIR = os.path.expanduser('~')
GOOGLE_SECRETS = os.path.join(HOME_DIR, '.cache/secrets/client_secrets.json')
HOME_DIR = os.path.expanduser("~")
GOOGLE_SECRETS = os.path.join(HOME_DIR, ".cache/secrets/client_secrets.json")
def get_credentials(*, scopes):
@ -22,10 +22,10 @@ def get_credentials(*, scopes):
Returns:
Credentials, the obtained credential.
"""
credential_dir = os.path.join(HOME_DIR, '.cache', __package__, 'credentials')
credential_dir = os.path.join(HOME_DIR, ".cache", __package__, "credentials")
if not os.path.exists(credential_dir):
os.makedirs(credential_dir)
credential_path = os.path.join(credential_dir, 'googleapis.json')
credential_path = os.path.join(credential_dir, "googleapis.json")
store = Storage(credential_path)
credentials = store.get()
@ -34,22 +34,22 @@ def get_credentials(*, scopes):
# kw: "incremental scopes"
if not credentials or credentials.invalid or not credentials.has_scopes(scopes):
flow = client.flow_from_clientsecrets(GOOGLE_SECRETS, scopes)
flow.user_agent = 'Bonobo ETL (https://www.bonobo-project.org/)'
flags = argparser.parse_args(['--noauth_local_webserver'])
flow.user_agent = "Bonobo ETL (https://www.bonobo-project.org/)"
flags = argparser.parse_args(["--noauth_local_webserver"])
credentials = tools.run_flow(flow, store, flags)
print('Storing credentials to ' + credential_path)
print("Storing credentials to " + credential_path)
return credentials
def get_google_spreadsheets_api_client(scopes=('https://www.googleapis.com/auth/spreadsheets', )):
def get_google_spreadsheets_api_client(scopes=("https://www.googleapis.com/auth/spreadsheets",)):
credentials = get_credentials(scopes=scopes)
http = credentials.authorize(httplib2.Http())
discoveryUrl = 'https://sheets.googleapis.com/$discovery/rest?version=v4'
return discovery.build('sheets', 'v4', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
discoveryUrl = "https://sheets.googleapis.com/$discovery/rest?version=v4"
return discovery.build("sheets", "v4", http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
def get_google_people_api_client(scopes=('https://www.googleapis.com/auth/contacts', )):
def get_google_people_api_client(scopes=("https://www.googleapis.com/auth/contacts",)):
credentials = get_credentials(scopes=scopes)
http = credentials.authorize(httplib2.Http())
discoveryUrl = 'https://people.googleapis.com/$discovery/rest?version=v1'
return discovery.build('people', 'v1', http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)
discoveryUrl = "https://people.googleapis.com/$discovery/rest?version=v1"
return discovery.build("people", "v1", http=http, discoveryServiceUrl=discoveryUrl, cache_discovery=False)

View File

@ -2,9 +2,7 @@ from bonobo.plugins.jupyter import JupyterOutputPlugin
def _jupyter_nbextension_paths():
return [{'section': 'notebook', 'src': 'static', 'dest': 'bonobo-jupyter', 'require': 'bonobo-jupyter/extension'}]
return [{"section": "notebook", "src": "static", "dest": "bonobo-jupyter", "require": "bonobo-jupyter/extension"}]
__all__ = [
'JupyterOutputPlugin',
]
__all__ = ["JupyterOutputPlugin"]

View File

@ -2,10 +2,10 @@ import ipywidgets as widgets
from traitlets import List, Unicode
@widgets.register('bonobo-widget.bonobo')
@widgets.register("bonobo-widget.bonobo")
class BonoboWidget(widgets.DOMWidget):
_view_name = Unicode('BonoboView').tag(sync=True)
_model_name = Unicode('BonoboModel').tag(sync=True)
_view_module = Unicode('bonobo-jupyter').tag(sync=True)
_model_module = Unicode('bonobo-jupyter').tag(sync=True)
_view_name = Unicode("BonoboView").tag(sync=True)
_model_name = Unicode("BonoboModel").tag(sync=True)
_view_module = Unicode("bonobo-jupyter").tag(sync=True)
_model_module = Unicode("bonobo-jupyter").tag(sync=True)
value = List().tag(sync=True)

View File

@ -9,24 +9,24 @@ from bonobo.util.objects import ValueHolder
def path_str(path):
return path if path.startswith('/') else '/' + path
return path if path.startswith("/") else "/" + path
class OpenDataSoftAPI(Configurable):
dataset = Option(str, positional=True)
endpoint = Option(str, required=False, default='{scheme}://{netloc}{path}')
scheme = Option(str, required=False, default='https')
netloc = Option(str, required=False, default='data.opendatasoft.com')
path = Option(path_str, required=False, default='/api/records/1.0/search/')
endpoint = Option(str, required=False, default="{scheme}://{netloc}{path}")
scheme = Option(str, required=False, default="https")
netloc = Option(str, required=False, default="data.opendatasoft.com")
path = Option(path_str, required=False, default="/api/records/1.0/search/")
rows = Option(int, required=False, default=500)
limit = Option(int, required=False)
timezone = Option(str, required=False, default='Europe/Paris')
timezone = Option(str, required=False, default="Europe/Paris")
kwargs = Option(dict, required=False, default=dict)
@ContextProcessor
def compute_path(self, context):
params = (('dataset', self.dataset), ('timezone', self.timezone)) + tuple(sorted(self.kwargs.items()))
yield self.endpoint.format(scheme=self.scheme, netloc=self.netloc, path=self.path) + '?' + urlencode(params)
params = (("dataset", self.dataset), ("timezone", self.timezone)) + tuple(sorted(self.kwargs.items()))
yield self.endpoint.format(scheme=self.scheme, netloc=self.netloc, path=self.path) + "?" + urlencode(params)
@ContextProcessor
def start(self, context, base_url):
@ -34,25 +34,19 @@ class OpenDataSoftAPI(Configurable):
def __call__(self, base_url, start, *args, **kwargs):
while (not self.limit) or (self.limit > start):
url = '{}&start={start}&rows={rows}'.format(
url = "{}&start={start}&rows={rows}".format(
base_url, start=start.value, rows=self.rows if not self.limit else min(self.rows, self.limit - start)
)
resp = requests.get(url)
records = resp.json().get('records', [])
records = resp.json().get("records", [])
if not len(records):
break
for row in records:
yield {
**row.get('fields', {}),
'geometry': row.get('geometry', {}),
'recordid': row.get('recordid'),
}
yield {**row.get("fields", {}), "geometry": row.get("geometry", {}), "recordid": row.get("recordid")}
start += self.rows
__all__ = [
'OpenDataSoftAPI',
]
__all__ = ["OpenDataSoftAPI"]

View File

@ -16,10 +16,7 @@ class InactiveWritableError(InactiveIOError):
class ValidationError(RuntimeError):
def __init__(self, inst, message):
super(ValidationError, self).__init__(
'Validation error in {class_name}: {message}'.format(
class_name=type(inst).__name__,
message=message,
)
"Validation error in {class_name}: {message}".format(class_name=type(inst).__name__, message=message)
)
@ -41,9 +38,8 @@ class AbstractError(UnrecoverableError, NotImplementedError):
def __init__(self, method):
super().__init__(
'Call to abstract method {class_name}.{method_name}(...): missing implementation.'.format(
class_name=get_name(method.__self__),
method_name=get_name(method),
"Call to abstract method {class_name}.{method_name}(...): missing implementation.".format(
class_name=get_name(method.__self__), method_name=get_name(method)
)
)

View File

@ -8,40 +8,21 @@ from bonobo.util.statistics import Timer
def get_argument_parser(parser=None):
parser = bonobo.get_argument_parser(parser=parser)
parser.add_argument("--limit", "-l", type=int, default=None, help="If set, limits the number of processed lines.")
parser.add_argument(
'--limit',
'-l',
type=int,
default=None,
help='If set, limits the number of processed lines.'
)
parser.add_argument(
'--print',
'-p',
action='store_true',
default=False,
help='If set, pretty prints before writing to output file.'
"--print", "-p", action="store_true", default=False, help="If set, pretty prints before writing to output file."
)
parser.add_argument(
'--strategy',
'-s',
type=str,
choices=STRATEGIES.keys(),
default=DEFAULT_STRATEGY,
)
parser.add_argument("--strategy", "-s", type=str, choices=STRATEGIES.keys(), default=DEFAULT_STRATEGY)
return parser
def get_graph_options(options):
_limit = options.pop('limit', None)
_print = options.pop('print', False)
_limit = options.pop("limit", None)
_print = options.pop("print", False)
return {
'_limit': (bonobo.Limit(_limit), ) if _limit else (),
'_print': (bonobo.PrettyPrinter(), ) if _print else (),
}
return {"_limit": (bonobo.Limit(_limit),) if _limit else (), "_print": (bonobo.PrettyPrinter(),) if _print else ()}
def run(get_graph, get_services, *, parser=None):
@ -49,38 +30,29 @@ def run(get_graph, get_services, *, parser=None):
with bonobo.parse_args(parser) as options:
with Timer() as timer:
print(
'Options:', ' '.join(
'{}={}'.format(k, v)
for k, v in sorted(options.items())
)
)
print("Options:", " ".join("{}={}".format(k, v) for k, v in sorted(options.items())))
retval = bonobo.run(
get_graph(**get_graph_options(options)),
services=get_services(),
strategy=options['strategy'],
get_graph(**get_graph_options(options)), services=get_services(), strategy=options["strategy"]
)
print('Execution time:', timer)
print('Return value:', retval)
print('XStatus:', retval.xstatus)
print("Execution time:", timer)
print("Return value:", retval)
print("XStatus:", retval.xstatus)
return retval.xstatus
def get_minor_version():
return '.'.join(bonobo.__version__.split('.')[:2])
return ".".join(bonobo.__version__.split(".")[:2])
def get_datasets_dir(*dirs):
home_dir = os.path.expanduser('~')
target_dir = os.path.join(
home_dir, '.cache/bonobo', get_minor_version(), *dirs
)
home_dir = os.path.expanduser("~")
target_dir = os.path.join(home_dir, ".cache/bonobo", get_minor_version(), *dirs)
os.makedirs(target_dir, exist_ok=True)
return target_dir
def get_services():
return {
'fs': bonobo.open_fs(get_datasets_dir('datasets')),
'fs.static': bonobo.open_examples_fs('datasets', 'static'),
"fs": bonobo.open_fs(get_datasets_dir("datasets")),
"fs.static": bonobo.open_examples_fs("datasets", "static"),
}

View File

@ -1,5 +1,5 @@
if __name__ == '__main__':
if __name__ == "__main__":
from bonobo.commands import entrypoint
import sys
entrypoint(['examples'] + sys.argv[1:])
entrypoint(["examples"] + sys.argv[1:])

View File

@ -14,15 +14,12 @@ def extract():
def get_graph():
graph = bonobo.Graph()
graph.add_chain(
extract,
print,
)
graph.add_chain(extract, print)
return graph
if __name__ == '__main__':
if __name__ == "__main__":
parser = bonobo.get_argument_parser()
with bonobo.parse_args(parser):
bonobo.run(get_graph())

View File

@ -6,24 +6,16 @@ from bonobo.examples import get_datasets_dir, get_minor_version, get_services
from bonobo.examples.datasets.coffeeshops import get_graph as get_coffeeshops_graph
from bonobo.examples.datasets.fablabs import get_graph as get_fablabs_graph
graph_factories = {
'coffeeshops': get_coffeeshops_graph,
'fablabs': get_fablabs_graph,
}
graph_factories = {"coffeeshops": get_coffeeshops_graph, "fablabs": get_fablabs_graph}
if __name__ == '__main__':
if __name__ == "__main__":
parser = examples.get_argument_parser()
parser.add_argument(
'--target', '-t', choices=graph_factories.keys(), nargs='+'
)
parser.add_argument('--sync', action='store_true', default=False)
parser.add_argument("--target", "-t", choices=graph_factories.keys(), nargs="+")
parser.add_argument("--sync", action="store_true", default=False)
with bonobo.parse_args(parser) as options:
graph_options = examples.get_graph_options(options)
graph_names = list(
options['target']
if options['target'] else sorted(graph_factories.keys())
)
graph_names = list(options["target"] if options["target"] else sorted(graph_factories.keys()))
# Create a graph with all requested subgraphs
graph = bonobo.Graph()
@ -32,29 +24,20 @@ if __name__ == '__main__':
bonobo.run(graph, services=get_services())
if options['sync']:
if options["sync"]:
# TODO: when parallel option for node will be implemented, need to be rewriten to use a graph.
import boto3
s3 = boto3.client('s3')
s3 = boto3.client("s3")
local_dir = get_datasets_dir()
for root, dirs, files in os.walk(local_dir):
for filename in files:
local_path = os.path.join(root, filename)
relative_path = os.path.relpath(local_path, local_dir)
s3_path = os.path.join(
get_minor_version(), relative_path
)
s3_path = os.path.join(get_minor_version(), relative_path)
try:
s3.head_object(
Bucket='bonobo-examples', Key=s3_path
)
s3.head_object(Bucket="bonobo-examples", Key=s3_path)
except Exception:
s3.upload_file(
local_path,
'bonobo-examples',
s3_path,
ExtraArgs={'ACL': 'public-read'}
)
s3.upload_file(local_path, "bonobo-examples", s3_path, ExtraArgs={"ACL": "public-read"})

View File

@ -1,63 +1,39 @@
"""
"""
import sys
import bonobo
from bonobo import examples
from bonobo.contrib.opendatasoft import OpenDataSoftAPI as ODSReader
from bonobo.examples import get_services
from bonobo.structs.graphs import PartialGraph
def get_graph(graph=None, *, _limit=(), _print=()):
graph = graph or bonobo.Graph()
producer = graph.add_chain(
ODSReader(
dataset='liste-des-cafes-a-un-euro',
netloc='opendata.paris.fr'
),
*_limit,
bonobo.UnpackItems(0),
bonobo.Rename(
name='nom_du_cafe',
address='adresse',
zipcode='arrondissement'
),
bonobo.Format(city='Paris', country='France'),
bonobo.OrderFields(
[
'name', 'address', 'zipcode', 'city', 'country',
'geometry', 'geoloc'
]
),
*_print,
producer = (
graph.get_cursor()
>> ODSReader(dataset="liste-des-cafes-a-un-euro", netloc="opendata.paris.fr")
>> PartialGraph(*_limit)
>> bonobo.UnpackItems(0)
>> bonobo.Rename(name="nom_du_cafe", address="adresse", zipcode="arrondissement")
>> bonobo.Format(city="Paris", country="France")
>> bonobo.OrderFields(["name", "address", "zipcode", "city", "country", "geometry", "geoloc"])
>> PartialGraph(*_print)
)
# Comma separated values.
graph.add_chain(
bonobo.CsvWriter(
'coffeeshops.csv',
fields=['name', 'address', 'zipcode', 'city'],
delimiter=','
),
_input=producer.output,
graph.get_cursor(producer.output) >> bonobo.CsvWriter(
"coffeeshops.csv", fields=["name", "address", "zipcode", "city"], delimiter=","
)
# Standard JSON
graph.add_chain(
bonobo.JsonWriter(path='coffeeshops.json'),
_input=producer.output,
)
graph.get_cursor(producer.output) >> bonobo.JsonWriter(path="coffeeshops.json")
# Line-delimited JSON
graph.add_chain(
bonobo.LdjsonWriter(path='coffeeshops.ldjson'),
_input=producer.output,
)
graph.get_cursor(producer.output) >> bonobo.LdjsonWriter(path="coffeeshops.ldjson")
return graph
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -25,27 +25,21 @@ from bonobo.examples import get_services
try:
import pycountry
except ImportError as exc:
raise ImportError(
'You must install package "pycountry" to run this example.'
) from exc
raise ImportError('You must install package "pycountry" to run this example.') from exc
API_DATASET = 'fablabs@public-us'
API_DATASET = "fablabs@public-us"
ROWS = 100
def _getlink(x):
return x.get('url', None)
return x.get("url", None)
def normalize(row):
result = {
**row,
'links':
list(filter(None, map(_getlink, json.loads(row.get('links'))))),
'country':
pycountry.countries.get(
alpha_2=row.get('country_code', '').upper()
).name,
"links": list(filter(None, map(_getlink, json.loads(row.get("links"))))),
"country": pycountry.countries.get(alpha_2=row.get("country_code", "").upper()).name,
}
return result
@ -58,10 +52,10 @@ def get_graph(graph=None, *, _limit=(), _print=()):
normalize,
bonobo.UnpackItems(0),
*_print,
bonobo.JsonWriter(path='fablabs.json'),
bonobo.JsonWriter(path="fablabs.json"),
)
return graph
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -19,7 +19,7 @@ def get_graph():
return graph
if __name__ == '__main__':
if __name__ == "__main__":
parser = bonobo.get_argument_parser()
with bonobo.parse_args(parser):
bonobo.run(get_graph())

View File

@ -21,7 +21,7 @@ def get_graph():
return graph
if __name__ == '__main__':
if __name__ == "__main__":
parser = bonobo.get_argument_parser()
with bonobo.parse_args(parser):
bonobo.run(get_graph())

View File

@ -7,12 +7,12 @@ from bonobo.examples.files.services import get_services
def get_graph(*, _limit=None, _print=False):
return bonobo.Graph(
bonobo.CsvReader('coffeeshops.csv'),
*((bonobo.Limit(_limit), ) if _limit else ()),
*((bonobo.PrettyPrinter(), ) if _print else ()),
bonobo.CsvWriter('coffeeshops.csv', fs='fs.output')
bonobo.CsvReader("coffeeshops.csv"),
*((bonobo.Limit(_limit),) if _limit else ()),
*((bonobo.PrettyPrinter(),) if _print else ()),
bonobo.CsvWriter("coffeeshops.csv", fs="fs.output")
)
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -9,24 +9,17 @@ def get_graph(*, _limit=None, _print=False):
graph = bonobo.Graph()
trunk = graph.add_chain(
bonobo.JsonReader('theaters.json', fs='fs.static'),
*((bonobo.Limit(_limit), ) if _limit else ()),
bonobo.JsonReader("theaters.json", fs="fs.static"), *((bonobo.Limit(_limit),) if _limit else ())
)
if _print:
graph.add_chain(bonobo.PrettyPrinter(), _input=trunk.output)
graph.add_chain(
bonobo.JsonWriter('theaters.output.json', fs='fs.output'),
_input=trunk.output
)
graph.add_chain(
bonobo.LdjsonWriter('theaters.output.ldjson', fs='fs.output'),
_input=trunk.output
)
graph.add_chain(bonobo.JsonWriter("theaters.output.json", fs="fs.output"), _input=trunk.output)
graph.add_chain(bonobo.LdjsonWriter("theaters.output.ldjson", fs="fs.output"), _input=trunk.output)
return graph
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -1,4 +1,4 @@
'''
"""
This example shows how a different file system service can be injected
into a transformation (as compressing pickled objects often makes sense
anyways). The pickle itself contains a list of lists as follows:
@ -25,7 +25,7 @@ https://www.kaggle.com/uciml/sms-spam-collection-dataset/downloads/sms-spam-coll
The transformation (1) reads the pickled data, (2) marks and shortens
messages categorized as spam, and (3) prints the output.
'''
"""
import sys
@ -36,14 +36,12 @@ from bonobo import examples
def cleanse_sms(category, sms):
if category == 'spam':
sms_clean = '**MARKED AS SPAM** ' + sms[0:50] + (
'...' if len(sms) > 50 else ''
)
elif category == 'ham':
if category == "spam":
sms_clean = "**MARKED AS SPAM** " + sms[0:50] + ("..." if len(sms) > 50 else "")
elif category == "ham":
sms_clean = sms
else:
raise ValueError('Unknown category {!r}.'.format(category))
raise ValueError("Unknown category {!r}.".format(category))
return category, sms, sms_clean
@ -53,7 +51,7 @@ def get_graph(*, _limit=(), _print=()):
graph.add_chain(
# spam.pkl is within the gzipped tarball
bonobo.PickleReader('spam.pkl'),
bonobo.PickleReader("spam.pkl"),
*_limit,
cleanse_sms,
*_print,
@ -63,11 +61,8 @@ def get_graph(*, _limit=(), _print=()):
def get_services():
return {
**examples.get_services(), 'fs':
TarFS(bonobo.get_examples_path('datasets', 'static', 'spam.tgz'))
}
return {**examples.get_services(), "fs": TarFS(bonobo.get_examples_path("datasets", "static", "spam.tgz"))}
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -2,7 +2,4 @@ from bonobo import examples, open_fs
def get_services():
return {
**examples.get_services(),
'fs.output': open_fs(),
}
return {**examples.get_services(), "fs.output": open_fs()}

View File

@ -7,20 +7,20 @@ from bonobo.examples.files.services import get_services
def skip_comments(line):
line = line.strip()
if not line.startswith('#'):
if not line.startswith("#"):
yield line
def get_graph(*, _limit=(), _print=()):
return bonobo.Graph(
bonobo.FileReader('passwd.txt', fs='fs.static'),
bonobo.FileReader("passwd.txt", fs="fs.static"),
skip_comments,
*_limit,
lambda s: s.split(':')[0],
lambda s: s.split(":")[0],
*_print,
bonobo.FileWriter('usernames.txt', fs='fs.output'),
bonobo.FileWriter("usernames.txt", fs="fs.output"),
)
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(examples.run(get_graph, get_services))

View File

@ -1,7 +1,7 @@
import bonobo
from bonobo.examples.types.strings import get_graph
if __name__ == '__main__':
if __name__ == "__main__":
parser = bonobo.get_argument_parser()
with bonobo.parse_args(parser):
bonobo.run(get_graph())

View File

@ -18,13 +18,13 @@ import bonobo
def extract():
yield 'foo'
yield 'bar'
yield 'baz'
yield "foo"
yield "bar"
yield "baz"
def transform(s):
return '{} ({})'.format(s.title(), randint(10, 99))
return "{} ({})".format(s.title(), randint(10, 99))
def load(s):
@ -35,7 +35,7 @@ def get_graph():
return bonobo.Graph(extract, transform, load)
if __name__ == '__main__':
if __name__ == "__main__":
parser = bonobo.get_argument_parser()
with bonobo.parse_args(parser):
bonobo.run(get_graph())

View File

@ -9,8 +9,4 @@ from bonobo.execution.contexts.graph import GraphExecutionContext
from bonobo.execution.contexts.node import NodeExecutionContext
from bonobo.execution.contexts.plugin import PluginExecutionContext
__all__ = [
'GraphExecutionContext',
'NodeExecutionContext',
'PluginExecutionContext',
]
__all__ = ["GraphExecutionContext", "NodeExecutionContext", "PluginExecutionContext"]

View File

@ -62,12 +62,12 @@ class Lifecycle:
"""
if self._defunct:
return '!'
return "!"
if not self.started:
return ' '
return " "
if not self.stopped:
return '+'
return '-'
return "+"
return "-"
def __enter__(self):
self.start()
@ -78,31 +78,31 @@ class Lifecycle:
def get_flags_as_string(self):
if self._defunct:
return term.red('[defunct]')
return term.red("[defunct]")
if self.killed:
return term.lightred('[killed]')
return term.lightred("[killed]")
if self.stopped:
return term.lightblack('[done]')
return ''
return term.lightblack("[done]")
return ""
def start(self):
if self.started:
raise RuntimeError('This context is already started ({}).'.format(get_name(self)))
raise RuntimeError("This context is already started ({}).".format(get_name(self)))
self._started = True
def stop(self):
if not self.started:
raise RuntimeError('This context cannot be stopped as it never started ({}).'.format(get_name(self)))
raise RuntimeError("This context cannot be stopped as it never started ({}).".format(get_name(self)))
self._stopped = True
def kill(self):
if not self.started:
raise RuntimeError('Cannot kill an unstarted context.')
raise RuntimeError("Cannot kill an unstarted context.")
if self.stopped:
raise RuntimeError('Cannot kill a stopped context.')
raise RuntimeError("Cannot kill a stopped context.")
self._killed = True
@ -119,10 +119,10 @@ class Lifecycle:
def as_dict(self):
return {
'status': self.status,
'name': self.name,
'stats': self.get_statistics_as_string(),
'flags': self.get_flags_as_string(),
"status": self.status,
"name": self.name,
"stats": self.get_statistics_as_string(),
"flags": self.get_flags_as_string(),
}

View File

@ -63,7 +63,7 @@ class BaseGraphExecutionContext(BaseContext):
self.services = create_container(services)
# Probably not a good idea to use it unless you really know what you're doing. But you can access the context.
self.services['__graph_context'] = self
self.services["__graph_context"] = self
for i, node_context in enumerate(self):
outputs = self.graph.outputs_of(i)

View File

@ -19,7 +19,7 @@ from bonobo.util.statistics import WithStatistics
logger = logging.getLogger(__name__)
UnboundArguments = namedtuple('UnboundArguments', ['args', 'kwargs'])
UnboundArguments = namedtuple("UnboundArguments", ["args", "kwargs"])
class NodeExecutionContext(BaseContext, WithStatistics):
@ -46,13 +46,13 @@ class NodeExecutionContext(BaseContext, WithStatistics):
:param _outputs: output queues (optional)
"""
BaseContext.__init__(self, wrapped, parent=parent)
WithStatistics.__init__(self, 'in', 'out', 'err', 'warn')
WithStatistics.__init__(self, "in", "out", "err", "warn")
# Services: how we'll access external dependencies
if services:
if self.parent:
raise RuntimeError(
'Having services defined both in GraphExecutionContext and child NodeExecutionContext is not supported, for now.'
"Having services defined both in GraphExecutionContext and child NodeExecutionContext is not supported, for now."
)
self.services = create_container(services)
else:
@ -70,11 +70,11 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self._stack = None
def __str__(self):
return self.__name__ + self.get_statistics_as_string(prefix=' ')
return self.__name__ + self.get_statistics_as_string(prefix=" ")
def __repr__(self):
name, type_name = get_name(self), get_name(type(self))
return '<{}({}{}){}>'.format(type_name, self.status, name, self.get_statistics_as_string(prefix=' '))
return "<{}({}{}){}>".format(type_name, self.status, name, self.get_statistics_as_string(prefix=" "))
def start(self):
"""
@ -97,13 +97,13 @@ class NodeExecutionContext(BaseContext, WithStatistics):
# Not normal to have a partially configured object here, so let's warn the user instead of having get into
# the hard trouble of understanding that by himself.
raise TypeError(
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(
"Configurables should be instanciated before execution starts.\nGot {!r}.\n".format(
self.wrapped
)
) from exc
else:
raise TypeError(
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(
"Configurables should be instanciated before execution starts.\nGot {!r}.\n".format(
self.wrapped
)
)
@ -120,7 +120,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
The actual infinite loop for this transformation.
"""
logger.debug('Node loop starts for {!r}.'.format(self))
logger.debug("Node loop starts for {!r}.".format(self))
while self.should_loop:
try:
@ -128,7 +128,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
except InactiveReadableError:
break
logger.debug('Node loop ends for {!r}.'.format(self))
logger.debug("Node loop ends for {!r}.".format(self))
def step(self):
try:
@ -137,10 +137,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
raise
except Empty:
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
except (
NotImplementedError,
UnrecoverableError,
):
except (NotImplementedError, UnrecoverableError):
self.fatal(sys.exc_info()) # exit loop
except Exception: # pylint: disable=broad-except
self.error(sys.exc_info()) # does not exit loop
@ -208,20 +205,20 @@ class NodeExecutionContext(BaseContext, WithStatistics):
def set_input_type(self, input_type):
if self._input_type is not None:
raise RuntimeError('Cannot override input type, already have %r.', self._input_type)
raise RuntimeError("Cannot override input type, already have %r.", self._input_type)
if type(input_type) is not type:
raise UnrecoverableTypeError('Input types must be regular python types.')
raise UnrecoverableTypeError("Input types must be regular python types.")
if not issubclass(input_type, tuple):
raise UnrecoverableTypeError('Input types must be subclasses of tuple (and act as tuples).')
raise UnrecoverableTypeError("Input types must be subclasses of tuple (and act as tuples).")
self._input_type = input_type
def get_input_fields(self):
return self._input_type._fields if self._input_type and hasattr(self._input_type, '_fields') else None
return self._input_type._fields if self._input_type and hasattr(self._input_type, "_fields") else None
def set_input_fields(self, fields, typename='Bag'):
def set_input_fields(self, fields, typename="Bag"):
self.set_input_type(BagType(typename, fields))
### Output type and fields
@ -231,20 +228,20 @@ class NodeExecutionContext(BaseContext, WithStatistics):
def set_output_type(self, output_type):
if self._output_type is not None:
raise RuntimeError('Cannot override output type, already have %r.', self._output_type)
raise RuntimeError("Cannot override output type, already have %r.", self._output_type)
if type(output_type) is not type:
raise UnrecoverableTypeError('Output types must be regular python types.')
raise UnrecoverableTypeError("Output types must be regular python types.")
if not issubclass(output_type, tuple):
raise UnrecoverableTypeError('Output types must be subclasses of tuple (and act as tuples).')
raise UnrecoverableTypeError("Output types must be subclasses of tuple (and act as tuples).")
self._output_type = output_type
def get_output_fields(self):
return self._output_type._fields if self._output_type and hasattr(self._output_type, '_fields') else None
return self._output_type._fields if self._output_type and hasattr(self._output_type, "_fields") else None
def set_output_fields(self, fields, typename='Bag'):
def set_output_fields(self, fields, typename="Bag"):
self.set_output_type(BagType(typename, fields))
### Attributes
@ -273,11 +270,11 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self.step()
def error(self, exc_info, *, level=logging.ERROR):
self.increment('err')
self.increment("err")
super().error(exc_info, level=level)
def fatal(self, exc_info, *, level=logging.CRITICAL):
self.increment('err')
self.increment("err")
super().fatal(exc_info, level=level)
self.input.shutdown()
@ -306,8 +303,9 @@ class NodeExecutionContext(BaseContext, WithStatistics):
input_bag = self._input_type(*input_bag)
except Exception as exc:
raise UnrecoverableTypeError(
'Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.'.
format(self.wrapped, input_bag, self._input_type)
"Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.".format(
self.wrapped, input_bag, self._input_type
)
) from exc
# Store or check input length, which is a soft fallback in case we're just using tuples
@ -315,12 +313,12 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self._input_length = len(input_bag)
elif len(input_bag) != self._input_length:
raise UnrecoverableTypeError(
'Input length changed between calls to {!r}.\nExpected {} but got {}: {!r}.'.format(
"Input length changed between calls to {!r}.\nExpected {} but got {}: {!r}.".format(
self.wrapped, self._input_length, len(input_bag), input_bag
)
)
self.increment('in') # XXX should that go before type check ?
self.increment("in") # XXX should that go before type check ?
return input_bag
@ -366,7 +364,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
"""
if not _control:
self.increment('out')
self.increment("out")
for output in self.outputs:
output.put(value)
@ -406,8 +404,9 @@ class AsyncNodeExecutionContext(NodeExecutionContext):
input_bag = self._input_type(*input_bag)
except Exception as exc:
raise UnrecoverableTypeError(
'Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.'.
format(self.wrapped, input_bag, self._input_type)
"Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.".format(
self.wrapped, input_bag, self._input_type
)
) from exc
# Store or check input length, which is a soft fallback in case we're just using tuples
@ -415,12 +414,12 @@ class AsyncNodeExecutionContext(NodeExecutionContext):
self._input_length = len(input_bag)
elif len(input_bag) != self._input_length:
raise UnrecoverableTypeError(
'Input length changed between calls to {!r}.\nExpected {} but got {}: {!r}.'.format(
"Input length changed between calls to {!r}.\nExpected {} but got {}: {!r}.".format(
self.wrapped, self._input_length, len(input_bag), input_bag
)
)
self.increment('in') # XXX should that go before type check ?
self.increment("in") # XXX should that go before type check ?
return input_bag
@ -443,18 +442,18 @@ def split_token(output):
flags, i, len_output, data_allowed = set(), 0, len(output), True
while i < len_output and isflag(output[i]):
if output[i].must_be_first and i:
raise ValueError('{} flag must be first.'.format(output[i]))
raise ValueError("{} flag must be first.".format(output[i]))
if i and output[i - 1].must_be_last:
raise ValueError('{} flag must be last.'.format(output[i - 1]))
raise ValueError("{} flag must be last.".format(output[i - 1]))
if output[i] in flags:
raise ValueError('Duplicate flag {}.'.format(output[i]))
raise ValueError("Duplicate flag {}.".format(output[i]))
flags.add(output[i])
data_allowed &= output[i].allows_data
i += 1
output = output[i:]
if not data_allowed and len(output):
raise ValueError('Output data provided after a flag that does not allow data.')
raise ValueError("Output data provided after a flag that does not allow data.")
return flags, output
@ -465,7 +464,7 @@ def concat_types(t1, l1, t2, l2):
if t1 == t2 == tuple:
return tuple
f1 = t1._fields if hasattr(t1, '_fields') else tuple(range(l1))
f2 = t2._fields if hasattr(t2, '_fields') else tuple(range(l2))
f1 = t1._fields if hasattr(t1, "_fields") else tuple(range(l1))
f2 = t2._fields if hasattr(t2, "_fields") else tuple(range(l2))
return BagType('Inherited', f1 + f2)
return BagType("Inherited", f1 + f2)

View File

@ -27,12 +27,12 @@
from whistle import Event
START = 'execution.start'
STARTED = 'execution.started'
TICK = 'execution.tick'
STOP = 'execution.stop'
STOPPED = 'execution.stopped'
KILL = 'execution.kill'
START = "execution.start"
STARTED = "execution.started"
TICK = "execution.tick"
STOP = "execution.stop"
STOPPED = "execution.stopped"
KILL = "execution.kill"
class ExecutionEvent(Event):

View File

@ -6,22 +6,23 @@ In the future, the two strategies that would really benefit bonobo are subproces
at home if you want to give it a shot.
"""
from bonobo.execution.strategies.executor import ProcessPoolExecutorStrategy, ThreadPoolExecutorStrategy, \
AsyncThreadPoolExecutorStrategy
from bonobo.execution.strategies.executor import (
ProcessPoolExecutorStrategy,
ThreadPoolExecutorStrategy,
AsyncThreadPoolExecutorStrategy,
)
from bonobo.execution.strategies.naive import NaiveStrategy
__all__ = [
'create_strategy',
]
__all__ = ["create_strategy"]
STRATEGIES = {
'naive': NaiveStrategy,
'processpool': ProcessPoolExecutorStrategy,
'threadpool': ThreadPoolExecutorStrategy,
'aio_threadpool': AsyncThreadPoolExecutorStrategy,
"naive": NaiveStrategy,
"processpool": ProcessPoolExecutorStrategy,
"threadpool": ThreadPoolExecutorStrategy,
"aio_threadpool": AsyncThreadPoolExecutorStrategy,
}
DEFAULT_STRATEGY = 'threadpool'
DEFAULT_STRATEGY = "threadpool"
def create_strategy(name=None):
@ -40,13 +41,13 @@ def create_strategy(name=None):
if name is None:
name = DEFAULT_STRATEGY
logging.debug('Creating execution strategy {!r}...'.format(name))
logging.debug("Creating execution strategy {!r}...".format(name))
try:
factory = STRATEGIES[name]
except KeyError as exc:
raise RuntimeError(
'Invalid strategy {}. Available choices: {}.'.format(repr(name), ', '.join(sorted(STRATEGIES.keys())))
"Invalid strategy {}. Available choices: {}.".format(repr(name), ", ".join(sorted(STRATEGIES.keys())))
) from exc
return factory()

View File

@ -6,6 +6,7 @@ class Strategy:
Base class for execution strategies.
"""
GraphExecutionContextType = GraphExecutionContext
def __init__(self, GraphExecutionContextType=None):
@ -13,7 +14,7 @@ class Strategy:
def create_graph_execution_context(self, graph, *args, GraphExecutionContextType=None, **kwargs):
if not len(graph):
raise ValueError('You provided an empty graph, which does not really make sense. Please add some nodes.')
raise ValueError("You provided an empty graph, which does not really make sense. Please add some nodes.")
return (GraphExecutionContextType or self.GraphExecutionContextType)(graph, *args, **kwargs)
def execute(self, graph, *args, **kwargs):

View File

@ -36,14 +36,14 @@ class ExecutorStrategy(Strategy):
try:
context.start(self.get_starter(executor, futures))
except Exception:
logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info())
logger.critical("Exception caught while starting execution context.", exc_info=sys.exc_info())
while context.alive:
try:
context.tick()
except KeyboardInterrupt:
logging.getLogger(__name__).warning(
'KeyboardInterrupt received. Trying to terminate the nodes gracefully.'
"KeyboardInterrupt received. Trying to terminate the nodes gracefully."
)
context.kill()
break
@ -61,13 +61,13 @@ class ExecutorStrategy(Strategy):
node.loop()
except Exception:
logging.getLogger(__name__).critical(
'Critical error in threadpool node starter.', exc_info=sys.exc_info()
"Critical error in threadpool node starter.", exc_info=sys.exc_info()
)
try:
futures.append(executor.submit(_runner))
except Exception:
logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info())
logging.getLogger(__name__).critical("futures.append", exc_info=sys.exc_info())
return starter
@ -85,7 +85,7 @@ class AsyncThreadPoolExecutorStrategy(ThreadPoolExecutorStrategy):
def __init__(self, GraphExecutionContextType=None):
if not settings.ALPHA.get():
raise NotImplementedError(
'{} is experimental, you need to explicitely activate it using ALPHA=True in system env.'.format(
"{} is experimental, you need to explicitely activate it using ALPHA=True in system env.".format(
get_name(self)
)
)

View File

@ -5,4 +5,4 @@ from bonobo.nodes.io import *
from bonobo.nodes.io import __all__ as _all_io
from bonobo.nodes.throttle import RateLimited
__all__ = _all_basics + _all_io + ['Filter', 'RateLimited']
__all__ = _all_basics + _all_io + ["Filter", "RateLimited"]

View File

@ -13,18 +13,18 @@ from bonobo.util.term import CLEAR_EOL
from mondrian import term
__all__ = [
'FixedWindow',
'Format',
'Limit',
'OrderFields',
'PrettyPrinter',
'Rename',
'SetFields',
'Tee',
'UnpackItems',
'count',
'identity',
'noop',
"FixedWindow",
"Format",
"Limit",
"OrderFields",
"PrettyPrinter",
"Rename",
"SetFields",
"Tee",
"UnpackItems",
"count",
"identity",
"noop",
]
@ -43,6 +43,7 @@ class Limit(Configurable):
TODO: simplify into a closure building factory?
"""
limit = Option(positional=True, default=10)
@ContextProcessor
@ -69,7 +70,7 @@ def Tee(f):
def _shorten(s, w):
if w and len(s) > w:
s = s[0:w - 3] + '...'
s = s[0 : w - 3] + "..."
return s
@ -78,28 +79,31 @@ class PrettyPrinter(Configurable):
int,
default=term.get_size()[0],
required=False,
__doc__='''
__doc__="""
If set, truncates the output values longer than this to this width.
'''
""",
)
filter = Method(
default=
(lambda self, index, key, value: (value is not None) and (not isinstance(key, str) or not key.startswith('_'))),
__doc__='''
default=(
lambda self, index, key, value: (value is not None)
and (not isinstance(key, str) or not key.startswith("_"))
),
__doc__="""
A filter that determine what to print.
Default is to ignore any key starting with an underscore and none values.
'''
""",
)
@ContextProcessor
def context(self, context):
context.setdefault('_jupyter_html', None)
context.setdefault("_jupyter_html", None)
yield context
if context._jupyter_html is not None:
from IPython.display import display, HTML
display(HTML('\n'.join(['<table>'] + context._jupyter_html + ['</table>'])))
display(HTML("\n".join(["<table>"] + context._jupyter_html + ["</table>"])))
def __call__(self, context, *args, **kwargs):
if not settings.QUIET:
@ -120,49 +124,44 @@ class PrettyPrinter(Configurable):
def format_quiet(self, index, key, value, *, fields=None):
# XXX should we implement argnames here ?
return ' '.join(((' ' if index else '-'), str(key), ':', str(value).strip()))
return " ".join(((" " if index else "-"), str(key), ":", str(value).strip()))
def print_console(self, context, *args, **kwargs):
print('\u250c')
print("\u250c")
for index, (key, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())):
if self.filter(index, key, value):
print(self.format_console(index, key, value, fields=context.get_input_fields()))
print('\u2514')
print("\u2514")
def format_console(self, index, key, value, *, fields=None):
fields = fields or []
if not isinstance(key, str):
if len(fields) > key and str(key) != str(fields[key]):
key = '{}{}'.format(fields[key], term.lightblack('[{}]'.format(key)))
key = "{}{}".format(fields[key], term.lightblack("[{}]".format(key)))
else:
key = str(index)
prefix = '\u2502 {} = '.format(key)
prefix = "\u2502 {} = ".format(key)
prefix_length = len(prefix)
def indent(text, prefix):
for i, line in enumerate(text.splitlines()):
yield (prefix if i else '') + line + CLEAR_EOL + '\n'
yield (prefix if i else "") + line + CLEAR_EOL + "\n"
repr_of_value = ''.join(
indent(pprint.pformat(value, width=self.max_width - prefix_length), '\u2502' + ' ' * (len(prefix) - 1))
repr_of_value = "".join(
indent(pprint.pformat(value, width=self.max_width - prefix_length), "\u2502" + " " * (len(prefix) - 1))
).strip()
return '{}{}{}'.format(prefix, repr_of_value.replace('\n', CLEAR_EOL + '\n'), CLEAR_EOL)
return "{}{}{}".format(prefix, repr_of_value.replace("\n", CLEAR_EOL + "\n"), CLEAR_EOL)
def print_jupyter(self, context, *args):
if not context._jupyter_html:
context._jupyter_html = [
'<thead><tr>',
*map('<th>{}</th>'.format, map(html.escape, map(str,
context.get_input_fields() or range(len(args))))),
'</tr></thead>',
"<thead><tr>",
*map("<th>{}</th>".format, map(html.escape, map(str, context.get_input_fields() or range(len(args))))),
"</tr></thead>",
]
context._jupyter_html += [
'<tr>',
*map('<td>{}</td>'.format, map(html.escape, map(repr, args))),
'</tr>',
]
context._jupyter_html += ["<tr>", *map("<td>{}</td>".format, map(html.escape, map(repr, args))), "</tr>"]
@use_no_input
@ -211,7 +210,7 @@ def OrderFields(fields):
@use_raw_input
def _OrderFields(context, row):
nonlocal fields
context.setdefault('remaining', None)
context.setdefault("remaining", None)
if not context.output_type:
context.remaining = list(sorted(set(context.get_input_fields()) - set(fields)))
context.set_output_fields(fields + context.remaining)

View File

@ -6,14 +6,14 @@ from .json import JsonReader, JsonWriter, LdjsonReader, LdjsonWriter
from .pickle import PickleReader, PickleWriter
__all__ = [
'CsvReader',
'CsvWriter',
'FileReader',
'FileWriter',
'JsonReader',
'JsonWriter',
'LdjsonReader',
'LdjsonWriter',
'PickleReader',
'PickleWriter',
"CsvReader",
"CsvWriter",
"FileReader",
"FileWriter",
"JsonReader",
"JsonWriter",
"LdjsonReader",
"LdjsonWriter",
"PickleReader",
"PickleWriter",
]

View File

@ -13,22 +13,39 @@ class FileHandler(Configurable):
"""
path = Option(
str, required=True, positional=True, __doc__='''
str,
required=True,
positional=True,
__doc__="""
Path to use within the provided filesystem.
'''
""",
) # type: str
eol = Option(str, default='\n', __doc__='''
eol = Option(
str,
default="\n",
__doc__="""
Character to use as line separator.
''') # type: str
mode = Option(str, __doc__='''
""",
) # type: str
mode = Option(
str,
__doc__="""
What mode to use for open() call.
''') # type: str
encoding = Option(str, default='utf-8', __doc__='''
""",
) # type: str
encoding = Option(
str,
default="utf-8",
__doc__="""
Encoding.
''') # type: str
fs = Service('fs', __doc__='''
""",
) # type: str
fs = Service(
"fs",
__doc__="""
The filesystem instance to use.
''') # type: str
""",
) # type: str
@ContextProcessor
def file(self, context, *, fs):

View File

@ -35,18 +35,18 @@ class CsvHandler(FileHandler):
quoting = Option(int, default=csv.excel.quoting, required=False)
# Fields (renamed from headers)
headers = RenamedOption('fields')
headers = RenamedOption("fields")
fields = Option(ensure_tuple, required=False)
def get_dialect_kwargs(self):
return {
'delimiter': self.delimiter,
'quotechar': self.quotechar,
'escapechar': self.escapechar,
'doublequote': self.doublequote,
'skipinitialspace': self.skipinitialspace,
'lineterminator': self.lineterminator,
'quoting': self.quoting,
"delimiter": self.delimiter,
"quotechar": self.quotechar,
"escapechar": self.escapechar,
"doublequote": self.doublequote,
"skipinitialspace": self.skipinitialspace,
"lineterminator": self.lineterminator,
"quoting": self.quoting,
}
@ -59,25 +59,25 @@ class CsvReader(FileReader, CsvHandler):
skip = Option(
int,
default=0,
__doc__='''
__doc__="""
If set and greater than zero, the reader will skip this amount of lines.
'''
""",
)
@Method(
positional=False,
__doc__='''
__doc__="""
Builds the CSV reader, a.k.a an object we can iterate, each iteration giving one line of fields, as an
iterable.
Defaults to builtin csv.reader(...), but can be overriden to fit your special needs.
'''
""",
)
def reader_factory(self, file):
return csv.reader(file, **self.get_dialect_kwargs())
def read(self, file, context, *, fs):
context.setdefault('skipped', 0)
context.setdefault("skipped", 0)
reader = self.reader_factory(file)
skip = self.skip
@ -96,18 +96,18 @@ class CsvReader(FileReader, CsvHandler):
@use_context
class CsvWriter(FileWriter, CsvHandler):
@Method(
__doc__='''
__doc__="""
Builds the CSV writer, a.k.a an object we can pass a field collection to be written as one line in the
target file.
Defaults to builtin csv.writer(...).writerow, but can be overriden to fit your special needs.
'''
"""
)
def writer_factory(self, file):
return csv.writer(file, **self.get_dialect_kwargs()).writerow
def write(self, file, context, *values, fs):
context.setdefault('lineno', 0)
context.setdefault("lineno", 0)
fields = context.get_input_fields()
if not context.lineno:
@ -120,7 +120,7 @@ class CsvWriter(FileWriter, CsvHandler):
if fields:
if len(values) != len(fields):
raise ValueError(
'Values length differs from input fields length. Expected: {}. Got: {}. Values: {!r}.'.format(
"Values length differs from input fields length. Expected: {}. Got: {}. Values: {!r}.".format(
len(fields), len(values), values
)
)

View File

@ -12,24 +12,28 @@ class FileReader(Reader, FileHandler):
present. Extending it is usually the right way to create more specific file readers (like json, csv, etc.)
"""
mode = Option(str, default='r', __doc__='''
mode = Option(
str,
default="r",
__doc__="""
What mode to use for open() call.
''') # type: str
""",
) # type: str
output_fields = Option(
ensure_tuple,
required=False,
__doc__='''
__doc__="""
Specify the field names of output lines.
Mutually exclusive with "output_type".
'''
""",
)
output_type = Option(
required=False,
__doc__='''
__doc__="""
Specify the type of output lines.
Mutually exclusive with "output_fields".
'''
""",
)
@ContextProcessor
@ -43,7 +47,7 @@ class FileReader(Reader, FileHandler):
output_type = self.output_type
if output_fields and output_type:
raise UnrecoverableError('Cannot specify both output_fields and output_type option.')
raise UnrecoverableError("Cannot specify both output_fields and output_type option.")
if self.output_type:
context.set_output_type(self.output_type)
@ -72,16 +76,20 @@ class FileWriter(Writer, FileHandler):
usually the right way to create more specific file writers (like json, csv, etc.)
"""
mode = Option(str, default='w+', __doc__='''
mode = Option(
str,
default="w+",
__doc__="""
What mode to use for open() call.
''') # type: str
""",
) # type: str
def write(self, file, context, line, *, fs):
"""
Write a row on the next line of opened file in context.
"""
context.setdefault('lineno', 0)
self._write_line(file, (self.eol if context.lineno else '') + line)
context.setdefault("lineno", 0)
self._write_line(file, (self.eol if context.lineno else "") + line)
context.lineno += 1
return NOT_MODIFIED

View File

@ -9,13 +9,13 @@ from bonobo.nodes.io.file import FileReader, FileWriter
class JsonHandler(FileHandler):
eol = ',\n'
prefix, suffix = '[', ']'
eol = ",\n"
prefix, suffix = "[", "]"
class LdjsonHandler(FileHandler):
eol = '\n'
prefix, suffix = '', ''
eol = "\n"
prefix, suffix = "", ""
class JsonReader(JsonHandler, FileReader):
@ -58,16 +58,16 @@ class JsonWriter(JsonHandler, FileWriter):
:param ctx:
:param row:
"""
context.setdefault('lineno', 0)
context.setdefault("lineno", 0)
fields = context.get_input_fields()
if fields:
prefix = self.eol if context.lineno else ''
prefix = self.eol if context.lineno else ""
self._write_line(file, prefix + json.dumps(OrderedDict(zip(fields, args))))
context.lineno += 1
else:
for arg in args:
prefix = self.eol if context.lineno else ''
prefix = self.eol if context.lineno else ""
self._write_line(file, prefix + json.dumps(arg))
context.lineno += 1

View File

@ -24,7 +24,7 @@ class PickleReader(FileReader, PickleHandler):
Reads a Python pickle object and yields the items in dicts.
"""
mode = Option(str, default='rb')
mode = Option(str, default="rb")
def read(self, file, context, *, fs):
data = pickle.load(file)
@ -47,7 +47,7 @@ class PickleReader(FileReader, PickleHandler):
for row in iterator:
if len(row) != fields_length:
raise ValueError('Received an object with {} items, expected {}.'.format(len(row), fields_length))
raise ValueError("Received an object with {} items, expected {}.".format(len(row), fields_length))
yield tuple(row.values() if is_dict else row)
@ -56,13 +56,13 @@ class PickleReader(FileReader, PickleHandler):
@use_context
class PickleWriter(FileWriter, PickleHandler):
mode = Option(str, default='wb')
mode = Option(str, default="wb")
def write(self, file, context, item, *, fs):
"""
Write a pickled item to the opened file.
"""
context.setdefault('lineno', 0)
context.setdefault("lineno", 0)
file.write(pickle.dumps(item))
context.lineno += 1
return NOT_MODIFIED

View File

@ -35,7 +35,7 @@ class ConsoleOutputPlugin(Plugin):
isatty = False
# Whether we're on windows, or a real operating system.
iswindows = (sys.platform == 'win32')
iswindows = sys.platform == "win32"
def __init__(self):
self.isatty = self._stdout.isatty()
@ -55,9 +55,9 @@ class ConsoleOutputPlugin(Plugin):
# Two options:
# - move state to context
# - forbid registering more than once
self.prefix = ''
self.prefix = ""
self.counter = 0
self._append_cache = ''
self._append_cache = ""
self.stdout = IOBuffer()
self.redirect_stdout = redirect_stdout(self._stdout if self.iswindows else self.stdout)
@ -78,13 +78,13 @@ class ConsoleOutputPlugin(Plugin):
self.redirect_stderr.__exit__(None, None, None)
self.redirect_stdout.__exit__(None, None, None)
def write(self, context, prefix='', rewind=True, append=None):
def write(self, context, prefix="", rewind=True, append=None):
t_cnt = len(context)
if not self.iswindows:
for line in self.stdout.switch().split('\n')[:-1]:
for line in self.stdout.switch().split("\n")[:-1]:
print(line + CLEAR_EOL, file=self._stdout)
for line in self.stderr.switch().split('\n')[:-1]:
for line in self.stderr.switch().split("\n")[:-1]:
print(line + CLEAR_EOL, file=self._stderr)
alive_color = Style.BRIGHT
@ -92,31 +92,36 @@ class ConsoleOutputPlugin(Plugin):
for i in context.graph.topologically_sorted_indexes:
node = context[i]
name_suffix = '({})'.format(i) if settings.DEBUG.get() else ''
name_suffix = "({})".format(i) if settings.DEBUG.get() else ""
liveliness_color = alive_color if node.alive else dead_color
liveliness_prefix = ' {}{}{} '.format(liveliness_color, node.status, Style.RESET_ALL)
_line = ''.join((
liveliness_prefix,
node.name,
name_suffix,
' ',
node.get_statistics_as_string(),
' ',
node.get_flags_as_string(),
Style.RESET_ALL,
' ',
))
liveliness_prefix = " {}{}{} ".format(liveliness_color, node.status, Style.RESET_ALL)
_line = "".join(
(
liveliness_prefix,
node.name,
name_suffix,
" ",
node.get_statistics_as_string(),
" ",
node.get_flags_as_string(),
Style.RESET_ALL,
" ",
)
)
print(prefix + _line + CLEAR_EOL, file=self._stderr)
if append:
# todo handle multiline
print(
''.join((
' `-> ', ' '.join('{}{}{}: {}'.format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
CLEAR_EOL
)),
file=self._stderr
"".join(
(
" `-> ",
" ".join("{}{}{}: {}".format(Style.BRIGHT, k, Style.RESET_ALL, v) for k, v in append),
CLEAR_EOL,
)
),
file=self._stderr,
)
t_cnt += 1
@ -129,16 +134,17 @@ class ConsoleOutputPlugin(Plugin):
if self.counter % 10 and self._append_cache:
append = self._append_cache
else:
self._append_cache = append = (('Memory', '{0:.2f} Mb'.format(memory_usage())),
# ('Total time', '{0} s'.format(execution_time(harness))),
)
self._append_cache = append = (
("Memory", "{0:.2f} Mb".format(memory_usage())),
# ('Total time', '{0} s'.format(execution_time(harness))),
)
else:
append = ()
self.write(context, prefix=self.prefix, append=append, rewind=rewind)
self.counter += 1
class IOBuffer():
class IOBuffer:
"""
The role of IOBuffer is to overcome the problem of multiple threads wanting to write to stdout at the same time. It
works a bit like a videogame: there are two buffers, one that is used to write, and one which is used to read from.
@ -165,5 +171,6 @@ class IOBuffer():
def memory_usage():
import os, psutil
process = psutil.Process(os.getpid())
return process.memory_info()[0] / float(2**20)
return process.memory_info()[0] / float(2 ** 20)

View File

@ -8,9 +8,9 @@ try:
import IPython.core.display
except ImportError as e:
logging.exception(
'You must install Jupyter to use the bonobo Jupyter extension. Easiest way is to install the '
"You must install Jupyter to use the bonobo Jupyter extension. Easiest way is to install the "
'optional "jupyter" dependencies with «pip install bonobo[jupyter]», but you can also install a '
'specific version by yourself.'
"specific version by yourself."
)

View File

@ -5,23 +5,23 @@ from bonobo.nodes import (
CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter, PickleReader, PickleWriter
)
FILETYPE_CSV = 'text/csv'
FILETYPE_JSON = 'application/json'
FILETYPE_PICKLE = 'pickle'
FILETYPE_PLAIN = 'text/plain'
FILETYPE_CSV = "text/csv"
FILETYPE_JSON = "application/json"
FILETYPE_PICKLE = "pickle"
FILETYPE_PLAIN = "text/plain"
READER = 'reader'
WRITER = 'writer'
READER = "reader"
WRITER = "writer"
class Registry:
ALIASES = {
'csv': FILETYPE_CSV,
'json': FILETYPE_JSON,
'pickle': FILETYPE_PICKLE,
'plain': FILETYPE_PLAIN,
'text': FILETYPE_PLAIN,
'txt': FILETYPE_PLAIN,
"csv": FILETYPE_CSV,
"json": FILETYPE_JSON,
"pickle": FILETYPE_PICKLE,
"plain": FILETYPE_PLAIN,
"text": FILETYPE_PLAIN,
"txt": FILETYPE_PLAIN,
}
FACTORIES = {
@ -41,10 +41,10 @@ class Registry:
def get_factory_for(self, kind, name, *, format=None):
if not kind in self.FACTORIES:
raise KeyError('Unknown factory kind {!r}.'.format(kind))
raise KeyError("Unknown factory kind {!r}.".format(kind))
if format is None and name is None:
raise RuntimeError('Cannot guess factory without at least a filename or a format.')
raise RuntimeError("Cannot guess factory without at least a filename or a format.")
# Guess mimetype if possible
if format is None:
@ -62,7 +62,7 @@ class Registry:
if format is None or not format in self.FACTORIES[kind]:
raise RuntimeError(
'Could not resolve {kind} factory for {name} ({format}).'.format(kind=kind, name=name, format=format)
"Could not resolve {kind} factory for {name} ({format}).".format(kind=kind, name=name, format=format)
)
return self.FACTORIES[kind][format]

View File

@ -10,7 +10,7 @@ def to_bool(s):
if type(s) is bool:
return s
if len(s):
if s.lower() in ('f', 'false', 'n', 'no', '0'):
if s.lower() in ("f", "false", "n", "no", "0"):
return False
return True
return False
@ -40,7 +40,7 @@ class Setting:
self.formatter = formatter
def __repr__(self):
return '<Setting {}={!r}>'.format(self.name, self.get())
return "<Setting {}={!r}>".format(self.name, self.get())
def __eq__(self, other):
return self.get() == other
@ -51,7 +51,7 @@ class Setting:
def set(self, value):
value = self.formatter(value) if self.formatter else value
if self.validator and not self.validator(value):
raise ValidationError(self, 'Invalid value {!r} for setting {!r}.'.format(value, self.name))
raise ValidationError(self, "Invalid value {!r} for setting {!r}.".format(value, self.name))
self.value = value
def set_if_true(self, value):
@ -78,40 +78,37 @@ class Setting:
# Debug/verbose mode.
DEBUG = Setting('DEBUG', formatter=to_bool, default=False)
DEBUG = Setting("DEBUG", formatter=to_bool, default=False)
# Profile mode.
PROFILE = Setting('PROFILE', formatter=to_bool, default=False)
PROFILE = Setting("PROFILE", formatter=to_bool, default=False)
# Alpha mode.
ALPHA = Setting('ALPHA', formatter=to_bool, default=False)
ALPHA = Setting("ALPHA", formatter=to_bool, default=False)
# Quiet mode.
QUIET = Setting('QUIET', formatter=to_bool, default=False)
QUIET = Setting("QUIET", formatter=to_bool, default=False)
# Logging level.
LOGGING_LEVEL = Setting(
'LOGGING_LEVEL',
"LOGGING_LEVEL",
formatter=logging._checkLevel,
validator=logging._checkLevel,
default=lambda: logging.DEBUG if DEBUG.get() else logging.INFO
default=lambda: logging.DEBUG if DEBUG.get() else logging.INFO,
)
# Input/Output format for transformations
IOFORMAT_ARG0 = 'arg0'
IOFORMAT_KWARGS = 'kwargs'
IOFORMAT_ARG0 = "arg0"
IOFORMAT_KWARGS = "kwargs"
IOFORMATS = {
IOFORMAT_ARG0,
IOFORMAT_KWARGS,
}
IOFORMATS = {IOFORMAT_ARG0, IOFORMAT_KWARGS}
IOFORMAT = Setting('IOFORMAT', default=IOFORMAT_KWARGS, validator=IOFORMATS.__contains__)
IOFORMAT = Setting("IOFORMAT", default=IOFORMAT_KWARGS, validator=IOFORMATS.__contains__)
def check():
if DEBUG.get() and QUIET.get():
raise RuntimeError('I cannot be verbose and quiet at the same time.')
raise RuntimeError("I cannot be verbose and quiet at the same time.")
clear_all = Setting.clear_all

View File

@ -9,24 +9,51 @@ from graphviz.dot import Digraph
from bonobo.constants import BEGIN
from bonobo.util import get_name
GraphRange = namedtuple('GraphRange', ['graph', 'input', 'output'])
GraphRange = namedtuple("GraphRange", ["graph", "input", "output"])
class GraphCursor:
def __init__(self, graph, node):
@property
def input(self):
return self.first
@property
def output(self):
return self.last
def __init__(self, graph, *, first=None, last=None):
self.graph = graph
self.node = node
self.first = first or last
self.last = last
def __rshift__(self, other):
""" Self >> Other """
chain = self.graph.add_chain(other, _input=self.node)
return GraphCursor(chain.graph, chain.output)
if other == ...:
raise NotImplementedError(
"Expected something looking like a node, but got an Ellipsis (...). Did you forget to complete the graph?"
)
nodes = other.nodes if hasattr(other, "nodes") else [other]
if len(nodes):
chain = self.graph.add_chain(*nodes, _input=self.last)
return GraphCursor(chain.graph, first=self.first, last=chain.output)
return self
class PartialGraph:
def __init__(self, *nodes):
self.nodes = list(nodes)
class Graph:
"""
Represents a directed graph of nodes.
"""
name = ''
name = ""
def __init__(self, *chain):
self.edges = {BEGIN: set()}
@ -46,7 +73,7 @@ class Graph:
return self.nodes[key]
def get_cursor(self, ref=BEGIN):
return GraphCursor(self, self._resolve_index(ref))
return GraphCursor(self, last=self._resolve_index(ref))
def outputs_of(self, idx, create=False):
""" Get a set of the outputs for a given node index.
@ -76,7 +103,7 @@ class Graph:
_last = self.add_node(node)
if not i and _name:
if _name in self.named:
raise KeyError('Duplicate name {!r} in graph.'.format(_name))
raise KeyError("Duplicate name {!r} in graph.".format(_name))
self.named[_name] = _last
if _first is None:
_first = _last
@ -86,7 +113,7 @@ class Graph:
if _output is not None:
self.outputs_of(_input, create=True).add(_output)
if hasattr(self, '_topologcally_sorted_indexes_cache'):
if hasattr(self, "_topologcally_sorted_indexes_cache"):
del self._topologcally_sorted_indexes_cache
return GraphRange(self, _first, _last)
@ -144,10 +171,10 @@ class Graph:
return self._graphviz
except AttributeError:
g = Digraph()
g.attr(rankdir='LR')
g.node('BEGIN', shape='point')
g.attr(rankdir="LR")
g.node("BEGIN", shape="point")
for i in self.outputs_of(BEGIN):
g.edge('BEGIN', str(i))
g.edge("BEGIN", str(i))
for ix in self.topologically_sorted_indexes:
g.node(str(ix), label=get_name(self[ix]))
for iy in self.outputs_of(ix):
@ -160,9 +187,9 @@ class Graph:
def _repr_html_(self):
try:
return '<div>{}</div><pre>{}</pre>'.format(self.graphviz._repr_svg_(), html.escape(repr(self)))
return "<div>{}</div><pre>{}</pre>".format(self.graphviz._repr_svg_(), html.escape(repr(self)))
except (ExecutableNotFound, FileNotFoundError) as exc:
return '<strong>{}</strong>: {}'.format(type(exc).__name__, str(exc))
return "<strong>{}</strong>: {}".format(type(exc).__name__, str(exc))
def _resolve_index(self, mixed):
"""
@ -182,10 +209,10 @@ class Graph:
if mixed in self.nodes:
return self.nodes.index(mixed)
raise ValueError('Cannot find node matching {!r}.'.format(mixed))
raise ValueError("Cannot find node matching {!r}.".format(mixed))
def _get_graphviz_node_id(graph, i):
escaped_index = str(i)
escaped_name = json.dumps(get_name(graph[i]))
return '{{{} [label={}]}}'.format(escaped_index, escaped_name)
return "{{{} [label={}]}}".format(escaped_index, escaped_name)

View File

@ -70,7 +70,7 @@ class Input(Queue, Readable, Writable):
# Check we are actually able to receive data.
if self._writable_runlevel < 1:
raise InactiveWritableError('Cannot put() on an inactive {}.'.format(Writable.__name__))
raise InactiveWritableError("Cannot put() on an inactive {}.".format(Writable.__name__))
if data == END:
self._writable_runlevel -= 1
@ -85,7 +85,7 @@ class Input(Queue, Readable, Writable):
def get(self, block=True, timeout=None):
if not self.alive:
raise InactiveReadableError('Cannot get() on an inactive {}.'.format(Readable.__name__))
raise InactiveReadableError("Cannot get() on an inactive {}.".format(Readable.__name__))
data = Queue.get(self, block, timeout)
@ -94,7 +94,7 @@ class Input(Queue, Readable, Writable):
if not self.alive:
raise InactiveReadableError(
'Cannot get() on an inactive {} (runlevel just reached 0).'.format(Readable.__name__)
"Cannot get() on an inactive {} (runlevel just reached 0).".format(Readable.__name__)
)
return self.get(block, timeout)

View File

@ -3,7 +3,7 @@ class Token:
self.__name__ = name
def __repr__(self):
return '<{}>'.format(self.__name__)
return "<{}>".format(self.__name__)
class Flag(Token):

View File

@ -16,25 +16,25 @@ from bonobo.util.inspect import (
istuple,
istype,
)
from bonobo.util.objects import (get_name, get_attribute_or_create, ValueHolder)
from bonobo.util.objects import get_name, get_attribute_or_create, ValueHolder
# Bonobo's util API
__all__ = [
'ValueHolder',
'cast',
'deprecated',
'deprecated_alias',
'ensure_tuple',
'get_attribute_or_create',
'get_name',
'inspect_node',
'isconfigurable',
'isconfigurabletype',
'iscontextprocessor',
'isdict',
'ismethod',
'isoption',
'istype',
'sortedlist',
'tuplize',
"ValueHolder",
"cast",
"deprecated",
"deprecated_alias",
"ensure_tuple",
"get_attribute_or_create",
"get_name",
"inspect_node",
"isconfigurable",
"isconfigurabletype",
"iscontextprocessor",
"isdict",
"ismethod",
"isoption",
"istype",
"sortedlist",
"tuplize",
]

View File

@ -12,14 +12,14 @@ class ApiHelper:
if graph:
# This function must comply to the "graph" API interface, meaning it can bahave like bonobo.run.
from inspect import signature
parameters = list(signature(x).parameters)
required_parameters = {'plugins', 'services', 'strategy'}
assert len(parameters
) > 0 and parameters[0] == 'graph', 'First parameter of a graph api function must be "graph".'
assert required_parameters.intersection(
parameters
) == required_parameters, 'Graph api functions must define the following parameters: ' + ', '.join(
sorted(required_parameters)
required_parameters = {"plugins", "services", "strategy"}
assert (
len(parameters) > 0 and parameters[0] == "graph"
), 'First parameter of a graph api function must be "graph".'
assert required_parameters.intersection(parameters) == required_parameters, (
"Graph api functions must define the following parameters: " + ", ".join(sorted(required_parameters))
)
self.__all__.append(get_name(x))

View File

@ -71,16 +71,18 @@ class {typename}(tuple):
{field_defs}
'''
_field_template = '''\
_field_template = """\
{name} = _property(_itemgetter({index:d}), doc={doc!r})
'''.strip('\n')
_reserved = frozenset(
['_', '_cls', '_attrs', '_fields', 'get', '_asdict', '_replace', '_make', 'self', '_self', 'tuple'] + dir(tuple)
""".strip(
"\n"
)
_multiple_underscores_pattern = re.compile('__+')
_slugify_allowed_chars_pattern = re.compile(r'[^a-z0-9_]+', flags=re.IGNORECASE)
_reserved = frozenset(
["_", "_cls", "_attrs", "_fields", "get", "_asdict", "_replace", "_make", "self", "_self", "tuple"] + dir(tuple)
)
_multiple_underscores_pattern = re.compile("__+")
_slugify_allowed_chars_pattern = re.compile(r"[^a-z0-9_]+", flags=re.IGNORECASE)
def _uniquify(f):
@ -90,13 +92,13 @@ def _uniquify(f):
def _uniquified(x):
nonlocal f, seen
x = str(x)
v = v0 = _multiple_underscores_pattern.sub('_', f(x))
v = v0 = _multiple_underscores_pattern.sub("_", f(x))
i = 0
# if last character is not "allowed", let's start appending indexes right from the first iteration
if len(x) and _slugify_allowed_chars_pattern.match(x[-1]):
v = '{}{}'.format(v0, i)
v = "{}{}".format(v0, i)
while v in seen:
v = '{}{}'.format(v0, i)
v = "{}{}".format(v0, i)
i += 1
seen.add(v)
return v
@ -106,13 +108,13 @@ def _uniquify(f):
def _make_valid_attr_name(x):
if iskeyword(x):
x = '_' + x
x = "_" + x
if x.isidentifier():
return x
x = slugify(x, separator='_', regex_pattern=_slugify_allowed_chars_pattern)
x = slugify(x, separator="_", regex_pattern=_slugify_allowed_chars_pattern)
if x.isidentifier():
return x
x = '_' + x
x = "_" + x
if x.isidentifier():
return x
raise ValueError(x)
@ -124,23 +126,23 @@ def BagType(typename, fields, *, verbose=False, module=None):
attrs = tuple(map(_uniquify(_make_valid_attr_name), fields))
if type(fields) is str:
raise TypeError('BagType does not support providing fields as a string.')
raise TypeError("BagType does not support providing fields as a string.")
fields = list(map(str, fields))
typename = str(typename)
for i, name in enumerate([typename] + fields):
if type(name) is not str:
raise TypeError('Type names and field names must be strings, got {name!r}'.format(name=name))
raise TypeError("Type names and field names must be strings, got {name!r}".format(name=name))
if not i:
if not name.isidentifier():
raise ValueError('Type names must be valid identifiers: {name!r}'.format(name=name))
raise ValueError("Type names must be valid identifiers: {name!r}".format(name=name))
if iskeyword(name):
raise ValueError('Type names cannot be a keyword: {name!r}'.format(name=name))
raise ValueError("Type names cannot be a keyword: {name!r}".format(name=name))
seen = set()
for name in fields:
if name in seen:
raise ValueError('Encountered duplicate field name: {name!r}'.format(name=name))
raise ValueError("Encountered duplicate field name: {name!r}".format(name=name))
seen.add(name)
# Fill-in the class template
@ -150,21 +152,24 @@ def BagType(typename, fields, *, verbose=False, module=None):
attrs=attrs,
num_fields=len(fields),
arg_list=repr(attrs).replace("'", "")[1:-1],
repr_fmt=', '.join(('%r' if isinstance(fields[index], int) else '{name}=%r').format(name=name)
for index, name in enumerate(attrs)),
field_defs='\n'.join(
repr_fmt=", ".join(
("%r" if isinstance(fields[index], int) else "{name}=%r").format(name=name)
for index, name in enumerate(attrs)
),
field_defs="\n".join(
_field_template.format(
index=index,
name=name,
doc='Alias for ' +
('field #{}'.format(index) if isinstance(fields[index], int) else repr(fields[index]))
) for index, name in enumerate(attrs)
)
doc="Alias for "
+ ("field #{}".format(index) if isinstance(fields[index], int) else repr(fields[index])),
)
for index, name in enumerate(attrs)
),
)
# Execute the template string in a temporary namespace and support
# tracing utilities by setting a value for frame.f_globals['__name__']
namespace = dict(__name__='namedtuple_%s' % typename)
namespace = dict(__name__="namedtuple_%s" % typename)
exec(class_definition, namespace)
result = namespace[typename]
result._source = class_definition
@ -178,7 +183,7 @@ def BagType(typename, fields, *, verbose=False, module=None):
# specified a particular module.
if module is None:
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
module = sys._getframe(1).f_globals.get("__name__", "__main__")
except (AttributeError, ValueError):
pass
if module is not None:

View File

@ -25,7 +25,7 @@ def _with_length_check(f):
if length is not None:
if length != len(result):
raise TypeError(
'Length check failed, expected {} fields but got {}: {!r}.'.format(length, len(result), result)
"Length check failed, expected {} fields but got {}: {!r}.".format(length, len(result), result)
)
return result
@ -54,7 +54,7 @@ def ensure_tuple(tuple_or_mixed, *, cls=None):
if isinstance(tuple_or_mixed, tuple):
return tuple.__new__(cls, tuple_or_mixed)
return tuple.__new__(cls, (tuple_or_mixed, ))
return tuple.__new__(cls, (tuple_or_mixed,))
def cast(type_):

View File

@ -5,13 +5,13 @@ import warnings
def deprecated_alias(alias, func):
@functools.wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.simplefilter("always", DeprecationWarning) # turn off filter
warnings.warn(
"Call to deprecated function alias {}, use {} instead.".format(alias, func.__name__),
category=DeprecationWarning,
stacklevel=2
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
warnings.simplefilter("default", DeprecationWarning) # reset filter
return func(*args, **kwargs)
return new_func
@ -24,11 +24,11 @@ def deprecated(func):
@functools.wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.simplefilter("always", DeprecationWarning) # turn off filter
warnings.warn(
"Call to deprecated function {}.".format(func.__name__), category=DeprecationWarning, stacklevel=2
)
warnings.simplefilter('default', DeprecationWarning) # reset filter
warnings.simplefilter("default", DeprecationWarning) # reset filter
return func(*args, **kwargs)
return new_func

View File

@ -1,8 +1,8 @@
from bonobo.structs.tokens import Flag
F_INHERIT = Flag('Inherit')
F_INHERIT = Flag("Inherit")
F_NOT_MODIFIED = Flag('NotModified')
F_NOT_MODIFIED = Flag("NotModified")
F_NOT_MODIFIED.must_be_first = True
F_NOT_MODIFIED.must_be_last = True
F_NOT_MODIFIED.allows_data = False

View File

@ -5,12 +5,12 @@ import re
import warnings
from contextlib import contextmanager
__escape_decoder = codecs.getdecoder('unicode_escape')
__posix_variable = re.compile('\$\{[^\}]*\}')
__escape_decoder = codecs.getdecoder("unicode_escape")
__posix_variable = re.compile("\$\{[^\}]*\}")
def parse_var(var):
name, value = var.split('=', 1)
name, value = var.split("=", 1)
def decode_escaped(escaped):
return __escape_decoder(escaped)[0]
@ -29,15 +29,15 @@ def load_env_from_file(filename):
Read an env file into a collection of (name, value) tuples.
"""
if not os.path.exists(filename):
raise FileNotFoundError('Environment file {} does not exist.'.format(filename))
raise FileNotFoundError("Environment file {} does not exist.".format(filename))
with open(filename) as f:
for lineno, line in enumerate(f):
line = line.strip()
if not line or line.startswith('#'):
if not line or line.startswith("#"):
continue
if '=' not in line:
raise SyntaxError('Invalid environment file syntax in {} at line {}.'.format(filename, lineno + 1))
if "=" not in line:
raise SyntaxError("Invalid environment file syntax in {} at line {}.".format(filename, lineno + 1))
name, value = parse_var(line)
@ -64,10 +64,10 @@ def get_argument_parser(parser=None):
global _parser
_parser = parser
_parser.add_argument('--default-env-file', '-E', action='append')
_parser.add_argument('--default-env', action='append')
_parser.add_argument('--env-file', action='append')
_parser.add_argument('--env', '-e', action='append')
_parser.add_argument("--default-env-file", "-E", action="append")
_parser.add_argument("--default-env", action="append")
_parser.add_argument("--env-file", action="append")
_parser.add_argument("--env", "-e", action="append")
return _parser
@ -89,10 +89,11 @@ def parse_args(mixed=None):
global _parser
if _parser is not None:
warnings.warn(
'You are calling bonobo.parse_args() without a parser argument, but it looks like you created a parser before. You probably want to pass your parser to this call, or if creating a new parser here is really what you want to do, please create a new one explicitely to silence this warning.'
"You are calling bonobo.parse_args() without a parser argument, but it looks like you created a parser before. You probably want to pass your parser to this call, or if creating a new parser here is really what you want to do, please create a new one explicitely to silence this warning."
)
# use the api from bonobo namespace, in case a command patched it.
import bonobo
mixed = bonobo.get_argument_parser()
if isinstance(mixed, argparse.ArgumentParser):
@ -117,14 +118,14 @@ def parse_args(mixed=None):
# env-file sets something.)
try:
# Set default environment
for name, value in map(parse_var, options.pop('default_env', []) or []):
for name, value in map(parse_var, options.pop("default_env", []) or []):
if not name in os.environ:
if not name in _backup:
_backup[name] = os.environ.get(name, None)
os.environ[name] = value
# Read and set default environment from file(s)
for filename in options.pop('default_env_file', []) or []:
for filename in options.pop("default_env_file", []) or []:
for name, value in load_env_from_file(filename):
if not name in os.environ:
if not name in _backup:
@ -132,14 +133,14 @@ def parse_args(mixed=None):
os.environ[name] = value
# Read and set environment from file(s)
for filename in options.pop('env_file', []) or []:
for filename in options.pop("env_file", []) or []:
for name, value in load_env_from_file(filename):
if not name in _backup:
_backup[name] = os.environ.get(name, None)
os.environ[name] = value
# Set environment
for name, value in map(parse_var, options.pop('env', []) or []):
for name, value in map(parse_var, options.pop("env", []) or []):
if not name in _backup:
_backup[name] = os.environ.get(name, None)
os.environ[name] = value

View File

@ -15,36 +15,32 @@ def sweeten_errors():
except Exception as exc:
SPACES = 2
w = term.white
prefix = w('' + ' ' * (SPACES - 1))
suffix = w(' ' * (SPACES - 1) + '')
prefix = w("" + " " * (SPACES - 1))
suffix = w(" " * (SPACES - 1) + "")
pre_re = re.compile('([^`]*)`([^`]*)`([^`]*)')
pre_re = re.compile("([^`]*)`([^`]*)`([^`]*)")
def format_arg(arg):
length = len(pre_re.sub('\\1\\2\\3', arg))
length = len(pre_re.sub("\\1\\2\\3", arg))
arg = pre_re.sub(w('\\1') + term.bold('\\2') + w('\\3'), arg)
arg = re.sub('^ \$ (.*)', term.lightblack(' $ ') + term.reset('\\1'), arg)
arg = pre_re.sub(w("\\1") + term.bold("\\2") + w("\\3"), arg)
arg = re.sub("^ \$ (.*)", term.lightblack(" $ ") + term.reset("\\1"), arg)
return (arg, length)
def f(*args):
return ''.join(args)
return "".join(args)
term_width, term_height = term.get_size()
line_length = min(80, term_width)
for arg in exc.args:
line_length = max(min(line_length, len(arg) + 2 * SPACES), 120)
print(f(w('' + '' * (line_length - 2) + '')))
print(f(w("" + "" * (line_length - 2) + "")))
for i, arg in enumerate(exc.args):
if i == 1:
print(f(
prefix,
' ' * (line_length - 2 * SPACES),
suffix,
))
print(f(prefix, " " * (line_length - 2 * SPACES), suffix))
arg_formatted, arg_length = format_arg(arg)
if not i:
@ -52,17 +48,17 @@ def sweeten_errors():
print(
f(
prefix,
term.red_bg(term.bold(' ' + type(exc).__name__ + ' ')),
' ',
term.red_bg(term.bold(" " + type(exc).__name__ + " ")),
" ",
w(arg_formatted),
' ' * (line_length - (arg_length + 3 + len(type(exc).__name__) + 2 * SPACES)),
" " * (line_length - (arg_length + 3 + len(type(exc).__name__) + 2 * SPACES)),
suffix,
)
)
else:
# other lines
print(f(prefix, arg_formatted + ' ' * (line_length - arg_length - 2 * SPACES), suffix))
print(f(prefix, arg_formatted + " " * (line_length - arg_length - 2 * SPACES), suffix))
print(f(w('' + '' * (line_length - 2) + '')))
print(f(w("" + "" * (line_length - 2) + "")))
logging.getLogger().debug('This error was caused by the following exception chain.', exc_info=exc_info())
logging.getLogger().debug("This error was caused by the following exception chain.", exc_info=exc_info())

View File

@ -9,6 +9,7 @@ def isconfigurable(mixed):
:return: bool
"""
from bonobo.config.configurables import Configurable
return isinstance(mixed, Configurable)
@ -32,7 +33,7 @@ def isconfigurabletype(mixed, *, strict=False):
if isinstance(mixed, PartiallyConfigured):
return True
if hasattr(mixed, '_partial') and mixed._partial:
if hasattr(mixed, "_partial") and mixed._partial:
return True
return False
@ -47,6 +48,7 @@ def isoption(mixed):
"""
from bonobo.config.options import Option
return isinstance(mixed, Option)
@ -58,6 +60,7 @@ def ismethod(mixed):
:return: bool
"""
from bonobo.config.options import Method
return isinstance(mixed, Method)
@ -69,6 +72,7 @@ def iscontextprocessor(x):
:return: bool
"""
from bonobo.config.processors import ContextProcessor
return isinstance(x, ContextProcessor)
@ -102,15 +106,7 @@ def istuple(mixed):
return isinstance(mixed, tuple)
ConfigurableInspection = namedtuple(
'ConfigurableInspection', [
'type',
'instance',
'options',
'processors',
'partial',
]
)
ConfigurableInspection = namedtuple("ConfigurableInspection", ["type", "instance", "options", "processors", "partial"])
ConfigurableInspection.__enter__ = lambda self: self
ConfigurableInspection.__exit__ = lambda *exc_details: None
@ -134,17 +130,11 @@ def inspect_node(mixed, *, _partial=None):
inst, typ = None, mixed
elif isconfigurable(mixed):
inst, typ = mixed, type(mixed)
elif hasattr(mixed, 'func'):
elif hasattr(mixed, "func"):
return inspect_node(mixed.func, _partial=(mixed.args, mixed.keywords))
else:
raise TypeError(
'Not a Configurable, nor a Configurable instance and not even a partially configured Configurable. Check your inputs.'
"Not a Configurable, nor a Configurable instance and not even a partially configured Configurable. Check your inputs."
)
return ConfigurableInspection(
typ,
inst,
list(typ.__options__),
list(typ.__processors__),
_partial,
)
return ConfigurableInspection(typ, inst, list(typ.__options__), list(typ.__processors__), _partial)

View File

@ -11,7 +11,7 @@ class Wrapper:
@property
def __name__(self):
return getattr(self.wrapped, '__name__', getattr(type(self.wrapped), '__name__', repr(self.wrapped)))
return getattr(self.wrapped, "__name__", getattr(type(self.wrapped), "__name__", repr(self.wrapped)))
name = __name__
@ -142,10 +142,10 @@ class ValueHolder:
return divmod(other, self._value)
def __pow__(self, other):
return self._value**other
return self._value ** other
def __rpow__(self, other):
return other**self._value
return other ** self._value
def __ipow__(self, other):
self._value **= other

View File

@ -4,5 +4,5 @@ from packaging.utils import canonicalize_name
bonobo_packages = {}
for p in pkg_resources.working_set:
name = canonicalize_name(p.project_name)
if name.startswith('bonobo'):
if name.startswith("bonobo"):
bonobo_packages[name] = p

View File

@ -23,8 +23,8 @@ class _ModulesRegistry(dict):
def require(self, name):
if name not in self:
bits = name.split('.')
filename = os.path.join(self.pathname, *bits[:-1], bits[-1] + '.py')
bits = name.split(".")
filename = os.path.join(self.pathname, *bits[:-1], bits[-1] + ".py")
self[name] = _RequiredModule(runpy.run_path(filename, run_name=name))
return self[name]
@ -37,7 +37,7 @@ def _parse_option(option):
:return: tuple
"""
try:
key, val = option.split('=', 1)
key, val = option.split("=", 1)
except ValueError:
return option, True
@ -75,7 +75,7 @@ def _resolve_transformations(transformations):
transformations = transformations or []
for t in transformations:
try:
mod, attr = t.split(':', 1)
mod, attr = t.split(":", 1)
yield getattr(registry.require(mod), attr)
except ValueError:
yield getattr(bonobo, t)

View File

@ -10,8 +10,8 @@ class WithStatistics:
return ((name, self.statistics[name]) for name in self.statistics_names)
def get_statistics_as_string(self, *args, **kwargs):
stats = tuple('{0}={1}'.format(name, cnt) for name, cnt in self.get_statistics(*args, **kwargs) if cnt > 0)
return (kwargs.get('prefix', '') + ' '.join(stats)) if len(stats) else ''
stats = tuple("{0}={1}".format(name, cnt) for name, cnt in self.get_statistics(*args, **kwargs) if cnt > 0)
return (kwargs.get("prefix", "") + " ".join(stats)) if len(stats) else ""
def increment(self, name, *, amount=1):
self.statistics[name] += amount
@ -35,4 +35,4 @@ class Timer:
return self.__finish - self.__start
def __str__(self):
return str(int(self.duration * 1000) / 1000.0) + 's'
return str(int(self.duration * 1000) / 1000.0) + "s"

View File

@ -1,2 +1,2 @@
CLEAR_EOL = '\033[0K'
MOVE_CURSOR_UP = lambda n: '\033[{}A'.format(n)
CLEAR_EOL = "\033[0K"
MOVE_CURSOR_UP = lambda n: "\033[{}A".format(n)

View File

@ -26,20 +26,20 @@ def optional_contextmanager(cm, *, ignore=False):
class FilesystemTester:
def __init__(self, extension='txt', mode='w', *, input_data=''):
def __init__(self, extension="txt", mode="w", *, input_data=""):
self.extension = extension
self.input_data = input_data
self.mode = mode
def get_services_for_reader(self, tmpdir):
fs, filename = open_fs(tmpdir), 'input.' + self.extension
fs, filename = open_fs(tmpdir), "input." + self.extension
with fs.open(filename, self.mode) as fp:
fp.write(self.input_data)
return fs, filename, {'fs': fs}
return fs, filename, {"fs": fs}
def get_services_for_writer(self, tmpdir):
fs, filename = open_fs(tmpdir), 'output.' + self.extension
return fs, filename, {'fs': fs}
fs, filename = open_fs(tmpdir), "output." + self.extension
return fs, filename, {"fs": fs}
class QueueList(list):
@ -60,7 +60,7 @@ class BufferingContext:
return self.buffer
def get_buffer_args_as_dicts(self):
return [row._asdict() if hasattr(row, '_asdict') else dict(row) for row in self.buffer]
return [row._asdict() if hasattr(row, "_asdict") else dict(row) for row in self.buffer]
class BufferingNodeExecutionContext(BufferingContext, NodeExecutionContext):
@ -106,43 +106,37 @@ def runner_entrypoint(args):
@runner
def runner_module(args):
""" Run bonobo using the bonobo.__main__ file, which is equivalent as doing "python -m bonobo ..."."""
with patch.object(sys, 'argv', ['bonobo', *args]):
return runpy.run_path(__main__.__file__, run_name='__main__')
with patch.object(sys, "argv", ["bonobo", *args]):
return runpy.run_path(__main__.__file__, run_name="__main__")
all_runners = pytest.mark.parametrize('runner', [runner_entrypoint, runner_module])
all_runners = pytest.mark.parametrize("runner", [runner_entrypoint, runner_module])
all_environ_targets = pytest.mark.parametrize(
'target', [
(get_examples_path('environ.py'), ),
(
'-m',
'bonobo.examples.environ',
),
]
"target", [(get_examples_path("environ.py"),), ("-m", "bonobo.examples.environ")]
)
@all_runners
@all_environ_targets
class EnvironmentTestCase():
class EnvironmentTestCase:
def run_quiet(self, runner, *args):
return runner('run', '--quiet', *args)
return runner("run", "--quiet", *args)
def run_environ(self, runner, *args, environ=None):
_environ = {'PATH': '/usr/bin'}
_environ = {"PATH": "/usr/bin"}
if environ:
_environ.update(environ)
with patch.dict('os.environ', _environ, clear=True):
with patch.dict("os.environ", _environ, clear=True):
out, err = self.run_quiet(runner, *args)
assert 'SECRET' not in os.environ
assert 'PASSWORD' not in os.environ
if 'PATH' in _environ:
assert 'PATH' in os.environ
assert os.environ['PATH'] == _environ['PATH']
assert "SECRET" not in os.environ
assert "PASSWORD" not in os.environ
if "PATH" in _environ:
assert "PATH" in os.environ
assert os.environ["PATH"] == _environ["PATH"]
assert err == ''
return dict(map(lambda line: line.split(' ', 1), filter(None, out.split('\n'))))
assert err == ""
return dict(map(lambda line: line.split(" ", 1), filter(None, out.split("\n"))))
class StaticNodeTest:
@ -202,8 +196,8 @@ class ReaderTest(ConfigurableNodeTest):
ReaderNodeType = None
extension = 'txt'
input_data = ''
extension = "txt"
input_data = ""
@property
def NodeType(self):
@ -216,12 +210,12 @@ class ReaderTest(ConfigurableNodeTest):
self.tmpdir = tmpdir
def get_create_args(self, *args):
return (self.filename, ) + args
return (self.filename,) + args
def test_customizable_output_type_transform_not_a_type(self):
context = self.NodeExecutionContextType(
self.create(*self.get_create_args(), output_type=str.upper, **self.get_create_kwargs()),
services=self.services
services=self.services,
)
with pytest.raises(TypeError):
context.start()
@ -229,9 +223,9 @@ class ReaderTest(ConfigurableNodeTest):
def test_customizable_output_type_transform_not_a_tuple(self):
context = self.NodeExecutionContextType(
self.create(
*self.get_create_args(), output_type=type('UpperString', (str, ), {}), **self.get_create_kwargs()
*self.get_create_args(), output_type=type("UpperString", (str,), {}), **self.get_create_kwargs()
),
services=self.services
services=self.services,
)
with pytest.raises(TypeError):
context.start()
@ -242,8 +236,8 @@ class WriterTest(ConfigurableNodeTest):
WriterNodeType = None
extension = 'txt'
input_data = ''
extension = "txt"
input_data = ""
@property
def NodeType(self):
@ -256,7 +250,7 @@ class WriterTest(ConfigurableNodeTest):
self.tmpdir = tmpdir
def get_create_args(self, *args):
return (self.filename, ) + args
return (self.filename,) + args
def readlines(self):
with self.fs.open(self.filename) as fp:

View File

@ -6,20 +6,14 @@ from bonobo.util.testing import all_runners
def test_entrypoint():
commands = {}
for command in pkg_resources.iter_entry_points('bonobo.commands'):
for command in pkg_resources.iter_entry_points("bonobo.commands"):
commands[command.name] = command
assert not {
'convert',
'init',
'inspect',
'run',
'version',
}.difference(set(commands))
assert not {"convert", "init", "inspect", "run", "version"}.difference(set(commands))
@all_runners
def test_no_command(runner):
_, err, exc = runner(catch_errors=True)
assert type(exc) == SystemExit
assert 'error: the following arguments are required: command' in err
assert "error: the following arguments are required: command" in err

View File

@ -8,10 +8,10 @@ from bonobo.util.testing import all_runners
@all_runners
def test_convert(runner, tmpdir):
csv_content = 'id;name\n1;Romain'
tmpdir.join('in.csv').write(csv_content)
csv_content = "id;name\n1;Romain"
tmpdir.join("in.csv").write(csv_content)
with change_working_directory(tmpdir):
runner('convert', 'in.csv', 'out.csv')
runner("convert", "in.csv", "out.csv")
assert tmpdir.join('out.csv').read().strip() == csv_content
assert tmpdir.join("out.csv").read().strip() == csv_content

View File

@ -9,7 +9,7 @@ from bonobo.util.testing import all_runners
@all_runners
def test_download_works_for_examples(runner):
expected_bytes = b'hello world'
expected_bytes = b"hello world"
class MockResponse(object):
def __init__(self):
@ -27,12 +27,13 @@ def test_download_works_for_examples(runner):
fout = io.BytesIO()
fout.close = lambda: None
with patch('bonobo.commands.download._open_url') as mock_open_url, \
patch('bonobo.commands.download.open') as mock_open:
with patch("bonobo.commands.download._open_url") as mock_open_url, patch(
"bonobo.commands.download.open"
) as mock_open:
mock_open_url.return_value = MockResponse()
mock_open.return_value = fout
runner('download', 'examples/datasets/coffeeshops.txt')
expected_url = EXAMPLES_BASE_URL + 'datasets/coffeeshops.txt'
runner("download", "examples/datasets/coffeeshops.txt")
expected_url = EXAMPLES_BASE_URL + "datasets/coffeeshops.txt"
mock_open_url.assert_called_once_with(expected_url)
assert fout.getvalue() == expected_bytes
@ -41,4 +42,4 @@ def test_download_works_for_examples(runner):
@all_runners
def test_download_fails_non_example(runner):
with pytest.raises(ValueError):
runner('download', 'something/entirely/different.txt')
runner("download", "something/entirely/different.txt")

View File

@ -8,22 +8,22 @@ from bonobo.util.testing import all_runners
@all_runners
def test_init_file(runner, tmpdir):
target = tmpdir.join('foo.py')
target = tmpdir.join("foo.py")
target_filename = str(target)
runner('init', target_filename)
runner("init", target_filename)
assert os.path.exists(target_filename)
out, err = runner('run', target_filename)
assert out.replace('\n', ' ').strip() == 'Hello World'
out, err = runner("run", target_filename)
assert out.replace("\n", " ").strip() == "Hello World"
assert not err
@all_runners
@pytest.mark.parametrize('template', InitCommand.TEMPLATES)
@pytest.mark.parametrize("template", InitCommand.TEMPLATES)
def test_init_file_templates(runner, template, tmpdir):
target = tmpdir.join('foo.py')
target = tmpdir.join("foo.py")
target_filename = str(target)
runner('init', target_filename)
runner("init", target_filename)
assert os.path.exists(target_filename)
out, err = runner('run', target_filename)
out, err = runner("run", target_filename)
assert not err

View File

@ -7,42 +7,42 @@ from bonobo.util.testing import all_runners
@all_runners
def test_run(runner):
out, err = runner('run', '--quiet', get_examples_path('types/strings.py'))
out = out.split('\n')
assert out[0].startswith('Foo ')
assert out[1].startswith('Bar ')
assert out[2].startswith('Baz ')
out, err = runner("run", "--quiet", get_examples_path("types/strings.py"))
out = out.split("\n")
assert out[0].startswith("Foo ")
assert out[1].startswith("Bar ")
assert out[2].startswith("Baz ")
@all_runners
def test_run_module(runner):
out, err = runner('run', '--quiet', '-m', 'bonobo.examples.types.strings')
out = out.split('\n')
assert out[0].startswith('Foo ')
assert out[1].startswith('Bar ')
assert out[2].startswith('Baz ')
out, err = runner("run", "--quiet", "-m", "bonobo.examples.types.strings")
out = out.split("\n")
assert out[0].startswith("Foo ")
assert out[1].startswith("Bar ")
assert out[2].startswith("Baz ")
@all_runners
def test_run_path(runner):
out, err = runner('run', '--quiet', get_examples_path('types'))
out = out.split('\n')
assert out[0].startswith('Foo ')
assert out[1].startswith('Bar ')
assert out[2].startswith('Baz ')
out, err = runner("run", "--quiet", get_examples_path("types"))
out = out.split("\n")
assert out[0].startswith("Foo ")
assert out[1].startswith("Bar ")
assert out[2].startswith("Baz ")
@all_runners
def test_install_requirements_for_dir(runner):
dirname = get_examples_path('types')
with patch('bonobo.commands.run._install_requirements') as install_mock:
runner('run', '--install', dirname)
install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt'))
dirname = get_examples_path("types")
with patch("bonobo.commands.run._install_requirements") as install_mock:
runner("run", "--install", dirname)
install_mock.assert_called_once_with(os.path.join(dirname, "requirements.txt"))
@all_runners
def test_install_requirements_for_file(runner):
dirname = get_examples_path('types')
with patch('bonobo.commands.run._install_requirements') as install_mock:
runner('run', '--install', os.path.join(dirname, 'strings.py'))
install_mock.assert_called_once_with(os.path.join(dirname, 'requirements.txt'))
dirname = get_examples_path("types")
with patch("bonobo.commands.run._install_requirements") as install_mock:
runner("run", "--install", os.path.join(dirname, "strings.py"))
install_mock.assert_called_once_with(os.path.join(dirname, "requirements.txt"))

View File

@ -5,103 +5,104 @@ from bonobo.util.testing import EnvironmentTestCase
@pytest.fixture
def env1(tmpdir):
env_file = tmpdir.join('.env_one')
env_file.write('\n'.join((
'SECRET=unknown',
'PASSWORD=sweet',
'PATH=first',
)))
env_file = tmpdir.join(".env_one")
env_file.write("\n".join(("SECRET=unknown", "PASSWORD=sweet", "PATH=first")))
return str(env_file)
@pytest.fixture
def env2(tmpdir):
env_file = tmpdir.join('.env_two')
env_file.write('\n'.join((
'PASSWORD=bitter',
"PATH='second'",
)))
env_file = tmpdir.join(".env_two")
env_file.write("\n".join(("PASSWORD=bitter", "PATH='second'")))
return str(env_file)
class TestDefaultEnvFile(EnvironmentTestCase):
def test_run_with_default_env_file(self, runner, target, env1):
env = self.run_environ(runner, *target, '--default-env-file', env1)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'sweet'
assert env.get('PATH') == '/usr/bin'
env = self.run_environ(runner, *target, "--default-env-file", env1)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "sweet"
assert env.get("PATH") == "/usr/bin"
def test_run_with_multiple_default_env_files(self, runner, target, env1, env2):
env = self.run_environ(runner, *target, '--default-env-file', env1, '--default-env-file', env2)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'sweet'
assert env.get('PATH') == '/usr/bin'
env = self.run_environ(runner, *target, "--default-env-file", env1, "--default-env-file", env2)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "sweet"
assert env.get("PATH") == "/usr/bin"
env = self.run_environ(runner, *target, '--default-env-file', env2, '--default-env-file', env1)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'bitter'
assert env.get('PATH') == '/usr/bin'
env = self.run_environ(runner, *target, "--default-env-file", env2, "--default-env-file", env1)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "bitter"
assert env.get("PATH") == "/usr/bin"
class TestEnvFile(EnvironmentTestCase):
def test_run_with_file(self, runner, target, env1):
env = self.run_environ(runner, *target, '--env-file', env1)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'sweet'
assert env.get('PATH') == 'first'
env = self.run_environ(runner, *target, "--env-file", env1)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "sweet"
assert env.get("PATH") == "first"
def test_run_with_multiple_files(self, runner, target, env1, env2):
env = self.run_environ(runner, *target, '--env-file', env1, '--env-file', env2)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'bitter'
assert env.get('PATH') == 'second'
env = self.run_environ(runner, *target, "--env-file", env1, "--env-file", env2)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "bitter"
assert env.get("PATH") == "second"
env = self.run_environ(runner, *target, '--env-file', env2, '--env-file', env1)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'sweet'
assert env.get('PATH') == 'first'
env = self.run_environ(runner, *target, "--env-file", env2, "--env-file", env1)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "sweet"
assert env.get("PATH") == "first"
class TestEnvFileCombinations(EnvironmentTestCase):
def test_run_with_both_env_files(self, runner, target, env1, env2):
env = self.run_environ(runner, *target, '--default-env-file', env1, '--env-file', env2)
assert env.get('SECRET') == 'unknown'
assert env.get('PASSWORD') == 'bitter'
assert env.get('PATH') == 'second'
env = self.run_environ(runner, *target, "--default-env-file", env1, "--env-file", env2)
assert env.get("SECRET") == "unknown"
assert env.get("PASSWORD") == "bitter"
assert env.get("PATH") == "second"
def test_run_with_both_env_files_then_overrides(self, runner, target, env1, env2):
env = self.run_environ(
runner, *target, '--default-env-file', env1, '--env-file', env2, '--env', 'PASSWORD=mine', '--env',
'SECRET=s3cr3t'
runner,
*target,
"--default-env-file",
env1,
"--env-file",
env2,
"--env",
"PASSWORD=mine",
"--env",
"SECRET=s3cr3t"
)
assert env.get('SECRET') == 's3cr3t'
assert env.get('PASSWORD') == 'mine'
assert env.get('PATH') == 'second'
assert env.get("SECRET") == "s3cr3t"
assert env.get("PASSWORD") == "mine"
assert env.get("PATH") == "second"
class TestEnvVars(EnvironmentTestCase):
def test_run_no_env(self, runner, target):
env = self.run_environ(runner, *target, environ={'USER': 'romain'})
assert env.get('USER') == 'romain'
env = self.run_environ(runner, *target, environ={"USER": "romain"})
assert env.get("USER") == "romain"
def test_run_env(self, runner, target):
env = self.run_environ(runner, *target, '--env', 'USER=serious', environ={'USER': 'romain'})
assert env.get('USER') == 'serious'
env = self.run_environ(runner, *target, "--env", "USER=serious", environ={"USER": "romain"})
assert env.get("USER") == "serious"
def test_run_env_mixed(self, runner, target):
env = self.run_environ(runner, *target, '--env', 'ONE=1', '--env', 'TWO="2"', environ={'USER': 'romain'})
assert env.get('USER') == 'romain'
assert env.get('ONE') == '1'
assert env.get('TWO') == '2'
env = self.run_environ(runner, *target, "--env", "ONE=1", "--env", 'TWO="2"', environ={"USER": "romain"})
assert env.get("USER") == "romain"
assert env.get("ONE") == "1"
assert env.get("TWO") == "2"
def test_run_default_env(self, runner, target):
env = self.run_environ(runner, *target, '--default-env', 'USER=clown')
assert env.get('USER') == 'clown'
env = self.run_environ(runner, *target, "--default-env", "USER=clown")
assert env.get("USER") == "clown"
env = self.run_environ(runner, *target, '--default-env', 'USER=clown', environ={'USER': 'romain'})
assert env.get('USER') == 'romain'
env = self.run_environ(runner, *target, "--default-env", "USER=clown", environ={"USER": "romain"})
assert env.get("USER") == "romain"
env = self.run_environ(
runner, *target, '--env', 'USER=serious', '--default-env', 'USER=clown', environ={'USER': 'romain'}
runner, *target, "--env", "USER=serious", "--default-env", "USER=clown", environ={"USER": "romain"}
)
assert env.get('USER') == 'serious'
assert env.get("USER") == "serious"

View File

@ -4,17 +4,17 @@ from bonobo.util.testing import all_runners
@all_runners
def test_version(runner):
out, err = runner('version')
out, err = runner("version")
out = out.strip()
assert out.startswith('bonobo ')
assert out.startswith("bonobo ")
assert __version__ in out
out, err = runner('version', '-q')
out, err = runner("version", "-q")
out = out.strip()
assert out.startswith('bonobo ')
assert out.startswith("bonobo ")
assert __version__ in out
out, err = runner('version', '-qq')
out, err = runner("version", "-qq")
out = out.strip()
assert not out.startswith('bonobo ')
assert __version__ in out
assert not out.startswith("bonobo ")
assert __version__ in out

View File

@ -11,7 +11,7 @@ class NoOptConfigurable(Configurable):
class MyConfigurable(Configurable):
required_str = Option(str)
default_str = Option(str, default='foo')
default_str = Option(str, default="foo")
integer = Option(int, required=False)
@ -20,7 +20,7 @@ class MyHarderConfigurable(MyConfigurable):
class MyBetterConfigurable(MyConfigurable):
required_str = Option(str, required=False, default='kaboom')
required_str = Option(str, required=False, default="kaboom")
class MyConfigurableUsingPositionalOptions(MyConfigurable):
@ -35,7 +35,7 @@ def test_missing_required_option_error():
with pytest.raises(TypeError) as exc:
MyConfigurable(_final=True)
assert exc.match('missing 1 required option:')
assert exc.match("missing 1 required option:")
def test_missing_required_options_error():
@ -44,29 +44,29 @@ def test_missing_required_options_error():
with pytest.raises(TypeError) as exc:
MyHarderConfigurable(_final=True)
assert exc.match('missing 2 required options:')
assert exc.match("missing 2 required options:")
def test_extraneous_option_error():
with pytest.raises(TypeError) as exc:
MyConfigurable(required_str='foo', hello='world')
assert exc.match('got 1 unexpected option:')
MyConfigurable(required_str="foo", hello="world")
assert exc.match("got 1 unexpected option:")
def test_extraneous_options_error():
with pytest.raises(TypeError) as exc:
MyConfigurable(required_str='foo', hello='world', acme='corp')
assert exc.match('got 2 unexpected options:')
MyConfigurable(required_str="foo", hello="world", acme="corp")
assert exc.match("got 2 unexpected options:")
def test_defaults():
o = MyConfigurable(required_str='hello')
o = MyConfigurable(required_str="hello")
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'hello'
assert o.default_str == 'foo'
assert o.required_str == "hello"
assert o.default_str == "foo"
assert o.integer is None
@ -76,30 +76,30 @@ def test_str_type_factory():
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == '42'
assert o.default_str == 'foo'
assert o.required_str == "42"
assert o.default_str == "foo"
assert o.integer is None
def test_int_type_factory():
o = MyConfigurable(required_str='yo', default_str='bar', integer='42')
o = MyConfigurable(required_str="yo", default_str="bar", integer="42")
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'yo'
assert o.default_str == 'bar'
assert o.required_str == "yo"
assert o.default_str == "bar"
assert o.integer == 42
def test_bool_type_factory():
o = MyHarderConfigurable(required_str='yes', also_required='True')
o = MyHarderConfigurable(required_str="yes", also_required="True")
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'yes'
assert o.default_str == 'foo'
assert o.required_str == "yes"
assert o.default_str == "foo"
assert o.integer is None
assert o.also_required is True
@ -110,22 +110,22 @@ def test_option_resolution_order():
with inspect_node(o) as ni:
assert not ni.partial
assert o.required_str == 'kaboom'
assert o.default_str == 'foo'
assert o.required_str == "kaboom"
assert o.default_str == "foo"
assert o.integer is None
def test_option_positional():
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
o = MyConfigurableUsingPositionalOptions("1", "2", "3", required_str="hello")
with inspect_node(o) as ni:
assert not ni.partial
assert o.first == '1'
assert o.second == '2'
assert o.third == '3'
assert o.required_str == 'hello'
assert o.default_str == 'foo'
assert o.first == "1"
assert o.second == "2"
assert o.third == "3"
assert o.required_str == "hello"
assert o.default_str == "foo"
assert o.integer is None

View File

@ -50,10 +50,7 @@ def test_define_with_decorator():
calls = []
def my_handler(*args, **kwargs):
calls.append((
args,
kwargs,
))
calls.append((args, kwargs))
Concrete = MethodBasedConfigurable(my_handler)
@ -64,7 +61,7 @@ def test_define_with_decorator():
assert ci.type == MethodBasedConfigurable
assert ci.partial
t = Concrete('foo', bar='baz')
t = Concrete("foo", bar="baz")
assert callable(t.handler)
assert len(calls) == 0
@ -75,15 +72,12 @@ def test_define_with_decorator():
def test_late_binding_method_decoration():
calls = []
@MethodBasedConfigurable(foo='foo')
@MethodBasedConfigurable(foo="foo")
def Concrete(*args, **kwargs):
calls.append((
args,
kwargs,
))
calls.append((args, kwargs))
assert callable(Concrete.handler)
t = Concrete(bar='baz')
t = Concrete(bar="baz")
assert callable(t.handler)
assert len(calls) == 0
@ -95,12 +89,9 @@ def test_define_with_argument():
calls = []
def concrete_handler(*args, **kwargs):
calls.append((
args,
kwargs,
))
calls.append((args, kwargs))
t = MethodBasedConfigurable(concrete_handler, 'foo', bar='baz')
t = MethodBasedConfigurable(concrete_handler, "foo", bar="baz")
assert callable(t.handler)
assert len(calls) == 0
t()
@ -112,12 +103,9 @@ def test_define_with_inheritance():
class Inheriting(MethodBasedConfigurable):
def handler(self, *args, **kwargs):
calls.append((
args,
kwargs,
))
calls.append((args, kwargs))
t = Inheriting('foo', bar='baz')
t = Inheriting("foo", bar="baz")
assert callable(t.handler)
assert len(calls) == 0
t()
@ -132,13 +120,10 @@ def test_inheritance_then_decorate():
@Inheriting
def Concrete(*args, **kwargs):
calls.append((
args,
kwargs,
))
calls.append((args, kwargs))
assert callable(Concrete.handler)
t = Concrete('foo', bar='baz')
t = Concrete("foo", bar="baz")
assert callable(t.handler)
assert len(calls) == 0
t()

View File

@ -12,11 +12,11 @@ class Bobby(Configurable):
@ContextProcessor
def think(self, context):
yield 'different'
yield "different"
def __call__(self, think, *args, **kwargs):
self.handler('1', *args, **kwargs)
self.handler2('2', *args, **kwargs)
self.handler("1", *args, **kwargs)
self.handler2("2", *args, **kwargs)
def test_partial():
@ -40,7 +40,7 @@ def test_partial():
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert ci.partial
assert ci.partial[0] == (f1, )
assert ci.partial[0] == (f1,)
assert not len(ci.partial[1])
# instanciate a more complete partial instance ...
@ -53,13 +53,10 @@ def test_partial():
assert len(ci.options) == 4
assert len(ci.processors) == 1
assert ci.partial
assert ci.partial[0] == (
f1,
f2,
)
assert ci.partial[0] == (f1, f2)
assert not len(ci.partial[1])
c = C('foo')
c = C("foo")
with inspect_node(c) as ci:
assert ci.type == Bobby

View File

@ -1,7 +1,7 @@
from operator import attrgetter
from bonobo.config import Configurable
from bonobo.config.processors import ContextProcessor, resolve_processors, ContextCurrifier, use_context_processor
from bonobo.config.processors import ContextCurrifier, ContextProcessor, resolve_processors, use_context_processor
class CP1(Configurable):
@ -11,11 +11,11 @@ class CP1(Configurable):
@ContextProcessor
def a(self):
yield 'this is A'
yield "this is A"
@ContextProcessor
def b(self, a):
yield a.upper()[:-1] + 'b'
yield a.upper()[:-1] + "b"
def __call__(self, a, b):
return a, b
@ -46,20 +46,20 @@ class CP3(CP2):
def get_all_processors_names(cls):
return list(map(attrgetter('__name__'), resolve_processors(cls)))
return list(map(attrgetter("__name__"), resolve_processors(cls)))
def test_inheritance_and_ordering():
assert get_all_processors_names(CP1) == ['c', 'a', 'b']
assert get_all_processors_names(CP2) == ['c', 'a', 'b', 'f', 'e', 'd']
assert get_all_processors_names(CP3) == ['c', 'a', 'b', 'f', 'e', 'd', 'c', 'b']
assert get_all_processors_names(CP1) == ["c", "a", "b"]
assert get_all_processors_names(CP2) == ["c", "a", "b", "f", "e", "d"]
assert get_all_processors_names(CP3) == ["c", "a", "b", "f", "e", "d", "c", "b"]
def test_setup_teardown():
o = CP1()
stack = ContextCurrifier(o)
stack.setup()
assert o(*stack.args) == ('this is A', 'THIS IS b')
assert o(*stack.args) == ("this is A", "THIS IS b")
stack.teardown()
@ -71,4 +71,4 @@ def test_processors_on_func():
def node(context):
pass
assert get_all_processors_names(node) == ['cp']
assert get_all_processors_names(node) == ["cp"]

View File

@ -4,11 +4,11 @@ import time
import pytest
from bonobo.config import Configurable, Container, Exclusive, Service, use
from bonobo.config.services import validate_service_name, create_container
from bonobo.config.services import create_container, validate_service_name
from bonobo.util import get_name
class PrinterInterface():
class PrinterInterface:
def print(self, *args):
raise NotImplementedError()
@ -18,46 +18,43 @@ class ConcretePrinter(PrinterInterface):
self.prefix = prefix
def print(self, *args):
return ';'.join((self.prefix, *args))
return ";".join((self.prefix, *args))
SERVICES = Container(
printer0=ConcretePrinter(prefix='0'),
printer1=ConcretePrinter(prefix='1'),
)
SERVICES = Container(printer0=ConcretePrinter(prefix="0"), printer1=ConcretePrinter(prefix="1"))
class MyServiceDependantConfigurable(Configurable):
printer = Service(PrinterInterface, )
printer = Service(PrinterInterface)
def __call__(self, *args, printer: PrinterInterface):
return printer.print(*args)
def test_service_name_validator():
assert validate_service_name('foo') == 'foo'
assert validate_service_name('foo.bar') == 'foo.bar'
assert validate_service_name('Foo') == 'Foo'
assert validate_service_name('Foo.Bar') == 'Foo.Bar'
assert validate_service_name('Foo.a0') == 'Foo.a0'
assert validate_service_name("foo") == "foo"
assert validate_service_name("foo.bar") == "foo.bar"
assert validate_service_name("Foo") == "Foo"
assert validate_service_name("Foo.Bar") == "Foo.Bar"
assert validate_service_name("Foo.a0") == "Foo.a0"
with pytest.raises(ValueError):
validate_service_name('foo.0')
validate_service_name("foo.0")
with pytest.raises(ValueError):
validate_service_name('0.foo')
validate_service_name("0.foo")
def test_service_dependency():
o = MyServiceDependantConfigurable(printer='printer0')
o = MyServiceDependantConfigurable(printer="printer0")
assert o('foo', 'bar', printer=SERVICES.get('printer0')) == '0;foo;bar'
assert o('bar', 'baz', printer=SERVICES.get('printer1')) == '1;bar;baz'
assert o('foo', 'bar', **SERVICES.kwargs_for(o)) == '0;foo;bar'
assert o("foo", "bar", printer=SERVICES.get("printer0")) == "0;foo;bar"
assert o("bar", "baz", printer=SERVICES.get("printer1")) == "1;bar;baz"
assert o("foo", "bar", **SERVICES.kwargs_for(o)) == "0;foo;bar"
def test_service_dependency_unavailable():
o = MyServiceDependantConfigurable(printer='printer2')
o = MyServiceDependantConfigurable(printer="printer2")
with pytest.raises(KeyError):
SERVICES.kwargs_for(o)
@ -72,15 +69,15 @@ class VCR:
def test_exclusive():
vcr = VCR()
vcr.append('hello')
vcr.append("hello")
def record(prefix, vcr=vcr):
with Exclusive(vcr):
for i in range(5):
vcr.append(' '.join((prefix, str(i))))
vcr.append(" ".join((prefix, str(i))))
time.sleep(0.05)
threads = [threading.Thread(target=record, args=(str(i), )) for i in range(5)]
threads = [threading.Thread(target=record, args=(str(i),)) for i in range(5)]
for thread in threads:
thread.start()
@ -90,8 +87,32 @@ def test_exclusive():
thread.join()
assert vcr.tape == [
'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3',
'2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4'
"hello",
"0 0",
"0 1",
"0 2",
"0 3",
"0 4",
"1 0",
"1 1",
"1 2",
"1 3",
"1 4",
"2 0",
"2 1",
"2 2",
"2 3",
"2 4",
"3 0",
"3 1",
"3 2",
"3 3",
"3 4",
"4 0",
"4 1",
"4 2",
"4 3",
"4 4",
]
@ -100,28 +121,25 @@ def test_requires():
services = Container(output=vcr.append)
@use('output')
@use("output")
def append(out, x):
out(x)
svcargs = services.kwargs_for(append)
assert len(svcargs) == 1
assert svcargs['output'] == vcr.append
assert svcargs["output"] == vcr.append
@pytest.mark.parametrize('services', [None, {}])
@pytest.mark.parametrize("services", [None, {}])
def test_create_container_empty_values(services):
c = create_container(services)
assert len(c) == 2
assert 'fs' in c and get_name(c['fs']) == 'OSFS'
assert 'http' in c and get_name(c['http']) == 'requests'
assert "fs" in c and get_name(c["fs"]) == "OSFS"
assert "http" in c and get_name(c["http"]) == "requests"
def test_create_container_override():
c = create_container({
'http': 'http',
'fs': 'fs',
})
c = create_container({"http": "http", "fs": "fs"})
assert len(c) == 2
assert 'fs' in c and c['fs'] == 'fs'
assert 'http' in c and c['http'] == 'http'
assert "fs" in c and c["fs"] == "fs"
assert "http" in c and c["http"] == "http"

View File

@ -5,22 +5,22 @@ from bonobo.config import use_raw_input
from bonobo.execution.contexts import GraphExecutionContext
from bonobo.util.bags import BagType
Extracted = namedtuple('Extracted', ['id', 'name', 'value'])
ExtractedBT = BagType('ExtractedBT', ['id', 'name', 'value'])
Extracted = namedtuple("Extracted", ["id", "name", "value"])
ExtractedBT = BagType("ExtractedBT", ["id", "name", "value"])
def extract_nt():
yield Extracted(id=1, name='Guido', value='.py')
yield Extracted(id=2, name='Larry', value='.pl')
yield Extracted(id=3, name='Dennis', value='.c')
yield Extracted(id=4, name='Yukihiro', value='.rb')
yield Extracted(id=1, name="Guido", value=".py")
yield Extracted(id=2, name="Larry", value=".pl")
yield Extracted(id=3, name="Dennis", value=".c")
yield Extracted(id=4, name="Yukihiro", value=".rb")
def extract_bt():
yield ExtractedBT(id=1, name='Guido', value='.py')
yield ExtractedBT(id=2, name='Larry', value='.pl')
yield ExtractedBT(id=3, name='Dennis', value='.c')
yield ExtractedBT(id=4, name='Yukihiro', value='.rb')
yield ExtractedBT(id=1, name="Guido", value=".py")
yield ExtractedBT(id=2, name="Larry", value=".pl")
yield ExtractedBT(id=3, name="Dennis", value=".c")
yield ExtractedBT(id=4, name="Yukihiro", value=".rb")
def transform_using_args(id, name, value):
@ -53,10 +53,18 @@ def test_execution():
with GraphExecutionContext(graph) as context:
context.run_until_complete()
assert result_args == [(2, 'Guido', 'guido.py'), (4, 'Larry', 'larry.pl'), (6, 'Dennis', 'dennis.c'),
(8, 'Yukihiro', 'yukihiro.rb')]
assert result_args == [
(2, "Guido", "guido.py"),
(4, "Larry", "larry.pl"),
(6, "Dennis", "dennis.c"),
(8, "Yukihiro", "yukihiro.rb"),
]
assert result_nt == [(1, 'GUIDO', '.py'), (2, 'LARRY', '.pl'), (3, 'DENNIS', '.c'), (4, 'YUKIHIRO', '.rb')]
assert result_nt == [(1, "GUIDO", ".py"), (2, "LARRY", ".pl"), (3, "DENNIS", ".c"), (4, "YUKIHIRO", ".rb")]
assert result_bt == [(2, 'Guido', 'guido.py'), (4, 'Larry', 'larry.pl'), (6, 'Dennis', 'dennis.c'),
(8, 'Yukihiro', 'yukihiro.rb')]
assert result_bt == [
(2, "Guido", "guido.py"),
(4, "Larry", "larry.pl"),
(6, "Dennis", "dennis.c"),
(8, "Yukihiro", "yukihiro.rb"),
]

View File

@ -1,14 +1,14 @@
from bonobo import Graph
from bonobo.constants import EMPTY, BEGIN, END
from bonobo.constants import BEGIN, EMPTY, END
from bonobo.execution.contexts import GraphExecutionContext
def raise_an_error(*args, **kwargs):
raise Exception('Careful, man, there\'s a beverage here!')
raise Exception("Careful, man, there's a beverage here!")
def raise_an_unrecoverrable_error(*args, **kwargs):
raise Exception('You are entering a world of pain!')
raise Exception("You are entering a world of pain!")
def test_lifecycle_of_empty_graph():

View File

@ -6,131 +6,131 @@ from bonobo import Graph
from bonobo.constants import EMPTY
from bonobo.execution.contexts.node import NodeExecutionContext, split_token
from bonobo.execution.strategies import NaiveStrategy
from bonobo.util.envelopes import F_NOT_MODIFIED, F_INHERIT
from bonobo.util.testing import BufferingNodeExecutionContext, BufferingGraphExecutionContext
from bonobo.util.envelopes import F_INHERIT, F_NOT_MODIFIED
from bonobo.util.testing import BufferingGraphExecutionContext, BufferingNodeExecutionContext
def test_node_string():
def f():
return 'foo'
return "foo"
with BufferingNodeExecutionContext(f) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ('foo', )
assert output[0] == ("foo",)
def g():
yield 'foo'
yield 'bar'
yield "foo"
yield "bar"
with BufferingNodeExecutionContext(g) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ('foo', )
assert output[1] == ('bar', )
assert output[0] == ("foo",)
assert output[1] == ("bar",)
def test_node_bytes():
def f():
return b'foo'
return b"foo"
with BufferingNodeExecutionContext(f) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == (b'foo', )
assert output[0] == (b"foo",)
def g():
yield b'foo'
yield b'bar'
yield b"foo"
yield b"bar"
with BufferingNodeExecutionContext(g) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == (b'foo', )
assert output[1] == (b'bar', )
assert output[0] == (b"foo",)
assert output[1] == (b"bar",)
def test_node_dict():
def f():
return {'id': 1, 'name': 'foo'}
return {"id": 1, "name": "foo"}
with BufferingNodeExecutionContext(f) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ({'id': 1, 'name': 'foo'}, )
assert output[0] == ({"id": 1, "name": "foo"},)
def g():
yield {'id': 1, 'name': 'foo'}
yield {'id': 2, 'name': 'bar'}
yield {"id": 1, "name": "foo"}
yield {"id": 2, "name": "bar"}
with BufferingNodeExecutionContext(g) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ({'id': 1, 'name': 'foo'}, )
assert output[1] == ({'id': 2, 'name': 'bar'}, )
assert output[0] == ({"id": 1, "name": "foo"},)
assert output[1] == ({"id": 2, "name": "bar"},)
def test_node_dict_chained():
strategy = NaiveStrategy(GraphExecutionContextType=BufferingGraphExecutionContext)
def f():
return {'id': 1, 'name': 'foo'}
return {"id": 1, "name": "foo"}
def uppercase_name(values):
return {**values, 'name': values['name'].upper()}
return {**values, "name": values["name"].upper()}
graph = Graph(f, uppercase_name)
context = strategy.execute(graph)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
assert output[0] == ({"id": 1, "name": "FOO"},)
def g():
yield {'id': 1, 'name': 'foo'}
yield {'id': 2, 'name': 'bar'}
yield {"id": 1, "name": "foo"}
yield {"id": 2, "name": "bar"}
graph = Graph(g, uppercase_name)
context = strategy.execute(graph)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ({'id': 1, 'name': 'FOO'}, )
assert output[1] == ({'id': 2, 'name': 'BAR'}, )
assert output[0] == ({"id": 1, "name": "FOO"},)
assert output[1] == ({"id": 2, "name": "BAR"},)
def test_node_tuple():
def f():
return 'foo', 'bar'
return "foo", "bar"
with BufferingNodeExecutionContext(f) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ('foo', 'bar')
assert output[0] == ("foo", "bar")
def g():
yield 'foo', 'bar'
yield 'foo', 'baz'
yield "foo", "bar"
yield "foo", "baz"
with BufferingNodeExecutionContext(g) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ('foo', 'bar')
assert output[1] == ('foo', 'baz')
assert output[0] == ("foo", "bar")
assert output[1] == ("foo", "baz")
def test_node_tuple_chained():
@ -140,50 +140,50 @@ def test_node_tuple_chained():
return tuple(map(str.upper, args))
def f():
return 'foo', 'bar'
return "foo", "bar"
graph = Graph(f, uppercase)
context = strategy.execute(graph)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ('FOO', 'BAR')
assert output[0] == ("FOO", "BAR")
def g():
yield 'foo', 'bar'
yield 'foo', 'baz'
yield "foo", "bar"
yield "foo", "baz"
graph = Graph(g, uppercase)
context = strategy.execute(graph)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ('FOO', 'BAR')
assert output[1] == ('FOO', 'BAZ')
assert output[0] == ("FOO", "BAR")
assert output[1] == ("FOO", "BAZ")
def test_node_tuple_dict():
def f():
return 'foo', 'bar', {'id': 1}
return "foo", "bar", {"id": 1}
with BufferingNodeExecutionContext(f) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 1
assert output[0] == ('foo', 'bar', {'id': 1})
assert output[0] == ("foo", "bar", {"id": 1})
def g():
yield 'foo', 'bar', {'id': 1}
yield 'foo', 'baz', {'id': 2}
yield "foo", "bar", {"id": 1}
yield "foo", "baz", {"id": 2}
with BufferingNodeExecutionContext(g) as context:
context.write_sync(EMPTY)
output = context.get_buffer()
assert len(output) == 2
assert output[0] == ('foo', 'bar', {'id': 1})
assert output[1] == ('foo', 'baz', {'id': 2})
assert output[0] == ("foo", "bar", {"id": 1})
assert output[1] == ("foo", "baz", {"id": 2})
def test_node_lifecycle_natural():
@ -229,9 +229,9 @@ def test_node_lifecycle_with_kill():
def test_split_token():
with pytest.deprecated_call():
assert split_token(('foo', 'bar')) == (set(), ('foo', 'bar'))
assert split_token(("foo", "bar")) == (set(), ("foo", "bar"))
assert split_token(()) == (set(), ())
assert split_token('') == (set(), ('', ))
assert split_token("") == (set(), ("",))
def test_split_token_duplicate():
@ -247,17 +247,17 @@ def test_split_token_duplicate():
def test_split_token_not_modified():
with pytest.deprecated_call():
with pytest.raises(ValueError):
split_token((F_NOT_MODIFIED, 'foo', 'bar'))
split_token((F_NOT_MODIFIED, "foo", "bar"))
with pytest.raises(ValueError):
split_token((F_NOT_MODIFIED, F_INHERIT))
with pytest.raises(ValueError):
split_token((F_INHERIT, F_NOT_MODIFIED))
assert split_token(F_NOT_MODIFIED) == ({F_NOT_MODIFIED}, ())
assert split_token((F_NOT_MODIFIED, )) == ({F_NOT_MODIFIED}, ())
assert split_token((F_NOT_MODIFIED,)) == ({F_NOT_MODIFIED}, ())
def test_split_token_inherit():
with pytest.deprecated_call():
assert split_token(F_INHERIT) == ({F_INHERIT}, ())
assert split_token((F_INHERIT, )) == ({F_INHERIT}, ())
assert split_token((F_INHERIT, 'foo', 'bar')) == ({F_INHERIT}, ('foo', 'bar'))
assert split_token((F_INHERIT,)) == ({F_INHERIT}, ())
assert split_token((F_INHERIT, "foo", "bar")) == ({F_INHERIT}, ("foo", "bar"))

View File

@ -6,9 +6,9 @@ from bonobo.execution import events
def test_names():
# This test looks useless, but as it's becoming the pliugin API, I want to make sure that nothing changes here, or
# notice it otherwise.
for name in 'start', 'started', 'tick', 'stop', 'stopped', 'kill':
for name in "start", "started", "tick", "stop", "stopped", "kill":
event_name = getattr(events, name.upper())
assert event_name == '.'.join(('execution', name))
assert event_name == ".".join(("execution", name))
def test_event_object():

View File

@ -14,16 +14,11 @@ class ResponseMock:
return {}
else:
self.count += 1
return {
'records': self.json_value,
}
return {"records": self.json_value}
def test_read_from_opendatasoft_api():
extract = OpenDataSoftAPI(dataset='test-a-set')
with patch('requests.get', return_value=ResponseMock([
{'fields': {'foo': 'bar'}},
{'fields': {'foo': 'zab'}},
])):
for line in extract('http://example.com/', ValueHolder(0)):
assert 'foo' in line
extract = OpenDataSoftAPI(dataset="test-a-set")
with patch("requests.get", return_value=ResponseMock([{"fields": {"foo": "bar"}}, {"fields": {"foo": "zab"}}])):
for line in extract("http://example.com/", ValueHolder(0)):
assert "foo" in line

View File

@ -1,27 +1,24 @@
from bonobo.util.envelopes import AppendingEnvelope
from bonobo.util.testing import BufferingNodeExecutionContext
messages = [
('Hello', ),
('Goodbye', ),
]
messages = [("Hello",), ("Goodbye",)]
def append(*args):
return AppendingEnvelope('!')
return AppendingEnvelope("!")
def test_inherit():
with BufferingNodeExecutionContext(append) as context:
context.write_sync(*messages)
assert context.get_buffer() == list(map(lambda x: x + ('!', ), messages))
assert context.get_buffer() == list(map(lambda x: x + ("!",), messages))
def test_inherit_bag_tuple():
with BufferingNodeExecutionContext(append) as context:
context.set_input_fields(['message'])
context.set_input_fields(["message"])
context.write_sync(*messages)
assert context.get_output_fields() == ('message', '0')
assert context.get_buffer() == list(map(lambda x: x + ('!', ), messages))
assert context.get_output_fields() == ("message", "0")
assert context.get_buffer() == list(map(lambda x: x + ("!",), messages))

Some files were not shown because too many files have changed in this diff Show More