From a84ccb44cc183d3b7afb0354aa77317663d6528a Mon Sep 17 00:00:00 2001
From: ljf <ljf+git@grimaud.me>
Date: Mon, 30 Nov 2020 10:40:18 +0100
Subject: [PATCH 1/5] [enh] Allow file type in actionmaps

---
 moulinette/interfaces/api.py | 53 ++++++++++++++++++++++++++++--------
 1 file changed, 42 insertions(+), 11 deletions(-)

diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py
index 3a61939b..f53dec63 100644
--- a/moulinette/interfaces/api.py
+++ b/moulinette/interfaces/api.py
@@ -5,14 +5,17 @@ import errno
 import logging
 import argparse
 from json import dumps as json_encode
+from tempfile import mkdtemp
 
 from gevent import sleep
 from gevent.queue import Queue
 from geventwebsocket import WebSocketError
 
-from bottle import request, response, Bottle, HTTPResponse
+from bottle import request, response, Bottle, HTTPResponse, FileUpload
 from bottle import abort
 
+from shutil import rmtree
+
 from moulinette import msignals, m18n, env
 from moulinette.actionsmap import ActionsMap
 from moulinette.core import MoulinetteError, MoulinetteValidationError
@@ -29,6 +32,8 @@ logger = log.getLogger("moulinette.interface.api")
 
 
 # API helpers ----------------------------------------------------------
+# We define a global variable to manage in a dirty way the upload...
+UPLOAD_DIR = None
 
 CSRF_TYPES = set(
     ["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"]
@@ -111,6 +116,7 @@ class _HTTPArgumentParser(object):
 
         self._positional = []  # list(arg_name)
         self._optional = {}  # dict({arg_name: option_strings})
+        self._upload_dir = None
 
     def set_defaults(self, **kwargs):
         return self._parser.set_defaults(**kwargs)
@@ -145,9 +151,9 @@ class _HTTPArgumentParser(object):
 
         # Append newly created action
         if len(action.option_strings) == 0:
-            self._positional.append(action.dest)
+            self._positional.append(action)
         else:
-            self._optional[action.dest] = action.option_strings
+            self._optional[action.dest] = action
 
         return action
 
@@ -155,11 +161,24 @@ class _HTTPArgumentParser(object):
         arg_strings = []
 
         # Append an argument to the current one
-        def append(arg_strings, value, option_string=None):
-            if isinstance(value, bool):
+        def append(arg_strings, value, action):
+            option_string = None
+            if len(action.option_strings) > 0:
+                option_string = action.option_strings[0]
+
+            if isinstance(value, bool) or isinstance(action.const, bool):
                 # Append the option string only
+                if option_string is not None and value != 0:
+                    arg_strings.append(option_string)
+            elif isinstance(value, FileUpload) and (isinstance(action.type, argparse.FileType) or action.type == open):
+                # Upload the file in a temp directory
+                global UPLOAD_DIR
+                if UPLOAD_DIR is None:
+                    UPLOAD_DIR = mkdtemp(prefix='moulinette_upload_')
+                value.save(UPLOAD_DIR)
                 if option_string is not None:
                     arg_strings.append(option_string)
+                arg_strings.append(UPLOAD_DIR + '/' + value.filename)
             elif isinstance(value, str):
                 if option_string is not None:
                     arg_strings.append(option_string)
@@ -192,14 +211,14 @@ class _HTTPArgumentParser(object):
             return arg_strings
 
         # Iterate over positional arguments
-        for dest in self._positional:
-            if dest in args:
-                arg_strings = append(arg_strings, args[dest])
+        for action in self._positional:
+            if action.dest in args:
+                arg_strings = append(arg_strings, args[action.dest], action)
 
         # Iterate over optional arguments
-        for dest, opt in self._optional.items():
+        for dest, action in self._optional.items():
             if dest in args:
-                arg_strings = append(arg_strings, args[dest], opt[0])
+                arg_strings = append(arg_strings, args[dest], action)
 
         return self._parser.parse_args(arg_strings, namespace)
 
@@ -319,8 +338,12 @@ class _ActionsMapPlugin(object):
             # Format boolean params
             for a in args:
                 params[a] = True
+
             # Append other request params
-            for k, v in request.params.decode().dict.items():
+            req_params = request.params.decode().dict.items()
+            # TODO test special chars in filename
+            req_params += request.files.dict.items()
+            for k, v in req_params:
                 v = _format(v)
                 if k not in params.keys():
                     params[k] = v
@@ -495,6 +518,14 @@ class _ActionsMapPlugin(object):
         else:
             return format_for_response(ret)
         finally:
+
+            # Clean upload directory
+            # FIXME do that in a better way
+            global UPLOAD_DIR
+            if UPLOAD_DIR is not None:
+                rmtree(UPLOAD_DIR, True)
+                UPLOAD_DIR = None
+
             # Close opened WebSocket by putting StopIteration in the queue
             try:
                 queue = self.log_queues[request.get_cookie("session.id")]

From 36cac914d664934ba1bad670daa7dab8f801d4dd Mon Sep 17 00:00:00 2001
From: ljf <ljf+git@grimaud.me>
Date: Sat, 8 May 2021 18:01:54 +0200
Subject: [PATCH 2/5] [enh] tox recommendation

---
 moulinette/interfaces/api.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py
index f53dec63..bd8d903a 100644
--- a/moulinette/interfaces/api.py
+++ b/moulinette/interfaces/api.py
@@ -170,15 +170,17 @@ class _HTTPArgumentParser(object):
                 # Append the option string only
                 if option_string is not None and value != 0:
                     arg_strings.append(option_string)
-            elif isinstance(value, FileUpload) and (isinstance(action.type, argparse.FileType) or action.type == open):
+            elif isinstance(value, FileUpload) and (
+                isinstance(action.type, argparse.FileType) or action.type == open
+            ):
                 # Upload the file in a temp directory
                 global UPLOAD_DIR
                 if UPLOAD_DIR is None:
-                    UPLOAD_DIR = mkdtemp(prefix='moulinette_upload_')
+                    UPLOAD_DIR = mkdtemp(prefix="moulinette_upload_")
                 value.save(UPLOAD_DIR)
                 if option_string is not None:
                     arg_strings.append(option_string)
-                arg_strings.append(UPLOAD_DIR + '/' + value.filename)
+                arg_strings.append(UPLOAD_DIR + "/" + value.filename)
             elif isinstance(value, str):
                 if option_string is not None:
                     arg_strings.append(option_string)

From 29292583fa7c28831ac776acd7de77c239a21d59 Mon Sep 17 00:00:00 2001
From: ljf <ljf+git@grimaud.me>
Date: Fri, 30 Jul 2021 18:35:19 +0200
Subject: [PATCH 3/5] [fix] 500 error

---
 moulinette/interfaces/api.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py
index bd8d903a..f6c6141a 100644
--- a/moulinette/interfaces/api.py
+++ b/moulinette/interfaces/api.py
@@ -342,9 +342,9 @@ class _ActionsMapPlugin(object):
                 params[a] = True
 
             # Append other request params
-            req_params = request.params.decode().dict.items()
+            req_params = list(request.params.decode().dict.items())
             # TODO test special chars in filename
-            req_params += request.files.dict.items()
+            req_params+=list(request.files.dict.items())
             for k, v in req_params:
                 v = _format(v)
                 if k not in params.keys():

From 8d8299c5fe5cdc56fb01ef5df45eee82bd3282f7 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin <alex.aubin@mailoo.org>
Date: Mon, 23 Aug 2021 15:01:15 +0200
Subject: [PATCH 4/5] Add autoblack github action (#283)

* Add autoblack github action

* autoblack: Uuuuh PR creation isnt triggered if black returns an error code ?

* autoblack: Only run autoblack when pushing on dev
---
 .github/workflows/autoblack.yml | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 .github/workflows/autoblack.yml

diff --git a/.github/workflows/autoblack.yml b/.github/workflows/autoblack.yml
new file mode 100644
index 00000000..35380607
--- /dev/null
+++ b/.github/workflows/autoblack.yml
@@ -0,0 +1,28 @@
+name: Check / auto apply Black
+on:
+  push:
+      branches:
+          - dev
+jobs:
+  black:
+    name: Check / auto apply black
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Check files using the black formatter
+        uses: rickstaa/action-black@v1
+        id: action_black
+        with:
+          black_args: "."
+        continue-on-error: true
+      - name: Create Pull Request
+        if: steps.action_black.outputs.is_formatted == 'true'
+        uses: peter-evans/create-pull-request@v3
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          title: "Format Python code with Black"
+          commit-message: ":art: Format Python code with Black"
+          body: |
+            This pull request uses the [psf/black](https://github.com/psf/black) formatter.
+          base: ${{ github.head_ref }} # Creates pull request onto pull request or commit branch
+          branch: actions/black

From 274a534bebef1d63be3470acacfa22e21b0e2bce Mon Sep 17 00:00:00 2001
From: alexAubin <alexAubin@users.noreply.github.com>
Date: Mon, 23 Aug 2021 13:25:51 +0000
Subject: [PATCH 5/5] :art: Format Python code with Black

---
 doc/conf.py                  | 82 ++++++++++++++++--------------
 moulinette/interfaces/api.py |  2 +-
 setup.py                     | 71 +++++++++++++-------------
 test/test_auth.py            |  4 +-
 test/test_i18n_keys.py       |  1 +
 test/test_ldap.py            | 96 +++++++++++++++++++++++++++---------
 6 files changed, 158 insertions(+), 98 deletions(-)

diff --git a/doc/conf.py b/doc/conf.py
index 3eafc4ea..b68fec4b 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -18,18 +18,21 @@
 
 import os
 import sys
-sys.path.insert(0, os.path.abspath('..'))
+
+sys.path.insert(0, os.path.abspath(".."))
 
 
 import sys
 from mock import Mock as MagicMock
 
+
 class Mock(MagicMock):
     @classmethod
     def __getattr__(cls, name):
-            return MagicMock()
+        return MagicMock()
 
-MOCK_MODULES = ['ldap', 'ldap.modlist', 'ldap.sasl']
+
+MOCK_MODULES = ["ldap", "ldap.modlist", "ldap.sasl"]
 sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
 
 
@@ -42,36 +45,38 @@ sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
-extensions = ['sphinx.ext.autodoc',
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.todo',
-    'sphinx.ext.viewcode']
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.todo",
+    "sphinx.ext.viewcode",
+]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 #
 # source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'Moulinette'
-copyright = u'2017, YunoHost Collective'
-author = u'YunoHost Collective'
+project = u"Moulinette"
+copyright = u"2017, YunoHost Collective"
+author = u"YunoHost Collective"
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = u'2.6.1'
+version = u"2.6.1"
 # The full version, including alpha/beta/rc tags.
-release = u'2.6.1'
+release = u"2.6.1"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -83,10 +88,10 @@ language = None
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # If true, `todo` and `todoList` produce output, else they produce nothing.
 todo_include_todos = True
@@ -97,7 +102,7 @@ todo_include_todos = True
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'classic'
+html_theme = "classic"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
@@ -108,7 +113,7 @@ html_theme = 'classic'
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -116,11 +121,11 @@ html_static_path = ['_static']
 # This is required for the alabaster theme
 # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
 html_sidebars = {
-    '**': [
+    "**": [
         # 'about.html',
         # 'navigation.html',
         # 'relations.html',  # needs 'show_related': True theme option to display
-        'searchbox.html',
+        "searchbox.html",
         # 'donate.html',
     ]
 }
@@ -129,7 +134,7 @@ html_sidebars = {
 # -- Options for HTMLHelp output ------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'Moulinettedoc'
+htmlhelp_basename = "Moulinettedoc"
 
 
 # -- Options for LaTeX output ---------------------------------------------
@@ -138,15 +143,12 @@ latex_elements = {
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -156,8 +158,13 @@ latex_elements = {
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'Moulinette.tex', u'Moulinette Documentation',
-     u'YunoHost Collective', 'manual'),
+    (
+        master_doc,
+        "Moulinette.tex",
+        u"Moulinette Documentation",
+        u"YunoHost Collective",
+        "manual",
+    ),
 ]
 
 
@@ -165,10 +172,7 @@ latex_documents = [
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'moulinette', u'Moulinette Documentation',
-     [author], 1)
-]
+man_pages = [(master_doc, "moulinette", u"Moulinette Documentation", [author], 1)]
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -177,13 +181,17 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'Moulinette', u'Moulinette Documentation',
-     author, 'Moulinette', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "Moulinette",
+        u"Moulinette Documentation",
+        author,
+        "Moulinette",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 
-
-
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None}
+intersphinx_mapping = {"https://docs.python.org/": None}
diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py
index f6c6141a..f9431676 100644
--- a/moulinette/interfaces/api.py
+++ b/moulinette/interfaces/api.py
@@ -344,7 +344,7 @@ class _ActionsMapPlugin(object):
             # Append other request params
             req_params = list(request.params.decode().dict.items())
             # TODO test special chars in filename
-            req_params+=list(request.files.dict.items())
+            req_params += list(request.files.dict.items())
             for k, v in req_params:
                 v = _format(v)
                 if k not in params.keys():
diff --git a/setup.py b/setup.py
index e77ecc26..2cad2db9 100755
--- a/setup.py
+++ b/setup.py
@@ -6,54 +6,55 @@ from setuptools import setup, find_packages
 from moulinette.globals import init_moulinette_env
 
 
-LOCALES_DIR = init_moulinette_env()['LOCALES_DIR']
+LOCALES_DIR = init_moulinette_env()["LOCALES_DIR"]
 
 # Extend installation
 locale_files = []
 
 if "install" in sys.argv:
     # Evaluate locale files
-    for f in os.listdir('locales'):
-        if f.endswith('.json'):
-            locale_files.append('locales/%s' % f)
+    for f in os.listdir("locales"):
+        if f.endswith(".json"):
+            locale_files.append("locales/%s" % f)
 
 install_deps = [
-    'argcomplete',
-    'psutil',
-    'pytz',
-    'pyyaml',
-    'toml',
-    'python-ldap',
-    'gevent-websocket',
-    'bottle',
+    "argcomplete",
+    "psutil",
+    "pytz",
+    "pyyaml",
+    "toml",
+    "python-ldap",
+    "gevent-websocket",
+    "bottle",
 ]
 
 test_deps = [
-    'pytest',
-    'pytest-cov',
-    'pytest-env',
-    'pytest-mock',
-    'requests',
-    'requests-mock',
-    'webtest'
+    "pytest",
+    "pytest-cov",
+    "pytest-env",
+    "pytest-mock",
+    "requests",
+    "requests-mock",
+    "webtest",
 ]
 extras = {
-    'install': install_deps,
-    'tests': test_deps,
+    "install": install_deps,
+    "tests": test_deps,
 }
 
 
-setup(name='Moulinette',
-      version='2.0.0',
-      description='Prototype interfaces quickly and easily',
-      author='Yunohost Team',
-      author_email='yunohost@yunohost.org',
-      url='http://yunohost.org',
-      license='AGPL',
-      packages=find_packages(exclude=['test']),
-      data_files=[(LOCALES_DIR, locale_files)],
-      python_requires='>=3.7.*,  <3.8',
-      install_requires=install_deps,
-      tests_require=test_deps,
-      extras_require=extras,
-      )
+setup(
+    name="Moulinette",
+    version="2.0.0",
+    description="Prototype interfaces quickly and easily",
+    author="Yunohost Team",
+    author_email="yunohost@yunohost.org",
+    url="http://yunohost.org",
+    license="AGPL",
+    packages=find_packages(exclude=["test"]),
+    data_files=[(LOCALES_DIR, locale_files)],
+    python_requires=">=3.7.*,  <3.8",
+    install_requires=install_deps,
+    tests_require=test_deps,
+    extras_require=extras,
+)
diff --git a/test/test_auth.py b/test/test_auth.py
index 6a42a5ab..498ecec3 100644
--- a/test/test_auth.py
+++ b/test/test_auth.py
@@ -158,7 +158,9 @@ class TestAuthAPI:
             == "Authentication required"
         )
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_login_ldap(self, moulinette_webapi, ldap_server, mocker):
         mocker.patch(
             "moulinette.authenticators.ldap.Authenticator._get_uri",
diff --git a/test/test_i18n_keys.py b/test/test_i18n_keys.py
index 815549b8..912cf448 100644
--- a/test/test_i18n_keys.py
+++ b/test/test_i18n_keys.py
@@ -38,6 +38,7 @@ def find_expected_string_keys():
                 continue
             yield m
 
+
 ###############################################################################
 #   Load en locale json keys                                                  #
 ###############################################################################
diff --git a/test/test_ldap.py b/test/test_ldap.py
index 236f4b76..64307c37 100644
--- a/test/test_ldap.py
+++ b/test/test_ldap.py
@@ -15,7 +15,9 @@ class TestLDAP:
             "extra": {},
         }
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_simple_bind_with_admin(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
@@ -24,7 +26,9 @@ class TestLDAP:
 
         assert ldap_interface.con
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_simple_bind_with_wrong_user(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"]["user_rdn"] = "cn=yoloswag,dc=yunohost,dc=org"
@@ -37,7 +41,9 @@ class TestLDAP:
         assert expected_msg in str(exception)
         assert ldap_interface.con is None
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_simple_bind_with_rdn_wrong_password(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
@@ -51,7 +57,9 @@ class TestLDAP:
 
         assert ldap_interface.con is None
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_simple_bind_anonymous(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"]["user_rdn"] = ""
@@ -60,7 +68,9 @@ class TestLDAP:
 
         assert ldap_interface.con
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_sasl_non_interactive_bind(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"][
@@ -73,7 +83,9 @@ class TestLDAP:
 
         assert ldap_interface.con
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_authenticate_server_down(self, ldap_server, mocker):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
@@ -99,7 +111,9 @@ class TestLDAP:
             ldap_interface.authenticate(password=password)
         return ldap_interface
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_admin_read(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -120,7 +134,9 @@ class TestLDAP:
         assert list(admin_info.keys()) == ["userPassword"]
         assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_sasl_read(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -142,7 +158,9 @@ class TestLDAP:
         assert list(admin_info.keys()) == ["userPassword"]
         assert admin_info["userPassword"][0].startswith("{CRYPT}$6$")
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_anonymous_read(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface("")
@@ -181,7 +199,9 @@ class TestLDAP:
             "uid=%s,ou=users,dc=yunohost,dc=org" % new_user, attrs=None
         )[0]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_admin_add(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -199,7 +219,9 @@ class TestLDAP:
         assert "inetOrgPerson" in new_user_info["objectClass"]
         assert "posixAccount" in new_user_info["objectClass"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_sasl_add(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -218,7 +240,9 @@ class TestLDAP:
         assert "inetOrgPerson" in new_user_info["objectClass"]
         assert "posixAccount" in new_user_info["objectClass"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_anonymous_add(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface("")
@@ -253,7 +277,9 @@ class TestLDAP:
         assert expected_error in str(exception)
         assert expected_message in str(exception)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_admin_remove(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -262,7 +288,9 @@ class TestLDAP:
 
         self.remove_new_user(ldap_interface)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_sasl_remove(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -272,7 +300,9 @@ class TestLDAP:
 
         self.remove_new_user(ldap_interface)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_anonymous_remove(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface("")
@@ -308,7 +338,9 @@ class TestLDAP:
             "uid=%s,ou=users,dc=yunohost,dc=org" % uid, attrs=None
         )[0]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_admin_update(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -320,7 +352,9 @@ class TestLDAP:
         assert new_user_info["uidNumber"] == ["555"]
         assert new_user_info["gidNumber"] == ["555"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_admin_update_new_rdn(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -332,7 +366,9 @@ class TestLDAP:
         assert new_user_info["uidNumber"] == ["555"]
         assert new_user_info["gidNumber"] == ["555"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_sasl_update(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -345,7 +381,9 @@ class TestLDAP:
         assert new_user_info["uidNumber"] == ["555"]
         assert new_user_info["gidNumber"] == ["555"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_sasl_update_new_rdn(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -357,7 +395,9 @@ class TestLDAP:
         assert new_user_info["uidNumber"] == ["555"]
         assert new_user_info["gidNumber"] == ["555"]
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_anonymous_update(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface("")
@@ -370,7 +410,9 @@ class TestLDAP:
         assert expected_error in str(exception)
         assert expected_message in str(exception)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_anonymous_update_new_rdn(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface("")
@@ -383,7 +425,9 @@ class TestLDAP:
         assert expected_error in str(exception)
         assert expected_message in str(exception)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_empty_update(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -399,7 +443,9 @@ class TestLDAP:
 
         assert ldap_interface.update("uid=%s,ou=users" % uid, new_user_info)
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_get_conflict(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(
@@ -418,7 +464,9 @@ class TestLDAP:
         conflict = ldap_interface.get_conflict({"uid": "not_a_user"})
         assert not conflict
 
-    @pytest.mark.skip(reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway...")
+    @pytest.mark.skip(
+        reason="Not passing because setup issue idk, to be removed or moved to Yunohost soon anyway..."
+    )
     def test_validate_uniqueness(self, ldap_server):
         self.ldap_conf["parameters"]["uri"] = ldap_server.uri
         ldap_interface = self.create_ldap_interface(