|
2 | 2 | Contributing guidelines |
3 | 3 | *********************** |
4 | 4 |
|
5 | | -In general |
6 | | -========== |
7 | | - |
8 | 5 | - `PEP 8`_, when sensible. |
9 | | -- Test ruthlessly. Write docs for new features. |
10 | | -- Even more important than Test-Driven Development--*Human-Driven Development*. |
11 | | -- If you add an extension to setup.py, add it to supportedextensions.md. |
12 | | -- Please update AUTHORS.rst when you contribute. |
| 6 | +- Test-driven: test ruthlessly and write docs for new features. |
| 7 | +- Human-driven: make sure any new logic is easy for others to understand. |
| 8 | +- If you add an extension to setup.py, add it to ``supportedextensions.md``. |
| 9 | +- Please update ``AUTHORS.rst`` when you contribute. |
13 | 10 |
|
14 | 11 | .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ |
15 | 12 |
|
16 | | -In particular |
17 | | -============= |
18 | | - |
19 | | - |
20 | 13 | Setting up for development |
21 | | --------------------------- |
| 14 | +========================== |
22 | 15 |
|
23 | | -Clone the repo: :: |
| 16 | +Clone the repo: |
| 17 | + |
| 18 | +.. code-block:: bash |
24 | 19 |
|
25 | 20 | $ git clone https://github.com/CenterForOpenScience/modular-file-renderer.git |
26 | 21 | $ cd modular-file-renderer |
27 | 22 |
|
28 | 23 | Configure development environment and install the development dependencies. |
29 | 24 |
|
30 | 25 | .. note:: |
| 26 | + Python 3.5 or greater, `R`_, and `pspp`_ are required. |
| 27 | + Python 3.6 is reccomended. 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. |
31 | 28 |
|
32 | | - It is recommended that you use a `virtualenv`_ with `virtualenvwrapper`_ during development. Python 3.5 or greater, `R`_, and `pspp`_ are required. |
33 | | - |
34 | | -.. _virtualenv: http://www.virtualenv.org/en/latest/ |
35 | | -.. _virtualenvwrapper: https://pypi.python.org/pypi/virtualenvwrapper |
36 | 29 | .. _R: https://www.r-project.org/ |
37 | 30 | .. _pspp: https://www.gnu.org/software/pspp/ |
| 31 | +.. _pyenv: https://github.com/pyenv/pyenv |
| 32 | +.. _pyenv-virtualenv: https://github.com/pyenv/pyenv-virtualenv |
| 33 | + |
| 34 | +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. |
38 | 35 |
|
39 | 36 | .. code-block:: bash |
40 | 37 |
|
41 | | - # For Mac OS X: Install the latest version of python3.5 |
42 | | - $ brew install python3 |
43 | 38 | $ brew install r pspp |
44 | | -
|
45 | | - # Linux users, probably the same thing but with apt-get |
46 | | - # If someone wants to update this guide, please do. |
47 | | -
|
48 | | - $ pip install virtualenv |
49 | | - $ pip install virtualenvwrapper |
50 | | - $ mkvirtualenv --python=`which python3` mfr |
| 39 | + $ pyenv virtualenv 3.6.4 mfr && echo mfr > .python-version |
51 | 40 | $ pip install setuptools==30.4.0 |
52 | 41 | $ pip install invoke==0.13.0 |
53 | 42 |
|
| 43 | +Lastly, install MFR requirements with the development option. |
| 44 | + |
| 45 | +.. code-block:: bash |
54 | 46 |
|
55 | | -Lastly, install mfr in development mode. :: |
| 47 | + $ inv install -d |
| 48 | + $ inv server |
56 | 49 |
|
57 | | - $ invoke install -d |
58 | | - $ invoke server |
59 | | - |
60 | 50 | Running tests |
61 | | -------------- |
| 51 | +============= |
| 52 | + |
| 53 | +To run all tests (requires ``pytest``) |
62 | 54 |
|
| 55 | +.. code-block:: bash |
63 | 56 |
|
64 | | -To run all tests (requires pytest) :: |
| 57 | + $ inv test |
65 | 58 |
|
66 | | - $ invoke test |
| 59 | +You can also use ``pytest`` directly. |
67 | 60 |
|
68 | | -You can also use pytest directly. :: |
| 61 | +.. code-block:: bash |
69 | 62 |
|
70 | | - $ py.test |
| 63 | + $ py.test --cov-report term-missing --cov mfr tests |
71 | 64 |
|
72 | 65 | Writing tests |
73 | | -------------- |
| 66 | +============= |
74 | 67 |
|
75 | 68 | Unit tests should be written for all rendering code. |
76 | 69 |
|
77 | | -Tests should be encapsulated within a class and written as functions, like so: |
| 70 | +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. |
78 | 71 |
|
79 | 72 | .. code-block:: python |
80 | 73 |
|
81 | 74 | # in test_myformat.py |
82 | 75 |
|
83 | | - from mfr_something import render |
84 | | -
|
85 | | -
|
86 | | - def test_render_html(): |
87 | | - with open('testfile.mp4') as fp: |
88 | | - assert render.render_html(fp) == '<p>rendered testfile.mp4</p>' |
89 | | -
|
90 | | -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. |
| 76 | + from mfr.extensions.my_extension.render import MyExtensionRenderer |
91 | 77 |
|
92 | | -The above test can be rewritten like so: |
93 | | - |
94 | | -.. code-block:: python |
95 | | -
|
96 | | - # in test_myformat.py |
| 78 | + @pytest.fixture |
| 79 | + def metadata(): |
| 80 | + return ProviderMetadata( |
| 81 | + 'file_name', |
| 82 | + '.extension', |
| 83 | + 'text/plain', |
| 84 | + '1234', |
| 85 | + 'http://wb.osf.io/file/file_name.extension?token=1234' |
| 86 | + ) |
97 | 87 |
|
98 | | - from mfr_something import render |
| 88 | + def test_render_html(extension, metadata, file_path, assets_url, export_url): |
| 89 | + assert MyExtensionRenderer( |
| 90 | + extension, |
| 91 | + file_metadata, |
| 92 | + file_path, |
| 93 | + assets_url |
| 94 | + ).render() == '<p>Rendered file for my_extension</p>' |
99 | 95 |
|
100 | | - def test_render_html(fakefile): |
101 | | - assert render.render_html(fakefile) == '<p>rendered testfile.mp4</p>' |
102 | | -
|
103 | | -.. _pytest fixtures: https://pytest.org/latest/fixture.html |
| 96 | +Check out `pytest`_ documentation to learn more about fixtures |
104 | 97 |
|
| 98 | +.. _pytest fixtures: https://docs.pytest.org/en/latest/fixture.html |
| 99 | +.. _pytest: https://docs.pytest.org/en/latest/ |
105 | 100 |
|
106 | 101 | Manual Local Testing |
107 | | --------------------- |
108 | | - |
109 | | -To make sure a new renderer is functioning properly, it's recommended that you try to render a file of that type locally. |
110 | | - |
111 | | -First, change the default provider to HTTP (in `/mfr/server/settings.py`), then update the provider domain in the ``ALLOWED_PROVIDER_DOMAINS`` whitelist (a space-separated string): |
112 | | - |
113 | | -.. code-block:: python |
114 | | -
|
115 | | - PROVIDER_NAME = config.get('PROVIDER_NAME', 'http') |
116 | | - ALLOWED_PROVIDER_DOMAINS = config.get('ALLOWED_PROVIDER_DOMAINS', 'http://localhost:8000/') |
117 | | -
|
118 | | -Because the MFR is passed a url to render, you also need to be running an http server. |
| 102 | +==================== |
119 | 103 |
|
120 | | -From a directory with a file you want to render: |
| 104 | +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. |
121 | 105 |
|
122 | | -.. code-block:: bash |
123 | | -
|
124 | | - python -m SimpleHTTPServer 8000 |
125 | | -
|
126 | | -Or for python 3 |
127 | | - |
128 | | -.. code-block:: bash |
129 | | -
|
130 | | - python3 -m http.server 8000 |
131 | | -
|
132 | | -With both the SimpleHTTPServer and the MFR server running, go to |
133 | | - |
134 | | -.. code-block:: bash |
135 | | -
|
136 | | - http://localhost:7778/render?url=http://localhost:8000/[filename].[ext] |
137 | | -
|
138 | | -
|
139 | | -Writing A File Format Package |
140 | | ------------------------------ |
| 106 | +Writing an extension |
| 107 | +==================== |
141 | 108 |
|
142 | | -There are two main pieces of a file format package are |
143 | | - |
144 | | -- Your custom rendering and/or exporting code |
145 | | -- Your :class:`FileHandler <mfr.core.FileHandler>` |
| 109 | +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``. |
146 | 110 |
|
| 111 | +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. |
147 | 112 |
|
148 | 113 | Rendering Code |
149 | | -++++++++++++++++++++++++ |
150 | | - |
151 | | -Renderers are simply callables (functions or methods) that take a file as their first argument and return |
| 114 | +-------------- |
152 | 115 |
|
153 | | -Here is a very simple example of function that takes a filepointer and outputs a render result with an HTML image tag. |
| 116 | +Renderers subclass ``mfr.core.extension.BaseRenderer``, and implement a render function, a ``file_required`` property, and a ``cache_result`` property. |
154 | 117 |
|
155 | 118 | .. code-block:: python |
156 | 119 |
|
157 | | - def render_img_tag(filepointer): |
158 | | - filename = filepointer.name |
159 | | - content = '<img src="{filename}" />'.format(filename=filename) |
160 | | - return RenderResult(content) |
| 120 | + import os |
161 | 121 |
|
162 | | -You can also write renderers as methods. |
163 | | - |
164 | | -.. code-block:: python |
| 122 | + from mako.lookup import TemplateLookup |
165 | 123 |
|
166 | | - # in mfr_video/render.py |
| 124 | + from mfr.core import extension |
167 | 125 |
|
168 | | - class VideoRenderer(object): |
169 | 126 |
|
170 | | - def render_html5_tag(self, fp): |
171 | | - content = '<video src="{filename}"></video>'.format(filename=fp.name) |
172 | | - return RenderResult(content) |
| 127 | + class ImageRenderer(extension.BaseRenderer): |
173 | 128 |
|
174 | | - def render_flash(self, fp): |
175 | | - # ... |
176 | | - pass |
| 129 | + TEMPLATE = TemplateLookup( |
| 130 | + directories=[ |
| 131 | + os.path.join(os.path.dirname(__file__), 'templates') |
| 132 | + ]).get_template('viewer.mako') |
177 | 133 |
|
| 134 | + def render(self): |
| 135 | + return self.TEMPLATE.render(base=self.assets_url, url=self.url.geturl()) |
178 | 136 |
|
179 | | -The FileHandler |
180 | | -+++++++++++++++ |
181 | | - |
182 | | -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. |
183 | | - |
184 | | -Your FileHandler **must** define a ``detect`` method which, given a file object, returns whether or not it can handle the file. |
185 | | - |
186 | | -**Your FileHandler class should be named Handler and should be defined in your `mfr_format/__init__.py` file.** |
187 | | - |
188 | | -.. code-block:: python |
189 | | -
|
190 | | - # in mfr_image/__init__.py |
191 | | -
|
192 | | - from mfr import FileHandler, get_file_extension |
193 | | -
|
194 | | - # Your custom code |
195 | | - from mfr_image.render import render_img_tag |
196 | | - from mfr_image.export import ImageExporter |
197 | | -
|
198 | | -
|
199 | | - class Handler(FileHandler): |
200 | | - renderers = { |
201 | | - 'html': render_img_tag, |
202 | | - } |
203 | | -
|
204 | | - exporters = { |
205 | | - 'png': ImageExporter().export_png, |
206 | | - 'jpg': ImageExporter().export_jpg, |
207 | | - # ... |
208 | | - } |
209 | | -
|
210 | | - def detect(self, fp): |
211 | | - return get_file_extension(fp.name) in ['.jpg', '.png', ] # and so on |
212 | | -
|
| 137 | + @property |
| 138 | + def file_required(self): |
| 139 | + return False |
213 | 140 |
|
| 141 | + @property |
| 142 | + def cache_result(self): |
| 143 | + return False |
214 | 144 |
|
215 | 145 | Organization |
216 | | -++++++++++++ |
217 | | - |
218 | | -Each package has its own directory. At a minimum, your package should include: |
| 146 | +------------ |
219 | 147 |
|
220 | | -- ``__init__.py``: Where your :class:`FileHandler <mfr.core.FileHandler>`` subclass will live. |
221 | | -- ``render-requirements.txt``: External dependencies for rendering functionality. |
222 | | -- ``export-requirements.txt``: External dependencies for export functionality. |
| 148 | +Each plugin has its own directory. At a minimum, a plugin should include: |
223 | 149 |
|
224 | | -Apart from those files, you are free to organize your rendering and export code however you want. |
| 150 | +- ``__init__.py``: This should export the ``mfr.core.extensions.BaseExporter`` and ``mfr.core.extensions.BaseRenderer`` subclasses provided by the plugin |
225 | 151 |
|
226 | 152 | A typical extension plugin directory structure might look like this: |
227 | 153 |
|
@@ -258,7 +184,7 @@ A typical extension plugin directory structure might look like this: |
258 | 184 |
|
259 | 185 |
|
260 | 186 | Documentation |
261 | | -------------- |
| 187 | +============= |
262 | 188 |
|
263 | 189 | Contributions to the documentation are welcome. Documentation is written in `reStructured Text`_ (rST). A quick rST reference can be found `here <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_. |
264 | 190 |
|
|
0 commit comments