.. _contributing:
==============
For Developers
==============
Developers should consult this section for detailed information relevant to development and maintenance
of ``fremor``, after familiarizing themselves with the rest of the user-targeted documentation.
Contributing to ``fremor``
==============================
Get a Copy of the Code
----------------------
Get your own copy of the code:
.. code-block:: bash
git clone --recursive git@github.com:NOAA-GFDL/fremor.git
Or replace with your fork's link (recommended).
Local/Editable Installation
---------------------------
Developers can test local changes by running ``pip install [-e] .`` inside of the root directory after
activating a virtual environment with ``python>=3.11`` and all requirements. This installs the
``fremor`` package locally with any local changes.
Development work on ``fremor`` should occur within a conda environment housing ``fremor``'s
requirements, and a local copy of the repository installed with ``pip`` using the ``-e/--editable`` flag.
.. code-block:: bash
# Create the conda environment from environment.yaml
conda env create -f environment.yaml
conda activate fremor
# Install in editable mode
pip install -e .
Testing Your Local Changes
--------------------------
There are several ways to test your efforts locally during your development cycle. A few examples and
``fremor``-specific recommendations are described here, but contributors are welcome to be creative
so long as they provide documentation of what they have contributed.
All contributed code should come with a corresponding unit test.
Running CLI Calls
~~~~~~~~~~~~~~~~~
Most development cycles will involve focused efforts isolated to a specific ``fremor COMMAND *ARGV``,
where ``*ARGV`` stands in for a shell-style argument vector (e.g. ``-d /path/to/input -l varlist.json``).
The code is housed in the ``fremor/`` package directory, with the ``click`` CLI entry-point in
``fremor/fremor.py``.
The developer usually uses ``fremor COMMAND *ARGV`` as a test, focused on seeing the changes they are
introducing, developing the code until the desired result is achieved. The specific ``fremor COMMAND *ARGV``
call can often become a unit test in one of the corresponding files in ``fremor/tests/``. The
sought-after changes should become ``assert`` conditions encoded within the unit test. Both success and
failure conditions should ideally be tested.
Running Without ``click``
~~~~~~~~~~~~~~~~~~~~~~~~~
Every ``fremor COMMAND *ARGV`` approximately maps to a single function call (a ``*_subtool`` function
in the corresponding module). To remove ``click`` and the CLI aspect from testing, assuming the code
being executed is in ``fremor/cmor_mixer.py`` in a function called ``cmor_run_subtool``:
.. code-block:: bash
python -i -c 'from fremor.cmor_mixer import cmor_run_subtool; cmor_run_subtool(**args)'
Writing ``pytest`` Unit Tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the functionality to test is that of a CLI call, tests should use the ``CliRunner`` approach shown in
``fremor/tests/``. ``click``-based CLI calls should **not** be tested with ``subprocess.run`` within
``pytest``. See `click's testing documentation `_
for more information.
If the functionality to test is removed from a CLI call, the usual pythonic testing approaches apply.
Run the test suite:
.. code-block:: bash
pytest fremor/tests/ -v
Running the Linter
~~~~~~~~~~~~~~~~~~
``fremor`` uses ``pylint`` for code quality checks:
.. code-block:: bash
pylint --rcfile pylintrc fremor/
Adding a New Requirement
------------------------
Currently, all required packages are ``conda`` packages listed in ``environment.yaml`` and also
equivalently in ``meta.yaml``. ``conda`` packages that have a corresponding ``pip`` package should
also be listed in ``pyproject.toml``.
New dependencies for ``fremor`` must have a ``conda`` package available through a non-proprietary
``conda`` channel, preferably the open-source ``conda-forge`` channel. Only ``conda-forge`` and
``noaa-gfdl`` channels are used.
Before adding a new requirement, the developer is responsible for verifying that the desired package
is safe, well-documented, and actively maintained. Consider the cost-benefit of introducing new
functionality via standard-library approaches first, and be prepared to defend the proposition of
adding a new third-party package.
How ``fremor`` is Updated
-----------------------------
``fremor`` is published and hosted as a Conda package on the
`NOAA-GFDL conda channel `_. On pushes to the ``main`` branch, the
package is automatically updated using the workflow defined in ``.github/workflows/publish_conda.yml``,
which is equivalent to ``.github/workflows/build_conda.yml`` with an extra ``conda publish`` step.
``logging`` Best Practices
--------------------------
Get Desired Verbosity
~~~~~~~~~~~~~~~~~~~~~
The ``logging`` module's configuration initially occurs in ``fremor/__init__.py``, and gets inherited
everywhere else ``logging`` creates a ``logger`` object under the ``fremor`` namespace. If your
development is being tested with a ``fremor COMMAND *ARGV`` style CLI call, add verbosity flags:
.. code-block:: bash
fremor -vv run ...
If your development does not fit that category, the next easiest thing is to adjust the base ``logger``
object in ``fremor/__init__.py``. Adjust it back to the default verbosity level before requesting
a merge.
``logging`` Practice to Avoid
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Avoid calling ``logging.basicConfig`` to re-configure logging behavior **outside** of
``fremor/__init__.py``. This creates another ``logging.handler`` without resolving the ambiguity
to previously defined loggers about which handler to use. If left in a PR at merge-time, this can cause
oddly silent logging behavior that is very tricky to debug.
Avoid ``os.chdir`` if You Can
-----------------------------
Directory changing in Python is not transient by default — if the interpreter changes directories, then
the result of ``os.getcwd()`` later in the program may change to an unexpected value, leading to
difficult bugs.
If you must use directory changing instead of managing directory targets explicitly as ``pathlib.Path``
instances, use the following pattern to safely ``chdir`` and ``chdir`` back:
.. code-block:: python
go_back_here = os.getcwd()
try:
os.chdir(target_dir)
# DO STUFF AFTER CHDIR HERE
except Exception as exc:
raise RuntimeError('some error explaining what went wrong') from exc
finally:
os.chdir(go_back_here)
``MANIFEST.in``
---------------
In the case where non-Python files like templates, examples, and outputs are to be included in the
``fremor`` package, ``MANIFEST.in`` can provide the solution. Ensure the file exists within the
correct folder and add a line such as ``include fremor/fileName.fileExtension``.
For more efficiency, if there are multiple files of the same type needed, use something like
``recursive-include fremor *.fileExtension`` which recursively includes every file matching that
extension within the specified directory and its subdirectories.
Contributing to Documentation
=============================
.. include:: contributing_to_docs.rst