Merge pull request #91 from hartym/develop

Doc and fixes.
This commit is contained in:
Romain Dorgueil
2017-05-28 14:19:54 -07:00
committed by GitHub
22 changed files with 313 additions and 112 deletions

View File

@ -9,8 +9,6 @@ install:
- make install-dev - make install-dev
- pip install coveralls - pip install coveralls
script: script:
- make clean docs test - make clean test
- pip install pycountry
- bin/run_all_examples.sh
after_success: after_success:
- coveralls - coveralls

View File

@ -1,7 +1,7 @@
# This file has been auto-generated. # This file has been auto-generated.
# All changes will be lost, see Projectfile. # All changes will be lost, see Projectfile.
# #
# Updated at 2017-05-28 16:50:32.109035 # Updated at 2017-05-28 22:04:19.262686
PACKAGE ?= bonobo PACKAGE ?= bonobo
PYTHON ?= $(shell which python) PYTHON ?= $(shell which python)

View File

@ -45,6 +45,7 @@ python.add_requirements(
'stevedore >=1.21,<2.0', 'stevedore >=1.21,<2.0',
dev=[ dev=[
'pytest-timeout >=1,<2', 'pytest-timeout >=1,<2',
'cookiecutter >=1.5,<1.6',
], ],
docker=[ docker=[
'bonobo-docker', 'bonobo-docker',

View File

@ -5,6 +5,7 @@ from stevedore import ExtensionManager
def entrypoint(args=None): def entrypoint(args=None):
logging.basicConfig()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='command') subparsers = parser.add_subparsers(dest='command')
@ -19,9 +20,7 @@ def entrypoint(args=None):
except Exception: except Exception:
logging.exception('Error while loading command {}.'.format(ext.name)) logging.exception('Error while loading command {}.'.format(ext.name))
mgr = ExtensionManager( mgr = ExtensionManager(namespace='bonobo.commands')
namespace='bonobo.commands',
)
mgr.map(register_extension) mgr.map(register_extension)
args = parser.parse_args(args).__dict__ args = parser.parse_args(args).__dict__

View File

@ -1,3 +1,4 @@
import importlib
import os import os
import runpy import runpy
@ -44,7 +45,13 @@ def execute(filename, module, install=False, quiet=False, verbose=False):
if os.path.isdir(filename): if os.path.isdir(filename):
if install: if install:
requirements = os.path.join(filename, 'requirements.txt') requirements = os.path.join(filename, 'requirements.txt')
pip.main(['install', '-qr', 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)
filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME) filename = os.path.join(filename, DEFAULT_GRAPH_FILENAME)
elif install: elif install:
raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).') raise RuntimeError('Cannot --install on a file (only available for dirs containing requirements.txt).')

View File

@ -0,0 +1,23 @@
import bonobo
def extract():
yield 'foo'
yield 'bar'
yield 'baz'
def transform(x):
return x.upper()
def load(x):
print(x)
graph = bonobo.Graph(extract, transform, load)
graph.__doc__ = 'hello'
if __name__ == '__main__':
bonobo.run(graph)

View File

@ -0,0 +1,10 @@
import bonobo
graph = bonobo.Graph(
['foo', 'bar', 'baz', ],
str.upper,
print,
)
if __name__ == '__main__':
bonobo.run(graph)

View File

@ -1,10 +1,16 @@
Examples Examples
======== ========
There are a few examples bundled with **bonobo**. You'll find them under the :mod:`bonobo.examples` package, and There are a few examples bundled with **bonobo**.
you can try them in a clone of bonobo by typing::
$ bonobo run bonobo/examples/.../file.py You'll find them under the :mod:`bonobo.examples` package, and you can run them directly as modules:
$ bonobo run -m bonobo.examples...module
.. toctree::
:maxdepth: 4
examples/tutorials
Datasets Datasets

View File

@ -0,0 +1,50 @@
Examples from the tutorial
==========================
Examples from :doc:`/tutorial/tut01`
::::::::::::::::::::::::::::::::::::
Example 1
---------
.. automodule:: bonobo.examples.tutorials.tut01e01
:members:
:undoc-members:
:show-inheritance:
Example 2
---------
.. automodule:: bonobo.examples.tutorials.tut01e02
:members:
:undoc-members:
:show-inheritance:
Examples from :doc:`/tutorial/tut02`
::::::::::::::::::::::::::::::::::::
Example 1: Read
---------------
.. automodule:: bonobo.examples.tutorials.tut02e01_read
:members:
:undoc-members:
:show-inheritance:
Example 2: Write
----------------
.. automodule:: bonobo.examples.tutorials.tut02e02_write
:members:
:undoc-members:
:show-inheritance:
Example 3: Write as map
-----------------------
.. automodule:: bonobo.examples.tutorials.tut02e02_writeasmap
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,58 +1,91 @@
Basic concepts Let's get started!
============== ==================
To begin with Bonobo, you need to install it in a working python 3.5+ environment: To begin with Bonobo, you need to install it in a working python 3.5+ environment, and you'll also need cookiecutter
to bootstrap your project.
.. code-block:: shell-session .. code-block:: shell-session
$ pip install bonobo $ pip install bonobo cookiecutter
See :doc:`/install` for more options. See :doc:`/install` for more options.
Let's write a first data transformation
:::::::::::::::::::::::::::::::::::::::
We'll start with the simplest transformation possible. Create an empty project
:::::::::::::::::::::::
In **Bonobo**, a transformation is a plain old python callable, not more, not less. Let's write one that takes a string Your ETL code will live in ETL projects, which are basically a bunch of files, including python code, that bonobo
and uppercases it. can run.
.. code-block:: shell-session
bonobo init tutorial
This will create a `tutorial` directory (`content description here <https://www.bonobo-project.org/with/cookiecutter>`_).
To run this project, use:
.. code-block:: shell-session
bonobo run tutorial
Write a first transformation
::::::::::::::::::::::::::::
Open `tutorial/__main__.py`, and delete all the code here.
A transformation can be whatever python can call, having inputs and outputs. Simplest transformations are functions.
Let's write one:
.. code-block:: python .. code-block:: python
def uppercase(x: str): def transform(x):
return x.upper() return x.upper()
Pretty straightforward. Easy.
You could even use :func:`str.upper` directly instead of writing a wrapper, as a type's method (unbound) will take an .. note::
instance of this type as its first parameter (what you'd call `self` in your method).
The type annotations written here are not used, but can make your code much more readable, and may very well be used as This is about the same as :func:`str.upper`, and in the real world, you'd use it directly.
validators in the future.
Let's write two more transformations: a generator to produce the data to be transformed, and something that outputs it, Let's write two more transformations for the "extract" and "load" steps. In this example, we'll generate the data from
because, yeah, feedback is cool. scratch, and we'll use stdout to simulate data-persistence.
.. code-block:: python .. code-block:: python
def generate_data(): def extract():
yield 'foo' yield 'foo'
yield 'bar' yield 'bar'
yield 'baz' yield 'baz'
def output(x: str): def load(x):
print(x) print(x)
Once again, you could have skipped the pain of writing this and simply use an iterable to generate the data and the Bonobo makes no difference between generators (yielding functions) and regular functions. It will, in all cases, iterate
builtin :func:`print` for the output, but we'll stick to writing our own transformations for now. on things returned, and a normal function will just be seen as a generator that yields only once.
Let's chain the three transformations together and run the transformation graph: .. note::
Once again, :func:`print` would be used directly in a real-world transformation.
Create a transformation graph
:::::::::::::::::::::::::::::
Bonobo main roles are two things:
* Execute the transformations in independant threads
* Pass the outputs of one thread to other(s) thread(s).
To do this, it needs to know what data-flow you want to achieve, and you'll use a :class:`bonobo.Graph` to describe it.
.. code-block:: python .. code-block:: python
import bonobo import bonobo
graph = bonobo.Graph(generate_data, uppercase, output) graph = bonobo.Graph(extract, transform, load)
if __name__ == '__main__': if __name__ == '__main__':
bonobo.run(graph) bonobo.run(graph)
@ -64,14 +97,60 @@ Let's chain the three transformations together and run the transformation graph:
stylesheet = "../_static/graphs.css"; stylesheet = "../_static/graphs.css";
BEGIN [shape="point"]; BEGIN [shape="point"];
BEGIN -> "generate_data" -> "uppercase" -> "output"; BEGIN -> "extract" -> "transform" -> "load";
} }
We use the :func:`bonobo.run` helper that hides the underlying object composition necessary to actually run the .. note::
transformations in parallel, because it's simpler.
Depending on what you're doing, you may use the shorthand helper method, or the verbose one. Always favor the shorter, The `if __name__ == '__main__':` section is not required, unless you want to run it directly using the python
if you don't need to tune the graph or the execution strategy (see below). interpreter.
Execute the job
:::::::::::::::
Save `tutorial/__main__.py` and execute your transformation:
.. code-block:: shell-session
bonobo run tutorial
This example is available in :mod:`bonobo.examples.tutorials.tut01e01`, and you can also run it as a module:
.. code-block:: shell-session
bonobo run -m bonobo.examples.tutorials.tut01e01
Rewrite it using builtins
:::::::::::::::::::::::::
There is a much simpler way to describe an equivalent graph:
.. code-block:: python
import bonobo
graph = bonobo.Graph(
['foo', 'bar', 'baz',],
str.upper,
print,
)
if __name__ == '__main__':
bonobo.run(graph)
We use a shortcut notation for the generator, with a list. Bonobo will wrap an iterable as a generator by itself if it
is added in a graph.
This example is available in :mod:`bonobo.examples.tutorials.tut01e02`, and you can also run it as a module:
.. code-block:: shell-session
bonobo run -m bonobo.examples.tutorials.tut01e02
You can now jump to the next part (:doc:`tut02`), or read a small summary of concepts and definitions introduced here
below.
Takeaways Takeaways
::::::::: :::::::::
@ -79,7 +158,7 @@ Takeaways
① The :class:`bonobo.Graph` class is used to represent a data-processing pipeline. ① The :class:`bonobo.Graph` class is used to represent a data-processing pipeline.
It can represent simple list-like linear graphs, like here, but it can also represent much more complex graphs, with It can represent simple list-like linear graphs, like here, but it can also represent much more complex graphs, with
branches and cycles. forks and joins.
This is what the graph we defined looks like: This is what the graph we defined looks like:
@ -97,10 +176,10 @@ either `return` or `yield` data to send it to the next step. Regular functions (
each call is guaranteed to return exactly one result, while generators (using `yield`) should be prefered if the each call is guaranteed to return exactly one result, while generators (using `yield`) should be prefered if the
number of output lines for a given input varies. number of output lines for a given input varies.
③ The `Graph` instance, or `transformation graph` is then executed using an `ExecutionStrategy`. You did not use it ③ The `Graph` instance, or `transformation graph` is executed using an `ExecutionStrategy`. You won't use it directly,
directly in this tutorial, but :func:`bonobo.run` created an instance of :class:`bonobo.ThreadPoolExecutorStrategy` but :func:`bonobo.run` created an instance of :class:`bonobo.ThreadPoolExecutorStrategy` under the hood (the default
under the hood (which is the default strategy). Actual behavior of an execution will depend on the strategy chosen, but strategy). Actual behavior of an execution will depend on the strategy chosen, but the default should be fine for most
the default should be fine in most of the basic cases. cases.
④ Before actually executing the `transformations`, the `ExecutorStrategy` instance will wrap each component in an ④ Before actually executing the `transformations`, the `ExecutorStrategy` instance will wrap each component in an
`execution context`, whose responsibility is to hold the state of the transformation. It enables to keep the `execution context`, whose responsibility is to hold the state of the transformation. It enables to keep the
@ -111,21 +190,22 @@ Concepts and definitions
* Transformation: a callable that takes input (as call parameters) and returns output(s), either as its return value or * Transformation: a callable that takes input (as call parameters) and returns output(s), either as its return value or
by yielding values (a.k.a returning a generator). by yielding values (a.k.a returning a generator).
* Transformation graph (or Graph): a set of transformations tied together in a :class:`bonobo.Graph` instance, which is a simple
directed acyclic graph (also refered as a DAG, sometimes). * Transformation graph (or Graph): a set of transformations tied together in a :class:`bonobo.Graph` instance, which is
* Node: a transformation within the context of a transformation graph. The node defines what to do with a a directed acyclic graph (or DAG).
transformation's output, and especially what other nodes to feed with the output.
* Node: a graph element, most probably a transformation in a graph.
* Execution strategy (or strategy): a way to run a transformation graph. It's responsibility is mainly to parallelize * Execution strategy (or strategy): a way to run a transformation graph. It's responsibility is mainly to parallelize
(or not) the transformations, on one or more process and/or computer, and to setup the right queuing mechanism for (or not) the transformations, on one or more process and/or computer, and to setup the right queuing mechanism for
transformations' inputs and outputs. transformations' inputs and outputs.
* Execution context (or context): a wrapper around a node that holds the state for it. If the node needs state, there * Execution context (or context): a wrapper around a node that holds the state for it. If the node needs state, there
are tools available in bonobo to feed it to the transformation using additional call parameters, and so every are tools available in bonobo to feed it to the transformation using additional call parameters, keeping
transformation will be atomic. transformations stateless.
Next Next
:::: ::::
You now know all the basic concepts necessary to build (batch-like) data processors. Time to jump to the second part: :doc:`tut02`.
Time to jump to the second part: :doc:`tut02`

View File

@ -1,11 +1,14 @@
Working with files Working with files
================== ==================
Bonobo would be a bit useless if the aim was just to uppercase small lists of strings. Bonobo would be pointless if the aim was just to uppercase small lists of strings.
In fact, Bonobo should not be used if you don't expect any gain from parallelization/distribution of tasks. In fact, Bonobo should not be used if you don't expect any gain from parallelization/distribution of tasks.
Let's take the following graph as an example: Some background...
::::::::::::::::::
Let's take the following graph:
.. graphviz:: .. graphviz::
@ -16,8 +19,8 @@ Let's take the following graph as an example:
"B" -> "D"; "B" -> "D";
} }
The execution strategy does a bit of under the scene work, wrapping every component in a thread (assuming you're using When run, the execution strategy wraps every component in a thread (assuming you're using the default
the :class:`bonobo.strategies.ThreadPoolExecutorStrategy`). :class:`bonobo.strategies.ThreadPoolExecutorStrategy`).
Bonobo will send each line of data in the input node's thread (here, `A`). Now, each time `A` *yields* or *returns* Bonobo will send each line of data in the input node's thread (here, `A`). Now, each time `A` *yields* or *returns*
something, it will be pushed on `B` input :class:`queue.Queue`, and will be consumed by `B`'s thread. something, it will be pushed on `B` input :class:`queue.Queue`, and will be consumed by `B`'s thread.
@ -25,9 +28,11 @@ something, it will be pushed on `B` input :class:`queue.Queue`, and will be cons
When there is more than one node linked as the output of a node (for example, with `B`, `C`, and `D`) , the same thing When there is more than one node linked as the output of a node (for example, with `B`, `C`, and `D`) , the same thing
happens except that each result coming out of `B` will be sent to both on `C` and `D` input :class:`queue.Queue`. happens except that each result coming out of `B` will be sent to both on `C` and `D` input :class:`queue.Queue`.
The great thing is that you generally don't have to think about it. Just be aware that your components will be run in One thing to keep in mind here is that as the objects are passed from thread to thread, you need to write "pure"
parallel (with the default strategy), and don't worry too much about blocking components, as they won't block their transformations (see :doc:`/guide/purity`).
siblings when run in bonobo.
You generally don't have to think about it. Just be aware that your nodes will run in parallel, and don't worry
too much about blocking nodes, as they won't block other nodes.
That being said, let's manipulate some files. That being said, let's manipulate some files.
@ -38,9 +43,10 @@ There are a few component builders available in **Bonobo** that let you read fro
All readers work the same way. They need a filesystem to work with, and open a "path" they will read from. All readers work the same way. They need a filesystem to work with, and open a "path" they will read from.
* :class:`bonobo.io.FileReader` * :class:`bonobo.CsvReader`
* :class:`bonobo.io.JsonReader` * :class:`bonobo.FileReader`
* :class:`bonobo.io.CsvReader` * :class:`bonobo.JsonReader`
* :class:`bonobo.PickleReader`
We'll use a text file that was generated using Bonobo from the "liste-des-cafes-a-un-euro" dataset made available by We'll use a text file that was generated using Bonobo from the "liste-des-cafes-a-un-euro" dataset made available by
Mairie de Paris under the Open Database License (ODbL). You can `explore the original dataset Mairie de Paris under the Open Database License (ODbL). You can `explore the original dataset
@ -49,35 +55,14 @@ Mairie de Paris under the Open Database License (ODbL). You can `explore the ori
You'll need the `example dataset <https://github.com/python-bonobo/bonobo/blob/0.3/bonobo/examples/datasets/coffeeshops.txt>`_, You'll need the `example dataset <https://github.com/python-bonobo/bonobo/blob/0.3/bonobo/examples/datasets/coffeeshops.txt>`_,
available in **Bonobo**'s repository. available in **Bonobo**'s repository.
.. literalinclude:: ../../bonobo/examples/tutorials/tut02_01_read.py .. literalinclude:: ../../bonobo/examples/tutorials/tut02e01_read.py
:language: python :language: python
You can run this script directly using the python interpreter: You can run this example as a module:
.. code-block:: shell-session .. code-block:: shell-session
$ python bonobo/examples/tutorials/tut02_01_read.py $ bonobo run -m bonobo.examples.tutorials.tut02e01_read
Another option is to use the bonobo cli, which allows more flexibility:
.. code-block:: shell-session
$ bonobo run bonobo/examples/tutorials/tut02_01_read.py
Using bonobo command line has a few advantages.
It will look for one and only one :class:`bonobo.Graph` instance in the file given as argument, configure an execution
strategy, eventually plugins, and execute it. It has the benefit of allowing to tune the "artifacts" surrounding the
transformation graph on command line (verbosity, plugins ...), and it will also ease the transition to run
transformation graphs in containers, as the syntax will be the same. Of course, it is not required, and the
containerization capabilities are provided by an optional and separate python package.
It also change a bit the way you can configure service dependencies. The CLI won't run the `if __name__ == '__main__'`
block, and thus it won't get the configured services passed to :func:`bonobo.run`. Instead, one option to configure
services is to define a `get_services()` function in a
`_services.py <https://github.com/python-bonobo/bonobo/blob/0.3/bonobo/examples/tutorials/_services.py>`_ file.
There will be more options using the CLI or environment to override things soon.
Writing to files Writing to files
:::::::::::::::: ::::::::::::::::
@ -86,22 +71,34 @@ Let's split this file's each lines on the first comma and store a json file mapp
Here are, like the readers, the classes available to write files Here are, like the readers, the classes available to write files
* :class:`bonobo.io.FileWriter` * :class:`bonobo.CsvWriter`
* :class:`bonobo.io.JsonWriter` * :class:`bonobo.FileWriter`
* :class:`bonobo.io.CsvWriter` * :class:`bonobo.JsonWriter`
* :class:`bonobo.PickleWriter`
Let's write a first implementation: Let's write a first implementation:
.. literalinclude:: ../../bonobo/examples/tutorials/tut02_02_write.py .. literalinclude:: ../../bonobo/examples/tutorials/tut02e02_write.py
:language: python :language: python
You can run it and read the output file, you'll see it misses the "map" part of the question. Let's extend (run it with :code:`bonobo run -m bonobo.examples.tutorials.tut02e02_write` or :code:`bonobo run myfile.py`)
:class:`bonobo.io.JsonWriter` to finish the job:
.. literalinclude:: ../../bonobo/examples/tutorials/tut02_03_writeasmap.py If you read the output file, you'll see it misses the "map" part of the problem.
Let's extend :class:`bonobo.io.JsonWriter` to finish the job:
.. literalinclude:: ../../bonobo/examples/tutorials/tut02e03_writeasmap.py
:language: python :language: python
You can now run it again, it should produce a nice map. We favored a bit hackish solution here instead of constructing a (run it with :code:`bonobo run -m bonobo.examples.tutorials.tut02e03_writeasmap` or :code:`bonobo run myfile.py`)
map in python then passing the whole to :func:`json.dumps` because we want to work with streams, if you have to
construct the whole data structure in python, you'll loose a lot of bonobo's benefits.
It should produce a nice map.
We favored a bit hackish solution here instead of constructing a map in python then passing the whole to
:func:`json.dumps` because we want to work with streams, if you have to construct the whole data structure in python,
you'll loose a lot of bonobo's benefits.
Next
::::
Time to write some more advanced transformations, with service dependencies: :doc:`tut03`.

9
docs/tutorial/tut03.rst Normal file
View File

@ -0,0 +1,9 @@
Configurables and Services
==========================
TODO
Next
::::
:doc:`tut04`.

4
docs/tutorial/tut04.rst Normal file
View File

@ -0,0 +1,4 @@
Working with databases
======================
TODO

View File

@ -1,24 +1,32 @@
-e .[dev] -e .[dev]
alabaster==0.7.10 alabaster==0.7.10
arrow==0.10.0
babel==2.4.0 babel==2.4.0
binaryornot==0.4.3
certifi==2017.4.17 certifi==2017.4.17
chardet==3.0.3 chardet==3.0.3
click==6.7
cookiecutter==1.5.1
coverage==4.4.1 coverage==4.4.1
docutils==0.13.1 docutils==0.13.1
future==0.16.0
idna==2.5 idna==2.5
imagesize==0.7.1 imagesize==0.7.1
jinja2-time==0.2.0
jinja2==2.9.6 jinja2==2.9.6
markupsafe==1.0 markupsafe==1.0
poyo==0.4.1
py==1.4.33 py==1.4.33
pygments==2.2.0 pygments==2.2.0
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-timeout==1.2.0 pytest-timeout==1.2.0
pytest==3.1.0 pytest==3.1.0
python-dateutil==2.6.0
pytz==2017.2 pytz==2017.2
requests==2.16.5 requests==2.16.5
six==1.10.0 six==1.10.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
sphinx==1.6.1 sphinx==1.6.2
sphinxcontrib-websupport==1.0.1 sphinxcontrib-websupport==1.0.1
typing==3.6.1
urllib3==1.21.1 urllib3==1.21.1
whichcraft==0.4.1

View File

@ -58,8 +58,8 @@ setup(
], ],
extras_require={ extras_require={
'dev': [ 'dev': [
'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)', 'pytest-cov (>= 2.5, < 3.0)', 'cookiecutter (>= 1.5, < 1.6)', 'coverage (>= 4.4, < 5.0)', 'pytest (>= 3.1, < 4.0)',
'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)' 'pytest-cov (>= 2.5, < 3.0)', 'pytest-timeout (>= 1, < 2)', 'sphinx (>= 1.6, < 2.0)'
], ],
'docker': ['bonobo-docker'], 'docker': ['bonobo-docker'],
'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)'] 'jupyter': ['ipywidgets (>= 6.0.0.beta5)', 'jupyter (>= 1.0, < 1.1)']

View File

@ -19,7 +19,8 @@ def test_write_csv_to_file(tmpdir):
context.step() context.step()
context.stop() context.stop()
assert fs.open(filename).read() == 'foo\nbar\nbaz\n' with fs.open(filename)as fp:
assert fp.read() == 'foo\nbar\nbaz\n'
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
getattr(context, 'file') getattr(context, 'file')
@ -27,7 +28,8 @@ def test_write_csv_to_file(tmpdir):
def test_read_csv_from_file(tmpdir): def test_read_csv_from_file(tmpdir):
fs, filename = open_fs(tmpdir), 'input.csv' fs, filename = open_fs(tmpdir), 'input.csv'
fs.open(filename, 'w').write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') with fs.open(filename, 'w') as fp:
fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar')
reader = CsvReader(path=filename, delimiter=',') reader = CsvReader(path=filename, delimiter=',')
@ -59,7 +61,8 @@ def test_read_csv_from_file(tmpdir):
def test_read_csv_kwargs_output_formater(tmpdir): def test_read_csv_kwargs_output_formater(tmpdir):
fs, filename = open_fs(tmpdir), 'input.csv' fs, filename = open_fs(tmpdir), 'input.csv'
fs.open(filename, 'w').write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar') with fs.open(filename, 'w') as fp:
fp.write('a,b,c\na foo,b foo,c foo\na bar,b bar,c bar')
reader = CsvReader(path=filename, delimiter=',', output_format='kwargs') reader = CsvReader(path=filename, delimiter=',', output_format='kwargs')

View File

@ -25,7 +25,8 @@ def test_file_writer_in_context(tmpdir, lines, output):
context.step() context.step()
context.stop() context.stop()
assert fs.open(filename).read() == output with fs.open(filename) as fp:
assert fp.read() == output
def test_file_writer_out_of_context(tmpdir): def test_file_writer_out_of_context(tmpdir):
@ -36,13 +37,15 @@ def test_file_writer_out_of_context(tmpdir):
with writer.open(fs) as fp: with writer.open(fs) as fp:
fp.write('Yosh!') fp.write('Yosh!')
assert fs.open(filename).read() == 'Yosh!' with fs.open(filename) as fp:
assert fp.read() == 'Yosh!'
def test_file_reader_in_context(tmpdir): def test_file_reader_in_context(tmpdir):
fs, filename = open_fs(tmpdir), 'input.txt' fs, filename = open_fs(tmpdir), 'input.txt'
fs.open(filename, 'w').write('Hello\nWorld\n') with fs.open(filename, 'w') as fp:
fp.write('Hello\nWorld\n')
reader = FileReader(path=filename) reader = FileReader(path=filename)
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) context = CapturingNodeExecutionContext(reader, services={'fs': fs})

View File

@ -17,7 +17,8 @@ def test_write_json_to_file(tmpdir):
context.step() context.step()
context.stop() context.stop()
assert fs.open(filename).read() == '[{"foo": "bar"}]' with fs.open(filename) as fp:
assert fp.read() == '[{"foo": "bar"}]'
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
getattr(context, 'file') getattr(context, 'file')
@ -28,7 +29,8 @@ def test_write_json_to_file(tmpdir):
def test_read_json_from_file(tmpdir): def test_read_json_from_file(tmpdir):
fs, filename = open_fs(tmpdir), 'input.json' fs, filename = open_fs(tmpdir), 'input.json'
fs.open(filename, 'w').write('[{"x": "foo"},{"x": "bar"}]') with fs.open(filename, 'w') as fp:
fp.write('[{"x": "foo"},{"x": "bar"}]')
reader = JsonReader(path=filename) reader = JsonReader(path=filename)
context = CapturingNodeExecutionContext(reader, services={'fs': fs}) context = CapturingNodeExecutionContext(reader, services={'fs': fs})

View File

@ -20,7 +20,8 @@ def test_write_pickled_dict_to_file(tmpdir):
context.step() context.step()
context.stop() context.stop()
assert pickle.loads(fs.open(filename, 'rb').read()) == {'foo': 'bar'} with fs.open(filename, 'rb') as fp:
assert pickle.loads(fp.read()) == {'foo': 'bar'}
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
getattr(context, 'file') getattr(context, 'file')
@ -28,8 +29,8 @@ def test_write_pickled_dict_to_file(tmpdir):
def test_read_pickled_list_from_file(tmpdir): def test_read_pickled_list_from_file(tmpdir):
fs, filename = open_fs(tmpdir), 'input.pkl' fs, filename = open_fs(tmpdir), 'input.pkl'
fs.open(filename, with fs.open(filename, 'wb') as fp:
'wb').write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']])) fp.write(pickle.dumps([['a', 'b', 'c'], ['a foo', 'b foo', 'c foo'], ['a bar', 'b bar', 'c bar']]))
reader = PickleReader(path=filename) reader = PickleReader(path=filename)