Repository: aiidaplugins/aiida-ase (language: python)

Issue:
please add "Framework::AiiDA" classifier to setup.json



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3486c70
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,81 @@
+name: continuous-integration
+
+on: [push, pull_request]
+
+jobs:
+
+    pre-commit:
+
+        runs-on: ubuntu-latest
+
+        steps:
+        -   uses: actions/checkout@v2
+
+        -   name: Cache python dependencies
+            id: cache-pip
+            uses: actions/cache@v1
+            with:
+                path: ~/.cache/pip
+                key: pip-pre-commit-${{ hashFiles('**/setup.json') }}
+                restore-keys:
+                    pip-pre-commit-
+
+        -   name: Set up Python
+            uses: actions/setup-python@v2
+            with:
+                python-version: 3.8
+
+        -   name: Install python dependencies
+            run:
+                pip install -e .[pre-commit,tests]
+
+        -   name: Run pre-commit
+            run:
+                pre-commit run --all-files || ( git status --short ; git diff ; exit 1 )
+
+    tests:
+
+        runs-on: ubuntu-latest
+        timeout-minutes: 10
+
+        strategy:
+            matrix:
+                python-version: [3.7, 3.8, 3.9]
+
+        services:
+            rabbitmq:
+                image: rabbitmq:latest
+                ports:
+                -   5672:5672
+
+        steps:
+        -   uses: actions/checkout@v2
+
+        -   name: Cache python dependencies
+            id: cache-pip
+            uses: actions/cache@v1
+            with:
+                path: ~/.cache/pip
+                key: pip-${{ matrix.python-version }}-tests-${{ hashFiles('**/setup.json') }}
+                restore-keys:
+                    pip-${{ matrix.python-version }}-tests
+
+        -   name: Set up Python ${{ matrix.python-version }}
+            uses: actions/setup-python@v2
+            with:
+                python-version: ${{ matrix.python-version }}
+
+        -   name: Install system dependencies
+            run: |
+                sudo apt update
+                sudo apt install postgresql
+
+        -   name: Install python dependencies
+            run: |
+                pip install --upgrade setuptools
+                pip install -e .[tests]
+                reentry scan
+
+        -   name: Run pytest
+            run:
+                pytest -sv tests
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..55b132b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,46 @@
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+    -   id: double-quote-string-fixer
+        exclude: &excludes >
+            (?x)^(
+                tests/.*(?<!\.py)$
+            )$
+    -   id: end-of-file-fixer
+        exclude: *excludes
+    -   id: fix-encoding-pragma
+    -   id: mixed-line-ending
+    -   id: trailing-whitespace
+        exclude: *excludes
+
+-   repo: https://github.com/ikamensh/flynt/
+    rev: '0.63'
+    hooks:
+    -   id: flynt
+        args: [
+            '--line-length=120',
+            '--fail-on-change',
+        ]
+
+-   repo: https://github.com/pre-commit/mirrors-yapf
+    rev: v0.31.0
+    hooks:
+    -   id: yapf
+        name: yapf
+        types: [python]
+        args: ['-i']
+        additional_dependencies: ['toml']
+        exclude: &exclude_files >
+            (?x)^(
+                docs/.*|
+                examples/.*|
+                tests/.*(?<!\.py)$
+            )$
+
+-   repo: https://github.com/PyCQA/pylint
+    rev: v2.8.2
+    hooks:
+    -   id: pylint
+        language: system
+        exclude: *exclude_files
diff --git a/README.md b/README.md
index 57bddb1..32a65b5 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,5 @@
 AiiDA plugin for ASE
 
 # Documentation
-The documentation for this package can be found on Read the Docs at 
+The documentation for this package can be found on Read the Docs at
 http://aiida-ase.readthedocs.io/en/latest/
-
diff --git a/aiida_ase/__init__.py b/aiida_ase/__init__.py
index 5c4105c..a360573 100644
--- a/aiida_ase/__init__.py
+++ b/aiida_ase/__init__.py
@@ -1,1 +1,3 @@
-__version__ = "1.0.1"
+# -*- coding: utf-8 -*-
+"""The official AiiDA plugin for ASE."""
+__version__ = '2.0.0a1'
diff --git a/aiida_ase/calculations/ase.py b/aiida_ase/calculations/ase.py
index 8f00a68..9176f7d 100644
--- a/aiida_ase/calculations/ase.py
+++ b/aiida_ase/calculations/ase.py
@@ -1,474 +1,414 @@
 # -*- coding: utf-8 -*-
-from aiida.orm.calculation.job import JobCalculation
-from aiida.orm.data.parameter import ParameterData 
-from aiida.orm.data.structure import StructureData
-from aiida.common.utils import classproperty
-from aiida.common.exceptions import InputValidationError
-from aiida.common.datastructures import CalcInfo, CodeInfo
-from aiida.orm.data.array.kpoints import KpointsData
-from aiida import get_file_header
-
-class AseCalculation(JobCalculation):
-    """
-    A generic plugin for calculations based on the ASE calculators.
-    
-    Requirement: the node should be able to import ase
-    """
-    
-    def _init_internal_params(self):
-        super(AseCalculation, self)._init_internal_params()
-        
-        self._INPUT_FILE_NAME = 'aiida_script.py'
-        self._OUTPUT_FILE_NAME = 'results.json'
-        self._input_aseatoms = "aiida_atoms.traj"
-        self._output_aseatoms = "aiida_out_atoms.traj"
-        self._default_parser = "ase"
-        self._TXT_OUTPUT_FILE_NAME = 'aiida.out'
-    
-    @classproperty
-    def _use_methods(cls):
-        """
-        Additional use_* methods for the namelists class.
-        """
-        retdict = JobCalculation._use_methods
-        retdict.update({
-            "parameters": {
-               'valid_types': ParameterData,
-               'additional_parameter': None,
-               'linkname': 'parameters',
-               'docstring': ("Use a node that specifies the input parameters "
-                             "for the namelists"),
-               },
-            "structure": {
-               'valid_types': StructureData,
-               'additional_parameter': None,
-               'linkname': 'structure',
-               'docstring': "Use a node for the structure",
-               },
-            "settings": {
-               'valid_types': ParameterData,
-               'additional_parameter': None,
-               'linkname': 'settings',
-               'docstring': ("Use a node that specifies the extra information "
-                             "to be used by the calculation"),
-               },
-            "kpoints": {
-               'valid_types': KpointsData,
-               'additional_parameter': None,
-               'linkname': 'kpoints',
-               'docstring': ("Use a node that specifies the kpoints"),
-               },
-            })
-        return retdict
-    
-    def _prepare_for_submission(self,tempfolder, inputdict):        
-        """
-        This is the routine to be called when you want to create
-        the input files and related stuff with a plugin.
-        
-        :param tempfolder: a aiida.common.folders.Folder subclass where
-                           the plugin should put all its files.
-        :param inputdict: a dictionary with the input nodes, as they would
-                be returned by get_inputdata_dict (without the Code!)
+"""`CalcJob` implementation that can be used to wrap around the ASE calculators."""
+from aiida import common
+from aiida import engine
+from aiida import orm
+from aiida import plugins
+
+Dict = plugins.DataFactory('dict')
+StructureData = plugins.DataFactory('structure')
+KpointsData = plugins.DataFactory('array.kpoints')
+
+
+class AseCalculation(engine.CalcJob):
+    """`CalcJob` implementation that can be used to wrap around the ASE calculators."""
+
+    _default_parser = 'ase.ase'
+    _INPUT_FILE_NAME = 'aiida_script.py'
+    _OUTPUT_FILE_NAME = 'results.json'
+    _TXT_OUTPUT_FILE_NAME = 'aiida.out'
+    _input_aseatoms = 'aiida_atoms.json'
+    _output_aseatoms = 'aiida_out_atoms.json'
+
+    @classmethod
+    def define(cls, spec):
+        # yapf: disable
+        super().define(spec)
+        spec.input('metadata.options.input_filename', valid_type=str, default=cls._INPUT_FILE_NAME,
+            help='Filename to which the input for the code that is to be run will be written.')
+        spec.input('metadata.options.output_filename', valid_type=str, default=cls._OUTPUT_FILE_NAME,
+            help='Filename to which the content of stdout of the code that is to be run will be written.')
+        spec.input('metadata.options.error_filename', valid_type=str, default='aiida.err',
+            help='Filename to which the content of stderr of the code that is to be run will be written.')
+        spec.input('metadata.options.parser_name', valid_type=str, default=cls._default_parser,
+            help='Define the parser to be used by setting its entry point name.')
+        spec.input('structure', valid_type=StructureData, help='The input structure.')
+        spec.input('kpoints', valid_type=KpointsData, required=False, help='The k-points to use for the calculation.')
+        spec.input('parameters', valid_type=Dict, help='Input parameters for the namelists.')
+        spec.input('settings', valid_type=Dict, required=False, help='Optional settings that control the plugin.')
+
+        spec.output('structure', valid_type=orm.StructureData, required=False)
+        spec.output('parameters', valid_type=orm.Dict, required=False)
+        spec.output('array', valid_type=orm.ArrayData, required=False)
+
+        spec.exit_code(300, 'ERROR_OUTPUT_FILES', message='One of the expected output files was missing.')
+        # yapf: enable
+
+    def prepare_for_submission(self, folder):
+        """This method is called prior to job submission with a set of calculation input nodes.
+
+        The inputs will be validated and sanitized, after which the necessary input files will be written to disk in a
+        temporary folder. A CalcInfo instance will be returned that contains lists of files that need to be copied to
+        the remote machine before job submission, as well as file lists that are to be retrieved after job completion.
+
+        :param folder: an aiida.common.folders.Folder to temporarily write files on disk
+        :returns: CalcInfo instance
         """
-        try:
-            code = inputdict.pop(self.get_linkname('code'))
-        except KeyError:
-            raise InputValidationError("No code specified for this "
-                                       "calculation")
+        # pylint: disable=too-many-locals,too-many-branches,too-many-statements
+        if 'settings' in self.inputs:
+            settings = self.inputs.settings.get_dict()
+        else:
+            settings = {}
 
-        try:
-            parameters = inputdict.pop(self.get_linkname('parameters'))
-        except KeyError:
-            raise InputValidationError("No parameters specified for this "
-                                       "calculation")
-        if not isinstance(parameters, ParameterData):
-            raise InputValidationError("parameters is not of type "
-                                       "ParameterData")
-        
-        try:
-            structure = inputdict.pop(self.get_linkname('structure'))
-        except KeyError:
-            raise InputValidationError("No structure specified for this "
-                                       "calculation")
-        if not isinstance(structure,StructureData):
-            raise InputValidationError("structure node is not of type"
-                                       "StructureData")
-        
-        try:
-            settings = inputdict.pop(self.get_linkname('settings'),None)
-        except KeyError:
-            pass
-        if settings is not None:
-            if not isinstance(parameters, ParameterData):
-                raise InputValidationError("parameters is not of type "
-                                           "ParameterData")
-        
-        try:
-            kpoints = inputdict.pop(self.get_linkname('kpoints'),None)
-        except KeyError:
-            pass
-        if kpoints is not None:
-            if not isinstance(kpoints, KpointsData):
-                raise InputValidationError("kpoints is not of type KpointsData")
-        
-        ##############################
-        # END OF INITIAL INPUT CHECK #
-        ##############################
-        
         # default atom getter: I will always retrieve the total energy at least
-        default_atoms_getters = [ ["total_energy",""] ] 
-        
+        default_atoms_getters = [['total_energy', '']]
+
         # ================================
-        
+
         # save the structure in ase format
-        atoms = structure.get_ase()
-        atoms.write(tempfolder.get_abs_path(self._input_aseatoms))
-        
+        atoms = self.inputs.structure.get_ase()
+
+        with folder.open(self._input_aseatoms, 'w') as handle:
+            atoms.write(handle)
+
         # ================== prepare the arguments of functions ================
-        
-        parameters_dict = parameters.get_dict()
-        settings_dict = settings.get_dict() if settings is not None else {}
-        
+
+        parameters_dict = self.inputs.parameters.get_dict()
+
         # ==================== fix the args of the optimizer
-        
-        optimizer = parameters_dict.pop("optimizer",None)
+
+        optimizer = parameters_dict.pop('optimizer', None)
         if optimizer is not None:
             # Validation
-            if not isinstance(optimizer,dict):
-                raise InputValidationError("optimizer key must contain a dictionary")
+            if not isinstance(optimizer, dict):
+                raise common.InputValidationError('optimizer key must contain a dictionary')
             # get the name of the optimizer
-            optimizer_name = optimizer.pop("name",None)
+            optimizer_name = optimizer.pop('name', None)
             if optimizer_name is None:
-                raise InputValidationError("Don't have access to the optimizer name")
-            
+                raise common.InputValidationError("Don't have access to the optimizer name")
+
             # prepare the arguments to be passed to the optimizer class
-            optimizer_argsstr = "atoms, " + convert_the_args(optimizer.pop("args",[]))
-            
+            optimizer_argsstr = 'atoms, ' + convert_the_args(optimizer.pop('args', []))
+
             # prepare the arguments to be passed to optimizer.run()
-            optimizer_runargsstr = convert_the_args(optimizer.pop("run_args",[]))
-            
+            optimizer_runargsstr = convert_the_args(optimizer.pop('run_args', []))
+
             # prepare the import string
             optimizer_import_string = get_optimizer_impstr(optimizer_name)
-            
+
         # ================= determine the calculator name and its import ====
-        
-        calculator = parameters_dict.pop("calculator",{})
-        calculator_import_string = get_calculator_impstr(calculator.pop("name",None))
-        
+
+        calculator = parameters_dict.pop('calculator', {})
+        calculator_import_string = get_calculator_impstr(calculator.pop('name', None))
+
         # =================== prepare the arguments for the calculator call
-        
-        read_calc_args = calculator.pop("args",[])
-        #calc_args = calculator.pop("args",None)
+
+        read_calc_args = calculator.pop('args', [])
         if read_calc_args is None:
-            calc_argsstr = ""
+            calc_argsstr = ''
         else:
             # transform a in "a" if a is a string (needed for formatting)
             calc_args = {}
-            for k,v in read_calc_args.iteritems():
-                if isinstance(v, basestring):
-                    the_v = '"{}"'.format(v)
+            for k, v in read_calc_args.items():
+                if isinstance(v, str):
+                    the_v = f'"{v}"'
                 else:
                     the_v = v
                 calc_args[k] = the_v
-            
+
             def return_a_function(v):
                 try:
-                    has_magic = "@function" in v.keys()
+                    has_magic = '@function' in v.keys()
                 except AttributeError:
                     has_magic = False
-                
+
                 if has_magic:
-                    
+
                     args_dict = {}
-                    for k2,v2 in v['args'].iteritems():
-                        if isinstance(v2,basestring):
-                            the_v = '"{}"'.format(v2)
+                    for k2, v2 in v['args'].items():
+                        if isinstance(v2, str):
+                            the_v = f'"{v2}"'
                         else:
                             the_v = v2
                         args_dict[k2] = the_v
-                    
-                    v2 = "{}({})".format(v['@function'],
-                                         ", ".join(["{}={}".format(k_,v_) 
-                                            for k_,v_ in args_dict.iteritems()]))
+
+                    v2 = '{}({})'.format(
+                        v['@function'], ', '.join(['{}={}'.format(k_, v_) for k_, v_ in args_dict.items()])
+                    )
                     return v2
-                else:
-                    return v
-            
-            tmp_list = [ "{}={}".format(k,return_a_function(v)) 
-                         for k,v in calc_args.iteritems() ]
-            
-            calc_argsstr = ", ".join( tmp_list )
+                return v
+
+            tmp_list = ['{}={}'.format(k, return_a_function(v)) for k, v in calc_args.items()]
+
+            calc_argsstr = ', '.join(tmp_list)
 
             # add kpoints if present
-            if kpoints:
+            if 'kpoints' in self.inputs:
                 #TODO: here only the mesh is supported
                 # maybe kpoint lists are supported as well in ASE calculators
                 try:
-                    mesh = kpoints.get_kpoints_mesh()[0]
+                    mesh = self.inputs.kpoints.get_kpoints_mesh()[0]
                 except AttributeError:
-                    raise InputValidationError("Coudn't find a mesh of kpoints"
-                                               " in the KpointsData")
-                calc_argsstr = ", ".join( [calc_argsstr] + ["kpts=({},{},{})".format( *mesh )] )
-                        
+                    raise common.InputValidationError("Coudn't find a mesh of kpoints" ' in the KpointsData')
+                calc_argsstr = ', '.join([calc_argsstr] + ['kpts=({},{},{})'.format(*mesh)])
+
         # =============== prepare the methods of atoms.get(), to save results
-        
-        atoms_getters = default_atoms_getters + convert_the_getters( parameters_dict.pop("atoms_getters",[]) )
-        
+
+        atoms_getters = default_atoms_getters + convert_the_getters(parameters_dict.pop('atoms_getters', []))
+
         # =============== prepare the methods of calculator.get(), to save results
-        
-        calculator_getters = convert_the_getters( parameters_dict.pop("calculator_getters",[]) )
-        
+
+        calculator_getters = convert_the_getters(parameters_dict.pop('calculator_getters', []))
+
         # ===================== build the strings with the module imports
-        
-        all_imports = ["import ase", 'import ase.io', "import json",
-                       "import numpy", calculator_import_string]
-        
+
+        all_imports = ['import ase', 'import ase.io', 'import json', 'import numpy', calculator_import_string]
+
         if optimizer is not None:
             all_imports.append(optimizer_import_string)
 
         try:
-            if "PW" in calc_args['mode'].values():
-                all_imports.append("from gpaw import PW")
+            if 'PW' in calc_args['mode'].values():
+                all_imports.append('from gpaw import PW')
         except KeyError:
             pass
-        
-        extra_imports = parameters_dict.pop("extra_imports",[])
+
+        extra_imports = parameters_dict.pop('extra_imports', [])
         for i in extra_imports:
-            if isinstance(i,basestring):
-                all_imports.append("import {}".format(i))
-            elif isinstance(i,(list,tuple)):
-                if not all( [isinstance(j,basestring) for j in i] ):
-                    raise ValueError("extra import must contain strings")
-                if len(i)==2:
-                    all_imports.append("from {} import {}".format(*i))
-                elif len(i)==3:
-                    all_imports.append("from {} import {} as {}".format(*i))
+            if isinstance(i, str):
+                all_imports.append(f'import {i}')
+            elif isinstance(i, (list, tuple)):
+                if not all([isinstance(j, str) for j in i]):
+                    raise ValueError('extra import must contain strings')
+                if len(i) == 2:
+                    all_imports.append('from {} import {}'.format(*i))
+                elif len(i) == 3:
+                    all_imports.append('from {} import {} as {}'.format(*i))
                 else:
-                    raise ValueError("format for extra imports not recognized")
+                    raise ValueError('format for extra imports not recognized')
             else:
-                raise ValueError("format for extra imports not recognized")
-        
-        if self.get_withmpi():
-            all_imports.append( "from ase.parallel import paropen" )
-        
-        all_imports_string = "\n".join(all_imports) + "\n"
-        
+                raise ValueError('format for extra imports not recognized')
+
+        if self.options.withmpi:
+            all_imports.append('from ase.parallel import paropen')
+
+        all_imports_string = '\n'.join(all_imports) + '\n'
+
         # =================== prepare the python script ========================
-        
-        input_txt = ""
-        input_txt += get_file_header()
-        input_txt += "# calculation pk: {}\n".format(self.pk)
-        input_txt += "\n"
-        input_txt += all_imports_string
-        input_txt += "\n"
-        
-        pre_lines = parameters_dict.pop("pre_lines",None)
+
+        input_txt = all_imports_string
+        input_txt += '\n'
+
+        pre_lines = parameters_dict.pop('pre_lines', None)
         if pre_lines is not None:
-            if not isinstance(pre_lines,(list,tuple)):
-                raise ValueError("Prelines must be a list of strings")
-            if not all( [isinstance(_,basestring) for _ in pre_lines] ):
-                raise ValueError("Prelines must be a list of strings")
-            input_txt += "\n".join(pre_lines) + "\n\n"
-        
-        input_txt += "atoms = ase.io.read('{}')\n".format(self._input_aseatoms)
-        input_txt += "\n"
-        input_txt += "calculator = custom_calculator({})\n".format(calc_argsstr)
-        input_txt += "atoms.set_calculator(calculator)\n"
-        input_txt += "\n"
-        
+            if not isinstance(pre_lines, (list, tuple)):
+                raise ValueError('Prelines must be a list of strings')
+            if not all([isinstance(_, str) for _ in pre_lines]):
+                raise ValueError('Prelines must be a list of strings')
+            input_txt += '\n'.join(pre_lines) + '\n\n'
+
+        input_txt += f"atoms = ase.io.read('{self._input_aseatoms}')\n"
+        input_txt += '\n'
+        input_txt += f'calculator = custom_calculator({calc_argsstr})\n'
+        input_txt += 'atoms.set_calculator(calculator)\n'
+        input_txt += '\n'
+
         if optimizer is not None:
             # here block the trajectory file name: trajectory = 'aiida.traj'
-            input_txt += "optimizer = custom_optimizer({})\n".format(optimizer_argsstr)
-            input_txt += "optimizer.run({})\n".format(optimizer_runargsstr)
-            input_txt += "\n"
-        
+            input_txt += f'optimizer = custom_optimizer({optimizer_argsstr})\n'
+            input_txt += f'optimizer.run({optimizer_runargsstr})\n'
+            input_txt += '\n'
+
         # now dump / calculate the results
-        input_txt += "results = {}\n"
-        for getter,getter_args in atoms_getters:
-            input_txt += "results['{}'] = atoms.get_{}({})\n".format(getter,
-                                                                     getter,
-                                                                     getter_args)
-        input_txt += "\n"
-        
-        for getter,getter_args in calculator_getters:
-            input_txt += "results['{}'] = calculator.get_{}({})\n".format(getter,
-                                                                          getter,
-                                                                          getter_args) 
-        input_txt += "\n"
-        
+        input_txt += 'results = {}\n'
+        for getter, getter_args in atoms_getters:
+            input_txt += f"results['{getter}'] = atoms.get_{getter}({getter_args})\n"
+        input_txt += '\n'
+
+        for getter, getter_args in calculator_getters:
+            input_txt += f"results['{getter}'] = calculator.get_{getter}({getter_args})\n"
+        input_txt += '\n'
+
         # Convert to lists
-        input_txt += "for k,v in results.iteritems():\n"
-        input_txt += "    if isinstance(results[k],(numpy.matrix,numpy.ndarray)):\n"
-        input_txt += "        results[k] = results[k].tolist()\n"
-        
-        input_txt += "\n"
-        
-        post_lines = parameters_dict.pop("post_lines",None)
+        input_txt += 'for k,v in results.items():\n'
+        input_txt += '    if isinstance(results[k],(numpy.matrix,numpy.ndarray)):\n'
+        input_txt += '        results[k] = results[k].tolist()\n'
+
+        input_txt += '\n'
+
+        post_lines = parameters_dict.pop('post_lines', None)
         if post_lines is not None:
-            if not isinstance(post_lines,(list,tuple)):
-                raise ValueError("Postlines must be a list of strings")
-            if not all( [isinstance(_,basestring) for _ in post_lines] ):
-                raise ValueError("Postlines must be a list of strings")
-            input_txt += "\n".join(post_lines) + "\n\n"
-
-        # Dump results to file        
-        right_open = "paropen" if self.get_withmpi() else "open"
-        input_txt += "with {}('{}', 'w') as f:\n".format(right_open, self._OUTPUT_FILE_NAME)
-        input_txt += "    json.dump(results,f)"
-        input_txt += "\n"
-        
+            if not isinstance(post_lines, (list, tuple)):
+                raise ValueError('Postlines must be a list of strings')
+            if not all([isinstance(_, str) for _ in post_lines]):
+                raise ValueError('Postlines must be a list of strings')
+            input_txt += '\n'.join(post_lines) + '\n\n'
+
+        # Dump results to file
+        right_open = 'paropen' if self.options.withmpi else 'open'
+        input_txt += f"with {right_open}('{self._OUTPUT_FILE_NAME}', 'w') as f:\n"
+        input_txt += '    json.dump(results,f)'
+        input_txt += '\n'
+
         # Dump trajectory if present
         if optimizer is not None:
-            input_txt += "atoms.write('{}')\n".format(self._output_aseatoms)
-            input_txt += "\n"
-        
+            input_txt += f"atoms.write('{self._output_aseatoms}')\n"
+            input_txt += '\n'
+
         # write all the input script to a file
-        input_filename = tempfolder.get_abs_path(self._INPUT_FILE_NAME)
-        with open(input_filename,'w') as infile:
-            infile.write(input_txt)
-            
+        with folder.open(self._INPUT_FILE_NAME, 'w') as handle:
+            handle.write(input_txt)
+
         # ============================ calcinfo ================================
-        
+
         # TODO: look at the qmmm infoL: it might be necessary to put
         # some singlefiles in the directory.
         # right now it has to be taken care in the pre_lines
-        
+
         local_copy_list = []
-        remote_copy_list = [] 
-        additional_retrieve_list = settings_dict.pop("ADDITIONAL_RETRIEVE_LIST",[])
-        
-        calcinfo = CalcInfo()
-        
+        remote_copy_list = []
+        additional_retrieve_list = settings.pop('ADDITIONAL_RETRIEVE_LIST', [])
+
+        calcinfo = common.CalcInfo()
+
         calcinfo.uuid = self.uuid
         # Empty command line by default
-        # calcinfo.cmdline_params = settings_dict.pop('CMDLINE', [])
+        # calcinfo.cmdline_params = settings.pop('CMDLINE', [])
         calcinfo.local_copy_list = local_copy_list
         calcinfo.remote_copy_list = remote_copy_list
-        
-        codeinfo = CodeInfo()
+
+        codeinfo = common.CodeInfo()
         codeinfo.cmdline_params = [self._INPUT_FILE_NAME]
         #calcinfo.stdin_name = self._INPUT_FILE_NAME
         codeinfo.stdout_name = self._TXT_OUTPUT_FILE_NAME
-        codeinfo.code_uuid = code.uuid
+        codeinfo.code_uuid = self.inputs.code.uuid
         calcinfo.codes_info = [codeinfo]
-        
+
         # Retrieve files
         calcinfo.retrieve_list = []
-        calcinfo.retrieve_list.append(self._OUTPUT_FILE_NAME)
+        calcinfo.retrieve_list.append(self.options.output_filename)
         calcinfo.retrieve_list.append(self._output_aseatoms)
         calcinfo.retrieve_list += additional_retrieve_list
-        
+
         # TODO: I should have two ways of running it: with gpaw-python in parallel
         # and executing python if in serial
-        
+
         return calcinfo
 
+
 def get_calculator_impstr(calculator_name):
     """
     Returns the import string for the calculator
     """
-    if calculator_name.lower() == "gpaw" or calculator_name is None:
-        return "from gpaw import GPAW as custom_calculator"
-    elif calculator_name.lower() == "espresso":
-        return "from espresso import espresso as custom_calculator"
-    else:
-        possibilities = {"abinit":"abinit.Abinit",
-                         "aims":"aims.Aims",
-                         "ase_qmmm_manyqm":"AseQmmmManyqm",
-                         "castep":"Castep",
-                         "dacapo":"Dacapo",
-                         "dftb":"Dftb",
-                         "eam":"EAM",
-                         "elk":"ELK",
-                         "emt":"EMT",
-                         "exciting":"Exciting",
-                         "fleur":"FLEUR",
-                         "gaussian":"Gaussian",
-                         "gromacs":"Gromacs",
-                         "mopac":"Mopac",
-                         "morse":"MorsePotential",
-                         "nwchem":"NWChem",
-                         'siesta':"Siesta",
-                         "tip3p":"TIP3P",
-                         "turbomole":"Turbomole",
-                         "vasp":"Vasp",
-                         }
-        
-        current_val = possibilities.get(calculator_name.lower())
-        
-        package, class_name = (calculator_name,current_val) if current_val else calculator_name.rsplit('.',1)
-        
-        return "from ase.calculators.{} import {} as custom_calculator".format(package, class_name)
+    if calculator_name is None or calculator_name.lower() == 'gpaw':
+        return 'from gpaw import GPAW as custom_calculator'
+
+    if calculator_name.lower() == 'espresso':
+        return 'from espresso import espresso as custom_calculator'
+
+    possibilities = {
+        'abinit': 'abinit.Abinit',
+        'aims': 'aims.Aims',
+        'ase_qmmm_manyqm': 'AseQmmmManyqm',
+        'castep': 'Castep',
+        'dacapo': 'Dacapo',
+        'dftb': 'Dftb',
+        'eam': 'EAM',
+        'elk': 'ELK',
+        'emt': 'EMT',
+        'exciting': 'Exciting',
+        'fleur': 'FLEUR',
+        'gaussian': 'Gaussian',
+        'gromacs': 'Gromacs',
+        'mopac': 'Mopac',
+        'morse': 'MorsePotential',
+        'nwchem': 'NWChem',
+        'siesta': 'Siesta',
+        'tip3p': 'TIP3P',
+        'turbomole': 'Turbomole',
+        'vasp': 'Vasp',
+    }
+
+    current_val = possibilities.get(calculator_name.lower())
+
+    package, class_name = (calculator_name, current_val) if current_val else calculator_name.rsplit('.', 1)
+
+    return f'from ase.calculators.{package} import {class_name} as custom_calculator'
+
 
 def get_optimizer_impstr(optimizer_name):
     """
     Returns the import string for the optimizer
     """
-    possibilities = {"bfgs":"BFGS",
-                     "bfgslinesearch":"BFGSLineSearch",
-                     "fire":"FIRE",
-                     "goodoldquasinewton":"GoodOldQuasiNewton",
-                     "hesslbfgs":"HessLBFGS",
-                     "lbfgs":"LBFGS",
-                     "lbfgslinesearch":"LBFGSLineSearch",
-                     "linelbfgs":"LineLBFGS",
-                     "mdmin":"MDMin",
-                     "ndpoly":"NDPoly",
-                     "quasinewton":"QuasiNewton",
-                     "scipyfmin":"SciPyFmin",
-                     "scipyfminbfgs":"SciPyFminBFGS",
-                     "scipyfmincg":"SciPyFminCG",
-                     "scipyfminpowell":"SciPyFminPowell",
-                     "scipygradientlessoptimizer":"SciPyGradientlessOptimizer",
-                     }
-    
+    possibilities = {
+        'bfgs': 'BFGS',
+        'bfgslinesearch': 'BFGSLineSearch',
+        'fire': 'FIRE',
+        'goodoldquasinewton': 'GoodOldQuasiNewton',
+        'hesslbfgs': 'HessLBFGS',
+        'lbfgs': 'LBFGS',
+        'lbfgslinesearch': 'LBFGSLineSearch',
+        'linelbfgs': 'LineLBFGS',
+        'mdmin': 'MDMin',
+        'ndpoly': 'NDPoly',
+        'quasinewton': 'QuasiNewton',
+        'scipyfmin': 'SciPyFmin',
+        'scipyfminbfgs': 'SciPyFminBFGS',
+        'scipyfmincg': 'SciPyFminCG',
+        'scipyfminpowell': 'SciPyFminPowell',
+        'scipygradientlessoptimizer': 'SciPyGradientlessOptimizer',
+    }
+
     current_val = possibilities.get(optimizer_name.lower())
-    
+
     if current_val:
-        return "from ase.optimize import {} as custom_optimizer".format(current_val)
-    else:
-        package,current_val = optimizer_name.rsplit('.',1)
-        return "from ase.optimize.{} import {} as custom_optimizer".format(package,current_val)
-        
+        return f'from ase.optimize import {current_val} as custom_optimizer'
+
+    package, current_val = optimizer_name.rsplit('.', 1)
+    return f'from ase.optimize.{package} import {current_val} as custom_optimizer'
+
+
 def convert_the_getters(getters):
     """
     A function used to prepare the arguments of calculator and atoms getter methods
     """
     return_list = []
     for getter in getters:
-        
-        if isinstance(getter,basestring):
-            out_args = ""
+
+        if isinstance(getter, str):
+            out_args = ''
             method_name = getter
-            
+
         else:
             method_name, a = getter
-            
+
             out_args = convert_the_args(a)
-            
-        return_list.append( (method_name, out_args) )
+
+        return_list.append((method_name, out_args))
+
     return return_list
 
+
 def convert_the_args(raw_args):
     """
     Function used to convert the arguments of methods
     """
     if not raw_args:
-        return ""
-    if isinstance(raw_args,dict):
-        out_args = ", ".join([ "{}={}".format(k,v) for k,v in raw_args.iteritems() ])
-        
-    elif isinstance(raw_args,(list,tuple)):
+        return ''
+
+    if isinstance(raw_args, dict):
+        out_args = ', '.join([f'{k}={v}' for k, v in raw_args.items()])
+
+    elif isinstance(raw_args, (list, tuple)):
         new_list = []
         for x in raw_args:
-            if isinstance(x,basestring):
+            if isinstance(x, str):
                 new_list.append(x)
-            elif isinstance(x,dict):
-                new_list.append( ", ".join([ "{}={}".format(k,v) for k,v in x.iteritems() ]) )
+            elif isinstance(x, dict):
+                new_list.append(', '.join([f'{k}={v}' for k, v in x.items()]))
             else:
-                raise ValueError("Error preparing the getters")
-        out_args = ", ".join(new_list)
+                raise ValueError('Error preparing the getters')
+        out_args = ', '.join(new_list)
     else:
         raise ValueError("Couldn't recognize list of getters")
     return out_args
diff --git a/aiida_ase/parsers/ase.py b/aiida_ase/parsers/ase.py
index 0832992..bf0cb8e 100644
--- a/aiida_ase/parsers/ase.py
+++ b/aiida_ase/parsers/ase.py
@@ -1,113 +1,67 @@
 # -*- coding: utf-8 -*-
-from aiida_ase.calculations.ase import AseCalculation
-from aiida.orm.data.folder import FolderData
-from aiida.parsers.parser import Parser
-from aiida.common.datastructures import calc_states
-from aiida.parsers.exceptions import OutputParsingError
-from aiida.common.exceptions import UniquenessError
-import numpy
-from aiida.orm.data.array import ArrayData
-from aiida.orm.data.parameter import ParameterData
-from aiida.orm.data.structure import StructureData
+"""Parser implementation for the ``AseCalculation``."""
 import json
+import numpy
 
+from aiida import parsers
+from aiida import plugins
+from ase.io import read
 
-class AseParser(Parser):
-    """
-    This class is the implementation of the Parser class for Ase calculators.
-    """
-
-    _outarray_name = 'output_array'
-    _outdict_name = 'output_parameters'
-    _outstruc_name = 'output_structure'
-
-    def __init__(self,calculation):
-        """
-        Initialize the instance of AseParser
-        """
-        # check for valid input
-        if not isinstance(calculation,AseCalculation):
-            raise OutputParsingError("Input calculation must be a AseCalculation")
-        self._calc = calculation
-
-    def parse_from_calc(self):
-        """
-        Parses the datafolder, stores results.
-        This parser for this simple code does simply store in the DB a node
-        representing the file of forces in real space
-        """
-        from aiida.common.exceptions import InvalidOperation
-        from aiida.common import aiidalogger
-        from aiida.backends.djsite.utils import get_dblogger_extra
-
-        import ase, ase.io
-
-        parserlogger = aiidalogger.getChild('aseparser')
-        logger_extra = get_dblogger_extra(self._calc)
+Dict = plugins.DataFactory('dict')
+ArrayData = plugins.DataFactory('array')
+StructureData = plugins.DataFactory('structure')
+AseCalculation = plugins.CalculationFactory('ase.ase')
 
-        # suppose at the start that the job is successful
-        successful = True
 
-        # check that calculation is in the right state
-        state = self._calc.get_state()
-        if state != calc_states.PARSING:
-            raise InvalidOperation("Calculation not in {} state"
-                                   .format(calc_states.PARSING) )
+class AseParser(parsers.Parser):
+    """Parser implementation for the ``AseCalculation``."""
 
-        # select the folder object
-        out_folder = self._calc.get_retrieved_node()
+    def parse(self, **kwargs):  # pylint: disable=inconsistent-return-statements
+        """Parse the retrieved files from a ``AseCalculation``."""
+        retrieved = self.retrieved
 
         # check what is inside the folder
-        list_of_files = out_folder.get_folder_list()
+        list_of_files = retrieved.list_object_names()
 
         # at least the stdout should exist
-        if not self._calc._OUTPUT_FILE_NAME in list_of_files:
-            successful = False
-            parserlogger.error("Standard output not found",extra=logger_extra)
-            return successful,()
+        if AseCalculation._OUTPUT_FILE_NAME not in list_of_files:  # pylint: disable=protected-access
+            self.logger.error('Standard output not found')
+            return self.exit_codes.ERROR_OUTPUT_FILES
 
         # output structure
-        has_out_atoms = True if self._calc._output_aseatoms in list_of_files else False
-        if has_out_atoms:
-            out_atoms = ase.io.read( out_folder.get_abs_path( self._calc._output_aseatoms ) )
-            out_structure = StructureData().set_ase(out_atoms)
+        if AseCalculation._output_aseatoms in list_of_files:  # pylint: disable=protected-access
+            with retrieved.open(AseCalculation._output_aseatoms, 'r') as handle:  # pylint: disable=protected-access
+                atoms = read(handle, format='json')
+                structure = StructureData(ase=atoms)
+                self.out('structure', structure)
+
+        filename_stdout = self.node.get_attribute('output_filename')
 
         # load the results dictionary
-        json_outfile = out_folder.get_abs_path( self._calc._OUTPUT_FILE_NAME )
-        with open(json_outfile,'r') as f:
-            json_params = json.load(f)
+        with retrieved.open(filename_stdout, 'r') as handle:
+            json_params = json.load(handle)
 
         # extract arrays from json_params
         dictionary_array = {}
-        for k,v in list(json_params.iteritems()):
-            if isinstance(v, (list,tuple)):
+        for k, v in list(json_params.items()):
+            if isinstance(v, (list, tuple)):
                 dictionary_array[k] = json_params.pop(k)
 
         # look at warnings
         warnings = []
-        with open(out_folder.get_abs_path( self._calc._SCHED_ERROR_FILE )) as f:
-            errors = f.read()
+        with retrieved.open('_scheduler-stderr.txt', 'r') as handle:
+            errors = handle.read()
         if errors:
             warnings = [errors]
         json_params['warnings'] = warnings
 
-        # save the outputs
-        new_nodes_list= []
-
-        # save the arrays
         if dictionary_array:
             array_data = ArrayData()
-            for k,v in dictionary_array.iteritems():
-                array_data.set_array(k,numpy.array(v))
-            new_nodes_list.append( (self._outarray_name, array_data) )
+            for k, v in dictionary_array.items():
+                array_data.set_array(k, numpy.array(v))
+            self.out('array', array_data)
 
-        # save the parameters
         if json_params:
-            parameter_data = ParameterData( dict=json_params )
-            new_nodes_list.append( (self._outdict_name, parameter_data) )
-
-        if has_out_atoms:
-            structure_data = StructureData()
-            new_nodes_list.append( (self._outstruc_name, structure_data) )
+            self.out('parameters', Dict(dict=json_params))
 
-        return successful,new_nodes_list
\ No newline at end of file
+        return
diff --git a/docs/.gitignore b/docs/.gitignore
index d163863..567609b 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -1,1 +1,1 @@
-build/
\ No newline at end of file
+build/
diff --git a/docs/Makefile b/docs/Makefile
index 7eb2e53..218b9cf 100755
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -21,7 +21,7 @@ I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
 
 .PHONY: all help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext customdefault
 
-## Runs with nitty-pick and converting warnings into errors to 
+## Runs with nitty-pick and converting warnings into errors to
 ## make sure the documentation is properly written
 customdefault:
 	$(SPHINXBUILD) -b html -nW $(ALLSPHINXOPTS) $(BUILDDIR)/html
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8aba271..e6bbbc7 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Template file provided by the aiida-plugin-template; modify 
+# Template file provided by the aiida-plugin-template; modify
 # according to your needs
 #
 # This file is execfile()d with the current directory set to its
@@ -59,11 +59,11 @@
 # General information about the project.
 project = u'aiida-ase'
 copyright_first_year = 2014
-copyright_owners = "ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE (Theory and Simulation of Materials (THEOS) and National Centre for Computational Design and Discovery of Novel Materials (NCCR MARVEL)), Switzerland"
+copyright_owners = 'ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE (Theory and Simulation of Materials (THEOS) and National Centre for Computational Design and Discovery of Novel Materials (NCCR MARVEL)), Switzerland'
 
 current_year = time.localtime().tm_year
-copyright_year_string = current_year if current_year == copyright_first_year else "{}-{}".format(copyright_first_year, current_year)
-copyright = u'{}, {}. All rights reserved'.format(copyright_year_string, copyright_owners)
+copyright_year_string = current_year if current_year == copyright_first_year else f'{copyright_first_year}-{current_year}'
+copyright = f'{copyright_year_string}, {copyright_owners}. All rights reserved'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -116,7 +116,6 @@
 # If true, keep warnings as "system message" paragraphs in the built documents.
 #keep_warnings = False
 
-
 # -- Options for HTML output ----------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
@@ -128,9 +127,9 @@
 # further.  For a list of options available for each theme, see the
 # documentation.
 #~ html_theme_options = {
-  #~ 'inner_theme': True,
-  #~ 'inner_theme_name': 'bootswatch-darkly',
-  #~ 'nav_fixed_top': False
+#~ 'inner_theme': True,
+#~ 'inner_theme_name': 'bootswatch-darkly',
+#~ 'nav_fixed_top': False
 #~ }
 
 # Add any paths that contain custom themes here, relative to this directory.
@@ -223,17 +222,17 @@
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
+    # The paper size ('letterpaper' or 'a4paper').
+    #'papersize': 'letterpaper',
 
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
+    # The font size ('10pt', '11pt' or '12pt').
+    #'pointsize': '10pt',
 
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # Additional stuff for the LaTeX preamble.
+    #'preamble': '',
 
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+    # Latex figure (float) alignment
+    #'figure_align': 'htbp',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
@@ -262,7 +261,6 @@
 # If false, no module index is generated.
 #latex_domain_indices = True
 
-
 # -- Options for manual page output ---------------------------------------
 
 # One entry per manual page. List of tuples
@@ -273,7 +271,6 @@
 # If true, show URL addresses after external links.
 #man_show_urls = False
 
-
 # -- Options for Texinfo output -------------------------------------------
 
 # Grouping the document tree into Texinfo files. List of tuples
@@ -294,17 +291,14 @@
 # If true, do not generate a @detailmenu in the "Top" node's menu.
 #texinfo_no_detailmenu = False
 
-
 ## BEFORE STARTING, LET'S LOAD THE CORRECT AIIDA DBENV
 # on_rtd is whether we are on readthedocs.org, this line of code grabbed
 # from docs.readthedocs.org
 # NOTE: it is needed to have these lines before load_dbenv()
 on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
 
-sys.path.append( os.path.join( os.path.split(__file__)[0],
-                                           os.pardir,os.pardir) )
-sys.path.append( os.path.join( os.path.split(__file__)[0],
-                                           os.pardir))
+sys.path.append(os.path.join(os.path.split(__file__)[0], os.pardir, os.pardir))
+sys.path.append(os.path.join(os.path.split(__file__)[0], os.pardir))
 
 os.environ['DJANGO_SETTINGS_MODULE'] = 'rtd_settings'
 
@@ -326,11 +320,8 @@
     # we don't want to create a profile there
     from aiida.backends import settings
     settings.IN_DOC_MODE = True
-    settings.BACKEND = "django"
-    settings.AIIDADB_PROFILE = "default"
-
-
-
+    settings.BACKEND = 'django'
+    settings.AIIDADB_PROFILE = 'default'
 
 # Warnings to ignore when using the -n (nitpicky) option
 # We should ignore any python built-in exception, for instance
diff --git a/docs/source/index.rst b/docs/source/index.rst
index c006162..d1d9ea8 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -14,18 +14,18 @@ Description
 
 The plugin is available at http://github.com/aiidateam/aiida-ase
 
-`ASE`_ (Atomic Simulation Environment) is a set of tools and Python modules for 
-setting up, manipulating, running, visualizing and analyzing atomistic 
-simulations. The ASE code is freely available under the GNU LGPL license (the 
+`ASE`_ (Atomic Simulation Environment) is a set of tools and Python modules for
+setting up, manipulating, running, visualizing and analyzing atomistic
+simulations. The ASE code is freely available under the GNU LGPL license (the
 ASE installation guide and the source can be found `here`_).
 
-Besides the manipulation of structures (``Atoms`` objects), one can attach 
+Besides the manipulation of structures (``Atoms`` objects), one can attach
 ``calculators`` to a structure and run it to compute, as an example, energies or
 forces.
 Multiple calculators are currently supported by ASE, like GPAW, Vasp, Abinit and
 many others.
 
-In AiiDA, we have developed a plugin which currently supports the use of ASE 
+In AiiDA, we have developed a plugin which currently supports the use of ASE
 calculators for total energy calculations and structure optimizations.
 
 .. _here: http://wiki.fysik.dtu.dk/ase/
@@ -61,4 +61,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/docs/source/module_guide/calculations.rst b/docs/source/module_guide/calculations.rst
index 0e8e0ac..99f3fe9 100644
--- a/docs/source/module_guide/calculations.rst
+++ b/docs/source/module_guide/calculations.rst
@@ -9,4 +9,3 @@ ASE plugin
 
 .. automodule:: aiida_ase.calculations.ase
    :members:
-
diff --git a/docs/source/module_guide/index.rst b/docs/source/module_guide/index.rst
index 09c0d48..d3f82ff 100644
--- a/docs/source/module_guide/index.rst
+++ b/docs/source/module_guide/index.rst
@@ -7,4 +7,3 @@ Modules provided with ASE plugin (API reference)
 
     calculations
     parsers
-
diff --git a/docs/source/module_guide/parsers.rst b/docs/source/module_guide/parsers.rst
index 247ae39..5308bc2 100644
--- a/docs/source/module_guide/parsers.rst
+++ b/docs/source/module_guide/parsers.rst
@@ -9,4 +9,3 @@ ASE parser plugin
 
 .. automodule:: aiida_ase.parsers.ase
    :members:
-
diff --git a/docs/source/user_guide/ase.rst b/docs/source/user_guide/ase.rst
index fbff9bd..ce22ebd 100644
--- a/docs/source/user_guide/ase.rst
+++ b/docs/source/user_guide/ase.rst
@@ -9,40 +9,40 @@ Requires the installation of ASE on the computer where AiiDA is running.
 
 Supported codes
 ---------------
-* tested on ASE v3.8.1 and on GPAW v0.10.0. 
+* tested on ASE v3.8.1 and on GPAW v0.10.0.
   ASE back compatibility is not guaranteed.
   Calculators different from GPAW should work, if they follow the interface
   description of ASE calculators, but have not been tested.
   Usage requires the installation of both ASE and of the software used by the
   calculator.
-  
+
 Inputs
 ------
 
 * **kpoints**, class :py:class:`KpointsData <aiida.orm.data.array.kpoints.KpointsData>` (optional)
-  Reciprocal space points on which to build the wavefunctions. Only kpoints 
+  Reciprocal space points on which to build the wavefunctions. Only kpoints
   meshes are currently supported.
 
 * **parameters**, class :py:class:`ParameterData <aiida.orm.data.parameter.ParameterData>`
   Input parameters that defines the calculations to be performed, and their
-  parameters. 
+  parameters.
   See the ASE documentation for more details.
-     
+
 * **structure**, class :py:class:`StructureData <aiida.orm.data.structure.StructureData>`
 
 * **settings**, class :py:class:`ParameterData <aiida.orm.data.parameter.ParameterData>` (optional)
   An optional dictionary that activates non-default operations. Possible values are:
-    
-    *  **'CMDLINE'**: list of strings. parameters to be put after the executable and before the input file. 
+
+    *  **'CMDLINE'**: list of strings. parameters to be put after the executable and before the input file.
        Example: ["-npool","4"] will produce `gpaw -npool 4 < aiida_input`
     *  **'ADDITIONAL_RETRIEVE_LIST'**: list of strings. Specify additional files to be retrieved.
-       By default, the output file and the xml file are already retrieved. 
+       By default, the output file and the xml file are already retrieved.
 
 Outputs
 -------
 Actual output production depends on the input provided.
 
-* output_parameters :py:class:`ParameterData <aiida.orm.data.parameter.ParameterData>` 
+* output_parameters :py:class:`ParameterData <aiida.orm.data.parameter.ParameterData>`
   (accessed by ``calculation.res``)
   Contains the scalar properties. Example: energy (in eV) or
   warnings (possible error messages generated in the run).
@@ -55,40 +55,40 @@ Actual output production depends on the input provided.
 
 Errors
 ------
-Errors of the parsing are reported in the log of the calculation (accessible 
-with the ``verdi calculation logshow`` command). 
+Errors of the parsing are reported in the log of the calculation (accessible
+with the ``verdi calculation logshow`` command).
 Moreover, they are stored in the ParameterData under the key ``warnings``, and are
 accessible with ``Calculation.res.warnings``.
 
 Examples
 --------
-The following example briefly describe the usage of GPAW within AiiDA, assuming 
+The following example briefly describe the usage of GPAW within AiiDA, assuming
 that both ASE and GPAW have been installed on the remote machine.
-Note that ASE calculators, at times, require the definition of environment 
+Note that ASE calculators, at times, require the definition of environment
 variables. Take your time to find them and make sure that they are loaded by the
 submission script of AiiDA (use the prepend text fields of a Code, for example).
- 
-First of all install the AiiDA Code as usual, noting that, if you plan to use 
+
+First of all install the AiiDA Code as usual, noting that, if you plan to use
 the serial version of GPAW (applies to all other calculators) the remote absolute
-path of the code has to point to the python executable (i.e. the output of 
+path of the code has to point to the python executable (i.e. the output of
 ``which python`` on the remote machine, typically it might be ``/usr/bin/python``).
 If the parallel version of GPAW is used, set instead the path to gpaw-python.
 
 To understand the plugin, it is probably easier to try to run one test, to see
 the python script which is produced and executed on the remote machine.
-We describe in the following some example script, which can be called through 
-the ``verdi run`` command (example: ``verdi run test_script.py``). You should 
+We describe in the following some example script, which can be called through
+the ``verdi run`` command (example: ``verdi run test_script.py``). You should
 see a folder ``submit_test`` created in the location from which you run
-the command. Here there is the input script that is going to be executed in 
+the command. Here there is the input script that is going to be executed in
 the remote machine, with the syntax of the ASE software.
 
-In :download:`this first example script <test_gpaw_1.py>` and execute it with 
+In :download:`this first example script <test_gpaw_1.py>` and execute it with
 the ``verdi run`` command.
-This is a minimal script that uses GPAW and a plane-wave basis to compute the 
+This is a minimal script that uses GPAW and a plane-wave basis to compute the
 total energy of a structure.
-Note that for a serial calculation, it is necessary to run the 
+Note that for a serial calculation, it is necessary to run the
 ``calculation.set_withmpi(False)`` method.
-Note also, that by default, only the total energy of the structure is computed 
+Note also, that by default, only the total energy of the structure is computed
 and retrieved.
 
 :download:`This second example <test_gpaw_2.py>` instead shows a demo of all
@@ -96,17 +96,14 @@ possible options supported by the current plugin.
 By specifying an optimizer key in the dictionary, the ASE optimizers are run.
 In the example, the QuasiNewton algorithm is run to minimize the forces and find
 the equilibrium structures.
-By specifying the key "calculator_getters", the code will get from the 
-calculator, the properties which are specified in the value, using the get 
-method of the calculator; similar applies for the ``atoms_getters``, which will 
-call the ``atoms.get`` method. 
-``extra_lines`` and ``post_lines`` are used to insert python commands that are 
+By specifying the key "calculator_getters", the code will get from the
+calculator, the properties which are specified in the value, using the get
+method of the calculator; similar applies for the ``atoms_getters``, which will
+call the ``atoms.get`` method.
+``extra_lines`` and ``post_lines`` are used to insert python commands that are
 executed before or after the call to the calculators.
 ``extra_imports`` is used to specify the import of more modules.
 
-Lastly, :download:`this script <test_gpaw_parallel.py>` is an example of how to 
-run GPAW parallel. Essentially, nothing has to be changed in input, except that 
+Lastly, :download:`this script <test_gpaw_parallel.py>` is an example of how to
+run GPAW parallel. Essentially, nothing has to be changed in input, except that
 there is no need to call the method ``calculation.set_withmpi(False)``.
-
-
-
diff --git a/docs/source/user_guide/index.rst b/docs/source/user_guide/index.rst
index 1269fbe..43a7255 100644
--- a/docs/source/user_guide/index.rst
+++ b/docs/source/user_guide/index.rst
@@ -6,4 +6,3 @@ User's guide
     :maxdepth: 3
 
     ase
-
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..fdb3789
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,33 @@
+[build-system]
+requires = ['setuptools>=40.8.0', 'wheel',]
+build-backend = 'setuptools.build_meta'
+
+[tool.pylint.basic]
+good-names = ['pk']
+
+[tool.pylint.format]
+max-line-length = 120
+
+[tool.pylint.messages_control]
+disable = [
+    'attribute-defined-outside-init',
+    'bad-continuation',
+    'duplicate-code',
+    'fixme',
+    'import-outside-toplevel',
+    'invalid-name',
+    'raise-missing-from',
+    'too-few-public-methods',
+    'too-many-ancestors',
+    'too-many-arguments',
+    'use-a-generator',
+]
+
+[tool.yapf]
+align_closing_bracket_with_visual_indent = true
+based_on_style = 'google'
+coalesce_brackets = true
+column_limit = 120
+dedent_closing_brackets = true
+indent_dictionary_value = false
+split_arguments_when_comma_terminated = true
diff --git a/setup.json b/setup.json
index ed51a66..039e94c 100644
--- a/setup.json
+++ b/setup.json
@@ -2,9 +2,15 @@
     "author": "The AiiDA team",
     "author_email": "developers@aiida.net",
     "classifiers": [
+        "Development Status :: 4 - Beta",
+        "Framework :: AiiDA",
         "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 2.7",
-        "Development Status :: 4 - Beta"
+        "Operating System :: POSIX :: Linux",
+        "Operating System :: MacOS :: MacOS X",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9"
     ],
     "description": "The official AiiDA plugin for ASE",
     "entry_points": {
@@ -15,21 +21,28 @@
             "ase.ase = aiida_ase.parsers.ase:AseParser"
         ]
     },
+    "python_requires": ">=3.6",
+    "install_requires": [
+        "aiida-core[atomic_tools]~=1.0"
+    ],
     "extras_require": {
-        "dev_precommit": [
-            "pre-commit"
-        ],
         "docs": [
-            "Sphinx",
             "docutils",
+            "sphinx",
             "sphinx_rtd_theme"
+        ],
+        "pre-commit": [
+            "pre-commit",
+            "pylint"
+        ],
+        "tests": [
+            "pgtest",
+            "pytest",
+            "pytest-regressions"
         ]
     },
-    "install_requires": [
-        "aiida-core>=0.10.0rc1"
-    ],
     "license": "MIT License",
-    "name": "aiida_ase",
+    "name": "aiida-ase",
     "url": "https://github.com/aiidateam/aiida-ase",
-    "version": "1.0.1"
-}
\ No newline at end of file
+    "version": "2.0.0a1"
+}
diff --git a/setup.py b/setup.py
index ccab773..6770865 100644
--- a/setup.py
+++ b/setup.py
@@ -1,15 +1,34 @@
-import json
-from setuptools import setup, find_packages
+# -*- coding: utf-8 -*-
+"""Define the setup for the `aiida-ase` plugin."""
+try:
+    import fastentrypoints  # pylint: disable=unused-import
+except ImportError:
+    # This should only occur when building the package, i.e. for `python setup.py sdist/bdist_wheel`
+    pass
+
+
+def setup_package():
+    """Install the `aiida-ase` package."""
+    import json
+    from setuptools import setup, find_packages
+
+    filename_setup_json = 'setup.json'
+    filename_description = 'README.md'
+
+    with open(filename_setup_json, 'r') as handle:
+        setup_json = json.load(handle)
+
+    with open(filename_description, 'r') as handle:
+        description = handle.read()
 
-if __name__ == '__main__':
-    with open('setup.json', 'r') as handle:
-        kwargs = json.load(handle)
     setup(
         include_package_data=True,
         packages=find_packages(),
-        setup_requires=[
-            'reentry'
-        ],
-        reentry_register=True,
-        **kwargs
-    )
\ No newline at end of file
+        long_description=description,
+        long_description_content_type='text/markdown',
+        **setup_json
+    )
+
+
+if __name__ == '__main__':
+    setup_package()


Test patch:
diff --git a/docs/source/user_guide/test_gpaw_1.py b/docs/source/user_guide/test_gpaw_1.py
index b7ac9cc..43a71f1 100644
--- a/docs/source/user_guide/test_gpaw_1.py
+++ b/docs/source/user_guide/test_gpaw_1.py
@@ -24,37 +24,58 @@
 settings = None
 
 code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
-    
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
+
 # BaTiO3 cubic structure
 s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ba'])
-s.append_atom(position=(alat/2.,alat/2.,alat/2.),symbols=['Ti'])
-s.append_atom(position=(alat/2.,alat/2.,0.),symbols=['O'])
-s.append_atom(position=(alat/2.,0.,alat/2.),symbols=['O'])
-s.append_atom(position=(0.,alat/2.,alat/2.),symbols=['O'])
+s.append_atom(position=(0., 0., 0.), symbols=['Ba'])
+s.append_atom(position=(alat / 2., alat / 2., alat / 2.), symbols=['Ti'])
+s.append_atom(position=(alat / 2., alat / 2., 0.), symbols=['O'])
+s.append_atom(position=(alat / 2., 0., alat / 2.), symbols=['O'])
+s.append_atom(position=(0., alat / 2., alat / 2.), symbols=['O'])
 
 kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
+kpoints.set_kpoints_mesh([2, 2, 2])
 
 parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300}
-                         }}},
-          }
-    )
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    }
+                }
+            }
+        },
+    }
+)
 
 calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
 calc.set_withmpi(False)
 
 calc.use_structure(s)
@@ -66,17 +87,10 @@
 
 if submit_test:
     subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
 else:
     calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
     calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/docs/source/user_guide/test_gpaw_2.py b/docs/source/user_guide/test_gpaw_2.py
index f5faaf9..63f45b9 100644
--- a/docs/source/user_guide/test_gpaw_2.py
+++ b/docs/source/user_guide/test_gpaw_2.py
@@ -25,62 +25,92 @@
 settings = None
 
 code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
 
 # a molecule of Ti and O
 s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ti'])
-s.append_atom(position=(1.4, 0., 0.),symbols=['O'])
+s.append_atom(position=(0., 0., 0.), symbols=['Ti'])
+s.append_atom(position=(1.4, 0., 0.), symbols=['O'])
 
 kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
+kpoints.set_kpoints_mesh([2, 2, 2])
 
 parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300},
-                                     }}}, # Translates to "calculator = GPAW(mode=PW(ecut=300))"
-          'atoms_getters':["temperature", # translate to "atoms.get_temperature()"
-                           ["forces",{'apply_constraint':True}], # translate to "atoms.get_forces(apply_constraint=True)"
-                           ["masses",{}], # translate to "atoms.get_masses()"
-                           ],
-          'calculator_getters':[["potential_energy",{}], # translate to "calculator.get_potential_energy()" 
-                                "spin_polarized", # translate to "calculator.get_spin_polarized()"
-                                ["stress",['atoms']], # translate to "calculator.get_stress(atoms)"
-                                #["orbital_dos",['atoms', {'spin':0}] ], # this syntax would translate to 
-                                                                         # "calculator.get_orbital_dos(atoms,spin=0)"
-                                ],
-          'optimizer':{'name':'QuasiNewton',    # optimizer = QuasiNewton(alpha=0.9)
-                       "args": {'alpha':0.9},
-                       'run_args':{"fmax":0.05} # optimizer.run(fmax=0.05)
-                       },
-          
-          "pre_lines":["# This is a set",         # a set of lines to be placed in the executed script
-                       "# of various pre-lines"], # right after the imports of modules, at the beginning
-          
-          "post_lines":["# This is a set",
-                        "# of various post-lines"], # a set of lines to be placed in the executed script
-                                                    # after the call to optimizers and calculators, before the storage of results
-          
-          "extra_imports":["os", # translate to "import os"
-                           ["numpy","array"], # translate to "from numpy import array"
-                           ["numpy","array","ar"], # translate to "from numpy import array as ar"
-                           ],
-          }
-    )
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    },
+                }
+            }
+        },  # Translates to "calculator = GPAW(mode=PW(ecut=300))"
+        'atoms_getters': [
+            'temperature',  # translate to "atoms.get_temperature()"
+            ['forces', {
+                'apply_constraint': True
+            }],  # translate to "atoms.get_forces(apply_constraint=True)"
+            ['masses', {}],  # translate to "atoms.get_masses()"
+        ],
+        'calculator_getters': [
+            ['potential_energy', {}],  # translate to "calculator.get_potential_energy()"
+            'spin_polarized',  # translate to "calculator.get_spin_polarized()"
+            ['stress', ['atoms']],  # translate to "calculator.get_stress(atoms)"
+            #["orbital_dos",['atoms', {'spin':0}] ], # this syntax would translate to
+            # "calculator.get_orbital_dos(atoms,spin=0)"
+        ],
+        'optimizer': {
+            'name': 'QuasiNewton',  # optimizer = QuasiNewton(alpha=0.9)
+            'args': {
+                'alpha': 0.9
+            },
+            'run_args': {
+                'fmax': 0.05
+            }  # optimizer.run(fmax=0.05)
+        },
+        'pre_lines': [
+            '# This is a set',  # a set of lines to be placed in the executed script
+            '# of various pre-lines'
+        ],  # right after the imports of modules, at the beginning
+        'post_lines': ['# This is a set', '# of various post-lines'
+                       ],  # a set of lines to be placed in the executed script
+        # after the call to optimizers and calculators, before the storage of results
+        'extra_imports': [
+            'os',  # translate to "import os"
+            ['numpy', 'array'],  # translate to "from numpy import array"
+            ['numpy', 'array', 'ar'],  # translate to "from numpy import array as ar"
+        ],
+    }
+)
 
-settings = ParameterData(dict={"ADDITIONAL_RETRIEVE_LIST":["an_extra_file.txt"]})
+settings = ParameterData(dict={'ADDITIONAL_RETRIEVE_LIST': ['an_extra_file.txt']})
 
 calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
 calc.set_withmpi(False)
 
 calc.use_structure(s)
@@ -93,17 +123,10 @@
 
 if submit_test:
     subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
 else:
     calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
     calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/docs/source/user_guide/test_gpaw_parallel.py b/docs/source/user_guide/test_gpaw_parallel.py
index 4fc5196..4527d0a 100644
--- a/docs/source/user_guide/test_gpaw_parallel.py
+++ b/docs/source/user_guide/test_gpaw_parallel.py
@@ -18,42 +18,64 @@
 StructureData = DataFactory('structure')
 KpointsData = DataFactory('array.kpoints')
 
-submit_test = True # set to false to launch the calculation
+submit_test = True  # set to false to launch the calculation
 codename = 'your aiida code label for parallel gpaw'
-queue = None    # optional
-settings = None # optional
+queue = None  # optional
+settings = None  # optional
 
 code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
 
 # BaTiO3 cubic structure
 s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ba'])
-s.append_atom(position=(alat/2.,alat/2.,alat/2.),symbols=['Ti'])
-s.append_atom(position=(alat/2.,alat/2.,0.),symbols=['O'])
-s.append_atom(position=(alat/2.,0.,alat/2.),symbols=['O'])
-s.append_atom(position=(0.,alat/2.,alat/2.),symbols=['O'])
+s.append_atom(position=(0., 0., 0.), symbols=['Ba'])
+s.append_atom(position=(alat / 2., alat / 2., alat / 2.), symbols=['Ti'])
+s.append_atom(position=(alat / 2., alat / 2., 0.), symbols=['O'])
+s.append_atom(position=(alat / 2., 0., alat / 2.), symbols=['O'])
+s.append_atom(position=(0., alat / 2., alat / 2.), symbols=['O'])
 
 kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
+kpoints.set_kpoints_mesh([2, 2, 2])
 
 parameters = ParameterData(
-                dict={"calculator": {"name":"gpaw",
-                                     "args":{"mode":{"@function":"PW",
-                                                     "args":{"ecut":300},
-                                                 }}},
-                      })
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    },
+                }
+            }
+        },
+    }
+)
 
 calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Testing a calculation with the parallel Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
+calc.label = 'Test Gpaw'
+calc.description = 'Testing a calculation with the parallel Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
 #calc.set_withmpi(False)
 
 calc.use_structure(s)
@@ -65,17 +87,10 @@
 
 if submit_test:
     subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
 else:
     calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
     calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/examples/test_gpaw.py b/examples/test_gpaw.py
new file mode 100644
index 0000000..ad8cad0
--- /dev/null
+++ b/examples/test_gpaw.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+###########################################################################
+# Copyright (c), The AiiDA team. All rights reserved.                     #
+# This file is part of the AiiDA code.                                    #
+#                                                                         #
+# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
+# For further information on the license, see the LICENSE.txt file        #
+# For further information please visit http://www.aiida.net               #
+###########################################################################
+
+import sys
+import os
+from aiida.common.exceptions import NotExistent
+
+################################################################
+
+ParameterData = DataFactory('parameter')
+StructureData = DataFactory('structure')
+KpointsData = DataFactory('array.kpoints')
+try:
+    dontsend = sys.argv[1]
+    if dontsend == '--dont-send':
+        submit_test = True
+    elif dontsend == '--send':
+        submit_test = False
+    else:
+        raise IndexError
+except IndexError:
+    print >> sys.stderr, ('The first parameter can only be either ' '--send or --dont-send')
+    sys.exit(1)
+
+try:
+    codename = sys.argv[2]
+except IndexError:
+    codename = None
+
+expected_code_type = 'asecalculators.gpaw'
+
+queue = None
+settings = None
+
+try:
+    if codename is None:
+        raise ValueError
+    code = Code.get(codename)
+    if code.get_input_plugin_name() != expected_code_type:
+        raise ValueError
+except (NotExistent, ValueError):
+    valid_code_labels = [
+        c.label for c in Code.query(dbattributes__key='input_plugin', dbattributes__tval=expected_code_type)
+    ]
+    if valid_code_labels:
+        print >> sys.stderr, 'Pass as further parameter a valid code label.'
+        print >> sys.stderr, 'Valid labels with a {} executable are:'.format(expected_code_type)
+        for l in valid_code_labels:
+            print >> sys.stderr, '*', l
+    else:
+        print >> sys.stderr, 'Code not valid, and no valid codes for {}. Configure at least one first using'.format(
+            expected_code_type
+        )
+        print >> sys.stderr, '    verdi code setup'
+    sys.exit(1)
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
+
+# BaTiO3 cubic structure
+s = StructureData(cell=cell)
+s.append_atom(position=(0., 0., 0.), symbols=['Ba'])
+s.append_atom(position=(alat / 2., alat / 2., alat / 2.), symbols=['Ti'])
+s.append_atom(position=(alat / 2., alat / 2., 0.), symbols=['O'])
+s.append_atom(position=(alat / 2., 0., alat / 2.), symbols=['O'])
+s.append_atom(position=(0., alat / 2., alat / 2.), symbols=['O'])
+
+kpoints = KpointsData()
+kpoints.set_kpoints_mesh([2, 2, 2])
+
+parameters = ParameterData(
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    }
+                }
+            }
+        },
+        'atoms_getters': ['temperature'],
+        'calculator_getters': [['potential_energy', {}]],
+        'optimizer': {
+            'name': 'QuasiNewton',
+            'args': {},
+            'run_args': {
+                'fmax': 0.05
+            },
+        }
+    }
+)
+
+calc = code.new_calc()
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
+calc.set_withmpi(False)
+
+calc.use_structure(s)
+calc.use_parameters(parameters)
+calc.use_kpoints(kpoints)
+
+if queue is not None:
+    calc.set_queue_name(queue)
+
+if submit_test:
+    subfolder, script_filename = calc.submit_test()
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
+else:
+    calc.store_all()
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
+    calc.submit()
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/tests/examples/test_gpaw_compact1.py b/examples/test_gpaw_compact1.py
similarity index 55%
rename from tests/examples/test_gpaw_compact1.py
rename to examples/test_gpaw_compact1.py
index 827b54b..7d9d709 100644
--- a/tests/examples/test_gpaw_compact1.py
+++ b/examples/test_gpaw_compact1.py
@@ -26,36 +26,58 @@
 #############################
 
 code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
-    
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
+
 # BaTiO3 cubic structure
 s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ba'])
-s.append_atom(position=(alat/2.,alat/2.,alat/2.),symbols=['Ti'])
-s.append_atom(position=(alat/2.,alat/2.,0.),symbols=['O'])
-s.append_atom(position=(alat/2.,0.,alat/2.),symbols=['O'])
-s.append_atom(position=(0.,alat/2.,alat/2.),symbols=['O'])
+s.append_atom(position=(0., 0., 0.), symbols=['Ba'])
+s.append_atom(position=(alat / 2., alat / 2., alat / 2.), symbols=['Ti'])
+s.append_atom(position=(alat / 2., alat / 2., 0.), symbols=['O'])
+s.append_atom(position=(alat / 2., 0., alat / 2.), symbols=['O'])
+s.append_atom(position=(0., alat / 2., alat / 2.), symbols=['O'])
 
 kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
+kpoints.set_kpoints_mesh([2, 2, 2])
 
 parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300}
-                         }}},
-          })
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    }
+                }
+            }
+        },
+    }
+)
 
 calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
 calc.set_withmpi(False)
 
 calc.use_structure(s)
@@ -67,17 +89,10 @@
 
 if submit_test:
     subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
 else:
     calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
     calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/tests/examples/test_gpaw_compact1_parallel.py b/examples/test_gpaw_compact1_parallel.py
similarity index 55%
rename from tests/examples/test_gpaw_compact1_parallel.py
rename to examples/test_gpaw_compact1_parallel.py
index 6b8ac6f..835a0fc 100644
--- a/tests/examples/test_gpaw_compact1_parallel.py
+++ b/examples/test_gpaw_compact1_parallel.py
@@ -26,37 +26,58 @@
 #############################
 
 code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
-    
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
+
 # BaTiO3 cubic structure
 s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ba'])
-s.append_atom(position=(alat/2.,alat/2.,alat/2.),symbols=['Ti'])
-s.append_atom(position=(alat/2.,alat/2.,0.),symbols=['O'])
-s.append_atom(position=(alat/2.,0.,alat/2.),symbols=['O'])
-s.append_atom(position=(0.,alat/2.,alat/2.),symbols=['O'])
+s.append_atom(position=(0., 0., 0.), symbols=['Ba'])
+s.append_atom(position=(alat / 2., alat / 2., alat / 2.), symbols=['Ti'])
+s.append_atom(position=(alat / 2., alat / 2., 0.), symbols=['O'])
+s.append_atom(position=(alat / 2., 0., alat / 2.), symbols=['O'])
+s.append_atom(position=(0., alat / 2., alat / 2.), symbols=['O'])
 
 kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
+kpoints.set_kpoints_mesh([2, 2, 2])
 
 parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300}
-                         }}},
-          }
-    )
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    }
+                }
+            }
+        },
+    }
+)
 
 calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
 #calc.set_withmpi(False)
 
 calc.use_structure(s)
@@ -68,17 +89,10 @@
 
 if submit_test:
     subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
 else:
     calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
     calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/examples/test_gpaw_compact2.py b/examples/test_gpaw_compact2.py
new file mode 100644
index 0000000..fbf532f
--- /dev/null
+++ b/examples/test_gpaw_compact2.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+###########################################################################
+# Copyright (c), The AiiDA team. All rights reserved.                     #
+# This file is part of the AiiDA code.                                    #
+#                                                                         #
+# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
+# For further information on the license, see the LICENSE.txt file        #
+# For further information please visit http://www.aiida.net               #
+###########################################################################
+
+import sys
+import os
+from aiida.common.exceptions import NotExistent
+
+################################################################
+
+ParameterData = DataFactory('parameter')
+StructureData = DataFactory('structure')
+KpointsData = DataFactory('array.kpoints')
+
+# CUSTOMIZE HERE ############
+submit_test = False
+codename = 'gpaw-serial'
+queue = None
+settings = None
+#############################
+
+code = Code.get(codename)
+
+alat = 4.  # angstrom
+cell = [
+    [
+        alat,
+        0.,
+        0.,
+    ],
+    [
+        0.,
+        alat,
+        0.,
+    ],
+    [
+        0.,
+        0.,
+        alat,
+    ],
+]
+
+# a molecule of Ti and O
+s = StructureData(cell=cell)
+s.append_atom(position=(0., 0., 0.), symbols=['Ti'])
+s.append_atom(position=(1.4, 0., 0.), symbols=['O'])
+
+kpoints = KpointsData()
+kpoints.set_kpoints_mesh([2, 2, 2])
+
+parameters = ParameterData(
+    dict={
+        'calculator': {
+            'name': 'gpaw',
+            'args': {
+                'mode': {
+                    '@function': 'PW',
+                    'args': {
+                        'ecut': 300
+                    }
+                }
+            }
+        },
+        'atoms_getters': [
+            'temperature',
+            ['forces', {
+                'apply_constraint': True
+            }],
+            ['masses', {}],
+        ],
+        'calculator_getters': [
+            ['potential_energy', {}],
+            'spin_polarized',
+            ['stress', ['atoms']],
+            #["orbital_dos",['atoms', {'spin':0}] ],
+        ],
+        'optimizer': {
+            'name': 'QuasiNewton',
+            'args': {
+                'alpha': 0.9
+            },
+            'run_args': {
+                'fmax': 0.05
+            }
+        },
+        'pre_lines': ['# This is a set', '# of various pre-lines'],
+        'post_lines': ['# This is a set', '# of various post-lines'],
+        'extra_imports': [
+            'os',
+            ['numpy', 'array'],
+            ['numpy', 'array', 'ar'],
+        ],
+    }
+)
+
+settings = ParameterData(dict={'ADDITIONAL_RETRIEVE_LIST': ['an_extra_file.txt']})
+
+calc = code.new_calc()
+calc.label = 'Test Gpaw'
+calc.description = 'Test calculation with the Gpaw code'
+calc.set_max_wallclock_seconds(30 * 60)  # 30 min
+calc.set_resources({'num_machines': 1, 'num_mpiprocs_per_machine': 1})
+calc.set_withmpi(False)
+
+calc.use_structure(s)
+calc.use_parameters(parameters)
+calc.use_kpoints(kpoints)
+calc.use_settings(settings)
+
+if queue is not None:
+    calc.set_queue_name(queue)
+
+if submit_test:
+    subfolder, script_filename = calc.submit_test()
+    print "Test_submit for calculation (uuid='{}')".format(calc.uuid)
+    print 'Submit file in {}'.format(os.path.join(os.path.relpath(subfolder.abspath), script_filename))
+else:
+    calc.store_all()
+    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
+    calc.submit()
+    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(calc.uuid, calc.dbnode.pk)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/calculations/__init__.py b/tests/calculations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/calculations/test_ase.py b/tests/calculations/test_ase.py
new file mode 100644
index 0000000..91d0150
--- /dev/null
+++ b/tests/calculations/test_ase.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+"""Tests for the ``AseCalculation`` class."""
+from aiida.common import datastructures
+
+from aiida_ase.calculations.ase import AseCalculation
+
+
+def test_default(fixture_sandbox, generate_calc_job, generate_inputs_ase, file_regression):
+    """Test a default ``AseCalculation``."""
+    entry_point_name = 'ase.ase'
+    inputs = generate_inputs_ase()
+
+    calc_info = generate_calc_job(fixture_sandbox, entry_point_name, inputs)
+
+    assert isinstance(calc_info, datastructures.CalcInfo)
+    assert isinstance(calc_info.codes_info[0], datastructures.CodeInfo)
+
+    with fixture_sandbox.open(AseCalculation._INPUT_FILE_NAME) as handle:  # pylint: disable=protected-access
+        input_written = handle.read()
+
+    # Checks on the files written to the sandbox folder as raw input
+    file_regression.check(input_written, encoding='utf-8', extension='.in')
diff --git a/tests/calculations/test_ase/test_default.in b/tests/calculations/test_ase/test_default.in
new file mode 100644
index 0000000..5cb98ad
--- /dev/null
+++ b/tests/calculations/test_ase/test_default.in
@@ -0,0 +1,34 @@
+import ase
+import ase.io
+import json
+import numpy
+from gpaw import GPAW as custom_calculator
+from ase.optimize import QuasiNewton as custom_optimizer
+from gpaw import PW
+
+atoms = ase.io.read('aiida_atoms.json')
+
+calculator = custom_calculator(mode=PW(ecut=300), kpts=(2,2,2))
+atoms.set_calculator(calculator)
+
+optimizer = custom_optimizer(atoms, alpha=0.9)
+optimizer.run(fmax=0.05)
+
+results = {}
+results['total_energy'] = atoms.get_total_energy()
+results['temperature'] = atoms.get_temperature()
+results['forces'] = atoms.get_forces(apply_constraint=True)
+results['masses'] = atoms.get_masses()
+
+results['potential_energy'] = calculator.get_potential_energy()
+results['spin_polarized'] = calculator.get_spin_polarized()
+results['stress'] = calculator.get_stress(atoms)
+
+for k,v in results.items():
+    if isinstance(results[k],(numpy.matrix,numpy.ndarray)):
+        results[k] = results[k].tolist()
+
+with open('results.json', 'w') as f:
+    json.dump(results,f)
+atoms.write('aiida_out_atoms.json')
+
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..4660037
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=redefined-outer-name
+"""Fixtures for the unit test suite."""
+import collections
+import re
+
+import pytest
+
+from aiida import orm
+
+pytest_plugins = ['aiida.manage.tests.pytest_fixtures']  # pylint: disable=invalid-name
+
+
+@pytest.fixture(scope='function')
+def fixture_sandbox():
+    """Return a ``SandboxFolder``."""
+    from aiida.common.folders import SandboxFolder
+    with SandboxFolder() as folder:
+        yield folder
+
+
+@pytest.fixture
+def fixture_localhost(aiida_localhost):
+    """Return a localhost ``Computer``."""
+    localhost = aiida_localhost
+    localhost.set_default_mpiprocs_per_machine(1)
+    return localhost
+
+
+@pytest.fixture
+def generate_parser():
+    """Fixture to load a parser class for testing parsers."""
+
+    def _generate_parser(entry_point_name):
+        """Fixture to load a parser class for testing parsers.
+
+        :param entry_point_name: entry point name of the parser class
+        :return: the `Parser` sub class
+        """
+        from aiida.plugins import ParserFactory
+        return ParserFactory(entry_point_name)
+
+    return _generate_parser
+
+
+@pytest.fixture
+def generate_calc_job():
+    """Fixture to construct a new ``CalcJob`` instance and call ``prepare_for_submission``.
+
+    The fixture will return the ``CalcInfo`` returned by ``prepare_for_submission`` and the temporary folder that was
+    passed to it, into which the raw input files will have been written.
+    """
+
+    def _generate_calc_job(folder, entry_point_name, inputs=None):
+        """Fixture to generate a mock ``CalcInfo`` for testing calculation jobs."""
+        from aiida.engine.utils import instantiate_process
+        from aiida.manage.manager import get_manager
+        from aiida.plugins import CalculationFactory
+
+        manager = get_manager()
+        runner = manager.get_runner()
+
+        process_class = CalculationFactory(entry_point_name)
+        process = instantiate_process(runner, process_class, **inputs)
+
+        calc_info = process.prepare_for_submission(folder)
+
+        return calc_info
+
+    return _generate_calc_job
+
+
+@pytest.fixture
+def generate_calc_job_node():
+    """Fixture to generate a mock `CalcJobNode` for testing parsers."""
+
+    def flatten_inputs(inputs, prefix=''):
+        """This function follows roughly the same logic as `aiida.engine.processes.process::Process._flatten_inputs`."""
+        flat_inputs = []
+        for key, value in inputs.items():
+            if isinstance(value, collections.abc.Mapping):
+                flat_inputs.extend(flatten_inputs(value, prefix=prefix + key + '__'))
+            else:
+                flat_inputs.append((prefix + key, value))
+        return flat_inputs
+
+    def _generate_calc_job_node(entry_point_name, computer, test_name=None, inputs=None, attributes=None):
+        """Fixture to generate a mock `CalcJobNode` for testing parsers.
+
+        :param entry_point_name: entry point name of the calculation class
+        :param computer: a `Computer` instance
+        :param test_name: relative path of directory with test output files in the `fixtures/{entry_point_name}` folder.
+        :param inputs: any optional nodes to add as input links to the corrent CalcJobNode
+        :param attributes: any optional attributes to set on the node
+        :return: `CalcJobNode` instance with an attached `FolderData` as the `retrieved` node
+        """
+        # pylint: disable=too-many-locals
+        import os
+        from aiida.common import LinkType
+        from aiida.plugins.entry_point import format_entry_point_string
+
+        entry_point = format_entry_point_string('aiida.calculations', entry_point_name)
+
+        node = orm.CalcJobNode(computer=computer, process_type=entry_point)
+        node.set_option('resources', {'num_machines': 1, 'num_mpiprocs_per_machine': 1})
+        node.set_option('max_wallclock_seconds', 1800)
+
+        if attributes:
+            node.set_attribute_many(attributes)
+
+        if inputs:
+            metadata = inputs.pop('metadata', {})
+            options = metadata.get('options', {})
+
+            for name, option in options.items():
+                node.set_option(name, option)
+
+            for link_label, input_node in flatten_inputs(inputs):
+                input_node.store()
+                node.add_incoming(input_node, link_type=LinkType.INPUT_CALC, link_label=link_label)
+
+        node.store()
+
+        if test_name is not None:
+            basepath = os.path.dirname(os.path.abspath(__file__))
+            filepath = os.path.join(basepath, 'parsers', 'fixtures', 'ase', test_name)
+
+            retrieved = orm.FolderData()
+            retrieved.put_object_from_tree(filepath)
+            retrieved.add_incoming(node, link_type=LinkType.CREATE, link_label='retrieved')
+            retrieved.store()
+
+            remote_folder = orm.RemoteData(computer=computer, remote_path='/tmp')
+            remote_folder.add_incoming(node, link_type=LinkType.CREATE, link_label='remote_folder')
+            remote_folder.store()
+
+        return node
+
+    return _generate_calc_job_node
+
+
+@pytest.fixture
+def generate_inputs_ase(generate_code, generate_structure, generate_kpoints_mesh):
+    """Generate inputs for an ``AseCalculation``."""
+
+    def _generate_inputs_ase():
+        parameters = {
+            'calculator': {
+                'name': 'gpaw',
+                'args': {
+                    'mode': {
+                        '@function': 'PW',
+                        'args': {
+                            'ecut': 300
+                        }
+                    }
+                }
+            },
+            'atoms_getters': [
+                'temperature',
+                ['forces', {
+                    'apply_constraint': True
+                }],
+                ['masses', {}],
+            ],
+            'calculator_getters': [
+                ['potential_energy', {}],
+                'spin_polarized',
+                ['stress', ['atoms']],
+            ],
+            'optimizer': {
+                'name': 'QuasiNewton',
+                'args': {
+                    'alpha': 0.9
+                },
+                'run_args': {
+                    'fmax': 0.05
+                }
+            },
+        }
+        inputs = {
+            'code': generate_code('ase.ase'),
+            'structure': generate_structure(),
+            'kpoints': generate_kpoints_mesh(2),
+            'parameters': orm.Dict(dict=parameters),
+            'metadata': {
+                'options': {
+                    'resources': {
+                        'num_machines': 1,
+                    }
+                }
+            }
+        }
+        return inputs
+
+    return _generate_inputs_ase
+
+
+@pytest.fixture
+def generate_code(fixture_localhost):
+    """Return a ``Code`` instance configured to run calculations of given entry point on localhost ``Computer``."""
+
+    def _generate_code(entry_point_name):
+        from aiida.common import exceptions
+        from aiida.orm import Code
+
+        label = f'test.{entry_point_name}'
+
+        try:
+            return Code.objects.get(label=label)  # pylint: disable=no-member
+        except exceptions.NotExistent:
+            return Code(
+                label=label,
+                input_plugin_name=entry_point_name,
+                remote_computer_exec=[fixture_localhost, '/bin/true'],
+            )
+
+    return _generate_code
+
+
+@pytest.fixture
+def generate_structure():
+    """Return a ``StructureData``."""
+
+    def _generate_structure(elements=('Ar',)):
+        """Return a ``StructureData``."""
+        from aiida.orm import StructureData
+
+        structure = StructureData(cell=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+
+        for index, element in enumerate(elements):
+            symbol = re.sub(r'[0-9]+', '', element)
+            structure.append_atom(position=(index * 0.5, index * 0.5, index * 0.5), symbols=symbol, name=element)
+
+        return structure
+
+    return _generate_structure
+
+
+@pytest.fixture
+def generate_kpoints_mesh():
+    """Return a `KpointsData` node."""
+
+    def _generate_kpoints_mesh(npoints):
+        """Return a `KpointsData` with a mesh of npoints in each direction."""
+        from aiida.orm import KpointsData
+
+        kpoints = KpointsData()
+        kpoints.set_kpoints_mesh([npoints] * 3)
+
+        return kpoints
+
+    return _generate_kpoints_mesh
diff --git a/tests/examples/test_gpaw.py b/tests/examples/test_gpaw.py
deleted file mode 100644
index 5a941c1..0000000
--- a/tests/examples/test_gpaw.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# -*- coding: utf-8 -*-
-###########################################################################
-# Copyright (c), The AiiDA team. All rights reserved.                     #
-# This file is part of the AiiDA code.                                    #
-#                                                                         #
-# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
-# For further information on the license, see the LICENSE.txt file        #
-# For further information please visit http://www.aiida.net               #
-###########################################################################
-
-import sys
-import os
-from aiida.common.exceptions import NotExistent
-
-################################################################
-
-ParameterData = DataFactory('parameter')
-StructureData = DataFactory('structure')
-KpointsData = DataFactory('array.kpoints')
-try:
-    dontsend = sys.argv[1]
-    if dontsend == "--dont-send":
-        submit_test = True
-    elif dontsend == "--send":
-        submit_test = False
-    else:
-        raise IndexError
-except IndexError:
-    print >> sys.stderr, ("The first parameter can only be either "
-                          "--send or --dont-send")
-    sys.exit(1)
-    
-try:
-    codename = sys.argv[2]
-except IndexError:
-    codename = None
-
-expected_code_type='asecalculators.gpaw'
-    
-queue = None
-settings = None
-
-try:
-    if codename is None:
-        raise ValueError
-    code = Code.get(codename)
-    if code.get_input_plugin_name() != expected_code_type:
-        raise ValueError
-except (NotExistent, ValueError):
-    valid_code_labels = [c.label for c in Code.query(
-            dbattributes__key="input_plugin",
-            dbattributes__tval=expected_code_type)]
-    if valid_code_labels:
-        print >> sys.stderr, "Pass as further parameter a valid code label."
-        print >> sys.stderr, "Valid labels with a {} executable are:".format(expected_code_type)
-        for l in valid_code_labels:
-            print >> sys.stderr, "*", l
-    else:
-        print >> sys.stderr, "Code not valid, and no valid codes for {}. Configure at least one first using".format(expected_code_type)
-        print >> sys.stderr, "    verdi code setup"
-    sys.exit(1)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
-    
-# BaTiO3 cubic structure
-s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ba'])
-s.append_atom(position=(alat/2.,alat/2.,alat/2.),symbols=['Ti'])
-s.append_atom(position=(alat/2.,alat/2.,0.),symbols=['O'])
-s.append_atom(position=(alat/2.,0.,alat/2.),symbols=['O'])
-s.append_atom(position=(0.,alat/2.,alat/2.),symbols=['O'])
-
-kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
-
-parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300}
-                         }}},
-          
-          'atoms_getters':["temperature"],
-          
-          'calculator_getters':[["potential_energy",{}]],
-          
-          'optimizer':{'name':'QuasiNewton',
-                       'args':{},
-                       'run_args':{"fmax":0.05},
-                       }})
-
-calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
-calc.set_withmpi(False)
-
-calc.use_structure(s)
-calc.use_parameters(parameters)
-calc.use_kpoints(kpoints)
-
-if queue is not None:
-    calc.set_queue_name(queue)
-
-if submit_test:
-    subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
-else:
-    calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-    calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
diff --git a/tests/examples/test_gpaw_compact2.py b/tests/examples/test_gpaw_compact2.py
deleted file mode 100644
index 1d3ad98..0000000
--- a/tests/examples/test_gpaw_compact2.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# -*- coding: utf-8 -*-
-###########################################################################
-# Copyright (c), The AiiDA team. All rights reserved.                     #
-# This file is part of the AiiDA code.                                    #
-#                                                                         #
-# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
-# For further information on the license, see the LICENSE.txt file        #
-# For further information please visit http://www.aiida.net               #
-###########################################################################
-
-import sys
-import os
-from aiida.common.exceptions import NotExistent
-
-################################################################
-
-ParameterData = DataFactory('parameter')
-StructureData = DataFactory('structure')
-KpointsData = DataFactory('array.kpoints')
-
-# CUSTOMIZE HERE ############
-submit_test = False
-codename = 'gpaw-serial'
-queue = None
-settings = None
-#############################
-
-code = Code.get(codename)
-    
-alat = 4. # angstrom
-cell = [[alat, 0., 0.,],
-        [0., alat, 0.,],
-        [0., 0., alat,],
-        ]
-
-# a molecule of Ti and O
-s = StructureData(cell=cell)
-s.append_atom(position=(0.,0.,0.),symbols=['Ti'])
-s.append_atom(position=(1.4, 0., 0.),symbols=['O'])
-
-kpoints = KpointsData()
-kpoints.set_kpoints_mesh([2,2,2])
-
-parameters = ParameterData(
-    dict={"calculator": {"name":"gpaw",
-                         "args":{"mode":{"@function":"PW",
-                                         "args":{"ecut":300}
-                         }}},
-          'atoms_getters':["temperature",
-                           ["forces",{'apply_constraint':True}],
-                           ["masses",{}],
-                           ],
-          'calculator_getters':[["potential_energy",{}],
-                                "spin_polarized",
-                                ["stress",['atoms']],
-                                #["orbital_dos",['atoms', {'spin':0}] ],
-                                ],
-          'optimizer':{'name':'QuasiNewton',
-                       "args": {'alpha':0.9},
-                       'run_args':{"fmax":0.05}
-                       },
-          
-          "pre_lines":["# This is a set",
-                       "# of various pre-lines"],
-          
-          "post_lines":["# This is a set",
-                       "# of various post-lines"],
-          
-          "extra_imports":["os",
-                           ["numpy","array"],
-                           ["numpy","array","ar"],
-                           ],
-          }
-    )
-
-settings = ParameterData(dict={"ADDITIONAL_RETRIEVE_LIST":["an_extra_file.txt"]})
-
-calc = code.new_calc()
-calc.label = "Test Gpaw"
-calc.description = "Test calculation with the Gpaw code"
-calc.set_max_wallclock_seconds(30*60) # 30 min
-calc.set_resources({"num_machines": 1,"num_mpiprocs_per_machine":1})
-calc.set_withmpi(False)
-
-calc.use_structure(s)
-calc.use_parameters(parameters)
-calc.use_kpoints(kpoints)
-calc.use_settings(settings)
-
-if queue is not None:
-    calc.set_queue_name(queue)
-
-if submit_test:
-    subfolder, script_filename = calc.submit_test()
-    print "Test_submit for calculation (uuid='{}')".format(
-        calc.uuid)
-    print "Submit file in {}".format(os.path.join(
-            os.path.relpath(subfolder.abspath),
-            script_filename
-            ))
-else:
-    calc.store_all()
-    print "created calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-    calc.submit()
-    print "submitted calculation; calc=Calculation(uuid='{}') # ID={}".format(
-        calc.uuid,calc.dbnode.pk)
-
diff --git a/tests/parsers/__init__.py b/tests/parsers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/parsers/fixtures/ase/default_gpaw/_scheduler-stderr.txt b/tests/parsers/fixtures/ase/default_gpaw/_scheduler-stderr.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/parsers/fixtures/ase/default_gpaw/aiida_out_atoms.json b/tests/parsers/fixtures/ase/default_gpaw/aiida_out_atoms.json
new file mode 100644
index 0000000..dbec969
--- /dev/null
+++ b/tests/parsers/fixtures/ase/default_gpaw/aiida_out_atoms.json
@@ -0,0 +1,16 @@
+{"1": {
+ "calculator": "gpaw",
+ "calculator_parameters": {"mode": {"name": "pw", "ecut": 300.0, "gammacentered": false}, "kpts": [2, 2, 2]},
+ "cell": {"array": [[4.0, 0.0, 0.0], [0.0, 4.0, 0.0], [0.0, 0.0, 4.0]], "pbc": [true, true, true], "__ase_objtype__": "cell"},
+ "ctime": 19.828993810413085,
+ "dipole": [-2.536138834331967e-15, -3.0863525924255426e-15, -1.0368739800914415e-15],
+ "energy": -21.867772543332897,
+ "masses": [137.327, 47.867, 15.9994, 15.9994, 15.9994],
+ "mtime": 19.828993810413085,
+ "numbers": [56, 22, 8, 8, 8],
+ "pbc": [true, true, true],
+ "positions": [[0.0, 0.0, 0.0], [2.0, 2.0, 2.0], [2.0, 2.0, 0.0], [2.0, 0.0, 2.0], [0.0, 2.0, 2.0]],
+ "unique_id": "63610117d789be193b9d4b81ca2bc2f7",
+ "user": "feltbo"},
+"ids": [1],
+"nextid": 2}
diff --git a/tests/parsers/fixtures/ase/default_gpaw/results.json b/tests/parsers/fixtures/ase/default_gpaw/results.json
new file mode 100644
index 0000000..71f8298
--- /dev/null
+++ b/tests/parsers/fixtures/ase/default_gpaw/results.json
@@ -0,0 +1,1 @@
+{"total_energy": -21.867772543332897}
diff --git a/tests/parsers/test_ase.py b/tests/parsers/test_ase.py
new file mode 100644
index 0000000..63acc73
--- /dev/null
+++ b/tests/parsers/test_ase.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=unused-argument
+"""Tests for the ``AseParser``."""
+from aiida.plugins import CalculationFactory
+
+AseCalculation = CalculationFactory('ase.ase')
+
+
+def test_default_gpaw(aiida_localhost, generate_calc_job_node, generate_parser, generate_inputs_ase, data_regression):
+    """Test a default GPAW calculator."""
+    name = 'default_gpaw'
+    entry_point_calc_job = 'ase.ase'
+    entry_point_parser = 'ase.ase'
+
+    attributes = {'output_filename': AseCalculation._OUTPUT_FILE_NAME}  # pylint: disable=protected-access
+
+    node = generate_calc_job_node(
+        entry_point_calc_job, aiida_localhost, name, generate_inputs_ase(), attributes=attributes
+    )
+    parser = generate_parser(entry_point_parser)
+    results, calcfunction = parser.parse_from_node(node, store_provenance=False)
+
+    assert calcfunction.is_finished, calcfunction.exception
+    assert calcfunction.is_finished_ok, calcfunction.exit_message
+    assert 'structure' in results
+    assert 'parameters' in results
+
+    data_regression.check({
+        'structure': results['structure'].attributes,
+        'parameters': results['parameters'].get_dict(),
+    })
diff --git a/tests/parsers/test_ase/test_default_gpaw.yml b/tests/parsers/test_ase/test_default_gpaw.yml
new file mode 100644
index 0000000..a4d4dbe
--- /dev/null
+++ b/tests/parsers/test_ase/test_default_gpaw.yml
@@ -0,0 +1,62 @@
+parameters:
+  total_energy: -21.867772543332897
+  warnings: []
+structure:
+  cell:
+  - - 4.0
+    - 0.0
+    - 0.0
+  - - 0.0
+    - 4.0
+    - 0.0
+  - - 0.0
+    - 0.0
+    - 4.0
+  kinds:
+  - mass: 137.327
+    name: Ba
+    symbols:
+    - Ba
+    weights:
+    - 1.0
+  - mass: 47.867
+    name: Ti
+    symbols:
+    - Ti
+    weights:
+    - 1.0
+  - mass: 15.9994
+    name: O
+    symbols:
+    - O
+    weights:
+    - 1.0
+  pbc1: true
+  pbc2: true
+  pbc3: true
+  sites:
+  - kind_name: Ba
+    position:
+    - 0.0
+    - 0.0
+    - 0.0
+  - kind_name: Ti
+    position:
+    - 2.0
+    - 2.0
+    - 2.0
+  - kind_name: O
+    position:
+    - 2.0
+    - 2.0
+    - 0.0
+  - kind_name: O
+    position:
+    - 2.0
+    - 0.0
+    - 2.0
+  - kind_name: O
+    position:
+    - 0.0
+    - 2.0
+    - 2.0