Python coding guide
===================

.. highlight:: python

General
-------

* The code should follow the Python coding style expressed in
  http://www.python.org/dev/peps/pep-0008/ PEP8 with the following
  reminders/exceptions

* Keep the length of the line below *80* characters when possible,
  and when it does not hurt readability, and below *100* characters
  at any case.

* Indentation is made of *four spaces*

* No trailing whitespace are allowed.

* Every text file must be pushed using *UNIX line endings*. (On windows, you
  are advised to set ``core.autocrlf`` to true).

* Variables, functions and modules are named ``like_this``
  *lower case, underscore*

* Classes are named ``LikeThis`` *camel case*

* Constants and globals are named ``LIKE_THIS`` *upper case, underscore*


For easy code re-use
--------------------


* *Every file* that ends with a .py *must* support to be imported, without
  doing anything or printing anything to the screen.

* ``import foo`` must never fails, unless there's a necessary module that could
  not be found. But do not catch the ImportError unless it is necessary, for
  example to deal with optional dependencies, for instance::

    import required_module

    HAS_NICE_FEATURE = True
    try:
      import nicefeature
    except ImportError:
      HAS_NICE_FEATURE = False

    ...

    if HAS_NICE_FEATURE:
      ....



* Even if you are sure you code is standalone, and is only supposed to be used
  as a script, please follow the following skeleton::

    """The foo script adds spam to the eggs """

    def add_eggs(spam, eggs):
      """Add some spam to the eggs """

      ...


    def main():
      """Parse command line """

      ...

      add_eggs(spam, eggs)

    if __name__ == "__main__":
      main()

Note that the ``main()`` function does nothing but parsing command line, the real
work being done by a nicely named ``add_eggs`` function.

Unless you have a good reason too, please do not call ``sys.exit()`` outside the
``main()`` function.

You will be glad to have written your ``foo.py`` script this way if you want to
add some spam to the eggs somewhere else :)


* Please avoid doing lots and lots of import at the beginning of
  the file::

    # BAD:
    import foo
    from foo.spam import Spam
    from foo.eggs import Eggs

    ...

    spam = Spam()
    eggs = Eggs()


    # OK:
    import foo

    ...

    spam = foo.spam.Spam()

    eggs = foo.eggs.Eggs()


For this to work, you will have to put something like this in
``foo/__init__.py`` ::

  from foo import spam
  from foo import eggs


File Paths
----------

* **Never** use strings to manipulate file paths. Use ``os.path.join``
  which will handle all the nasty stuff for you::

    # BAD : you are doomed if you ever want to
    # generate a .bat file with bar_path
    bar_path = spam_path + "/" + "bar"

    # OK:
    bar_path = os.path.join(spam_path, "bar")

* When using ``os.path.join``, use one argument per file part::

    # BAD: you can end up with an ugly path like c:\path\to/foo/bar
    my_path = os.path.join(base_dir, "foo/bar")

    # OK:
    my_path = os.path.join(base_dir, "foo", "bar")


* **Always** convert files coming from the user to native, absolute path::

    user_input = ...
    my_path = qibuild.sh.to_native_path(user_input)

* Always store and manipulate native paths (using ``os.path``), and if needed
  convert to POSIX or Windows format at the last moment.

.. note:: If you need to build POSIX paths, don't use string operations
   either, use `posixpath.join`  (This works really well to build URL, for
   instance)

* Pro-tip: hard-coding paths on Windows:

Use `r"` rather than ugly `\\\\` ::

  # UGLY:
  WIN_PATH = "c:\\windows\\spam\\eggs"

  # NICE:
  WIN_PATH = r"c:\windows\spam\eggs"


Environment Variables
---------------------

Please make sure to **never** modify ``os.environ``

Remember that ``os.environ`` is in fact a huge global variable, and we all know
it's a bad idea to use global variables ...

Instead, use a copy of ``os.environ``, for instance::

  import qibuild

  # Note the .copy() !
  # If you forget it, build_env is a *reference* to
  # os.environ, so os.environ will be modified ...
  cmd_env = os.environ.copy()
  cmd_env["SPAM"] = "eggs"
  # Assuming foobar need SPAM environment variable set to 'eggs'
  cmd = ["foobar"]
  qibuild.command.call(foobar, env=cmd_env)


In more complex cases, especially when handling the
%PATH% environment variable, you can use ``qibuild.envsetter.EnvSetter``.

A small example::

  import qibuild

  envsetter = qibuild.envsetter.EnvSetter()
  envsetter.append_to_path(r"c:\Program Files\Foobar\bin")
  build_env = envsetter.get_build_env()
  cmd = ["foobar", "/spam:eggs"]
  qibuild.command.call(cmd, env=build_env)


Logging
-------

* Usage of the logging module is advised. It enables you to display nice,
  colorful messages to the user, helps to debug with the ``-v`` option, has a
  nice syntax...
  Please do not use print unless you have a very good reason to.

* Get a logger with::

    import logging

    LOGGER = logging.getLogger(__name__)

This makes sure the names of the loggers are always consistent with the source code.

Debugging
---------

When something goes wrong, you will just have the last error message printed,
with no other information. (Which is nice for the end user!)

If it's an *unexpected* error message, here is what you can do:

* run qibuild with ``-v`` flag to display debug messages

* run qibuild with ``--backtrace`` to print the full backtrace

* run qibuild with ``--pdb`` to drop to a pdb session when an uncaught exception is raised.


Error messages
--------------

Please do not overlook those. Often, when writing code you do something like::

  try:
     something_really_complicated()
  except SomeStrangeError, e:
     log.error("Error occured: %s", e)


Because you are in an hurry, and just are thinking "Great, I've handled the
exception, now I can go back to write some code ..."

The problem is: the end user does not care you are glad you have handled the
exception, he needs to **understand** what just happens.

So you need to take a step back, think a little. "What path would lead to
this exception? What was the end user probably doing? How can I help him
understand what went wrong, and how he can fix this?"

So here is a short list of do's and don'ts when you are writing your error
message.

* Wording should look like::

    Could not < descritiion for what went wrong >
    <Detailed explanation>
    Please < suggestion of a solution >

  For instance::

    Could not open configuration file
    'path/to/inexistant.cfg' does not exist
    Please check your configuration.


* Put filenames between quotes. For instance, if you are using a path given
  via a GUI, or via a prompt, it's possible that you forgot to strip it before
  using it, thus trying to create ``'/path/to/foo '`` or ``'path/to/foo\n'``.
  Unless you are putting the filename between quotes, this kind of error is hard
  to find.


* Put commands to use like this::

    Please try running: `qibuild configure -c linux32 foo'


* Give information

  Code like this makes little kitten cry::

    try:
      with open(config_file, "w") as fp:
        config = fp.read()
    except IOError, err:
      raise Exception("Could not open config file for writing")


  It's not helpful at all!
  It does not answer those basic questions:

    * What was the config file?
    * What was the problem with opening the config file?
    * ...

  So the end user has **no clue** what to do...

  And the fix is so simple! Just add a few lines::

    try:
      with open(config_file, "w") as fp:
        config = fp.read()
    except IOError, err:
      mess   = "Could not open config '%s' file for writing\n" % config_file
      mess += "Error was: %s" % err
      raise Exception(mess)

  So the error message would then be ::

    Could not open '/etc/foo/bar.cfg' for writing
    Error was: [Errno 13] Permission denied

  Which is much more helpful.



* Suggest a solution

  This is the harder part, but it is nice if the user can figure out what to do
  next.

  Here are a few examples::

    $ qibuild configure -c foo

    Error: Invalid configuration foo
     * No toolchain named foo. Known toolchains are:
        ['linux32', 'linux64']
     * No custom cmake file for config foo found.
       (looked in /home/dmerejkowsky/work/tmp/qi/.qi/foo.cmake)


    $ qibuild install foo (when build dir does not exists)

    Error: Could not find build directory:
      /home/dmerejkowsky/work/tmp/qi/foo/build-linux64-release
    If you were trying to install the project, make sure that you have configured
    and built it first


    $ qibuild configure # when not in a worktree

    Error: Could not find a work tree. please try from a valid work tree,
    specify an existing work tree with '--work-tree {path}', or create a new
    work tree with 'qibuild init'


    $ qibuild configure # at the root for the worktree

    Error: Could not guess project name from the working tree. Please try
    from a subdirectory of a project or specify the name of the project.