Merge branch 'services_init' into 17_positional_options

Conflicts:
	.coveragerc
This commit is contained in:
Romain Dorgueil
2017-05-01 15:32:05 +02:00
45 changed files with 490 additions and 292 deletions

4
.codacy.yml Normal file
View File

@ -0,0 +1,4 @@
---
exclude_paths:
- bonobo/examples/
- bonobo/ext/

View File

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

1
.gitignore vendored
View File

@ -25,6 +25,7 @@
/.idea
/.release
/bonobo.iml
/bonobo/examples/work_in_progress/
/bonobo/ext/jupyter/js/node_modules/
/build/
/coverage.xml

View File

@ -1,7 +1,7 @@
# This file has been auto-generated.
# All changes will be lost, see Projectfile.
#
# Updated at 2017-04-30 10:12:39.793759
# Updated at 2017-05-01 08:35:15.162008
PYTHON ?= $(shell which python)
PYTHON_BASENAME ?= $(shell basename $(PYTHON))
@ -10,6 +10,7 @@ PYTHON_REQUIREMENTS_DEV_FILE ?= requirements-dev.txt
QUICK ?=
VIRTUAL_ENV ?= .virtualenv-$(PYTHON_BASENAME)
PIP ?= $(VIRTUAL_ENV)/bin/pip
PIP_INSTALL_OPTIONS ?=
PYTEST ?= $(VIRTUAL_ENV)/bin/pytest
PYTEST_OPTIONS ?= --capture=no --cov=bonobo --cov-report html
SPHINX_OPTS ?=
@ -24,13 +25,13 @@ YAPF_OPTIONS ?= -rip
# Installs the local project dependencies.
install: $(VIRTUAL_ENV)
if [ -z "$(QUICK)" ]; then \
$(PIP) install -U pip wheel -r $(PYTHON_REQUIREMENTS_FILE) ; \
$(PIP) install -U pip wheel $(PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_FILE) ; \
fi
# Installs the local project dependencies, including development-only libraries.
install-dev: $(VIRTUAL_ENV)
if [ -z "$(QUICK)" ]; then \
$(PIP) install -U pip wheel -r $(PYTHON_REQUIREMENTS_DEV_FILE) ; \
$(PIP) install -U pip wheel $(PIP_INSTALL_OPTIONS) -r $(PYTHON_REQUIREMENTS_DEV_FILE) ; \
fi
# Cleans up the local mess.

View File

@ -24,6 +24,8 @@ environment:
PYTHON_VERSION: "3.6.1"
PYTHON_ARCH: "64"
build: false
install:
# If there is a newer build queued for the same PR, cancel this one.
# The AppVeyor 'rollout builds' option is supposed to serve the same
@ -61,18 +63,18 @@ install:
# compiled extensions and are not provided as pre-built wheel packages,
# pip will build them from source using the MSVC compiler matching the
# target Python version and architecture
- "%CMD_IN_ENV% pip install -e ."
- "%CMD_IN_ENV% pip install -e .[dev]"
test_script:
# Run the project tests
- "%CMD_IN_ENV% pytest --capture=no --cov=bonobo --cov-report html"
after_test:
# If tests are successful, create binary packages for the project.
- "%CMD_IN_ENV% python setup.py bdist_wheel"
- "%CMD_IN_ENV% python setup.py bdist_wininst"
- "%CMD_IN_ENV% python setup.py bdist_msi"
- ps: "ls dist"
#after_test:
# If tests are successful, create binary packages for the project.
# - "%CMD_IN_ENV% python setup.py bdist_wheel"
# - "%CMD_IN_ENV% python setup.py bdist_wininst"
# - "%CMD_IN_ENV% python setup.py bdist_msi"
# - ps: "ls dist"
artifacts:
# Archive the generated packages in the ci.appveyor.com build report.

View File

@ -11,6 +11,9 @@ import sys
assert (sys.version_info >= (3, 5)), 'Python 3.5+ is required to use Bonobo.'
from bonobo._api import *
from bonobo._api import __all__
from bonobo._version import __version__
__all__ = ['__version__'] + __all__
__version__ = __version__
__all__ = __all__
del sys

View File

@ -1,72 +1,52 @@
from bonobo._version import __version__
import warnings
__all__ = [
'__version__',
]
from bonobo.structs import Bag, Graph
__all__ += ['Bag', 'Graph']
# Filesystem. This is a shortcut from the excellent filesystem2 library, that we make available there for convenience.
from fs import open_fs as _open_fs
open_fs = lambda url, *args, **kwargs: _open_fs(str(url), *args, **kwargs)
__all__ += ['open_fs']
# Basic transformations.
from bonobo.basics import *
from bonobo.basics import __all__ as _all_basics
__all__ += _all_basics
# Execution strategies.
from bonobo.basics import Limit, PrettyPrint, Tee, count, identity, noop, pprint
from bonobo.strategies import create_strategy
from bonobo.structs import Bag, Graph
from bonobo.util.objects import get_name
from bonobo.io import CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter
__all__ += ['create_strategy']
# Extract and loads from stdlib.
from bonobo.io import *
from bonobo.io import __all__ as _all_io
__all__ += _all_io
__all__ = []
# XXX This may be belonging to the bonobo.examples package.
def get_examples_path(*pathsegments):
import os
import pathlib
return str(pathlib.Path(os.path.dirname(__file__), 'examples', *pathsegments))
def register_api(x, __all__=__all__):
__all__.append(get_name(x))
return x
def open_examples_fs(*pathsegments):
return open_fs(get_examples_path(*pathsegments))
__all__.append(get_examples_path.__name__)
__all__.append(open_examples_fs.__name__)
def register_api_group(*args):
for attr in args:
register_api(attr)
def _is_interactive_console():
import sys
return sys.stdout.isatty()
def _is_jupyter_notebook():
try:
return get_ipython().__class__.__name__ == 'ZMQInteractiveShell'
except NameError:
return False
# @api
@register_api
def run(graph, *chain, strategy=None, plugins=None, services=None):
"""
Main entry point of bonobo. It takes a graph and creates all the necessary plumbery around to execute it.
The only necessary argument is a :class:`Graph` instance, containing the logic you actually want to execute.
By default, this graph will be executed using the "threadpool" strategy: each graph node will be wrapped in a
thread, and executed in a loop until there is no more input to this node.
You can provide plugins factory objects in the plugins list, this function will add the necessary plugins for
interactive console execution and jupyter notebook execution if it detects correctly that it runs in this context.
You'll probably want to provide a services dictionary mapping service names to service instances.
:param Graph graph: The :class:`Graph` to execute.
:param str strategy: The :class:`bonobo.strategies.base.Strategy` to use.
:param list plugins: The list of plugins to enhance execution.
:param dict services: The implementations of services this graph will use.
:return bonobo.execution.graph.GraphExecutionContext:
"""
if len(chain):
warnings.warn('DEPRECATED. You should pass a Graph instance instead of a chain.')
from bonobo import Graph
graph = Graph(graph, *chain)
strategy = create_strategy(strategy)
plugins = []
plugins = plugins or []
if _is_interactive_console():
from bonobo.ext.console import ConsoleOutputPlugin
@ -81,4 +61,66 @@ def run(graph, *chain, strategy=None, plugins=None, services=None):
return strategy.execute(graph, plugins=plugins, services=services)
__all__.append(run.__name__)
# bonobo.structs
register_api_group(Bag, Graph)
# bonobo.strategies
register_api(create_strategy)
# Shortcut to filesystem2's open_fs, that we make available there for convenience.
@register_api
def open_fs(fs_url, *args, **kwargs):
"""
Wraps :func:`fs.open_fs` function with a few candies.
:param str fs_url: A filesystem URL
:param parse_result: A parsed filesystem URL.
:type parse_result: :class:`ParseResult`
:param bool writeable: True if the filesystem must be writeable.
:param bool create: True if the filesystem should be created if it does not exist.
:param str cwd: The current working directory (generally only relevant for OS filesystems).
:param str default_protocol: The protocol to use if one is not supplied in the FS URL (defaults to ``"osfs"``).
:returns: :class:`~fs.base.FS` object
"""
from fs import open_fs as _open_fs
return _open_fs(str(fs_url), *args, **kwargs)
# bonobo.basics
register_api_group(
Limit,
PrettyPrint,
Tee,
count,
identity,
noop,
pprint,
)
# bonobo.io
register_api_group(CsvReader, CsvWriter, FileReader, FileWriter, JsonReader, JsonWriter)
def _is_interactive_console():
import sys
return sys.stdout.isatty()
def _is_jupyter_notebook():
try:
return get_ipython().__class__.__name__ == 'ZMQInteractiveShell'
except NameError:
return False
@register_api
def get_examples_path(*pathsegments):
import os
import pathlib
return str(pathlib.Path(os.path.dirname(__file__), 'examples', *pathsegments))
@register_api
def open_examples_fs(*pathsegments):
return open_fs(get_examples_path(*pathsegments))

View File

@ -4,7 +4,6 @@ from pprint import pprint as _pprint
from colorama import Fore, Style
from bonobo.config.processors import contextual
from bonobo.constants import NOT_MODIFIED
from bonobo.structs.bags import Bag
from bonobo.util.objects import ValueHolder
from bonobo.util.term import CLEAR_EOL
@ -25,6 +24,7 @@ def identity(x):
def Limit(n=10):
from bonobo.constants import NOT_MODIFIED
i = 0
def _limit(*args, **kwargs):
@ -38,6 +38,8 @@ def Limit(n=10):
def Tee(f):
from bonobo.constants import NOT_MODIFIED
@functools.wraps(f)
def wrapped(*args, **kwargs):
nonlocal f
@ -63,6 +65,8 @@ pprint = Tee(_pprint)
def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True):
from bonobo.constants import NOT_MODIFIED
def _pprint(*args, **kwargs):
nonlocal title_keys, sort, print_values
@ -98,4 +102,5 @@ def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True
def noop(*args, **kwargs): # pylint: disable=unused-argument
from bonobo.constants import NOT_MODIFIED
return NOT_MODIFIED

View File

@ -3,14 +3,12 @@ import os
def execute():
try:
import edgy.project
from edgy.project.__main__ import handle_init
except ImportError as exc:
raise ImportError(
'You must install "edgy.project" to use this command.\n\n $ pip install edgy.project\n'
) from exc
from edgy.project.__main__ import handle_init
return handle_init(os.path.join(os.getcwd(), 'Projectfile'))

View File

@ -20,7 +20,7 @@ def get_default_services(filename, services=None):
}
try:
exec(code, context)
except Exception as exc:
except Exception:
raise
return {
**context[DEFAULT_SERVICES_ATTR](),
@ -55,7 +55,7 @@ def execute(file, quiet=False):
'but it is something that will be implemented in the future.\n\nExpected: 1, got: {}.'
).format(len(graphs))
name, graph = list(graphs.items())[0]
graph = list(graphs.values())[0]
# todo if console and not quiet, then add the console plugin
# todo when better console plugin, add it if console and just disable display

View File

@ -83,4 +83,4 @@ class Configurable(metaclass=ConfigurableMeta):
)
for name, value in kwargs.items():
setattr(self, name, kwargs[name])
setattr(self, name, value)

View File

@ -52,9 +52,9 @@ def contextual(cls_or_func):
setattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR, [])
_processors = getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR)
for name, value in cls_or_func.__dict__.items():
if isinstance(value, ContextProcessor):
_processors.append(value)
for processor in cls_or_func.__dict__.values():
if isinstance(processor, ContextProcessor):
_processors.append(processor)
# This is needed for python 3.5, python 3.6 should be fine, but it's considered an implementation detail.
_processors.sort(key=lambda proc: proc._creation_counter)

View File

@ -0,0 +1,4 @@
[style]
based_on_style = pep8
column_limit = 74
dedent_closing_brackets = true

View File

@ -1,3 +1,18 @@
"""
Extracts a list of parisian bars where you can buy a coffee for a reasonable price, and store them in a flat text file.
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "ODS()" -> "transform" -> "FileWriter()";
}
"""
import bonobo
from bonobo.commands.run import get_default_services
from bonobo.ext.opendatasoft import OpenDataSoftAPI

View File

@ -1,3 +1,19 @@
"""
Extracts a list of fablabs in the world, restrict to the ones in france, then format it both for a nice console output
and a flat txt file.
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "ODS()" -> "normalize" -> "filter_france" -> "Tee()" -> "JsonWriter()";
}
"""
import json
from colorama import Fore, Style
@ -9,7 +25,9 @@ from bonobo.ext.opendatasoft import OpenDataSoftAPI
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-in-the-world'
API_NETLOC = 'datanova.laposte.fr'
@ -41,20 +59,41 @@ def display(row):
address = list(
filter(
None, (
' '.join(filter(None, (row.get('postal_code', None), row.get('city', None)))), row.get('county', None),
row.get('country'),
' '.join(
filter(
None, (
row.get('postal_code', None),
row.get('city', None)
)
)
), row.get('county', None), row.get('country'),
)
)
)
print(' - {}address{}: {address}'.format(Fore.BLUE, Style.RESET_ALL, address=', '.join(address)))
print(' - {}links{}: {links}'.format(Fore.BLUE, Style.RESET_ALL, links=', '.join(row['links'])))
print(' - {}geometry{}: {geometry}'.format(Fore.BLUE, Style.RESET_ALL, **row))
print(' - {}source{}: {source}'.format(Fore.BLUE, Style.RESET_ALL, source='datanova/' + API_DATASET))
print(
' - {}address{}: {address}'.
format(Fore.BLUE, Style.RESET_ALL, address=', '.join(address))
)
print(
' - {}links{}: {links}'.
format(Fore.BLUE, Style.RESET_ALL, links=', '.join(row['links']))
)
print(
' - {}geometry{}: {geometry}'.
format(Fore.BLUE, Style.RESET_ALL, **row)
)
print(
' - {}source{}: {source}'.format(
Fore.BLUE, Style.RESET_ALL, source='datanova/' + API_DATASET
)
)
graph = bonobo.Graph(
OpenDataSoftAPI(dataset=API_DATASET, netloc=API_NETLOC, timezone='Europe/Paris'),
OpenDataSoftAPI(
dataset=API_DATASET, netloc=API_NETLOC, timezone='Europe/Paris'
),
normalize,
filter_france,
bonobo.Tee(display),

View File

@ -1,5 +1,4 @@
[
{"city": "Lannion", "kind_name": "fab_lab", "links": ["http://fablab-lannion.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production", "url": "https://www.fablabs.io/labs/fablablannion", "coordinates": [48.7317261, -3.4509764], "name": "Fablab Lannion - KerNEL", "phone": "+33 2 96 37 84 46", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/27/c6c015ba-26c6-4620-833f-8441123a4afc/Fablab Lannion - KerNEL.jpg", "postal_code": "22300", "longitude": -3.45097639999994, "country_code": "fr", "latitude": 48.7317261, "address_notes": "Use the small portal", "email": "contact@fablab-lannion.org", "address_1": "14 Rue de Beauchamp", "geometry": {"type": "Point", "coordinates": [-3.4509764, 48.7317261]}, "country": "France"},
[{"city": "Lannion", "kind_name": "fab_lab", "links": ["http://fablab-lannion.org"], "capabilities": "three_d_printing;cnc_milling;circuit_production", "url": "https://www.fablabs.io/labs/fablablannion", "coordinates": [48.7317261, -3.4509764], "name": "Fablab Lannion - KerNEL", "phone": "+33 2 96 37 84 46", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/27/c6c015ba-26c6-4620-833f-8441123a4afc/Fablab Lannion - KerNEL.jpg", "postal_code": "22300", "longitude": -3.45097639999994, "country_code": "fr", "latitude": 48.7317261, "address_notes": "Use the small portal", "email": "contact@fablab-lannion.org", "address_1": "14 Rue de Beauchamp", "geometry": {"type": "Point", "coordinates": [-3.4509764, 48.7317261]}, "country": "France"},
{"city": "Villeneuve-d'Ascq", "kind_name": "fab_lab", "links": ["http://www.flickr.com/photos/fablablille/", "https://twitter.com/FabLab_Lille", "http://www.fablablille.fr"], "url": "https://www.fablabs.io/labs/fablablille", "coordinates": [50.642869867, 3.1386641], "county": "Nord-Pas-de-Calais", "phone": "+33 9 72 29 47 65", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/10/34/147c88ca-2acd-42a4-aeb0-17b2dc830903/FabLab Lille.jpg", "postal_code": "59650", "longitude": 3.13866410000003, "country_code": "fr", "latitude": 50.6428698670218, "address_1": "2 All\u00e9e Lakanal", "name": "FabLab Lille", "geometry": {"type": "Point", "coordinates": [3.1386641, 50.642869867]}, "country": "France"},
{"city": "Dijon", "name": "L'abscisse", "links": ["http://fablab.coagul.org"], "url": "https://www.fablabs.io/labs/lab6", "longitude": 5.04147999999998, "county": "France", "parent_id": 545, "kind_name": "mini_fab_lab", "postal_code": "2100", "coordinates": [47.322047, 5.04148], "address_2": "6, impasse Quentin", "latitude": 47.322047, "country_code": "fr", "email": "c-bureau@outils.coagul.org", "address_1": "Dijon", "geometry": {"type": "Point", "coordinates": [5.04148, 47.322047]}, "country": "France"},
{"city": "Montreuil", "kind_name": "fab_lab", "links": ["http://www.apedec.org ", "http://webtv.montreuil.fr/festival-m.u.s.i.c-et-fablab-video-415-12.html", "http://www.wedemain.fr/A-Montreuil-un-fab-lab-circulaire-dans-une-usine-verticale_a421.html"], "capabilities": "three_d_printing", "url": "https://www.fablabs.io/labs/ecodesignfablab", "name": "ECODESIGN FAB LAB", "email": "contact@apedec.org", "coordinates": [48.8693157, 2.4564764], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/51/53/74898eb4-e94d-49fc-9e57-18246d1901c8/ECODESIGN FAB LAB.jpg", "phone": "+33 1 (0)9.81.29.17.31", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/13/33b98c6f-b6c1-4cfd-8b63-401c4441f964/ECODESIGN FAB LAB.jpg", "postal_code": "93106", "longitude": 2.45647640000004, "country_code": "fr", "latitude": 48.8693157, "address_1": "Montreuil", "address_notes": "lot 38 D", "address_2": "2 \u00e0 20 avenue Allende, MOZINOR", "blurb": "FAB LAB specialized in upcycling and ecodesign with furniture production based on diverted source of industrial waste, located in a industrial zone, in the heart of a popular city.", "description": "Based on the roof of an industrial zone of 50 SMEs (and 500 workers), Ecodesign Fab Lab is now open to address upcycling and eco-innovation, thanks waste collection, designers and classical wood equipment, but also 3D printers (and CNC equipment in the next weeks).", "geometry": {"type": "Point", "coordinates": [2.4564764, 48.8693157]}, "country": "France"},
@ -133,5 +132,4 @@
{"city": "Bron", "kind_name": "fab_lab", "links": ["http://fablab-lyon.fr"], "capabilities": "three_d_printing;cnc_milling;laser;vinyl_cutting", "url": "https://www.fablabs.io/labs/fabriquedobjetslibres", "name": "Fabrique d'Objets Libres", "email": "contact@fabriquedobjetslibres.fr", "coordinates": [45.7429334, 4.9082135], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/50/01/0190e790-aaec-4f2f-9985-11156655145d/Fabrique d'Objets Libres.jpg", "county": "Rh\u00f4ne", "phone": "+33 7 68 01 40 26 (Tue-Sat 2pm-6pm)", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/13/49/73ea9f2d-0216-4f52-a6bf-2ff97ee474b2/Fabrique d'Objets Libres.jpg", "postal_code": "69500", "longitude": 4.90821349999999, "country_code": "fr", "latitude": 45.7429334, "address_1": "All\u00e9e Gaillard Romanet", "address_notes": "Au sous-sol de la MJC. Downstairs inside the MJC.", "address_2": "MJC Louis Aragon", "blurb": "Le fablab lyonnais, install\u00e9 \u00e0 la MJC Louis Aragon de Bron, ouvert tous les mercredis et formation hebdomadaire de fabrication num\u00e9rique. Projets autour du handicap, des arts et du recyclage.", "description": "La Fabrique d'Objets Libres est un fablab associatif sur Lyon et sa r\u00e9gion. Install\u00e9 \u00e0 la MJC Louis Aragon de Bron depuis janvier 2013, c'est un espace de cr\u00e9ation et de fabrication num\u00e9rique ouvert \u00e0 tous, qui permet \u00e0 chacun de d\u00e9couvrir, d'inventer et de fabriquer tout type d'objet.\r\n \r\nV\u00e9ritable laboratoire citoyen de fabrication, la Fabrique d\u2019Objets Libres met \u00e0 disposition de ses adh\u00e9rents des outils \u00e0 commande num\u00e9rique et des mati\u00e8res premi\u00e8res secondaires permettant de concevoir et de fabriquer localement des objets libres.\r\nC\u2019est une plate-forme pluridisciplinaire collaborative qui m\u00eale les profils (techniciens, informaticiens, ing\u00e9nieurs, scientifiques, bricoleurs, cr\u00e9ateurs...) et les g\u00e9n\u00e9rations afin de r\u00e9unir tous types de comp\u00e9tences.\r\n\r\nLe fablab est ouvert tous les mercredis pour les \"temps libres\", durant lesquels les adh\u00e9rents utilisent les machines librement. Par ailleurs, il propose un atelier hebdomadaire aux adh\u00e9rents de la MJC, \"De l'id\u00e9e \u00e0 l'objet\": en une dizaine de s\u00e9ances sur un trimestre, les participants apprennent \u00e0 utiliser toutes les machines du fablab pour r\u00e9aliser leurs objets, et r\u00e9fl\u00e9chissent autour d'une th\u00e9matique sociale comme le handicap, la musique, ou la ville.\r\n\r\nL'association organise \u00e9galement des \u00e9v\u00e9nements et ateliers th\u00e9matiques utilisant la fabrication num\u00e9rique autour de sujet plus vastes, comme l'art, avec les machines \u00e0 dessiner, ou le handicap, dans le cadre du projet Handilab, ou encore la fin de vie des objets, avec le Laboratoire de l'Obsolescence D\u00e9programm\u00e9e. Enfin, le fablab s'associe \u00e0 d'autres associations et des entreprises pour des projets communs.", "geometry": {"type": "Point", "coordinates": [4.9082135, 45.7429334]}, "country": "France"},
{"city": "N\u00e9ons-sur-Creuse", "kind_name": "fab_lab", "links": ["http://www.rurallab.org"], "url": "https://www.fablabs.io/labs/rurallab", "coordinates": [46.744746, 0.931698], "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/49/00/95c7b9f2-a034-4b2b-931d-43ced33ddfb1/RuralLab.jpg", "phone": "+33603318810", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/12/49/ec5f7c54-e6ce-40fd-b5c5-c4142d208e6b/RuralLab.jpg", "postal_code": "36220", "longitude": 0.931697999999983, "country_code": "fr", "latitude": 46.744746, "address_1": "Rue de l'\u00c9cole", "email": "rurallab36@gmail.com", "blurb": "A FabLab in the countryside in Neons sur Creuse, France", "name": "RuralLab", "geometry": {"type": "Point", "coordinates": [0.931698, 46.744746]}, "country": "France"},
{"city": "Gif-sur-Yvette", "kind_name": "supernode", "links": ["http://fablab.digiscope.fr/#!/", "http://fablabdigiscope.wordpress.com"], "url": "https://www.fablabs.io/labs/fablabdigiscope", "name": "(Fab)Lab Digiscope", "longitude": 2.16830979999997, "header_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/13/52/18/8d63351d-c2fb-4a90-8e58-bb45422202a6/(Fab)Lab Digiscope.jpg", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/15/46/51553da4-b295-426c-837f-934c311933ba/(Fab)Lab Digiscope.jpg", "postal_code": "91190", "coordinates": [48.7117632, 2.1683098], "country_code": "fr", "latitude": 48.7117632, "address_1": "660 Rue Noetzlin", "capabilities": "three_d_printing;cnc_milling;circuit_production;laser;precision_milling;vinyl_cutting", "email": "fablabdigiscope@gmail.com", "blurb": "(FAB)LAB DIGISCOPE is a fabrication laboratory dedicated to research in sciences | design | education | art | engineering and what ever field of research you come from. Open to Everyone. Book now!", "description": "(FAB)LAB DIGISCOPE is a fabrication laboratory dedicated to research in sciences | design | education | arts | engineering and what ever field of research you come from. We host Fab Academy and Bio Academy. We host Digital Fabrication Classes for EITC Master. Open to Everyone since the beginning.\r\n\r\nFablab Digiscope started in 2013 when Aviz-INRIA research team director Jean-Daniel Fekete and colleague researcher Pierre Dragicevic hired Romain Di Vozzo as a R&D Engineer to be the fablab manager of what would later become an attractive place on the new Campus Paris-Saclay. Fablab Digiscope is part of the Digiscope Project, a network of 10 high-performance platforms for interactive visualization of large datasets and complex computation for which Michel Beaudouin-Lafon is the scientific Director. Fablab Digiscope is mutualised between 10 institutions involved in research and education.\r\n\r\nRomain Di Vozzo runs and develops Fablab Digiscope everyday, trains publics, designs objects, shares creative thoughts, gives advices on designs, etc. Romain also actively collaborates to the globally distributed fablab network and with the Fab Foundation by operating as Fab Academy SuperNode, as Instructor for Fab Academy and Bio Academy, by giving conferences and workshops in France and abroad and by performing very small tasks that make the fablab network grow.", "geometry": {"type": "Point", "coordinates": [2.1683098, 48.7117632]}, "country": "France"},
{"city": "Metz", "kind_name": "fab_lab", "links": ["http://graoulab.org/wiki", "http://graoulab.org"], "url": "https://www.fablabs.io/labs/graoulab", "coordinates": [49.1262692, 6.182086], "name": "GraouLab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/24/af4709d8-1f60-48a7-ba35-4c42ef40a195/GraouLab.jpg", "postal_code": "57000", "longitude": 6.18208600000003, "country_code": "fr", "latitude": 49.1262692, "capabilities": "three_d_printing;laser", "email": "contact@graoulab.org", "blurb": "The FabLab of Metz. A place for folks innovation.", "address_1": "7 Avenue de Blida", "geometry": {"type": "Point", "coordinates": [6.182086, 49.1262692]}, "country": "France"}
]
{"city": "Metz", "kind_name": "fab_lab", "links": ["http://graoulab.org/wiki", "http://graoulab.org"], "url": "https://www.fablabs.io/labs/graoulab", "coordinates": [49.1262692, 6.182086], "name": "GraouLab", "avatar_url": "http://fablabs.io.s3.amazonaws.com/2017/01/28/11/18/24/af4709d8-1f60-48a7-ba35-4c42ef40a195/GraouLab.jpg", "postal_code": "57000", "longitude": 6.18208600000003, "country_code": "fr", "latitude": 49.1262692, "capabilities": "three_d_printing;laser", "email": "contact@graoulab.org", "blurb": "The FabLab of Metz. A place for folks innovation.", "address_1": "7 Avenue de Blida", "geometry": {"type": "Point", "coordinates": [6.182086, 49.1262692]}, "country": "France"}]

View File

@ -1,9 +1,11 @@
import bonobo
from bonobo.commands.run import get_default_services
def get_fields(row):
return row['fields']
graph = bonobo.Graph(
bonobo.JsonReader(path='datasets/theaters.json'),
get_fields,

View File

@ -6,6 +6,6 @@ graph = bonobo.Graph(
)
if __name__ == '__main__':
bonobo.run(graph, services={
'fs': bonobo.open_examples_fs('datasets')
})
bonobo.run(
graph, services={'fs': bonobo.open_examples_fs('datasets')}
)

View File

@ -12,6 +12,6 @@ graph = bonobo.Graph(
)
if __name__ == '__main__':
bonobo.run(graph, services={
'fs': bonobo.open_examples_fs('datasets')
})
bonobo.run(
graph, services={'fs': bonobo.open_examples_fs('datasets')}
)

View File

@ -22,6 +22,6 @@ graph = bonobo.Graph(
)
if __name__ == '__main__':
bonobo.run(graph, services={
'fs': bonobo.open_examples_fs('datasets')
})
bonobo.run(
graph, services={'fs': bonobo.open_examples_fs('datasets')}
)

View File

@ -1,7 +1,21 @@
import bonobo
import bonobo.basics
"""
Simple example of :func:`bonobo.count` usage.
graph = bonobo.Graph(range(42), bonobo.basics.count, print)
.. graphviz::
digraph {
rankdir = LR;
stylesheet = "../_static/graphs.css";
BEGIN [shape="point"];
BEGIN -> "range()" -> "count" -> "print";
}
"""
import bonobo
graph = bonobo.Graph(range(42), bonobo.count, print)
if __name__ == '__main__':
bonobo.run(graph)

View File

@ -1,9 +1,9 @@
import sys
import traceback
from time import sleep
from bonobo.config import Container
from bonobo.config.processors import resolve_processors
from bonobo.util.errors import print_error
from bonobo.util.iterators import ensure_tuple
from bonobo.util.objects import Wrapper
@ -43,16 +43,13 @@ class LoopingExecutionContext(Wrapper):
False), ('{}.start() can only be called on a new node.').format(type(self).__name__)
assert self._context is None
self._started = True
try:
if self.parent:
self._context = self.parent.services.args_for(self.wrapped)
elif self.services:
self._context = self.services.args_for(self.wrapped)
else:
self._context = ()
except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc())
raise
if self.parent:
self._context = self.parent.services.args_for(self.wrapped)
elif self.services:
self._context = self.services.args_for(self.wrapped)
else:
self._context = ()
for processor in resolve_processors(self.wrapped):
try:
@ -80,41 +77,22 @@ class LoopingExecutionContext(Wrapper):
if self._stopped:
return
assert self._context is not None
self._stopped = True
while len(self._stack):
processor = self._stack.pop()
try:
# todo yield from ? how to ?
next(processor)
except StopIteration as exc:
# This is normal, and wanted.
pass
except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc())
raise
else:
# No error ? We should have had StopIteration ...
raise RuntimeError('Context processors should not yield more than once.')
if self._context is not None:
while len(self._stack):
processor = self._stack.pop()
try:
# todo yield from ? how to ?
next(processor)
except StopIteration as exc:
# This is normal, and wanted.
pass
except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc())
raise
else:
# No error ? We should have had StopIteration ...
raise RuntimeError('Context processors should not yield more than once.')
def handle_error(self, exc, trace):
"""
Error handler. Whatever happens in a plugin or component, if it looks like an exception, taste like an exception
or somehow make me think it is an exception, I'll handle it.
:param exc: the culprit
:param trace: Hercule Poirot's logbook.
:return: to hell
"""
from colorama import Fore, Style
print(
Style.BRIGHT,
Fore.RED,
'\U0001F4A3 {} in {}'.format(type(exc).__name__, self.wrapped),
Style.RESET_ALL,
sep='',
file=sys.stderr,
)
print(trace)
return print_error(exc, trace, context=self.wrapped)

View File

@ -25,15 +25,15 @@ class GraphExecutionContext:
self.plugins = [PluginExecutionContext(plugin, parent=self) for plugin in plugins or ()]
self.services = Container(services) if services else Container()
for i, component_context in enumerate(self):
for i, node_context in enumerate(self):
try:
component_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
except KeyError:
continue
component_context.input.on_begin = partial(component_context.send, BEGIN, _control=True)
component_context.input.on_end = partial(component_context.send, END, _control=True)
component_context.input.on_finalize = partial(component_context.stop)
node_context.input.on_begin = partial(node_context.send, BEGIN, _control=True)
node_context.input.on_end = partial(node_context.send, END, _control=True)
node_context.input.on_finalize = partial(node_context.stop)
def __getitem__(self, item):
return self.nodes[item]

View File

@ -7,7 +7,8 @@ from bonobo.core.inputs import Input
from bonobo.core.statistics import WithStatistics
from bonobo.errors import InactiveReadableError
from bonobo.execution.base import LoopingExecutionContext
from bonobo.structs.bags import Bag, ErrorBag
from bonobo.structs.bags import Bag
from bonobo.util.errors import is_error
from bonobo.util.iterators import iter_if_not_sequence
@ -32,7 +33,13 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
return (('+' if self.alive else '-') + ' ' + self.__name__ + ' ' + self.get_statistics_as_string()).strip()
def __repr__(self):
return '<' + self.__str__() + '>'
stats = self.get_statistics_as_string().strip()
return '<{}({}{}){}>'.format(
type(self).__name__,
'+' if self.alive else '',
self.__name__,
(' ' + stats) if stats else '',
)
def recv(self, *messages):
"""
@ -116,10 +123,6 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
self.push(_resolve(input_bag, result))
def is_error(bag):
return isinstance(bag, ErrorBag)
def _resolve(input_bag, output):
# NotModified means to send the input unmodified to output.
if output is NOT_MODIFIED:

View File

@ -14,10 +14,6 @@ from edgy.project.feature import Feature, SUPPORT_PRIORITY
class BonoboFeature(Feature):
requires = {'python'}
@subscribe('edgy.project.feature.make.on_generate', priority=SUPPORT_PRIORITY)
def on_make_generate(self, event):
makefile = event.makefile
@subscribe('edgy.project.on_start', priority=SUPPORT_PRIORITY)
def on_start(self, event):
package_path = event.setup['name'].replace('.', os.sep)

View File

@ -22,13 +22,3 @@ class JupyterOutputPlugin(Plugin):
self.widget.value = [repr(node) for node in self.context.parent.nodes]
finalize = run
"""
TODO JUPYTER WIDGET
###################
# close the widget? what does it do?
https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Basics.html#Closing-widgets
"""

View File

@ -50,7 +50,7 @@ class CsvReader(CsvHandler, FileReader):
field_count = len(headers.value)
if self.skip and self.skip > 0:
for i in range(0, self.skip):
for _ in range(0, self.skip):
next(reader)
for row in reader:

View File

@ -1,10 +1,12 @@
import time
import traceback
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
from bonobo.constants import BEGIN, END
from bonobo.strategies.base import Strategy
from bonobo.structs.bags import Bag
from bonobo.util.errors import print_error
class ExecutorStrategy(Strategy):
@ -29,18 +31,32 @@ class ExecutorStrategy(Strategy):
for plugin_context in context.plugins:
def _runner(plugin_context=plugin_context):
plugin_context.start()
plugin_context.loop()
plugin_context.stop()
try:
plugin_context.start()
plugin_context.loop()
plugin_context.stop()
except Exception as exc:
print_error(exc, traceback.format_exc(), prefix='Error in plugin context', context=plugin_context)
futures.append(executor.submit(_runner))
for node_context in context.nodes:
def _runner(node_context=node_context):
node_context.start()
node_context.loop()
node_context.stop()
try:
node_context.start()
except Exception as exc:
print_error(
exc, traceback.format_exc(), prefix='Could not start node context', context=node_context
)
node_context.input.on_end()
else:
node_context.loop()
try:
node_context.stop()
except Exception as exc:
print_error(exc, traceback.format_exc(), prefix='Could not stop node context', context=node_context)
futures.append(executor.submit(_runner))

View File

@ -9,6 +9,30 @@ __all__ = [
class Bag:
"""
Bags are simple datastructures that holds arguments and keyword arguments together, that may be applied to a
callable.
Example:
>>> from bonobo import Bag
>>> def myfunc(foo, *, bar):
... print(foo, bar)
...
>>> bag = Bag('foo', bar='baz')
>>> bag.apply(myfunc)
foo baz
A bag can inherit another bag, allowing to override only a few arguments without touching the parent.
Example:
>>> bag2 = Bag(bar='notbaz', _parent=bag)
>>> bag2.apply(myfunc)
foo notbaz
"""
def __init__(self, *args, _flags=None, _parent=None, **kwargs):
self._flags = _flags or ()
self._parent = _parent

31
bonobo/util/errors.py Normal file
View File

@ -0,0 +1,31 @@
import sys
from bonobo.structs.bags import ErrorBag
def is_error(bag):
return isinstance(bag, ErrorBag)
def print_error(exc, trace, context=None, prefix=''):
"""
Error handler. Whatever happens in a plugin or component, if it looks like an exception, taste like an exception
or somehow make me think it is an exception, I'll handle it.
:param exc: the culprit
:param trace: Hercule Poirot's logbook.
:return: to hell
"""
from colorama import Fore, Style
print(
Style.BRIGHT,
Fore.RED,
'\U0001F4A3 {}{}{}'.format(
(prefix + ': ') if prefix else '', type(exc).__name__, ' in {!r}'.format(context) if context else ''
),
Style.RESET_ALL,
sep='',
file=sys.stderr,
)
print(trace)

View File

@ -1,2 +1,2 @@
.node {
}
}

View File

@ -1,6 +1,21 @@
Contributing
============
There's a lot of different ways you can contribute, and not all of them includes coding. Do not think that the codeless
contributions have less value, all contributions are very important.
* You can contribute to the documentation.
* You can help reproducing errors and giving more infos in the issues.
* You can open issues with problems you're facing.
* You can help creating better presentation material.
* You can talk about it in your local python user group.
* You can enhance examples.
* You can enhance tests.
* etc.
Code-related contributions (including tests and examples)
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Contributing to bonobo is usually done this way:
* Discuss ideas in the `issue tracker <https://github.com/python-bonobo/bonobo>`_ or on `Slack <https://bonobo-slack.herokuapp.com/>`_.

View File

@ -22,5 +22,6 @@ available as an optional extra dependency, and the maturity stage of each extens
:maxdepth: 2
ext/docker
ext/jupyter
ext/selenium
ext/sqlalchemy

View File

@ -1,54 +1,13 @@
Public API
Bonobo API
==========
All the "public api" callables, classes and other callables are available under the root :mod:`bonobo` package, even if
they are documented within their sub-namespace, for convenience.
The Bonobo API, available directly under the :mod:`bonobo` package, contains all the tools you need to get started with
bonobo.
The :mod:`bonobo` package
:::::::::::::::::::::::::
.. automodule:: bonobo
:members: create_strategy, get_examples_path, run
:undoc-members:
:show-inheritance:
Config
------
.. automodule:: bonobo.config
:members:
:undoc-members:
:show-inheritance:
Context
-------
.. automodule:: bonobo.context
:members:
:undoc-members:
:show-inheritance:
Core
----
.. automodule:: bonobo.core
:members:
:undoc-members:
:show-inheritance:
IO
--
.. automodule:: bonobo.io
:members:
:undoc-members:
:show-inheritance:
Util
----
.. automodule:: bonobo.util
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,10 @@
Config API
==========
The Config API, located under the :mod:`bonobo.config` namespace, contains all the tools you need to create
configurable transformations, either class-based or function-based.
.. automodule:: bonobo.config
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,5 +1,5 @@
Commands Reference
==================
Command-line
============
Bonobo Init
:::::::::::

View File

@ -1,13 +1,44 @@
Examples
========
There are a few examples bundled with **bonobo**. You'll find them under the :mod:`bonobo.examples` package.
There are a few examples bundled with **bonobo**. You'll find them under the :mod:`bonobo.examples` package, and
you can try them in a clone of bonobo by typing::
$ bonobo run bonobo/examples/.../file.py
Datasets
::::::::
.. module:: bonobo.examples.datasets
The :mod:`bonobo.examples.datasets` package contains examples that generates datasets locally for other examples to
use. As of today, we commit the content of those datasets to git, even if that may be a bad idea, so all the examples
are easily runnable. Later, we'll see if we favor a "missing dependency exception" approach.
Coffeeshops
-----------
.. automodule:: bonobo.examples.datasets.coffeeshops
:members:
:undoc-members:
:show-inheritance:
Fablabs
-------
.. automodule:: bonobo.examples.datasets.fablabs
:members:
:undoc-members:
:show-inheritance:
Types
:::::
bonobo.examples.types.strings
-----------------------------
Strings
-------
.. automodule:: bonobo.examples.types.strings
:members: graph, extract, transform, load
@ -15,8 +46,8 @@ bonobo.examples.types.strings
:show-inheritance:
bonobo.examples.types.dicts
---------------------------
Dicts
-----
.. automodule:: bonobo.examples.types.dicts
:members: graph, extract, transform, load
@ -24,8 +55,8 @@ bonobo.examples.types.dicts
:show-inheritance:
bonobo.examples.types.bags
--------------------------
Bags
----
.. automodule:: bonobo.examples.types.bags
:members: graph, extract, transform, load
@ -33,4 +64,15 @@ bonobo.examples.types.bags
:show-inheritance:
Utils
:::::
Count
-----
.. automodule:: bonobo.examples.utils.count
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,11 +1,10 @@
References
==========
.. todo:: write the fucking doc!
.. toctree::
:maxdepth: 4
commands
api
api_config
commands
examples

View File

@ -10,7 +10,7 @@ We strongly advice that even if you're an advanced python developper, you go thr
reasons: that should be sufficient to do anything possible with **Bonobo** and that's a good moment to learn the few
concepts you'll see everywhere in the software.
If you're not familiar with python, you should first read :doc:`./python`.
If you're not familiar with python, you should first read :doc:`python`.
.. toctree::
:maxdepth: 2

View File

@ -6,16 +6,6 @@ Just enough Python for Bonobo
This is a work in progress and it is not yet available. Please come back later or even better, help us write this
guide!
This guide is intended to help programmers or enthusiasts to grasp the python basics necessary to use Bonobo. It should
definately not be considered as a general python introduction, neither a deep dive into details.
.. toctree::
:maxdepth: 2
python01
python02
python03
python04
python05
This guide is intended to help programmers or enthusiasts to grasp the python basics necessary to use Bonobo. It
should definately not be considered as a general python introduction, neither a deep dive into details.

View File

@ -8,59 +8,74 @@ root_dir = os.path.dirname(os.path.abspath(__file__))
tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n'))))
def read(filename, flt=None):
try:
with open(filename) as f:
with open(filename, 'rt') as f:
content = f.read().strip()
return flt(content) if callable(flt) else content
except FileNotFoundError:
except EnvironmentError:
# File not found? Let's say it's empty.
return ''
except UnicodeError:
# Problem decoding the file? Let's not stop on this (but it's a temp fix).
return ''
# Py3 compatibility hacks, borrowed from IPython.
try:
execfile
except NameError:
def execfile(fname, globs, locs=None):
locs = locs or globs
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
version_ns = {}
execfile(os.path.join(root_dir, 'bonobo/_version.py'), version_ns)
version = version_ns.get('__version__', 'dev')
try:
execfile(os.path.join(root_dir, 'bonobo/_version.py'), version_ns)
except EnvironmentError:
version = 'dev'
else:
version = version_ns.get('__version__', 'dev')
setup(
name = 'bonobo',
description = 'Bonobo',
license = 'Apache License, Version 2.0',
install_requires = ['colorama >=0.3,<1.0',
'fs >=2.0,<3.0',
'psutil >=5.2,<6.0',
'requests >=2.0,<3.0',
'stevedore >=1.21,<2.0'],
version = version,
long_description = read('README.rst'),
classifiers = read('classifiers.txt', tolines),
packages = find_packages(exclude=['ez_setup', 'example', 'test']),
include_package_data = True,
data_files = [('share/jupyter/nbextensions/bonobo-jupyter',
['bonobo/ext/jupyter/static/extension.js',
'bonobo/ext/jupyter/static/index.js',
'bonobo/ext/jupyter/static/index.js.map'])],
extras_require = {'dev': ['coverage >=4,<5',
'pylint >=1,<2',
'pytest >=3,<4',
'pytest-cov >=2,<3',
'pytest-timeout >=1,<2',
'sphinx',
'sphinx_rtd_theme',
'yapf'],
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']},
entry_points = {'bonobo.commands': ['init = bonobo.commands.init:register',
'run = bonobo.commands.run:register',
'version = bonobo.commands.version:register'],
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
'edgy.project.features': ['bonobo = '
'bonobo.ext.edgy.project.feature:BonoboFeature']},
url = 'https://www.bonobo-project.org/',
download_url = 'https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version),
name='bonobo',
description='Bonobo',
license='Apache License, Version 2.0',
install_requires=[
'colorama >=0.3,<1.0', 'fs >=2.0,<3.0', 'psutil >=5.2,<6.0', 'requests >=2.0,<3.0', 'stevedore >=1.21,<2.0'
],
version=version,
long_description=read('README.rst'),
classifiers=read('classifiers.txt', tolines),
packages=find_packages(exclude=['ez_setup', 'example', 'test']),
include_package_data=True,
data_files=[
(
'share/jupyter/nbextensions/bonobo-jupyter', [
'bonobo/ext/jupyter/static/extension.js', 'bonobo/ext/jupyter/static/index.js',
'bonobo/ext/jupyter/static/index.js.map'
]
)
],
extras_require={
'dev': [
'coverage >=4,<5', 'pylint >=1,<2', 'pytest >=3,<4', 'pytest-cov >=2,<3', 'pytest-timeout >=1,<2', 'sphinx',
'sphinx_rtd_theme', 'yapf'
],
'jupyter': ['jupyter >=1.0,<1.1', 'ipywidgets >=6.0.0.beta5']
},
entry_points={
'bonobo.commands': [
'init = bonobo.commands.init:register', 'run = bonobo.commands.run:register',
'version = bonobo.commands.version:register'
],
'console_scripts': ['bonobo = bonobo.commands:entrypoint'],
'edgy.project.features': ['bonobo = '
'bonobo.ext.edgy.project.feature:BonoboFeature']
},
url='https://www.bonobo-project.org/',
download_url='https://github.com/python-bonobo/bonobo/tarball/{version}'.format(version=version),
)

View File

@ -1,4 +1,4 @@
from mock import patch
from unittest.mock import patch
from bonobo.ext.opendatasoft import OpenDataSoftAPI
from bonobo.util.objects import ValueHolder

View File

@ -21,7 +21,7 @@ def test_file_writer_in_context(tmpdir, lines, output):
context.start()
context.recv(BEGIN, *map(Bag, lines), END)
for i in range(len(lines)):
for _ in range(len(lines)):
context.step()
context.stop()

View File

@ -1,4 +1,4 @@
from mock import Mock
from unittest.mock import Mock
from bonobo import Bag
from bonobo.constants import INHERIT_INPUT

View File

@ -18,7 +18,7 @@ def test_entrypoint():
def test_no_command(capsys):
with pytest.raises(SystemExit):
entrypoint([])
out, err = capsys.readouterr()
_, err = capsys.readouterr()
assert 'error: the following arguments are required: command' in err