def check_pep8(folder, ignore=('E265', 'W504'), skip=None,
               complexity=-1, stop_after=100, fLOG=None,
               pylint_ignore=('C0103', 'C1801',
                              'R0201', 'R1705',
                              'W0108', 'W0613',
                              'W0107', 'C0415',
                              'C0209'),
               recursive=True, neg_pattern=None, extended=None,
               max_line_length=143, pattern=".*[.]py$",
               run_lint=True, verbose=False, run_cmd_filter=None):
    """
    Checks if :epkg:`PEP8`,
    the function calls command :epkg:`pycodestyle`
    on a specific folder.

    @param      folder              folder to look into
    @param      ignore              list of warnings to skip when raising an exception if
                                    :epkg:`PEP8` is not verified, see also
                                    `Error Codes <http://pep8.readthedocs.org/en/latest/intro.html#error-codes>`_
    @param      pylint_ignore       ignore :epkg:`pylint` issues, see
                                    :epkg:`pylint error codes`
    @param      complexity          see `check_file <https://pycodestyle.pycqa.org/en/latest/api.html>`_
    @param      stop_after          stop after *stop_after* issues
    @param      skip                skip a warning if a substring in this list is found
    @param      neg_pattern         skip files verifying this regular expressions
    @param      extended            list of tuple (name, function), see below
    @param      max_line_length     maximum allowed length of a line of code
    @param      recursive           look into subfolder
    @param      pattern             only file matching this pattern will be checked
    @param      run_lint            run :epkg:`pylint`
    @param      verbose             :epkg:`pylint` is slow, tells which file is
                                    investigated (but it is even slower)
    @param      run_cmd_filter      some files makes :epkg:`pylint` crashes (``import yaml``),
                                    the test for this is run in a separate process
                                    if the function *run_cmd_filter* returns True of the filename,
                                    *verbose* is set to True in that case
    @param      fLOG                logging function
    @return                         output

    Functions mentioned in *extended* takes two parameters (file name and line)
    and they returned None or an error message or a tuple (position in the line, error message).
    When the return is not empty, a warning will be added to the ones
    printed by :epkg:`pycodestyle`.
    A few codes to ignore:

    * *E501*: line too long (?? characters)
    * *E265*: block comments should have a space after #
    * *W504*: line break after binary operator, this one is raised
      after the code is modified by @see fn remove_extra_spaces_and_pep8.

    The full list is available at :epkg:`PEP8 codes`. In addition,
    the function adds its own codes:

    * *ECL1*: line too long for a specific reason.

    Some errors to disable with :epkg:`pylint`:

    * *C0103*: variable name is not conform
    * *C0111*: missing function docstring
    * *C1801*: do not use `len(SEQUENCE)` to determine if a sequence is empty
    * *R0201*: method could be a function
    * *R0205*: Class '?' inherits from object, can be safely removed from bases in python3 (pylint)
    * *R0901*: too many ancestors
    * *R0902*: too many instance attributes
    * *R0911*: too many return statements
    * *R0912*: too many branches
    * *R0913*: too many arguments
    * *R0914*: too many local variables
    * *R0915*: too many statements
    * *R1702*: too many nested blocks
    * *R1705*: unnecessary "else" after "return"
    * *W0107*: unnecessary pass statements
    * *W0108*: Lambda may not be necessary
    * *W0613*: unused argument

    The full list is available at :epkg:`pylint error codes`.
    :epkg:`pylint` was added used to check the code.
    It produces the following list of errors
    :epkg:`pylint error codes`.

    If *neg_pattern* is empty, it populates with a default value
    which skips unnecessary folders:
    ``".*[/\\\\\\\\]((_venv)|([.]git)|(__pycache__)|(temp_)).*"``.
    """
    # delayed import to speed up import time of pycode
    import pycodestyle
    from ..filehelper.synchelper import explore_folder_iterfile
    if fLOG is None:
        from ..loghelper.flog import noLOG  # pragma: no cover
        fLOG = noLOG  # pragma: no cover

    def extended_checkings(fname, content, buf, extended):
        for i, line in enumerate(content):
            for name, fu in extended:
                r = fu(fname, line)
                if isinstance(r, tuple):
                    c, r = r
                else:
                    c = 1
                if r is not None:
                    buf.write("{0}:{1}:{4} F{2} {3}\n".format(
                        fname, i + 1, name, r, c))

    def fkeep(s):
        if len(s) == 0:
            return False
        if skip is not None:
            for kip in skip:
                if kip in s:
                    return False
        return True

    if max_line_length is not None:
        if extended is None:
            extended = []
        else:
            extended = extended.copy()

        def check_lenght_line(fname, line):
            if len(line) > max_line_length and not line.lstrip().startswith('#'):
                if ">`_" in line:
                    return "line too long (link) {0} > {1}".format(len(line), max_line_length)
                if ":math:`" in line:
                    return "line too long (:math:) {0} > {1}".format(  # pragma: no cover
                        len(line), max_line_length)
                if "ERROR: " in line:
                    return "line too long (ERROR:) {0} > {1}".format(  # pragma: no cover
                        len(line), max_line_length)
            return None

        extended.append(("[ECL1]", check_lenght_line))

    if ignore is None:
        ignore = tuple()
    elif isinstance(ignore, list):
        ignore = tuple(ignore)

    if neg_pattern is None:
        neg_pattern = ".*[/\\\\]((_venv)|([.]git)|(__pycache__)|(temp_)|([.]egg)|(bin)).*"

    try:
        regneg_filter = None if neg_pattern is None else re.compile(
            neg_pattern)
    except re.error as e:  # pragma: no cover
        raise ValueError("Unable to compile '{0}'".format(neg_pattern)) from e

    # pycodestyle
    fLOG("[check_pep8] code style on '{0}'".format(folder))
    files_to_check = []
    skipped = []
    buf = StringIO()
    with redirect_stdout(buf):
        for file in explore_folder_iterfile(folder, pattern=pattern,
                                            recursive=recursive):
            if regneg_filter is not None:
                if regneg_filter.search(file):
                    skipped.append(file)
                    continue
            if file.endswith("__init__.py"):
                ig = ignore + ('F401',)
            else:
                ig = ignore
            if file is None:
                raise RuntimeError(  # pragma: no cover
                    "file cannot be None")
            if len(file) == 0:
                raise RuntimeError(  # pragma: no cover
                    "file cannot be empty")

            # code style
            files_to_check.append(file)
            try:
                style = pycodestyle.StyleGuide(
                    ignore=ig, complexity=complexity, format='pylint',
                    max_line_length=max_line_length)
                res = style.check_files([file])
            except TypeError as e:  # pragma: no cover
                ext = "This is often due to an instruction from . import... The imported module has no name."
                raise TypeError("Issue with pycodesyle for module '{0}' ig={1} complexity={2}\n{3}".format(
                    file, ig, complexity, ext)) from e

            if extended is not None:
                with open(file, "r", errors="ignore") as f:
                    content = f.readlines()
                extended_checkings(file, content, buf, extended)

            if res.total_errors + res.file_errors > 0:
                res.print_filename = True
                lines = [_ for _ in buf.getvalue().split("\n") if fkeep(_)]
                if len(lines) > stop_after:
                    raise PEP8Exception(  # pragma: no cover
                        "{0} lines\n{1}".format(len(lines), "\n".join(lines)))

    lines = [_ for _ in buf.getvalue().split("\n") if fkeep(_)]
    if len(lines) > 10:
        raise PEP8Exception(  # pragma: no cover
            "{0} lines\n{1}".format(len(lines), "\n".join(lines)))

    if len(files_to_check) == 0:
        mes = skipped[0] if skipped else "-no skipped file-"
        raise FileNotFoundError(  # pragma: no cover
            "No file found in '{0}'\n pattern='{1}'\nskipped='{2}'".format(
                folder, pattern, mes))

    # pylint
    if not run_lint:
        return "\n".join(lines)
    fLOG("[check_pep8] pylint with {0} files".format(len(files_to_check)))
    memout = sys.stdout

    try:
        fLOG('', OutputStream=memout)
        regular_print = False
    except TypeError:  # pragma: no cover
        regular_print = True

    def myprint(s):
        "local print, chooses the right function"
        if regular_print:  # pragma: no cover
            memout.write(s + "\n")
        else:  # pragma: no cover
            fLOG(s, OutputStream=memout)

    neg_pat = ".*temp[0-9]?_.*,doc_.*"
    if neg_pattern is not None:
        neg_pat += ',' + neg_pattern

    if run_cmd_filter is not None:
        verbose = True  # pragma: no cover

    PyLinterRunV = _get_PyLinterRunV()
    sout = StringIO()
    serr = StringIO()
    with redirect_stdout(sout):
        with redirect_stderr(serr):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", DeprecationWarning)
                opt = ["--ignore-patterns=" + neg_pat, "--persistent=n",
                       '--jobs=1', '--suggestion-mode=n', "--score=n",
                       '--max-args=30', '--max-locals=50', '--max-returns=30',
                       '--max-branches=50', '--max-parents=25',
                       '--max-attributes=50', '--min-public-methods=0',
                       '--max-public-methods=100', '--max-bool-expr=10',
                       '--max-statements=200',
                       '--msg-template={abspath}:{line}: {msg_id}: {msg} (pylint)']
                if pylint_ignore:
                    opt.append('--disable=' + ','.join(pylint_ignore))
                if max_line_length:
                    opt.append("--max-line-length=%d" % max_line_length)
                if verbose:  # pragma: no cover
                    for i, name in enumerate(files_to_check):
                        cop = list(opt)
                        cop.append(name)
                        if run_cmd_filter is None or not run_cmd_filter(name):
                            myprint(
                                "[check_pep8] lint file {0}/{1} - '{2}'\n".format(i + 1, len(files_to_check), name))
                            PyLinterRunV(cop, do_exit=False)
                        else:
                            # delayed import to speed up import time of pycode
                            from ..loghelper import run_cmd
                            # runs from command line
                            myprint(
                                "[check_pep8] cmd-lint file {0}/{1} - '{2}'\n".format(i + 1, len(files_to_check), name))
                            cmd = "{0} -m pylint {1}".format(
                                sys.executable, " ".join('"{0}"'.format(_) for _ in cop))
                            out = run_cmd(cmd, wait=True)[0]
                            lines.extend(_ for _ in out.split(
                                '\n') if _.strip('\r '))
                else:
                    opt.extend(files_to_check)
                    PyLinterRunV(opt, do_exit=False)

    pylint_lines = sout.getvalue().split('\n')
    pylint_lines = [
        _ for _ in pylint_lines if (
            '(pylint)' in _ and fkeep(_) and _[0] != ' ' and len(_.split(':')) > 2)]
    pylint_lines = [_ for _ in pylint_lines if not _.startswith(
        "except ") and not _.startswith("else:") and not _.startswith(
        "try:") and "# noqa" not in _]
    lines.extend(pylint_lines)
    if len(lines) > 0:
        raise PEP8Exception(
            "{0} lines\n{1}".format(len(lines), "\n".join(lines)))

    return "\n".join(lines)