- PEP 8, when sensible.
- Test-driven: test ruthlessly and write docs for new features.
- Human-driven: make sure any new logic is easy for others to understand.
- If you add an extension to setup.py, add it to
supportedextensions.md
. - 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
Python 3.6 or greater, R, and pspp are required. It's recommended that a python version manager such as pyenv is used and that you use a virtual environment such as pyenv-virtualenv during development.
For Mac OS, here is an example of the commands that might be run to set up MFR. Linux users will probably do the same thing but with a different package manager. If someone wants to update this guide, please do.
$ brew install r pspp
$ pyenv virtualenv 3.6.4 mfr && echo mfr > .python-version
$ pip install setuptools==30.4.0
$ pip install invoke==0.13.0
Lastly, install MFR requirements with the development option.
$ inv install -d
$ inv server
To run all tests (requires pytest
)
$ inv test
You can also use pytest
directly.
$ py.test --cov-report term-missing --cov mfr tests
Unit tests should be written for all rendering code.
Tests should be encapsulated within a class and written as functions. 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.
# in test_myformat.py
from mfr.extensions.my_extension.render import MyExtensionRenderer
@pytest.fixture
def metadata():
return ProviderMetadata(
'file_name',
'.extension',
'text/plain',
'1234',
'http://wb.osf.io/file/file_name.extension?token=1234'
)
def test_render_html(extension, metadata, file_path, assets_url, export_url):
assert MyExtensionRenderer(
extension,
file_metadata,
file_path,
assets_url
).render() == '<p>Rendered file for my_extension</p>'
Check out pytest documentation to learn more about fixtures
To make sure a new renderer is functioning properly, it's recommended that you try to render a file of that type locally. The easiest way to do this would be to use the docker-compose
files available inside the osf repository to get the MFR running, and then it should be straightforward to interact with the service using a tool such as postman. Alternatively, if you are familiar with OSF and its services, you can run full OSF and render files directly with it.
An extension provides a 'renderer' and/or an 'exporter', and is registered in setup.py to allow the plugin to load when it is needed. Renderers and exporters subclasses mfr.core.extension.BaseRenderer
or mfr.core.extension.BaseExporter
respectively. A renderer takes a file path and some file metadata and returns a string of HTML that provides a representation of the file. The logic for the rendering happens in a renderer's render()
function. This is an abstract base class method, and thus is required for the implementation of a renderer. Similarly, BaseExporter
has an export()
method. This method should take a file and convert it to the desired output, and create the newly converted file at the ouput_file_path
.
Renderers have an abstract property file_required
. This is used to determine if the renderer needs the actual content of the file in order to render it. Renderers also have a property cache_result
; this is used to determine whether the ouput of the renderer may be cached to improve future requests for the rendered version of the file.
Renderers subclass mfr.core.extension.BaseRenderer
, and implement a render function, a file_required
property, and a cache_result
property.
import os
from mako.lookup import TemplateLookup
from mfr.core import extension
class ImageRenderer(extension.BaseRenderer):
TEMPLATE = TemplateLookup(
directories=[
os.path.join(os.path.dirname(__file__), 'templates')
]).get_template('viewer.mako')
def render(self):
return self.TEMPLATE.render(base=self.assets_url, url=self.url.geturl())
@property
def file_required(self):
return False
@property
def cache_result(self):
return False
Each plugin has its own directory. At a minimum, a plugin should include:
__init__.py
: This should export themfr.core.extensions.BaseExporter
andmfr.core.extensions.BaseRenderer
subclasses provided by the plugin
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.