- PEP 8, when sensible.
- Test ruthlessly. Write docs for new features.
- Even more important than Test-Driven Development--Human-Driven Development.
- Please update AUTHORS.rst when you contribute.
Clone the repo:
$ git clone https://github.com/CenterForOpenScience/modular-file-renderer.git $ cd modular-file-renderer
Configure development environment and install the development dependencies.
Note
It is recommended that you use a virtualenv with virtualenvwrapper during development. Python 3.5 or greater, R, and pspp are required.
# For Mac OS X: Install the latest version of python3.5
$ brew install python3
$ brew install r pspp
# Linux users, probably the same thing but with apt-get
# If someone wants to update this guide, please do.
$ pip install virtualenv
$ pip install virtualenvwrapper
$ mkvirtualenv --python=`which python3` mfr
$ pip install setuptools==30.4.0
$ pip install invoke==0.13.0
Lastly, install mfr in development mode.
$ invoke install -d $ invoke server
To run all tests (requires pytest)
$ invoke test
You can also use pytest directly.
$ py.test
Unit tests should be written for all rendering code.
Tests should be encapsulated within a class and written as functions, like so:
# in test_myformat.py
from mfr_something import render
def test_render_html():
with open('testfile.mp4') as fp:
assert render.render_html(fp) == '<p>rendered testfile.mp4</p>'
There are a few pytest fixtures to help you mock files. You can use them by simply including them as parameters to your test functions. For example, the fakefile
fixture is a fake file-like object whose name and content you can set to any value.
The above test can be rewritten like so:
# in test_myformat.py
from mfr_something import render
def test_render_html(fakefile):
assert render.render_html(fakefile) == '<p>rendered testfile.mp4</p>'
To make sure a new renderer is functioning properly, it's recommended that you try to render a file of that type locally.
First, change the default provider to HTTP (in /mfr/server/settings.py) and update the provider domain whitelist:
PROVIDER_NAME = config.get('PROVIDER_NAME', 'http')
ALLOWED_PROVIDER_DOMAINS = config.get('ALLOWED_PROVIDER_DOMAINS', ['http://localhost:8000/'])
Because the MFR is passed a url to render, you also need to be running an http server.
From a directory with a file you want to render:
python -m SimpleHTTPServer 8000
Or for python 3
python3 -m http.server 8000
With both the SimpleHTTPServer the MFR server running, go to
http://localhost:7778/render?url=http://localhost:8000/[filename].[ext]
There are two main pieces of a file format package are
- Your custom rendering and/or exporting code
- Your :class:`FileHandler <mfr.core.FileHandler>`
Renderers are simply callables (functions or methods) that take a file as their first argument and return
Here is a very simple example of function that takes a filepointer and outputs a render result with an HTML image tag.
def render_img_tag(filepointer):
filename = filepointer.name
content = '<img src="{filename}" />'.format(filename=filename)
return RenderResult(content)
You can also write renderers as methods.
# in mfr_video/render.py
class VideoRenderer(object):
def render_html5_tag(self, fp):
content = '<video src="{filename}"></video>'.format(filename=fp.name)
return RenderResult(content)
def render_flash(self, fp):
# ...
pass
A file handler is responsible for using your custom rendering and exporting code to actually render and export a file. When you call :func:`mfr.detect <mfr.detect>`, you receive a list of :class:`FileHandler <mfr.core.FileHandler>` classes.
Your FileHandler must define a detect
method which, given a file object, returns whether or not it can handle the file.
Your FileHandler class should be named Handler and should be defined in your `mfr_format/__init__.py` file.
# in mfr_image/__init__.py
from mfr import FileHandler, get_file_extension
# Your custom code
from mfr_image.render import render_img_tag
from mfr_image.export import ImageExporter
class Handler(FileHandler):
renderers = {
'html': render_img_tag,
}
exporters = {
'png': ImageExporter().export_png,
'jpg': ImageExporter().export_jpg,
# ...
}
def detect(self, fp):
return get_file_extension(fp.name) in ['.jpg', '.png', ] # and so on
Each package has its own directory. At a minimum, your package should include:
__init__.py
: Where your :class:`FileHandler <mfr.core.FileHandler>`` subclass will live.render-requirements.txt
: External dependencies for rendering functionality.export-requirements.txt
: External dependencies for export functionality.
Apart from those files, you are free to organize your rendering and export code however you want.
A typical extension plugin directory structure might look like this:
modular-file-renderer ├── mfr │ ├── __init__.py │ └── extensions │ ├── __init__.py │ └── custom-plugin │ ├── __init__.py │ ├── render.py │ ├── export.py │ ├── settings.py │ ├── static │ │ ├── css │ │ └── js │ ├── templates │ │ └── viewer.mako │ └── libs │ ├── __init__.py │ └── tools.py ├── tests │ ├── __init__.py │ └── extensions │ ├── __init__.py │ └── custom-plugin │ ├── __init__.py │ └── test_custom_plugin.py ├── setup.py ├── README.md └── requirements.txt
Contributions to the documentation are welcome. Documentation is written in reStructured Text (rST). A quick rST reference can be found here. Builds are powered by Sphinx.
To build docs:
$ pip install -r doc-requirements.txt $ cd docs $ make html $ open _build/html/index.html
The -b
(for "browse") automatically opens up the docs in your browser after building.