|
| 1 | +.. _coding_guidelines: |
| 2 | + |
| 3 | +***************** |
| 4 | +Coding guidelines |
| 5 | +***************** |
| 6 | + |
| 7 | +While the current state of the Matplotlib code base is not compliant with all |
| 8 | +of these guidelines, our goal in enforcing these constraints on new |
| 9 | +contributions is that it improves the readability and consistency of the code base |
| 10 | +going forward. |
| 11 | + |
| 12 | +API changes |
| 13 | +=========== |
| 14 | + |
| 15 | +If you are adding new features, changing behavior or function signatures, or |
| 16 | +removing classes, functions, methods, or properties, please see the :ref:`api_changes` |
| 17 | +guide. |
| 18 | + |
| 19 | +PEP8, as enforced by flake8 |
| 20 | +=========================== |
| 21 | + |
| 22 | +Formatting should follow the recommendations of PEP8_, as enforced by flake8_. |
| 23 | +Matplotlib modifies PEP8 to extend the maximum line length to 88 |
| 24 | +characters. You can check flake8 compliance from the command line with :: |
| 25 | + |
| 26 | + python -m pip install flake8 |
| 27 | + flake8 /path/to/module.py |
| 28 | + |
| 29 | +or your editor may provide integration with it. Note that Matplotlib intentionally |
| 30 | +does not use the black_ auto-formatter (1__), in particular due to its inability |
| 31 | +to understand the semantics of mathematical expressions (2__, 3__). |
| 32 | + |
| 33 | +.. _PEP8: https://www.python.org/dev/peps/pep-0008/ |
| 34 | +.. _flake8: https://flake8.pycqa.org/ |
| 35 | +.. _black: https://black.readthedocs.io/ |
| 36 | +.. __: https://github.com/matplotlib/matplotlib/issues/18796 |
| 37 | +.. __: https://github.com/psf/black/issues/148 |
| 38 | +.. __: https://github.com/psf/black/issues/1984 |
| 39 | + |
| 40 | + |
| 41 | +Package imports |
| 42 | +=============== |
| 43 | + |
| 44 | +Import the following modules using the standard scipy conventions:: |
| 45 | + |
| 46 | + import numpy as np |
| 47 | + import numpy.ma as ma |
| 48 | + import matplotlib as mpl |
| 49 | + import matplotlib.pyplot as plt |
| 50 | + import matplotlib.cbook as cbook |
| 51 | + import matplotlib.patches as mpatches |
| 52 | + |
| 53 | +In general, Matplotlib modules should **not** import `.rcParams` using ``from |
| 54 | +matplotlib import rcParams``, but rather access it as ``mpl.rcParams``. This |
| 55 | +is because some modules are imported very early, before the `.rcParams` |
| 56 | +singleton is constructed. |
| 57 | + |
| 58 | +Variable names |
| 59 | +============== |
| 60 | + |
| 61 | +When feasible, please use our internal variable naming convention for objects |
| 62 | +of a given class and objects of any child class: |
| 63 | + |
| 64 | ++------------------------------------+---------------+------------------------------------------+ |
| 65 | +| base class | variable | multiples | |
| 66 | ++====================================+===============+==========================================+ |
| 67 | +| `~matplotlib.figure.FigureBase` | ``fig`` | | |
| 68 | ++------------------------------------+---------------+------------------------------------------+ |
| 69 | +| `~matplotlib.axes.Axes` | ``ax`` | | |
| 70 | ++------------------------------------+---------------+------------------------------------------+ |
| 71 | +| `~matplotlib.transforms.Transform` | ``trans`` | ``trans_<source>_<target>`` | |
| 72 | ++ + + + |
| 73 | +| | | ``trans_<source>`` when target is screen | |
| 74 | ++------------------------------------+---------------+------------------------------------------+ |
| 75 | + |
| 76 | +Generally, denote more than one instance of the same class by adding suffixes to |
| 77 | +the variable names. If a format isn't specified in the table, use numbers or |
| 78 | +letters as appropriate. |
| 79 | + |
| 80 | +.. _type-hints: |
| 81 | + |
| 82 | +Type hints |
| 83 | +========== |
| 84 | + |
| 85 | +If you add new public API or change public API, update or add the |
| 86 | +corresponding `mypy <https://mypy.readthedocs.io/en/latest/>`_ type hints. |
| 87 | +We generally use `stub files |
| 88 | +<https://typing.readthedocs.io/en/latest/source/stubs.html#type-stubs>`_ |
| 89 | +(``*.pyi``) to store the type information; for example ``colors.pyi`` contains |
| 90 | +the type information for ``colors.py``. A notable exception is ``pyplot.py``, |
| 91 | +which is type hinted inline. |
| 92 | + |
| 93 | +Type hints are checked by the mypy :ref:`pre-commit hook <pre-commit-hooks>` |
| 94 | +and can often be verified using ``tools\stubtest.py`` and occasionally may |
| 95 | +require the use of ``tools\check_typehints.py``. |
| 96 | + |
| 97 | +New modules and files: installation |
| 98 | +=================================== |
| 99 | + |
| 100 | +* If you have added new files or directories, or reorganized existing ones, make sure the |
| 101 | + new files are included in the :file:`meson.build` in the corresponding directories. |
| 102 | +* New modules *may* be typed inline or using parallel stub file like existing modules. |
| 103 | + |
| 104 | +C/C++ extensions |
| 105 | +================ |
| 106 | + |
| 107 | +* Extensions may be written in C or C++. |
| 108 | + |
| 109 | +* Code style should conform to PEP7 (understanding that PEP7 doesn't |
| 110 | + address C++, but most of its admonitions still apply). |
| 111 | + |
| 112 | +* Python/C interface code should be kept separate from the core C/C++ |
| 113 | + code. The interface code should be named :file:`FOO_wrap.cpp` or |
| 114 | + :file:`FOO_wrapper.cpp`. |
| 115 | + |
| 116 | +* Header file documentation (aka docstrings) should be in Numpydoc |
| 117 | + format. We don't plan on using automated tools for these |
| 118 | + docstrings, and the Numpydoc format is well understood in the |
| 119 | + scientific Python community. |
| 120 | + |
| 121 | +* C/C++ code in the :file:`extern/` directory is vendored, and should be kept |
| 122 | + close to upstream whenever possible. It can be modified to fix bugs or |
| 123 | + implement new features only if the required changes cannot be made elsewhere |
| 124 | + in the codebase. In particular, avoid making style fixes to it. |
| 125 | + |
| 126 | +.. _keyword-argument-processing: |
| 127 | + |
| 128 | +Keyword argument processing |
| 129 | +=========================== |
| 130 | + |
| 131 | +Matplotlib makes extensive use of ``**kwargs`` for pass-through customizations |
| 132 | +from one function to another. A typical example is |
| 133 | +`~matplotlib.axes.Axes.text`. The definition of `matplotlib.pyplot.text` is a |
| 134 | +simple pass-through to `matplotlib.axes.Axes.text`:: |
| 135 | + |
| 136 | + # in pyplot.py |
| 137 | + def text(x, y, s, fontdict=None, **kwargs): |
| 138 | + return gca().text(x, y, s, fontdict=fontdict, **kwargs) |
| 139 | + |
| 140 | +`matplotlib.axes.Axes.text` (simplified for illustration) just |
| 141 | +passes all ``args`` and ``kwargs`` on to ``matplotlib.text.Text.__init__``:: |
| 142 | + |
| 143 | + # in axes/_axes.py |
| 144 | + def text(self, x, y, s, fontdict=None, **kwargs): |
| 145 | + t = Text(x=x, y=y, text=s, **kwargs) |
| 146 | + |
| 147 | +and ``matplotlib.text.Text.__init__`` (again, simplified) |
| 148 | +just passes them on to the `matplotlib.artist.Artist.update` method:: |
| 149 | + |
| 150 | + # in text.py |
| 151 | + def __init__(self, x=0, y=0, text='', **kwargs): |
| 152 | + super().__init__() |
| 153 | + self.update(kwargs) |
| 154 | + |
| 155 | +``update`` does the work looking for methods named like |
| 156 | +``set_property`` if ``property`` is a keyword argument. i.e., no one |
| 157 | +looks at the keywords, they just get passed through the API to the |
| 158 | +artist constructor which looks for suitably named methods and calls |
| 159 | +them with the value. |
| 160 | + |
| 161 | +As a general rule, the use of ``**kwargs`` should be reserved for |
| 162 | +pass-through keyword arguments, as in the example above. If all the |
| 163 | +keyword args are to be used in the function, and not passed |
| 164 | +on, use the key/value keyword args in the function definition rather |
| 165 | +than the ``**kwargs`` idiom. |
| 166 | + |
| 167 | +In some cases, you may want to consume some keys in the local |
| 168 | +function, and let others pass through. Instead of popping arguments to |
| 169 | +use off ``**kwargs``, specify them as keyword-only arguments to the local |
| 170 | +function. This makes it obvious at a glance which arguments will be |
| 171 | +consumed in the function. For example, in |
| 172 | +:meth:`~matplotlib.axes.Axes.plot`, ``scalex`` and ``scaley`` are |
| 173 | +local arguments and the rest are passed on as |
| 174 | +:meth:`~matplotlib.lines.Line2D` keyword arguments:: |
| 175 | + |
| 176 | + # in axes/_axes.py |
| 177 | + def plot(self, *args, scalex=True, scaley=True, **kwargs): |
| 178 | + lines = [] |
| 179 | + for line in self._get_lines(*args, **kwargs): |
| 180 | + self.add_line(line) |
| 181 | + lines.append(line) |
| 182 | + |
| 183 | +.. _using_logging: |
| 184 | + |
| 185 | +Using logging for debug messages |
| 186 | +================================ |
| 187 | + |
| 188 | +Matplotlib uses the standard Python `logging` library to write verbose |
| 189 | +warnings, information, and debug messages. Please use it! In all those places |
| 190 | +you write `print` calls to do your debugging, try using `logging.debug` |
| 191 | +instead! |
| 192 | + |
| 193 | + |
| 194 | +To include `logging` in your module, at the top of the module, you need to |
| 195 | +``import logging``. Then calls in your code like:: |
| 196 | + |
| 197 | + _log = logging.getLogger(__name__) # right after the imports |
| 198 | + |
| 199 | + # code |
| 200 | + # more code |
| 201 | + _log.info('Here is some information') |
| 202 | + _log.debug('Here is some more detailed information') |
| 203 | + |
| 204 | +will log to a logger named ``matplotlib.yourmodulename``. |
| 205 | + |
| 206 | +If an end-user of Matplotlib sets up `logging` to display at levels more |
| 207 | +verbose than ``logging.WARNING`` in their code with the Matplotlib-provided |
| 208 | +helper:: |
| 209 | + |
| 210 | + plt.set_loglevel("debug") |
| 211 | + |
| 212 | +or manually with :: |
| 213 | + |
| 214 | + import logging |
| 215 | + logging.basicConfig(level=logging.DEBUG) |
| 216 | + import matplotlib.pyplot as plt |
| 217 | + |
| 218 | +Then they will receive messages like |
| 219 | + |
| 220 | +.. code-block:: none |
| 221 | +
|
| 222 | + DEBUG:matplotlib.backends:backend MacOSX version unknown |
| 223 | + DEBUG:matplotlib.yourmodulename:Here is some information |
| 224 | + DEBUG:matplotlib.yourmodulename:Here is some more detailed information |
| 225 | +
|
| 226 | +Avoid using pre-computed strings (``f-strings``, ``str.format``,etc.) for logging because |
| 227 | +of security and performance issues, and because they interfere with style handlers. For |
| 228 | +example, use ``_log.error('hello %s', 'world')`` rather than ``_log.error('hello |
| 229 | +{}'.format('world'))`` or ``_log.error(f'hello {s}')``. |
| 230 | + |
| 231 | +Which logging level to use? |
| 232 | +--------------------------- |
| 233 | + |
| 234 | +There are five levels at which you can emit messages. |
| 235 | + |
| 236 | +- `logging.critical` and `logging.error` are really only there for errors that |
| 237 | + will end the use of the library but not kill the interpreter. |
| 238 | +- `logging.warning` and `._api.warn_external` are used to warn the user, |
| 239 | + see below. |
| 240 | +- `logging.info` is for information that the user may want to know if the |
| 241 | + program behaves oddly. They are not displayed by default. For instance, if |
| 242 | + an object isn't drawn because its position is ``NaN``, that can usually |
| 243 | + be ignored, but a mystified user could call |
| 244 | + ``logging.basicConfig(level=logging.INFO)`` and get an error message that |
| 245 | + says why. |
| 246 | +- `logging.debug` is the least likely to be displayed, and hence can be the |
| 247 | + most verbose. "Expected" code paths (e.g., reporting normal intermediate |
| 248 | + steps of layouting or rendering) should only log at this level. |
| 249 | + |
| 250 | +By default, `logging` displays all log messages at levels higher than |
| 251 | +``logging.WARNING`` to `sys.stderr`. |
| 252 | + |
| 253 | +The `logging tutorial`_ suggests that the difference between `logging.warning` |
| 254 | +and `._api.warn_external` (which uses `warnings.warn`) is that |
| 255 | +`._api.warn_external` should be used for things the user must change to stop |
| 256 | +the warning (typically in the source), whereas `logging.warning` can be more |
| 257 | +persistent. Moreover, note that `._api.warn_external` will by default only |
| 258 | +emit a given warning *once* for each line of user code, whereas |
| 259 | +`logging.warning` will display the message every time it is called. |
| 260 | + |
| 261 | +By default, `warnings.warn` displays the line of code that has the ``warn`` |
| 262 | +call. This usually isn't more informative than the warning message itself. |
| 263 | +Therefore, Matplotlib uses `._api.warn_external` which uses `warnings.warn`, |
| 264 | +but goes up the stack and displays the first line of code outside of |
| 265 | +Matplotlib. For example, for the module:: |
| 266 | + |
| 267 | + # in my_matplotlib_module.py |
| 268 | + import warnings |
| 269 | + |
| 270 | + def set_range(bottom, top): |
| 271 | + if bottom == top: |
| 272 | + warnings.warn('Attempting to set identical bottom==top') |
| 273 | + |
| 274 | +running the script:: |
| 275 | + |
| 276 | + from matplotlib import my_matplotlib_module |
| 277 | + my_matplotlib_module.set_range(0, 0) # set range |
| 278 | + |
| 279 | +will display |
| 280 | + |
| 281 | +.. code-block:: none |
| 282 | +
|
| 283 | + UserWarning: Attempting to set identical bottom==top |
| 284 | + warnings.warn('Attempting to set identical bottom==top') |
| 285 | +
|
| 286 | +Modifying the module to use `._api.warn_external`:: |
| 287 | + |
| 288 | + from matplotlib import _api |
| 289 | + |
| 290 | + def set_range(bottom, top): |
| 291 | + if bottom == top: |
| 292 | + _api.warn_external('Attempting to set identical bottom==top') |
| 293 | + |
| 294 | +and running the same script will display |
| 295 | + |
| 296 | +.. code-block:: none |
| 297 | +
|
| 298 | + UserWarning: Attempting to set identical bottom==top |
| 299 | + my_matplotlib_module.set_range(0, 0) # set range |
| 300 | +
|
| 301 | +.. _logging tutorial: https://docs.python.org/3/howto/logging.html#logging-basic-tutorial |
| 302 | + |
| 303 | + |
| 304 | +.. _licence-coding-guide: |
| 305 | + |
| 306 | +.. include:: license.rst |
| 307 | + :start-line: 2 |
| 308 | + |
| 309 | +.. toctree:: |
| 310 | + :hidden: |
| 311 | + |
| 312 | + license.rst |
0 commit comments