Repository: MathisFederico/LearnRL (language: python)

Issue:
Fix/pylint errors learnrl



Discussion / hints:

Fix patch:
diff --git a/.coverage b/.coverage
deleted file mode 100644
index 94d5c7e..0000000
Binary files a/.coverage and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 5a46c98..358ab49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@ pip-delete-this-directory.txt
 htmlcov/
 .tox/
 .nox/
+.coverage
 .coverage.*
 .cache
 nosetests.xml
diff --git a/.pylintrc b/.pylintrc
index e61ea1d..f66a82a 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -31,7 +31,7 @@ limit-inference-results=100
 
 # List of plugins (as comma separated values of python module names) to load,
 # usually to register additional checkers.
-load-plugins=
+load-plugins=pylint_pytest
 
 # Pickle collected data for later comparisons.
 persistent=yes
@@ -63,6 +63,9 @@ confidence=
 disable=print-statement,
         no-name-in-module,
         no-member,
+        no-self-use,
+        too-many-locals,
+        duplicate-code,
         unused-wildcard-import,
         wildcard-import,
         redefined-outer-name,
@@ -145,7 +148,8 @@ disable=print-statement,
         xreadlines-attribute,
         deprecated-sys-function,
         exception-escape,
-        comprehension-escape
+        comprehension-escape,
+        cyclic-import
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
diff --git a/bandit.yml b/bandit.yml
new file mode 100644
index 0000000..b7aee05
--- /dev/null
+++ b/bandit.yml
@@ -0,0 +1,4 @@
+# Do not check paths including `/tests/`:
+# they use `assert`, leading to B101 false positives.
+exclude_dirs:
+    - '/tests/'
diff --git a/learnrl/__init__.py b/learnrl/__init__.py
index 97ba322..b6bc176 100644
--- a/learnrl/__init__.py
+++ b/learnrl/__init__.py
@@ -1,6 +1,8 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""LearnRL a python library to learn and use reinforcement learning"""
+
 __version__ = "0.3.0"
 
 from learnrl.agent import Agent
diff --git a/learnrl/agent.py b/learnrl/agent.py
index 9c7f97f..6203fef 100644
--- a/learnrl/agent.py
+++ b/learnrl/agent.py
@@ -1,13 +1,19 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""Agent to represent any learning or static agent"""
+
 from typing import Dict, Union
+from abc import abstractmethod
+
 import numpy as np
 
+
 class Agent():
 
     """A general structure for any learning agent."""
 
+    @abstractmethod
     def act(self, observation, greedy: bool=False) -> Union[int, float, np.ndarray]:
         """How the :ref:`Agent` act given an observation.
 
@@ -16,7 +22,6 @@ def act(self, observation, greedy: bool=False) -> Union[int, float, np.ndarray]:
             greedy: If True, act greedely (without exploration).
 
         """
-        raise NotImplementedError
 
     def learn(self) -> Dict[str, Union[int, float, np.ndarray]]:
         """How the :ref:`Agent` learns from his experiences.
@@ -48,4 +53,3 @@ def remember(self,
             ...                       info, **param)
 
         """
-        pass
diff --git a/learnrl/callbacks/__init__.py b/learnrl/callbacks/__init__.py
index c8c8ec9..bc31e66 100644
--- a/learnrl/callbacks/__init__.py
+++ b/learnrl/callbacks/__init__.py
@@ -1,27 +1,28 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
-from learnrl.callbacks.callback import Callback, CallbackList
+"""Callbacks to perform actions while running a playground"""
+
+import importlib
 
+from learnrl.callbacks.callback import Callback, CallbackList
 from learnrl.callbacks.logging_callback import LoggingCallback, MetricList, Metric
 from learnrl.callbacks.logger import Logger
 
-import importlib
 tensorflow_spec = importlib.util.find_spec('tensorflow')
-
 if tensorflow_spec is not None:
     from learnrl.callbacks.tensorboard import TensorboardCallback
 else:
     class TensorboardCallback():
+        """Dummy TensorboardCallback when tensorflow in not installed"""
         def __init__(self, log_dir):
             raise ImportError('Missing dependency : tensorflow >= 2.0.0')
 
 wandb_spec = importlib.util.find_spec('wandb')
-
 if wandb_spec is not None:
     from learnrl.callbacks.wandb import WandbCallback
 else:
     class WandbCallback():
+        """Dummy WandbCallback when tensorflow in not installed"""
         def __init__(self):
             raise ImportError('Missing dependency : wandb >= 0.10')
-
diff --git a/learnrl/callbacks/callback.py b/learnrl/callbacks/callback.py
index 4eff7b9..60788f1 100644
--- a/learnrl/callbacks/callback.py
+++ b/learnrl/callbacks/callback.py
@@ -1,6 +1,8 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""Callback abstract classes"""
+
 import time
 
 class Callback():
@@ -28,43 +30,103 @@ def __init__(self):
         self.playground = None
 
     def set_params(self, params):
+        """Sets run parameters"""
         self.params = params
 
     def set_playground(self, playground):
+        """Sets reference to the used playground"""
         self.playground = playground
 
-    def on_step_begin(self, step, logs=None):
-        pass
+    def on_step_begin(self, step: int, logs: dict=None):
+        """Triggers on each step beginning
 
-    def on_step_end(self, step, logs=None):
-        pass
+        Args:
+            step: current step.
+            logs: current logs.
 
-    def on_steps_cycle_begin(self, step, logs=None):
-        pass
+        """
 
-    def on_steps_cycle_end(self, step, logs=None):
-        pass
+    def on_step_end(self, step: int, logs: dict=None):
+        """Triggers on each step end
 
-    def on_episode_begin(self, episode, logs=None):
-        pass
+        Args:
+            step: current step.
+            logs: current logs.
 
-    def on_episode_end(self, episode, logs=None):
-        pass
+        """
 
-    def on_episodes_cycle_begin(self, episode, logs=None):
-        pass
+    def on_steps_cycle_begin(self, step: int, logs: dict=None):
+        """Triggers on each step cycle beginning
 
-    def on_episodes_cycle_end(self, episode, logs=None):
-        pass
+        Args:
+            step: current step.
+            logs: current logs.
 
-    def on_run_begin(self, logs=None):
-        pass
+        """
 
-    def on_run_end(self, logs=None):
-        pass
+    def on_steps_cycle_end(self, step: int, logs: dict=None):
+        """Triggers on each step cycle end
+
+        Args:
+            step: current step.
+            logs: current logs.
+
+        """
+
+    def on_episode_begin(self, episode: int, logs: dict=None):
+        """Triggers on each episode beginning
+
+        Args:
+            episode: current episode.
+            logs: current logs.
+
+        """
+
+    def on_episode_end(self, episode: int, logs: dict=None):
+        """Triggers on each episode end
 
+        Args:
+            episode: current episode.
+            logs: current logs.
 
-class CallbackList():
+        """
+
+    def on_episodes_cycle_begin(self, episode: int, logs: dict=None):
+        """Triggers on each episode cycle beginning
+
+        Args:
+            episode: current episode.
+            logs: current logs.
+
+        """
+
+    def on_episodes_cycle_end(self, episode: int, logs: dict=None):
+        """Triggers on each episode cycle end
+
+        Args:
+            episode: current episode.
+            logs: current logs.
+
+        """
+
+    def on_run_begin(self, logs: dict=None):
+        """Triggers on each run beginning
+
+        Args:
+            logs: current logs.
+
+        """
+
+    def on_run_end(self, logs: dict=None):
+        """Triggers on run end
+
+        Args:
+            logs: current logs.
+
+        """
+
+
+class CallbackList(Callback):
     """ An wrapper to use a list of :class:`Callback`.
 
     Call all concerned callbacks While the :class:`~learnrl.playground.Playground` is running.
@@ -72,9 +134,8 @@ class CallbackList():
     """
 
     def __init__(self, callbacks=()):
+        super().__init__()
         self.callbacks = callbacks
-        self.params = {}
-        self.playground = None
 
     def set_params(self, params):
         self.params = params
diff --git a/learnrl/callbacks/logger.py b/learnrl/callbacks/logger.py
index b36344a..a27e5c5 100644
--- a/learnrl/callbacks/logger.py
+++ b/learnrl/callbacks/logger.py
@@ -1,6 +1,8 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""Default Logger to show progress in console"""
+
 from typing import List
 import numpy as np
 
@@ -77,11 +79,11 @@ def on_step_end(self, step, logs=None):
             sep, end = (' | ', '\n') if self.verbose == 4 else (None, '')
             if self.n_agents > 1:
                 print(f"Agent {agent_id}", end=sep)
-            self._print_metrics(self.step_metrics, 'logs',
+            self._print_metrics(self.step_metrics,
                 agent_id=agent_id, logs=logs, sep=sep, end=end)
 
         if self.verbose == 5:
-            self._print_metrics(self.detailed_step_metrics, 'logs', logs=logs)
+            self._print_metrics(self.detailed_step_metrics, logs=logs)
             self._print_bar('-')
 
     def on_steps_cycle_begin(self, step, logs=None):
@@ -104,7 +106,7 @@ def on_steps_cycle_end(self, step, logs=None):
             sep, end = (' | ', '\n')
             if self.n_agents > 1:
                 print(f"Agent {agent_id}", end=sep)
-            self._print_metrics(self.steps_cycle_metrics, 'attrs', prefix='steps_cycle',
+            self._print_metrics(self.steps_cycle_metrics, prefix='steps_cycle',
                                 agent_id=agent_id, logs=logs, sep=sep, end=end)
 
     def on_episode_begin(self, episode, logs=None):
@@ -140,12 +142,12 @@ def on_episode_end(self, episode, logs=None):
 
                 titles_on_top = False if self.verbose >= 3 else None
 
-                self._print_metrics(self.episode_metrics, 'attrs', prefix='episode',
+                self._print_metrics(self.episode_metrics, prefix='episode',
                     agent_id=agent_id, sep=' | ', titles_on_top=titles_on_top
                 )
 
             self._print_metrics(
-                self.episode_only_metrics, 'logs',
+                self.episode_only_metrics,
                 logs=logs, sep=' | ', titles_on_top=False
             )
 
@@ -177,8 +179,8 @@ def on_episodes_cycle_end(self, episode, logs=None):
             for agent_id in range(self.n_agents):
                 if self.n_agents > 1:
                     print(end=f'\n    Agent {agent_id} | ')
-                self._print_metrics(self.episodes_cycle_metrics, 'attrs', prefix='episodes_cycle',
-                                    agent_id=agent_id, sep=' | ')
+                self._print_metrics(self.episodes_cycle_metrics,
+                    prefix='episodes_cycle', agent_id=agent_id, sep=' | ')
 
             print()
 
@@ -199,7 +201,7 @@ def on_run_begin(self, logs=None):
             self._print_titles(metrics_to_print,
                 prefix='', offset=' ' * offset_len + ' |', end='\n')
 
-    def _print_metrics(self, metric_list:MetricList, source:str, prefix=None, agent_id=None,
+    def _print_metrics(self, metric_list:MetricList, prefix=None, agent_id=None,
                              logs=None, sep=None, end='', titles_on_top=None):
         """ Print a metric list """
         titles_on_top = titles_on_top if titles_on_top is not None else self.titles_on_top
diff --git a/learnrl/callbacks/logging_callback.py b/learnrl/callbacks/logging_callback.py
index 5e6afb1..017b79e 100644
--- a/learnrl/callbacks/logging_callback.py
+++ b/learnrl/callbacks/logging_callback.py
@@ -1,7 +1,9 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
-from typing import List
+"""LoggingCallback tracks aggregated metrics during runs"""
+
+from typing import List, Union
 from learnrl.callbacks.callback import Callback
 
 class Metric():
@@ -48,6 +50,7 @@ def __add__(self, metric_list: List[Metric]):
         return MetricList(new_list)
 
     def append(self, metric: Metric):
+        """Adds a new metric to the MetricList"""
         if metric not in self:
             self.metric_list += [self.to_metric(metric)]
             self.metric_names += [metric.name]
@@ -58,11 +61,11 @@ def __str__(self):
         return str(self.metric_list)
 
     @staticmethod
-    def to_metric(metric: Metric):
+    def to_metric(metric: Union[str, Metric]):
+        """Convert a metric_code into a true Metric"""
         if isinstance(metric, Metric):
             return metric
-        else:
-            return Metric(metric)
+        return Metric(metric)
 
 
 class LoggingCallback(Callback):
@@ -90,6 +93,7 @@ def __init__(self,
 
     @staticmethod
     def _extract_lists(metrics):
+        """Extract the metric lists from a nested metrics argument"""
         step_metrics = []
         steps_cycle_metrics = []
         episode_metrics = []
@@ -108,14 +112,14 @@ def _extract_lists(metrics):
         return step_metrics, steps_cycle_metrics, episode_metrics, episodes_cycle_metrics
 
     def _reset_attr(self, attr_name, operator):
-        """ Reset a metric attribute based on the metric operator """
+        """Reset a metric attribute based on the metric operator"""
         setattr(self, attr_name, 'N/A')
         if operator == 'avg':
             metric_seen = attr_name + '_seen'
             setattr(self, metric_seen, 0)
 
     def _update_attr(self, attr_name, last_value, operator):
-        """ Update a metric attribute based on the metric operator and the last metric value """
+        """Update a metric attribute based on the metric operator and the last metric value"""
         previous_value = getattr(self, attr_name)
 
         if previous_value == 'N/A':
@@ -135,15 +139,14 @@ def _update_attr(self, attr_name, last_value, operator):
 
     @staticmethod
     def _get_attr_name(prefix, metric, agent_id=None):
-        """ Get the attribute name of a metric/agent couple """
+        """Get the attribute name of a metric/agent couple"""
         if agent_id:
             return '_'.join((prefix, "agent" + str(agent_id), metric.name))
-        else:
-            return '_'.join((prefix, metric.name))
+        return '_'.join((prefix, metric.name))
 
     @staticmethod
     def _extract_metric_from_logs(metric_name, logs, agent_id=None):
-        """ Extract the last value of a metric from logs (specified for an agent or not) """
+        """Extract the last value of a metric from logs (specified for an agent or not)"""
 
         def _search_logs(metric_name, logs:dict):
             if logs is None or metric_name in logs:
@@ -172,7 +175,16 @@ def _search_logs(metric_name, logs:dict):
         value = _logs.get(metric_name) if _logs is not None else 'N/A'
         return value
 
-    def _get_value(self, metric:Metric, prefix=None, agent_id=None, logs=None):
+    def _get_value(self, metric:Metric, prefix: str=None, agent_id: int=None, logs: dict=None):
+        """Gather the value of a specific metric
+
+        Args:
+            metric: The metric to look for.
+            prefix: The level of aggregation to look for.
+            agent_id: The specific agent to look into.
+            logs: The logs to look into if the value was not found.
+
+        """
         # Search for prefix-wise attr
         if prefix is not None:
             src_name = self._get_attr_name(prefix, metric, agent_id)
@@ -194,7 +206,7 @@ def _update_metrics(self,
             agent_id=None,
             reset=False
         ):
-        """ Update the logger attributes based on a metric list """
+        """Update the logger attributes based on a metric list"""
         for metric in metric_list:
             target_name = self._get_attr_name(target_prefix, metric, agent_id)
 
@@ -213,6 +225,7 @@ def _update_metrics_all_agents(self,
             target_prefix,
             **kwargs
         ):
+        """Update the logger attributes of all agents based on a metric list"""
         if self.n_agents > 1:
             for agent_id in range(self.n_agents):
                 self._update_metrics(metric_list, target_prefix, agent_id=agent_id, **kwargs)
diff --git a/learnrl/callbacks/tensorboard.py b/learnrl/callbacks/tensorboard.py
index fabb9ef..50638f3 100644
--- a/learnrl/callbacks/tensorboard.py
+++ b/learnrl/callbacks/tensorboard.py
@@ -1,6 +1,12 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""TensorboardCallback to log metrics into tensorboard
+
+https://www.tensorflow.org/tensorboard/get_started
+
+"""
+
 import os, datetime
 from typing import List, Tuple, Dict, Any
 
diff --git a/learnrl/callbacks/wandb.py b/learnrl/callbacks/wandb.py
index 5baa232..4c8bdb0 100644
--- a/learnrl/callbacks/wandb.py
+++ b/learnrl/callbacks/wandb.py
@@ -1,6 +1,12 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""WandbCallback to log metrics into weight&biases
+
+https://wandb.ai/site
+
+"""
+
 from typing import List
 
 import wandb
diff --git a/learnrl/envs.py b/learnrl/envs.py
index 3776e8b..22b98fa 100644
--- a/learnrl/envs.py
+++ b/learnrl/envs.py
@@ -1,6 +1,9 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""Environment types, extentions from the classic gym.Env"""
+
+from abc import abstractmethod
 from gym import Env
 
 class TurnEnv(Env):
@@ -27,6 +30,7 @@ class TurnEnv(Env):
 
     """
 
+    @abstractmethod
     def step(self, action):
         """Perform a step of the environement.
 
@@ -40,8 +44,8 @@ def step(self, action):
             info (dict): Additional informations given by the |gym.Env|.
 
         """
-        raise NotImplementedError
 
+    @abstractmethod
     def turn(self, state) -> int:
         """Give the turn to the next agent to play.
 
@@ -56,8 +60,8 @@ def turn(self, state) -> int:
             agent_id (int): The next player id
 
         """
-        raise NotImplementedError
 
+    @abstractmethod
     def reset(self):
         """Reset the environement and returns the initial state.
 
@@ -65,4 +69,3 @@ def reset(self):
             observation: The observation for the first :class:`Agent` to play
 
         """
-        raise NotImplementedError
diff --git a/learnrl/playground.py b/learnrl/playground.py
index 1293170..2429c67 100644
--- a/learnrl/playground.py
+++ b/learnrl/playground.py
@@ -1,10 +1,12 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+"""Playground to manage interactions between environment and agent(s)"""
 
 import warnings
 
 from typing import List, Union, Callable
+from abc import abstractmethod
 
 import numpy as np
 from gym import Env
@@ -24,11 +26,29 @@ class DoneHandler():
 
     """
 
-    def done(self, observation, action, reward, done, info, next_observation) -> bool:
-        raise NotImplementedError
+    @abstractmethod
+    def done(self, previous_observation, action, reward, done, info, observation) -> bool:
+        """Replace the environment done.
+
+        Often used to make episodes shorter when the agent is stuck for example.
+
+        Args:
+            previous_observation: Previous observation.
+            action: Current action.
+            reward: Current reward.
+            done: done given by the environment.
+            info: Addition informations given by the environment.
+            observation: Current observation.
+
+        """
 
     def reset(self):
-        pass
+        """Reset the DoneHandler
+
+        Called automaticaly in :meth:`Playground.run`.
+        Used only if variables are stored by the DoneHandler.
+
+        """
 
     def _done(self, *args) -> bool:
         done = self.done(*args)
@@ -51,14 +71,30 @@ class RewardHandler():
 
     """
 
-    def reward(self,
-            observation, action, reward,
-            done, info, next_observation
-        )-> Union[int, float]:
-        raise NotImplementedError
+    @abstractmethod
+    def reward(self, previous_observation, action, reward,
+            done, info, observation)-> float:
+        """Replace the environment reward.
+
+        Often used to scale rewards or to do reward shaping.
+
+        Args:
+            previous_observation: Previous observation.
+            action: Current action.
+            reward: Current reward.
+            done: done given by the environment.
+            info: Addition informations given by the environment.
+            observation: Current observation.
+
+        """
 
     def reset(self):
-        pass
+        """Reset the RewardHandler
+
+        Called automaticaly in :meth:`Playground.run`.
+        Useful only if variables are stored by the RewardHandler.
+
+        """
 
     def _reward(self, *args) -> float:
         reward = self.reward(*args)
@@ -74,11 +110,19 @@ def __call__(self, *args) -> float:
 
 class Playground():
 
+    """A playground is used to run agent(s) on an environement
+
+    Attributes:
+        env (gym.Env):  Environement in which the agent(s) will play.
+        agents (list of learnrl.Agent): List of agents to play.
+
+    """
+
     def __init__(self, environement:Env, agents:Union[Agent, List[Agent]]):
-        """A playground is used to run agent(s) on an environement.
+        """A playground is used to run agent(s) on an environement
 
         Args:
-            env: Environement in which agents will play.
+            env: Environement in which the agent(s) will play.
             agents: List of agents to play (can be only one agent).
 
         """
@@ -190,68 +234,21 @@ def run(self,
 
             while not done:
 
-                # Render the environment
-                if render:
-                    self.env.render()
-
-                # Get playing agent (TurnEnv)
-                agent_id = self.env.turn(observation) if isinstance(self.env, TurnEnv) else 0
-                if agent_id >= len(previous):
-                    raise ValueError(f'Not enough agents to play environement {self.env}')
-                agent = self.agents[agent_id]
-
-                # If the agent has played before, perform a learning step
-                prev = previous[agent_id]
-                if learn and prev['observation'] is not None:
-                    agent.remember(
-                        prev['observation'], prev['action'], prev['reward'],
-                        prev['done'], observation, prev['info']
-                    )
-                    agent_logs = agent.learn()
-                    logs.update({f'agent_{agent_id}': agent_logs})
-
-                # Adds step informations to logs
-                logs.update({'step':step, 'agent_id':agent_id, 'observation':observation})
-
                 if step % steps_cycle_len == 0:
                     callbacks.on_steps_cycle_begin(step, logs)
 
                 callbacks.on_step_begin(step, logs)
 
-                # Ask action to agent
-                action = agent.act(observation, greedy=not learn)
-                # Perform environment step
-                next_observation, reward, done, info = self.env.step(action)
-
-                # Perform reward handling
-                logs.update({'reward': reward})
-                if reward_handler is not None:
-                    reward = reward_handler(
-                        observation, action, reward, done, info, next_observation
-                    )
-                    logs.update({'handled_reward': reward})
-
-                # Perform done handling
-                logs.update({'done': done})
-                if done_handler is not None:
-                    done = done_handler(
-                        observation, action, reward, done, info, next_observation
-                    )
-                    logs.update({'handled_done': done})
-
-                # Perform a learning step
-                if learn:
-                    for key, value in zip(prev, [observation, action, reward, done, info]):
-                        prev[key] = value
-                    if done:
-                        agent.remember(
-                            observation, action, reward, done, next_observation, info
-                        )
-                        agent_logs = agent.learn()
-                        logs.update({f'agent_{agent_id}': agent_logs})
-
-                logs.update({'action': action, 'next_observation': next_observation})
-                logs.update(info)
+                next_observation, logs = self._run_step(
+                    step=step,
+                    observation=observation,
+                    previous=previous,
+                    learn=learn,
+                    render=render,
+                    reward_handler=reward_handler,
+                    done_handler=done_handler,
+                    logs=logs
+                )
 
                 callbacks.on_step_end(step, logs)
 
@@ -272,6 +269,69 @@ def run(self,
 
         callbacks.on_run_end(logs)
 
+    def _run_step(self, step, observation, previous,
+        learn, render, reward_handler, done_handler, logs):
+        """Run a single step"""
+        # Render the environment
+        if render:
+            self.env.render()
+
+        # Get playing agent (TurnEnv)
+        agent_id = self.env.turn(observation) if isinstance(self.env, TurnEnv) else 0
+        if agent_id >= len(previous):
+            raise ValueError(f'Not enough agents to play environement {self.env}')
+        agent = self.agents[agent_id]
+
+        # If the agent has played before, perform a learning step
+        prev = previous[agent_id]
+        if learn and prev['observation'] is not None:
+            agent.remember(
+                prev['observation'], prev['action'], prev['reward'],
+                prev['done'], observation, prev['info']
+            )
+            agent_logs = agent.learn()
+            logs.update({f'agent_{agent_id}': agent_logs})
+
+        # Adds step informations to logs
+        logs.update({'step':step, 'agent_id':agent_id, 'observation':observation})
+
+        # Ask action to agent
+        action = agent.act(observation, greedy=not learn)
+        # Perform environment step
+        next_observation, reward, done, info = self.env.step(action)
+
+        # Perform reward handling
+        logs.update({'reward': reward})
+        if reward_handler is not None:
+            reward = reward_handler(
+                observation, action, reward, done, info, next_observation
+            )
+            logs.update({'handled_reward': reward})
+
+        # Perform done handling
+        logs.update({'done': done})
+        if done_handler is not None:
+            done = done_handler(
+                observation, action, reward, done, info, next_observation
+            )
+            logs.update({'handled_done': done})
+
+        # Perform a learning step
+        if learn:
+            for key, value in zip(prev, [observation, action, reward, done, info]):
+                prev[key] = value
+            if done:
+                agent.remember(
+                    observation, action, reward, done, next_observation, info
+                )
+                agent_logs = agent.learn()
+                logs.update({f'agent_{agent_id}': agent_logs})
+
+        logs.update({'action': action, 'next_observation': next_observation})
+        logs.update(info)
+        return next_observation, logs
+
+
     def fit(self, episodes, **kwargs):
         """Train the agent(s) on the environement for a number of episodes."""
         learn = kwargs.pop('learn', True)
diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index e6e326e..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-[tool.pytest.ini_options]
-filterwarnings = [
-    "ignore::UserWarning:gym",
-    "ignore::DeprecationWarning"
-]
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 8f160c0..2fa0ff0 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,4 +1,8 @@
 pytest
 pytest-mock
+pytest-check
+pytest-pspec
+pylint
+pylint-pytest
 wandb >= 0.10.0
-tensorboard >= 2.3.0
\ No newline at end of file
+tensorflow >= 2.3.0


Test patch:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index b595b74..db3c6a0 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -30,11 +30,11 @@ jobs:
         python -m pip install --upgrade pip
         pip install tensorflow
         pip install -r requirements.txt
+        pip install -r requirements-dev.txt
     - name: Lint with pylint
       run: |
-        pip install pylint
-        pylint learnrl/ tests/
+        pylint learnrl/
+        pylint --exit-zero tests/
     - name: Test with pytest
       run: |
-        pip install -r requirements-dev.txt
-        pytest --runslow
+        pytest
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..451a5c9
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+filterwarnings =
+    ignore::UserWarning:gym
+    ignore::DeprecationWarning
+addopts = --pspec 
diff --git a/tests/__init__.py b/tests/__init__.py
index 8b8e4dd..8ce7997 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,2 +1,2 @@
 # LearnRL a python library to learn and use reinforcement learning
-# Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
\ No newline at end of file
+# Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
diff --git a/tests/conftest.py b/tests/conftest.py
index 73d3802..fe6cdf3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,50 +1,37 @@
+""" Configure tests and define markers """
+
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
-import pytest
 import importlib
+import pytest
 import learnrl
 
-def pytest_addoption(parser):
-    parser.addoption(
-        "--runslow", action="store_true", default=False, help="run slow tests"
-    )
-
-
-def pytest_configure(config):
-    config.addinivalue_line("markers", "slow: mark test as slow to run")
-
-
-def pytest_collection_modifyitems(config, items):
-    if config.getoption("--runslow"):
-        # --runslow given in cli: do not skip slow tests
-        return
-    skip_slow = pytest.mark.skip(reason="need --runslow option to run")
-    for item in items:
-        if "slow" in item.keywords:
-            item.add_marker(skip_slow)
-
 @pytest.fixture
 def reload_tensorflow():
+    """ Reload tensorflow after hiding it from the modules list """
+
     yield
     importlib.reload(learnrl.callbacks)
     importlib.reload(learnrl)
 
 @pytest.fixture
 def hide_tensorflow(reload_tensorflow, mocker):
+    """ Hide tensorflow from the modules list """
     mocker.patch.dict('sys.modules', { 'tensorflow': None })
     importlib.reload(learnrl.callbacks)
     importlib.reload(learnrl)
 
 @pytest.fixture
 def reload_wandb():
+    """ Reload wandb after hiding it form the modules list """
     yield
     importlib.reload(learnrl.callbacks)
     importlib.reload(learnrl)
 
 @pytest.fixture
 def hide_wandb(reload_wandb, mocker):
+    """ Hide wandb from the modules list """
     mocker.patch.dict('sys.modules', { 'wandb': None })
     importlib.reload(learnrl.callbacks)
     importlib.reload(learnrl)
-
diff --git a/tests/pipelines/__init__.py b/tests/integrations/__init__.py
similarity index 100%
rename from tests/pipelines/__init__.py
rename to tests/integrations/__init__.py
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
index 486f7aa..8ce7997 100644
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -1,3 +1,2 @@
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
-
diff --git a/tests/unit/callbacks/test_logging.py b/tests/unit/callbacks/test_logging.py
index 83b27d9..7f8d0f5 100644
--- a/tests/unit/callbacks/test_logging.py
+++ b/tests/unit/callbacks/test_logging.py
@@ -1,15 +1,19 @@
+""" Test the logging callback """
+
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
 import numpy as np
+import pytest_check as check
+import pytest
 
 from learnrl.callbacks import LoggingCallback, CallbackList
 
-
 class DummyPlayground():
+    """ Dummy playground for testing """
 
-    def __init__(self, agents=[0]):
-        self.agents = agents
+    def __init__(self, agents=None):
+        self.agents = agents if agents is not None else [0]
 
     def run(self, callbacks, eps_end_func=None, verbose=0):
 
@@ -104,7 +108,7 @@ def run(self, callbacks, eps_end_func=None, verbose=0):
                     for metric_name in ('reward', 'loss'):
                         logs.update({
                             f'{metric_name}_steps_sum': steps_cycle_tracker[metric_name],
-                            f'{metric_name}_steps_avg': steps_cycle_tracker[metric_name]/steps_cycle_seen,
+                            f'{metric_name}_steps_avg': steps_cycle_tracker[metric_name] / steps_cycle_seen,
                         })
                     callbacks.on_steps_cycle_end(step, logs)
 
@@ -155,151 +159,152 @@ def run(self, callbacks, eps_end_func=None, verbose=0):
 
         callbacks.on_run_end(logs)
 
+class TestExtractFromLogs():
+    """ extract_from_logs """
+
+    @pytest.fixture(autouse=True)
+    def setup_logs(self):
+        """ Setup fake logs and retrieve function to test """
+
+        self.extract_from_logs = LoggingCallback._extract_metric_from_logs
+        self.logs = {
+            'value': 0,
+            'step': 2,
+            'agent_0': {
+                'value': 1,
+            },
+            'agent_1': {
+                'value': 2,
+                'specific_value': 42,
+            }
+        }
 
-def test_extract_from_logs():
+    def test_find_value(self):
+        """ should find value in logs. """
 
-    extract_from_logs = LoggingCallback._extract_metric_from_logs
+        value = self.extract_from_logs('value', self.logs)
+        check.equal(value, 0)
 
-    logs = {
-        'value': 0,
-        'step': 2,
-        'agent_0': {
-            'value': 1,
-        },
-        'agent_1': {
-            'value': 2,
-            'specific_value': 42,
-        }
-    }
-
-    value = extract_from_logs('value', logs)
-    if value != 0:
-        raise ValueError('We should find values in logs')
-
-    nothing = extract_from_logs('nothing', logs)
-    if nothing != 'N/A':
-        raise ValueError('Nothing should return N/A')
-
-    specific_value = extract_from_logs('specific_value', logs)
-    if specific_value != 42:
-        raise ValueError(
-            'If no agent is specified, it should find any specific_value in agent.'
-            f'Here specific_value was {specific_value} instead of 42.'
-        )
-
-    value_0 = extract_from_logs('value', logs, agent_id=0)
-    if value_0 != 1:
-        raise ValueError(
-            'We should find specific agent values.'
-            f'Here value for agent 0 was {value_0} instead of 1.'
-        )
-    value_1 = extract_from_logs('value', logs, agent_id=1)
-    if value_1 != 2:
-        raise ValueError(
-            'We should find specific agent values.'
-            f'Here value for agent 1 was {value_1} instead of 2.'
-        )
-
-    step = extract_from_logs('step', logs, agent_id=0)
-    if step != 2:
-        raise ValueError(
-            'When agent is specified, it should return outer value.'
-            f'if a specific value is not found. Here we found {step} instead of 2'
-        )
-
-def test_extract_lists():
-
-    extract_lists = LoggingCallback._extract_lists
-
-    metrics = [
-        ('reward~rwd', {'steps': 'sum', 'episode': 'sum'}),
-        ('loss', {'episodes': 'last'}),
-    ]
-
-    metric_lists = extract_lists(metrics)
-
-    expected_metric_lists = [
-        ['reward~rwd', 'loss'],
-        ['reward~rwd.sum', 'loss.avg'],
-        ['reward~rwd.sum', 'loss.avg'],
-        ['reward~rwd.avg', 'loss.last']
-    ]
-
-    metric_lists_names = [
-        'step_metrics',
-        'steps_cycle_metrics',
-        'episode_metrics',
-        'episodes_cycle_metrics'
-    ]
-
-    iterator = zip(metric_lists, expected_metric_lists, metric_lists_names)
-    for metric_list, expected_metric_list, metric_list_name in iterator:
-        if metric_list != expected_metric_list:
-            raise ValueError(
-                f'Unexpected metric list got {metric_list}'
-                f'instead of {expected_metric_list} for {metric_list_name}'
-            )
+    def test_find_nothing(self):
+        """ should return N/A when there is no value. """
 
-def test_logging_steps_avg_sum():
-    for cycle_operator in ['avg', 'sum']:
-        print(cycle_operator, '\n')
+        nothing = self.extract_from_logs('nothing', self.logs)
+        check.equal(nothing, 'N/A')
 
-        logging_callback = LoggingCallback(
-            metrics=[('reward', {'steps':cycle_operator}), ('loss', {'steps':cycle_operator})],
-        )
 
-        def check_function(callbacks, logs):
-            callback_dict = callbacks.callbacks[0].__dict__
-            for metric_name in ('reward', 'loss'):
-                expected = logs.get(f'{metric_name}_steps_{cycle_operator}')
-                logged = callback_dict[f'steps_cycle_{metric_name}']
-                if expected is not None:
-                    print(metric_name, logged, expected)
-                    if logged == 'N/A' or not np.isclose(logged, expected):
-                        raise ValueError(f'Logged {logged} instead of {expected}')
+    def test_find_any_specific_value(self):
+        """ should find any specific value in agent when no agent is specified. """
 
-        pg = DummyPlayground()
-        pg.run([logging_callback], eps_end_func=check_function, verbose=3)
-        print()
+        specific_value = self.extract_from_logs('specific_value', self.logs)
+        check.equal(specific_value, 42)
 
+    def test_find_agent_specific_value(self):
+        """ should find specific agent values. """
 
-def test_logging_episodes_avg_sum():
+        value_0 = self.extract_from_logs('value', self.logs, agent_id=0)
+        value_1 = self.extract_from_logs('value', self.logs, agent_id=1)
 
-    for eps_operator in ['avg', 'sum']:
+        check.equal(value_0, 1)
+        check.equal(value_1, 2)
 
-        for cycle_operator in ['avg', 'sum']:
+    def test_return_outer_value_when_no_specific_agent_value(self):
+        """ should return outer value if no specific value is found when an agent is specified. """
 
-            print(eps_operator, cycle_operator, '\n')
+        step = self.extract_from_logs('step', self.logs, agent_id=0)
+        check.equal(step, 2)
 
-            logging_callback = LoggingCallback(
-                detailed_step_metrics=[],
-                metrics=[('reward', {'episode': eps_operator, 'episodes': cycle_operator}),
-                         ('loss', {'episode': eps_operator, 'episodes': cycle_operator})],
-            )
+class TestExtractLists:
+    """ extract_lists """
 
-            def check_function(callbacks, logs):
-                callback_dict = callbacks.callbacks[0].__dict__
+    def test_extract_lists(self):
+        """ should extract the correct metrics list. """
 
-                for position in ('episode', 'episodes_cycle'):
-                    for metric_name in ('reward', 'loss'):
+        extract_lists = LoggingCallback._extract_lists
 
-                        if position == 'episode':
-                            expected = logs.get(f'{metric_name}_episode_{eps_operator}')
-                        elif position == 'episodes_cycle':
-                            expected = logs.get(f'{metric_name}_{eps_operator}_cycle_{cycle_operator}')
+        metrics = [
+            ('reward~rwd', {'steps': 'sum', 'episode': 'sum'}),
+            ('loss', {'episodes': 'last'}),
+        ]
 
-                        logged = callback_dict[f'{position}_{metric_name}']
-                        if expected is not None:
-                            print(position.capitalize(), metric_name, logged, expected)
-                            if logged == 'N/A' or not np.isclose(logged, expected):
-                                raise ValueError(f'Logged {logged} instead of {expected}')
+        metric_lists = extract_lists(metrics)
 
-            pg = DummyPlayground()
-            pg.run([logging_callback], eps_end_func=check_function, verbose=1)
-            print()
+        expected_metric_lists = [
+            ['reward~rwd', 'loss'],
+            ['reward~rwd.sum', 'loss.avg'],
+            ['reward~rwd.sum', 'loss.avg'],
+            ['reward~rwd.avg', 'loss.last']
+        ]
+
+        metric_lists_names = [
+            'step_metrics',
+            'steps_cycle_metrics',
+            'episode_metrics',
+            'episodes_cycle_metrics'
+        ]
+
+        iterator = zip(metric_lists, expected_metric_lists, metric_lists_names)
+        for metric_list, expected_metric_list, metric_list_name in iterator:
+            assert metric_list == expected_metric_list, \
+                f'Unexpected metric list got {metric_list} ' \
+                f'instead of {expected_metric_list} for {metric_list_name}'
+
+@pytest.mark.parametrize('cycle_operator', ['avg', 'sum'])
+@pytest.mark.parametrize('metric_name', ['reward', 'loss'])
+def test_logging_steps_operators_(cycle_operator, metric_name):
+
+    print(cycle_operator, metric_name)
+    logging_callback = LoggingCallback(
+        metrics=[('reward', {'steps':cycle_operator}), ('loss', {'steps':cycle_operator})],
+    )
+
+    def check_function(callbacks, logs):
+        callback_dict = callbacks.callbacks[0].__dict__
+        expected = logs.get(f'{metric_name}_steps_{cycle_operator}')
+        logged = callback_dict[f'steps_cycle_{metric_name}']
+        if expected is not None:
+            print(metric_name, logged, expected)
+            assert logged != 'N/A' and np.isclose(logged, expected), \
+                f'Logged {logged} instead of {expected}'
+
+    playground = DummyPlayground()
+    playground.run([logging_callback], eps_end_func=check_function, verbose=3)
+    print()
+
+@pytest.mark.parametrize('eps_operator', ['avg', 'sum'])
+@pytest.mark.parametrize('cycle_operator', ['avg', 'sum'])
+@pytest.mark.parametrize('metric_name', ['reward', 'loss'])
+def test_logging_episodes_operators(eps_operator, cycle_operator, metric_name):
+
+    print(eps_operator, cycle_operator, '\n')
+
+    logging_callback = LoggingCallback(
+        detailed_step_metrics=[],
+        metrics=[('reward', {'episode': eps_operator, 'episodes': cycle_operator}),
+                 ('loss', {'episode': eps_operator, 'episodes': cycle_operator})],
+    )
+
+    def check_function(callbacks, logs):
+        callback_dict = callbacks.callbacks[0].__dict__
+
+        for position in ('episode', 'episodes_cycle'):
+            if position == 'episode':
+                expected = logs.get(f'{metric_name}_episode_{eps_operator}')
+            elif position == 'episodes_cycle':
+                expected = logs.get(f'{metric_name}_{eps_operator}_cycle_{cycle_operator}')
+
+            logged = callback_dict[f'{position}_{metric_name}']
+            if expected is not None:
+                print(position.capitalize(), metric_name, logged, expected)
+                assert logged != 'N/A' and np.isclose(logged, expected), \
+                    f'Logged {logged} instead of {expected}'
+
+    playground = DummyPlayground()
+    playground.run([logging_callback], eps_end_func=check_function, verbose=1)
+    print()
 
 
 def test_display():
+    """ Test display mode """
 
     from learnrl.callbacks import Logger
 
@@ -312,7 +317,7 @@ def test_display():
 
             print(f'Verbose {verbose}, Title_on_top {titles_on_top}\n')
 
-            pg = DummyPlayground()
-            pg.run([logging_callback], verbose=verbose)
+            playground = DummyPlayground()
+            playground.run([logging_callback], verbose=verbose)
 
             print()
diff --git a/tests/unit/callbacks/test_tensorboard.py b/tests/unit/callbacks/test_tensorboard.py
index d370e46..16f84e2 100644
--- a/tests/unit/callbacks/test_tensorboard.py
+++ b/tests/unit/callbacks/test_tensorboard.py
@@ -1,17 +1,23 @@
+""" Test Tensorboard callback """
+
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+import importlib
 import pytest
+
 import learnrl
-import importlib
+
 tensorflow_spec = importlib.util.find_spec('tensorflow')
 
 if tensorflow_spec is not None:
     def test_instanciate_without_tensorflow(hide_tensorflow):
+        """ should raise an ImportError when tensorflow is not installed. """
         with pytest.raises(ImportError, match=r".*tensorflow >= 2.*"):
             learnrl.callbacks.TensorboardCallback(log_dir='logs')
 
     def test_instanciate_with_tensorflow():
+        """ should load normally when tensorflow is installed. """
         learnrl.callbacks.TensorboardCallback(log_dir='logs')
 else:
     pass
diff --git a/tests/unit/callbacks/test_wandb.py b/tests/unit/callbacks/test_wandb.py
index a15c9bf..81e99c4 100644
--- a/tests/unit/callbacks/test_wandb.py
+++ b/tests/unit/callbacks/test_wandb.py
@@ -1,17 +1,23 @@
+""" Test wandb callback """
+
 # LearnRL a python library to learn and use reinforcement learning
 # Copyright (C) 2020 Mathïs FEDERICO <https://www.gnu.org/licenses/>
 
+import importlib
 import pytest
+
 import learnrl
-import importlib
+
 wandb_spec = importlib.util.find_spec('wandb')
 
 if wandb_spec is not None:
     def test_instanciate_without_wandb(hide_wandb):
+        """ should raise an ImportError when wandb is not installed. """
         with pytest.raises(ImportError, match=r".*wandb >= 0.10.*"):
             learnrl.callbacks.WandbCallback()
 
     def test_instanciate_with_wandb():
+        """ should load normally when wandb is installed """
         learnrl.callbacks.WandbCallback()
 else:
     pass