Contributing and feedback guidelines
####################################

There are many ways to contribute to Pelican. You can improve the
documentation, add missing features, and fix bugs (or just report them). You
can also help out by reviewing and commenting on
`existing issues <https://github.com/getpelican/pelican/issues>`_.

Don't hesitate to fork Pelican and submit an issue or pull request on GitHub.
When doing so, please adhere to the following guidelines.

.. include:: ../CONTRIBUTING.rst

Setting up the development environment
======================================

While there are many ways to set up one's development environment, we recommend
using `Virtualenv <https://virtualenv.pypa.io/en/stable/>`_. This tool allows
you to set up separate environments for separate Python projects that are
isolated from one another so you can use different packages (and package
versions) for each.

If you don't have ``virtualenv`` installed, you can install it via::

    $ pip install virtualenv

Use ``virtualenv`` to create and activate a virtual environment::

    $ virtualenv ~/virtualenvs/pelican
    $ cd ~/virtualenvs/pelican
    $ . bin/activate

Clone the Pelican source into a subfolder called ``src/pelican``::

    $ git clone https://github.com/getpelican/pelican.git src/pelican
    $ cd src/pelican

Install the development dependencies::

    $ pip install -r requirements/developer.pip

Install Pelican and its dependencies::

    $ python setup.py develop

Or using ``pip``::

    $ pip install -e .

To conveniently test on multiple Python versions, we also provide a ``.tox``
file.


Building the docs
=================

If you make changes to the documentation, you should preview your changes
before committing them::

    $ pip install -r requirements/docs.pip
    $ cd docs
    $ make html

Open ``_build/html/index.html`` in your browser to preview the documentation.

Running the test suite
======================

Each time you add a feature, there are two things to do regarding tests: check
that the existing tests pass, and add tests for the new feature or bugfix.

The tests live in ``pelican/tests`` and you can run them using the
"discover" feature of ``unittest``::

    $ python -Wd -m unittest discover

After making your changes and running the tests, you may see a test failure
mentioning that "some generated files differ from the expected functional tests
output." If you have made changes that affect the HTML output generated by
Pelican, and the changes to that output are expected and deemed correct given
the nature of your changes, then you should update the output used by the
functional tests. To do so, **make sure you have both** ``en_EN.utf8`` **and**
``fr_FR.utf8`` **locales installed**, and then run the following two commands::

    $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/custom/ \
        -s samples/pelican.conf.py samples/content/
    $ LC_ALL=fr_FR.utf8 pelican -o pelican/tests/output/custom_locale/ \
        -s samples/pelican.conf_FR.py samples/content/
    $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/basic/ \
        samples/content/

You may also find that some tests are skipped because some dependency (e.g.,
Pandoc) is not installed. This does not automatically mean that these tests
have passed; you should at least verify that any skipped tests are not affected
by your changes.

You should run the test suite under each of the supported versions of Python.
This is best done by creating a separate Python environment for each version.
Tox_ is a useful tool to automate running tests inside ``virtualenv``
environments.

.. _Tox: https://tox.readthedocs.io/en/latest/

Python 2/3 compatibility development tips
=========================================

Here are some tips that may be useful for writing code that is compatible with
both Python 2.7 and Python 3:

- Use new syntax. For example:

  - ``print .. -> print(..)``
  - ``except .., e -> except .. as e``

- Use new methods. For example:

  - ``dict.iteritems() -> dict.items()``
  - ``xrange(..) - > list(range(..))``

- Use ``six`` where necessary. For example:

  - ``isinstance(.., basestring) -> isinstance(.., six.string_types)``
  - ``isinstance(.., unicode) -> isinstance(.., six.text_type)``

- Assume every string and literal is Unicode:

  - Use ``from __future__ import unicode_literals``
  - Do not use the prefix ``u'`` before strings.
  - Do not encode/decode strings in the middle of something. Follow the code to
    the source/target of a string and encode/decode at the first/last possible
    point.
  - In particular, write your functions to expect and to return Unicode.
  - Encode/decode strings if the string is the output of a Python function that
    is known to handle this badly. For example, ``strftime()`` in Python 2.
  - Do not use the magic method ``__unicode()__`` in new classes. Use only
    ``__str()__`` and decorate the class with ``@python_2_unicode_compatible``.

- ``setlocale()`` in Python 2 fails when we give the locale name as Unicode,
  and since we are using ``from __future__ import unicode_literals``, we do
  that everywhere! As a workaround, enclose the locale name with ``str()``;
  in Python 2 this casts the name to a byte string, while in Python 3 this
  should do nothing, because the locale name was already Unicode.
- Do not start integer literals with a zero. This is a syntax error in Python 3.
- Unfortunately there seems to be no octal notation that is valid in both
  Python 2 and 3. Use decimal notation instead.


Logging tips
============

Try to use logging with appropriate levels.

For logging messages that are not repeated, use the usual Python way::

    # at top of file
    import logging
    logger = logging.getLogger(__name__)

    # when needed
    logger.warning("A warning with %s formatting", arg_to_be_formatted)

Do not format log messages yourself. Use ``%s`` formatting in messages and pass
arguments to logger. This is important, because Pelican logger will preprocess
some arguments (like Exceptions) for Py2/Py3 compatibility.

Limiting extraneous log messages
--------------------------------

If the log message can occur several times, you may want to limit the log to
prevent flooding. In order to do that, use the ``extra`` keyword argument for
the logging message in the following format::

    logger.warning("A warning with %s formatting", arg_to_be_formatted,
        extra={'limit_msg': 'A generic message for too many warnings'})

Optionally, you can also set ``'limit_args'`` as a tuple of arguments in
``extra`` dict if your generic message needs formatting.

Limit is set to ``5``, i.e, first four logs with the same ``'limit_msg'`` are
outputted normally but the fifth one will be logged using ``'limit_msg'`` (and
``'limit_args'`` if present). After the fifth, corresponding log messages will
be ignored.

For example, if you want to log missing resources, use the following code::

    for resource in resources:
        if resource.is_missing:
            logger.warning(
                'The resource %s is missing', resource.name,
                extra={'limit_msg': 'Other resources were missing'})

The log messages will be displayed as follows::

    WARNING: The resource prettiest_cat.jpg is missing
    WARNING: The resource best_cat_ever.jpg is missing
    WARNING: The resource cutest_cat.jpg is missing
    WARNING: The resource lolcat.jpg is missing
    WARNING: Other resources were missing


Outputting traceback in the logs
--------------------------------

If you're logging inside an ``except`` block, you may want to provide the
traceback information as well. You can do that by setting ``exc_info`` keyword
argument to ``True`` during logging. However, doing so by default can be
undesired because tracebacks are long and can be confusing to regular users.
Try to limit them to ``--debug`` mode like the following::

    try:
        some_action()
    except Exception as e:
        logger.error('Exception occurred: %s', e,
            exc_info=settings.get('DEBUG', False))
