oppy: How To’s (for developers)

These documents act as a guideline for general tasks with oppy.

  • Documentation fo the oppy module
  • Unittests for the oppy module
  • How to use the Results and Options classes for new methods in oppy
  • How you should use git while working with oppy

Documentation for the oppy module

We will split this HowTo into two main parts:

  1. Writing the docstrings inside the oppy module.
  2. Compiling the docs and creating .html-files using the sphinx API.

Writing the documentation

In this part, we will give some basic rules and an easy guideline on how to document your class, method, etc. inside the oppy module.

Docstrings in general and how to create them

Docstrings are released within a webpage for any good module. They provide the interface between the developers and the user. They do not only document the module itself, they are also key if a new user wants to learn how to use the module.

If you have implemented a function my_function.py and you want to create a docstring for it, spyder provides and easy quick start. Just move your cursor in the line after def my_function(): and type """, spyder will ask you to automatically create a first version of the docstring. Spyder will basically add every input and output variable inside the docstring and you just have to add a description and the type of the variables.

If you use PyCharm instead of Spyder you can do as follows. Place the caret somewhere within the function you want to document. Press Alt + Enter to show the available intention actions and choose Insert documentation string stub.

Pleas check the documentation if you use another IDE.

The numpy guideline and an example docstring

In the settings of your IDE (e.g. Spyder, PyCharm, etc) you can also set the docstring guide, you want to use.

Important: We are using the numpy docstrings style. On this website you can also find the most important rules and an easy guideline on what and how to document. Please read it carefully and follow the instructions. Here you can also find a minimal docstring example. We copied the file example.py into this document:

"""Docstring for the example.py module.

Modules names should have short, all-lowercase names.  The module name may
have underscores if this improves readability.

Every module should have a docstring at the very top of the file.  The
module's docstring may extend over multiple lines.  If your docstring does
extend over multiple lines, the closing three quotation marks must be on
a line by itself, preferably preceded by a blank line.

"""
from __future__ import division, absolute_import, print_function

import os  # standard library imports first

# Do NOT import using *, e.g. from numpy import *
#
# Import the module using
#
#   import numpy
#
# instead or import individual functions as needed, e.g
#
#  from numpy import array, zeros
#
# If you prefer the use of abbreviated module names, we suggest the
# convention used by NumPy itself::

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

# These abbreviated names are not to be used in docstrings; users must
# be able to paste and execute docstrings after importing only the
# numpy module itself, unabbreviated.


def foo(var1, var2, *args, long_var_name='hi', **kwargs):
    r"""Summarize the function in one line.

    Several sentences providing an extended description. Refer to
    variables using back-ticks, e.g. `var`.

    Parameters
    ----------
    var1 : array_like
        Array_like means all those objects -- lists, nested lists, etc. --
        that can be converted to an array.  We can also refer to
        variables like `var1`.
    var2 : int
        The type above can either refer to an actual Python type
        (e.g. ``int``), or describe the type of the variable in more
        detail, e.g. ``(N,) ndarray`` or ``array_like``.
    *args : iterable
        Other arguments.
    long_var_name : {'hi', 'ho'}, optional
        Choices in brackets, default first when optional.
    **kwargs : dict
        Keyword arguments.

    Returns
    -------
    type
        Explanation of anonymous return value of type ``type``.
    describe : type
        Explanation of return value named `describe`.
    out : type
        Explanation of `out`.
    type_without_description

    Other Parameters
    ----------------
    only_seldom_used_keywords : type
        Explanation.
    common_parameters_listed_above : type
        Explanation.

    Raises
    ------
    BadException
        Because you shouldn't have done that.

    See Also
    --------
    numpy.array : Relationship (optional).
    numpy.ndarray : Relationship (optional), which could be fairly long, in
                    which case the line wraps here.
    numpy.dot, numpy.linalg.norm, numpy.eye

    Notes
    -----
    Notes about the implementation algorithm (if needed).

    This can have multiple paragraphs.

    You may include some math:

    .. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n}

    And even use a Greek symbol like :math:`\omega` inline.

    References
    ----------
    Cite the relevant literature, e.g. [1]_.  You may also cite these
    references in the notes section above.

    .. [1] O. McNoleg, "The integration of GIS, remote sensing,
       expert systems and adaptive co-kriging for environmental habitat
       modelling of the Highland Haggis using object-oriented, fuzzy-logic
       and neural-network techniques," Computers & Geosciences, vol. 22,
       pp. 585-588, 1996.

    Examples
    --------
    These are written in doctest format, and should illustrate how to
    use the function.

    >>> a = [1, 2, 3]
    >>> print([x + 3 for x in a])
    [4, 5, 6]
    >>> print("a\nb")
    a
    b
    """
    # After closing class docstring, there should be one blank line to
    # separate following codes (according to PEP257).
    # But for function, method and module, there should be no blank lines
    # after closing the docstring.
    pass

The docstrings also support LaTeX-commands. To be able to render them correctly, one needs an r in front of the """, so the line after the definition of the function should start with r""".

Compiling the docs using sphinx

We are currently building the documentation via the sphinx tool.

Compiling the docs, if you already have the .rst and .html-files

If you already have the precompiled docs and you just want to update the documentation, you can execute the shell script

bash make_html.sh

You can use the flags -c (copy) and -d (delete), in order to extended the functionality of the script, e.g.

  • Copy the jupyter notebooks into the into the ./Notebooks_Web folder:
bash make_html.sh -c
  • Remove the ./build folder to get a cleaner update:
bash make_html.sh -d
  • Delete the ./build folder and copy the jupyter notebooks into ./Notebooks_Web:
bash make_html.sh -d -c

or (the order of arguments is not relevent)

bash make_html.sh -c -d

To execute this script under Windows you need to install git for windows.

You can find an introduction/tutorial for sphinx under documenting your project with sphinx.

The restructed text format

All of the files generated by sphinx are in the so called reStructuredText (.rst)-format. You can find a short introduction under Quick reStructuredText. We also like the page reST and sphinx Cheatsheet. Inside the documentation of the oppy package, we also use intersphinx and so-called cross references (you can refer in the docstring of your function to another function, class, package that you have written or is elsewhere implemented and documented properly). To organize cross references sphinx uses so-called domains, you can find the basic markup and further information under sphinx domains.

Unittests for the oppy module

What are (unit) tests and why should I start writing (unit)tests?

Unit tests or tests in general provide a framework to automatically test written code in a specific programming language. Tests make sure that already written code work properly, e.g., tests spot bugs, coding mistakes and help the developer to check if functions or classes still work after changes in the source code. Tests are really important guarantee functional and reliable code, or, as Julien Danjou, the author of the book Serious Python, says: Writing code that is not tested is fundamentally useless.

What is the unittest package in python?

The unittest python package provides a framework to write tests, run them automatically and parallelize them, in case of large libraries. The main resource for this package is the unittest documentation. This package supports tests for function outputs, class attributes, raised warnings and exceptions and a lot more.

How do set up a unit test?

There are two standards on where to locate and how to name test methods. We decided to build the structure of unit test as the following. Assuming a library structured as:

oppy_package
	|oppy
	|	|submodule1
	|	|	|method1.py
	|docs
	|	|...
	|...

Then one creates a submodule tests and in the first step we copy the structure of the module your_module. For each method or class like method1.py, the corresponding test gets name as test_method1.py, such that the resulting structure is of the form:

oppy_package
	|oppy
	|	|submodule1
	|	|	|method1.py
	|	|tests
	|	|	|submodule1
	|	|	|	|test_method1.py
	|docs
	|	|...
	|...

The unit test class: unittest.TestCase

Now, as we know where to locate our unit test, we want to know how we can test our methods and classes. First, we have to import the unittest module and setup a test case:

import unittest

class TestYourMethod(unittest.TestCase):
	pass

There are several functions provided by the unittest framework, which will help you, to setup your unittest. You can use the setUpClass(self) method, which will be called once before all test methods. You can also use the setUp(self) method, which will be called before each test. To tidy up after the last test, you can use the tearDown(self) method, which will be executed after all test methods. Now your class could look like this:

import unittest

class TestYourMethod(unittest.TestCase):
	@classmethod
	def setUpClass(self):
		# this method will be called before all tests
		pass
	def setUp(self):
		# this method runs before each test (so overall multiple times)
		pass
	# YOUR TEST METHODS BELONG HERE, for example
	def test_my_method(self):
		# test whether the output is correct
		pass
	def tearDown(self):
		# executed after all tests
		pass

# execute the test
if __name__ == "__main__":
    unittest.main()

All of the three methods are documented in unittest TestCase. We added the last two lines, to be able to execute the test and to allow the unittest framework to automatically run all tests in the long run (for this task we also have a function called test_oppy_all.py located in the oppy/tests folder).

Methods to test functions, classes, warnings, etc.: assert()

As we now know how to set up the basic structure of a test, we want to take a look at methods to test specific things, e.g. if a method raises a warning based on a specific input. All basic functions to check for attributes, return values and class types are listed under assert functions. We will now take a closer look at this example function:

import warnings
import numpy as np

def my_sqrt(x):
	if x>=0:
		return np.sqrt(x)
	else:
		warnings.warn("x has to be non-negative, None is returned.")
		return None

For simplicity we did not include a docstring and we just assume that the argument x is always a scalar, to avoid try and catch blocks. In our first test, we want to check whether the function computes the right values, if we put in correct input values (in this case values that do not provoke any errors or warnings). We expand our test with two methods. The method test_my_sqrt_correct_input checks whether our method yields correct resutls, of the input data is valid. Therefore we use the self.assertEqual function, which checks if the two inputs are the same (in a high precision numerical sense). The second function self.assertAlmostEqual is for our cases more practical, as we can set the comparison tolerance manually by changing the default argument delta. We can now also see how the setUpClass method is used. Here you should define relevant constants (e.g. the comparison precision, the number of test cases, relevant messages if a method fails a test, etc.). You can find all of these methods in the unittest documentation.

import unittest
# we want to use math.sqrt, to check whether our function works
import math
import numpy as np


def TestMySqrt(unittest.TestCase):
	@classmethod
	def setUpClass(self):
		# we set this number e.g. globally, as we would want to use this number in every test
		# with an input case
		self.test_size = 100
		# for almost equal tests, we have to set a desired tolerance
		self.tolerance = 10e-12
		# here we can define some strings that will be shown if the method fails a test
		self.msg_unequal = "Own sqrt function failed to give the right result."
		self.msg_not_almost_equal = "Own sqrt function failed to yield results in the desired tolerance"

	# test the method for correct input values and check, if the method gives the right result
	def test_my_sqrt_correct_input(self):
		# this only produces numbers in the intervall [0,1], in a real test, one would need
		# to expand this interval
		x = np.random.rand(self.test_size)
		for i in range(0, self.test_size):
			own_sqrt = my_sqrt(x[i])
			perfect_sqrt = math.sqrt(x[i])
			self.assertEqual(own_sqrt, perfect_sqrt, msg=self.msg_unequal)
			self.assertAlmostEqual(own_sqrt, perfect_sqrt, delta=self.tolerance, msg=self.msg_not_almost_equal)

	# in this case we put a negative value into the function and check, whether a warning is raised
	def test_my_sqrt_warnings(self):
		x = -1
		with self.assertWarns(Warning):
			result = my_sqrt(x)
if __name__ == "__main__":
    unittest.main()

Side note: We know that in this case it is not really useful to test the np.sqrt function against the math.sqrt function and that the np.sqrt function has already built in warnings, but we wanted to simulate an easy example. To test own solvers of linear equation systems one could e.g. use the scipy solver, define the right-hand side based on the already given solution manually or use equation systems, where the solution is already known. Here you can be relly creative and most of the times many ways work.

How we test our classes and methods in oppy

We have no definite rules for what and how we test oppy, but here are a few rules of thumb:

  1. Test every method, every class method and every constructur your file/package has.
  2. Test at least two different outputs with valid inputs. For a solver or an optimization algorithm we usually test the solver with default options for one problem, then we use the same problem, change the options and go for a solution with the maximum precision. In both cases we e.g. set upper bounds for the number of steps the algorithm should take and compare them to literature values. If a value is set once, it is a good indicator for improved version of this algorithm (the new version should in most cases not take more steps than before).
  3. Test every possible warning and exception. Here you have to be really creative. Think of possible wrong inputs and check how your method or algorithm performs with this type of nonsene - Is a warning raised? Is the text of this warning really useful? etc.

Include your method and/or subpacke inside the test_oppy_all method

In order to run all tests at once, we need to embed our subpackage and methods in the test_oppy_all framework. We distinguish

  1. New subpackage: Create a file named run_tests_YourSubpackage.py. The structure of the file should be similar to the following file:
"""Run all Tests."""

import unittest

# import your test modules
from oppy.tests.YourSubpackage import test_method_1, test_method_2


def test_oppy_YourSubpackage(show=False):
    """
    Run all YourSubpackage unittests at once.

    Parameters
    ----------
    show : boolean, optional
        Flag whether additional information should be shown. The default is
        False.

    Returns
    -------
    result : Test result information
        Holds all information about the unittest (e.g. number of tests run,
        collection of failures, etc.)

    """
    # initialize the test suite
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()

    # add tests to the test suite
    suite.addTests(loader.loadTestsFromModule(test_method_1))
    suite.addTests(loader.loadTestsFromModule(test_method_2))

    # initialize a runner, pass it your suite and run it
    if show is False:
        # run without show anything
        # runner = unittest.TextTestRunner(verbosity=0)
        # run with some dots running
        runner = unittest.TextTestRunner(verbosity=1)
    else:
        # show some details
        runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    return result


# execute the test
if __name__ == "__main__":
    test_oppy_YourSubpackage()

Inside the method test_oppy_YourSubpackage you should include all unittests of your subpackage, such that all of these tests are executed when running the method test_oppy_YourSubpackage(). Now, you need to extend the test_oppy_all file.

"""test_oppy_all"""

from oppy.tests.conOpt import run_tests_conOpt
from oppy.tests.itMet import run_tests_itMet
from oppy.tests.linOpt import run_tests_linOpt
from oppy.tests.multOpt import run_tests_multOpt
from oppy.tests.options import run_tests_options
from oppy.tests.results import run_tests_results
from oppy.tests.unconOpt import run_tests_unconOpt
# IMPORT YOUR METHOD HERE
from oppy.tests.YourMethod import run_tests_YourSubpackage


def test_oppy_all(show=False):
    """
    Run all oppy tests at once.

    Method to run all implemented unittests inside the oppy package at once.

    Parameters
    ----------
    show : boolean, optional
        Flag to decide, wether one wants to see intermediate steps of the tests
        or not. The default is ``show=False``.

    Returns
    -------
    None.

    """
    # execute all itMet tests
    run_tests_conOpt.test_oppy_conOpt(show)
    run_tests_itMet.test_oppy_itMet(show)
    run_tests_linOpt.test_oppy_linOpt(show)
    run_tests_multOpt.test_oppy_multOpt(show)
    run_tests_options.test_oppy_options(show)
    run_tests_results.test_oppy_results(show)
    run_tests_unconOpt.test_oppy_unconOpt(show)
    # RUN ALL OF YOUR TESTS HERE
    run_tests_YourSubpackge.test_oppy_YourSubpackge(show)


# execute the test
if __name__ == "__main__":
    test_oppy_all()

Finally, we can execute all tests at once by running the test_oppy_all() function.

  1. New method: Include the unittest of your method in the corresponding run_tests_package of the method’s package.

.. _execute-all-tests:

Execute tests automatically

Inside the oppy package we provide two additional functions to simplify our unittest framework. First, we have the oppy/tests/test_oppy_all.py function, which executes all unittests of the oppy module. We also added the oppy.test function inside the __init__-file of the package, which calls the test_oppy_all method and further simplifies the process of running all unittests at once.

Second, we implemented the oppy/tests/run_all_tests_in_dir.py method. This function basically behaves as a wrapper for the unittest discover method. By calling this function inside a directory, the unittest framework automatically runs all unittests inside this directory and all subdirectories. This can be useful, if you are only interested in running tests for a single subpackage of the module.

How to use the Results and Options classes for new methods in oppy

Based on the idea of the scipy OptimizeResult class, we decided to use classes for the Options and Results of all optimization algorithms and solvers of the oppy package. In this HowTo you will learn how these two classes basically work and how use them when including new algorithms into the oppy package.

What are the Results and Options classes and what framework do they provide?

The Options class

The Options class, located under oppy/options/options/Options.py provided a simple but powerful framework to manually change the options for any method or algorithm inside the oppy package. The constructor of the method is really generic. In this method all the internal dictionaries are created, that will later be used to set the default options and check for additional constraints. You can manually set options in two ways.

First, just use an empty constructor and set the attributes of the class via the class.attribute relationship:

from oppy.options import Options
example_options = Options()
example_options.nmax = 10000
example_options.initstep = 1

Or you can use the built in **kwargs, to set the options directly inside the constructor call:

from oppy.options import Options
example_options = Options(nmax=10000, initstep=1)

Up to this point, the Options are still very generic and the class does not know yet, inside which method the class will be used.

The core method of the Options class is the ops method. This method is called inside the currently used function and gets the so called caller_method from the internal stack and completes, based on the methods name, the class. Inside this method missing attributes (if the user does not set all options manually) are set and additional mathematical constraints (e.g. the maximum number of iterations have to be positive) are checked.

The Results class

The Results class follows a similar idea and is located under oppy/results/results/Results.py and can be imported via:

from oppy.results import Results

The constructor of this class gets called directly inside each method and needs as an input variable the methods name. An optional argument is the options.results variable, where the user can choose between three different output types and one custom output option. The output options are the following:

sol: This is an abbreviation for solution (only). Here the user should only get the solution vector of the optimization process or the linear solver. Nothing else will be returned.

normal (default): The normal mode is the default option. Here the method should return the solution vector, the number of iterations and a list with the norm of the residuals.

all: Using this option, the user should get any possible information. Possible are for example internal linesearch status flags, a list of all intermediate solution vectors and a list with all step width the linesearch used. As a developer you can be really creative, just include anything that could be interesting.

list: This is the already mentioned custom options. Using this option, the user can pass a list with names of variables he wants to be returned. This list has to be a subset of the set of all possible outputs.

In all cases an instance of the class Results will be returned containing the desired output variables as class attributes.

The core method of the Results-class is the update_results(update_dict) method which is called inside each iteration of a method and updates the attributes (e.g. the list containing the norm of the gradients) of the Results-instance. We will explain this method in further details in the next section

How can I bind in a new method and/or subpackage?

Options

New subpackage: First, you should add a list with the names of all methods included in this subpackage as a self-attribute inside the __init__ file of the Options-class. Second, you should add the desired common options (the same for all methods of the subpackage) and the desired special options (individual options for each method) as a new elif-case inside the ops-method. If you want some options to satisfy specific constraints (e.g. an option should be greater to zero), you need to define a common_options_constraints and a special_options_constraints dict.

If you need further constraint for the options of the subpackage, you can extend the self.constraints_dict and the self.map_constraints_to_attributes dicts. We want to give a short example on how these dicts work. Let us further assume, all of our new methods have a common option tolerance which should be greater to zero. A simple way to automatically check this constraint, is to define the keyword 'greater_zero' in both of the dicts

self.constraints_dict = {'greater_zero': lambda x: self.__dict__[x] > 0}
self.map_constraints_to_attributes = {'greater_zero' : lambda x: [x]}

The first dict is used to iterate over all the constraints and in each iteration create a checker_func. The second function turns a constraint back into a list of iterable class attribute, so it can be set to default if the user’s choice is invalid. For further details see the check_constraints method.

If you need further flags, dummy functions, etc. which should be carried as a self-attribute inside the class construction and are irrelevant for the user, please include them inside the self.repr_ignore list. Hence, they are internally available but will not be shown in the representation of the class.

For all methods inside the subpackage, follow the guide below. If you just want to include a single method inside an already existing subpackage, the guide below is also sufficient.

New method: Your new method should be contained in a subpackage list. Besides the common options of the subpackage, you can customize the options of your new method inside the self.special_options dict. Here, you need to create a key with the name of your method. The corresponding entry should be a dict containing keys with name of the individual option and their default value. Be careful: the name of an option you choose inside this dict will be the name of the class attribute later.

After you have set all the options inside the constructor of the class, you can finally include the options directly into your method. You simply shift all options (in general all arguments which are not necessarily needed) into the options class and include

from oppy.options import Options

def my_new_method(A, x, options=None):
    """
    My personal docstring
    """
    if not options:
        options = Options()
    if not options.cache:
        options.ops()

    # How to get the options:
    my_option = options.my_option
    another_option = options.another_option

    # the code of your method can stay the same

after the docstring of your method.

Results

New subpackage: First, make a list including all methods inside this subpackage. Second, clarify which variables should be part of the Results class inside this subpackage. In a next step, compare your list to the existing ones – maybe the new subpackage has the same attributes as an already existing subpackage (e.g. the unconstrained and constrained optimization problems share the same Results attributes). If you need a new case, you have to define the possible_returns_with_init and the results_options_dict dictionaries. The first dict contains all possible attributes of the Results class and their corresponding default value, while the second one specifies the concrete realization of the Results class in each of the four cases ('sol', 'normal'(default), 'all', 'list').

Now you can execute the following steps for each method inside your subpackage. New method: First, we need to create an instance of the Results class via

from oppy.options import Options
from oppy.results import Results

def my_new_method(A, x, options=None):
    """
    My personal docstring
    """
    if not options:
        options = Options()
    if not options.cache:
        options.ops()

    # How to get the options:
    my_option = options.my_option
    another_option = options.another_option

    # create an instance of the Results class with the flag set by the user
    results = Results('my_new_method', options.results)
    
    # add the options as additional into the return class
    results.update_results({'additional': options.get_ops()})

    # the code of your method can stay the same

inside our function. Second, we can call the results.update_results(dict) in each of the iteration loops (or just one time, if your method does not have an iteration process). We assume you want your method to have the current solution x, a list with the solution iterates X, an iteration counter iteration and a list with the norm of the residuals res. In this case, your update process is of the form

from oppy.options import Options
from oppy.results import Results

def my_new_method(A, x, options=None):
    """
    My personal docstring
    """
    if not options:
        options = Options()
    if not options.cache:
        options.ops()

    # How to get the options:
    my_option = options.my_option
    another_option = options.another_option

    # create an instance of the Results class with the flag set by the user
    results = Results('my_new_method', options.results)
    
    # add the options as additional into the return class
    results.update_results({'additional': options.get_ops()})

    # the code of your method can stay the same
    while my_iteration_condition:
        results.update_results({'X': x, # append the current solution
                                'iteration': True, # increase the iteration counter
                                'res': res[-1]}) # append the norm of the current residual
        # your code is here
    
    # after the loop
    results.update_results({'x': x, # solution found by your algorithm
                            'X': x, # append the solution at the end of the iteration list
                            'iteration': True, # increase the iteration counter
                            'res': res[-1]}) # append the current residual
    # call the finalize method to remove internal variables
    results.finalize()
    return results

Before returning the results instance, we call results.finalize() to remove internal variables which are irrelevant for the users output.

How you should use git while working with oppy

This How to is split into two sections. First, you need to set up your system with git. In the second section we provide practical ‘best-practice’ tips on how to use git with oppy.

Install and set up git

First we need to install and set up git. Depending on your system you need to do different things.

Windows

On Windows you need to install git for windows. To do so go to

https://git-scm.com/download/win

and afterwards choose 32-bit or 64-bit. Now follow the setup instructions. You can leave the default settings (we recommend that). As default editor you can choose (after installing) Notepad++ which is a great editor. You can find it here:

https://www.heise.de/download/product/notepad-26659

After the installation process is done you need to configure your git environment. To do so, open a Git Bash and type into the terminal

git config --global user.name "your_git_username"
git config --global user.email "your_email_address"

where you replace “your_git_username” by (usually) Vorname Nachname and “your_email_address” by vorname.nachname@uni-konstanz.de. You can also save your password (we do not recommend that as it is saved in plaintext on your machine and everybody could read it). To do so type in a Git Bash

git config --global credential.helper store

then

git pull

provide a username and password and those details will then be remembered later. The credentials are stored in a file on the disk, with the disk permissions of “just user readable/writable” but still in plaintext.

If you want to change the password later

git pull

Will fail, because the password is incorrect, git then removes the offending user+password from the ~/.git-credentials file, so now re-run

git pull

to provide a new password so it works as earlier. Found on stack overflow at:

https://stackoverflow.com/questions/35942754/how-to-save-username-and-password-in-git

Ubuntu

On Ubuntu, open a terminal and run

sudo apt install git-all

After the installation process is done you need to configure your git environment. To do so, open a Git Bash and type into the terminal

git config --global user.name "your_git_username"
git config --global user.email "your_email_address"

where you replace “your_git_username” by (usually) Vorname Nachname and “your_email_address” by vorname.nachname@uni-konstanz.de.

If you want to save your password you can use a ssh-key. There are many instructions available online. Just follow one of them (depending on your system). Finally, add the new ssh-key into your gitlab account.

Mac

First, we need to install Git. You can find a guide under Git installation. Afterwards open the terminal and type

git config --global user.name "your_git_username"
git config --global user.email "your_email_address"

where you replace “your_git_username” by (usually) Vorname Nachname and “your_email_address” by vorname.nachname@uni-konstanz.de.

To make your life easier, we highly recommend using a ssh-key, as you do not have to enter your password every time.

Use git in your daily work

First time

If you are using git for the very first time, please first create a folder where you want to save your oppy code. Then open a terminal in this folder and use:

git clone git@gitlab.inf.uni-konstanz.de:ag-volkwein/oppy.git

This will clone all codes, Notebooks, the doumentation, etc. from oppy’s master branch into your folder. Before you get the other branches (just to be sure) we do:

git pull

To get the other branches (e.g. dev) use:

git checkout -b dev origin/dev

To see all branches use

git branch -a

Now you are ready to create your own branch

git checkout -b [name of your new branch]

and push it to git

git push origin [name of your new branch]

Daily work

Let’s assume you want to work in oppy. First check that your system is up to date. With

git status

you can see which branch you are using. Execute

git pull

on your branch and on the dev branch. Then merge the code from dev into your branch via

git checkout [name of your new branch]
git merge dev
git push

Now you can work on your code. If you are done, first upload it on your branch

git add .
git commit -m 'your commit message'
git push

And now switch back to dev and merge your branch into dev.

git checkout dev
git pull
git merge [name of your new branch] -m 'your merge message‘
git push

Now you are done. Maybe (to be safe for the next time) you checkout into your branch again:

git checkout [name of your new branch]

Advanced git setting

If you also use git for your personal projects, it is possible, to run Gitlab and Github at the same time, using different accounts/email addresses. You can find a really good instruction under Different Accounts for Github and Gitlab and a shorter version under SSH key config for git.