Skip to content

Implement a YAML linter hook #358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
46c1ff9
imlement yaml linter hook
ericLemanissier Oct 6, 2021
c02f3c4
CI: install yamllint
ericLemanissier Oct 6, 2021
1e914e6
Update yaml_linter.py
ericLemanissier Oct 6, 2021
f846701
Update yaml_linter.py
ericLemanissier Oct 6, 2021
3cfc6cd
Update yaml_linter.py
ericLemanissier Oct 6, 2021
a470d0d
Update yaml_linter.py
ericLemanissier Oct 6, 2021
cedc758
Update recipe_linter.py
ericLemanissier Oct 6, 2021
3932064
be explicit when yamllint is not installed
ericLemanissier Oct 7, 2021
2a9a972
fix test
ericLemanissier Oct 7, 2021
0876647
lint all yml files
ericLemanissier Oct 8, 2021
5dbb4d1
Update test_yaml_linter.py
ericLemanissier Oct 8, 2021
65b0f5d
Update test_yaml_linter.py
ericLemanissier Oct 8, 2021
58f9019
Update requirements_test.txt
ericLemanissier Oct 8, 2021
9a403d4
Update generate_env_windows.bat
ericLemanissier Oct 8, 2021
bc02165
Update requirements_linux.txt
ericLemanissier Oct 8, 2021
d9a5a3c
Update test_yaml_linter.py
ericLemanissier Oct 8, 2021
a088850
Update recipe_linter.py
ericLemanissier Oct 8, 2021
d99c718
force recreation of tox environment
ericLemanissier Oct 8, 2021
a33d237
Update test_yaml_linter.py
ericLemanissier Oct 8, 2021
b7f9671
Scan also ../config.yml
ericLemanissier Oct 8, 2021
3b1ab55
Update hooks/yaml_linter.py
ericLemanissier Oct 8, 2021
67f855a
don't exit if yamllint is not available
ericLemanissier Oct 14, 2021
f4fdc61
Update yaml_linter.py
ericLemanissier Oct 14, 2021
6d7cc69
Update yaml_linter.py
ericLemanissier Oct 14, 2021
77059c5
Update tox.ini
ericLemanissier Oct 14, 2021
4df32e6
Update README.md
ericLemanissier Oct 15, 2021
9ba6953
Update README.md
ericLemanissier Oct 15, 2021
88abe9b
Merge branch 'master' into yamllinter
ericLemanissier Oct 15, 2021
197247b
Update README.md
ericLemanissier Oct 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Repository to develop **experimental** [Conan](https://conan.io) hooks for Conan
* [Member typo checker](#members-typo-checker)
* [SPDX checker](#spdx-checker)
* [Recipe linter](#recipe-linter)
* [Non ASCII](#non-ascii)
* [YAML linter](#yaml-linter)


## Hook setup
Expand Down Expand Up @@ -176,6 +178,14 @@ Separate KB-H047 from Conan Center, which is no longer required due Python 2.7 d

Validates if `conanfile.py` and `test_package/conanfile.py` contain a non-ascii present, when there is a character, it logs an error.

### [YAML linter](hooks/yaml_linter.py)

This hook runs [yamllint](https://yamllint.readthedocs.io/) over the yaml files
in a recipe before exporting them (it runs in the `pre_export` hook), it can be
really useful to check for typos.

This hook requires additional dependencies to work: `pip install yamllint`.

## License

[MIT License](LICENSE)
63 changes: 63 additions & 0 deletions hooks/yaml_linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# coding=utf-8

import os
import platform
import subprocess

from conans.errors import ConanException
from conans.tools import logger


CONAN_HOOK_YAMLLINT_WERR = "CONAN_YAMLLINT_WERR"


def pre_export(output, conanfile_path, *args, **kwargs):
try:
import yamllint
except ImportError as e:
output.error("Install yamllint to use 'yaml_linter' hook: 'pip install yamllint'")
return
output.info("Lint yaml '{}'".format(conanfile_path))
conanfile_dirname = os.path.dirname(conanfile_path)

rules = {
"document-start": "disable",
"line-length": "disable",
"new-lines": "{level: warning}",
"empty-lines": "{level: warning}",
"indentation": "{level: warning}",
"trailing-spaces": "{level: warning}",
}

lint_args = ['-f', 'parsable',
'-d', '"{extends: default, rules: {%s}}"' %
", ".join("%s: %s" % (r, rules[r]) for r in rules)]
lint_args.append('"%s"' % conanfile_dirname.replace('\\', '/'))
configfile = os.path.join(conanfile_dirname, "..", "config.yml")
if os.path.isfile(configfile):
lint_args.append('"%s"' % configfile.replace('\\', '/'))

try:
command = ['yamllint'] + lint_args
command = " ".join(command)
shell = bool(platform.system() != "Windows")
p = subprocess.Popen(command, shell=shell, bufsize=10,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
yamllint_stdout, yamllint_stderr = p.communicate()
yamllint_stdout = yamllint_stdout.decode('utf-8')
except Exception as exc:
output.error("Unexpected error running linter: {}".format(exc))
return
errors = 0
for line in yamllint_stdout.splitlines():
output.info(line)
i = line.find(":")
line = line[i:]
i = line.find(":")
line = line[i:]
parts = line.split(' ')
errors += int(parts[1] == "[error]")

output.info("YAML Linter detected '{}' errors".format(errors))
if os.getenv(CONAN_HOOK_YAMLLINT_WERR) and errors:
raise ConanException("Package recipe has YAML linter errors. Please fix them.")
3 changes: 2 additions & 1 deletion tests/requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ responses
pluggy==0.11.0
pylint==2.10.2
astroid
spdx_lookup
spdx_lookup
yamllint
74 changes: 74 additions & 0 deletions tests/test_hooks/test_yaml_linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# coding=utf-8

import os
import textwrap
import unittest

from parameterized import parameterized

from conans import tools
from conans.client.command import ERROR_GENERAL, SUCCESS
from conans.tools import environment_append
from tests.utils.test_cases.conan_client import ConanClientTestCase


class YAMLLinterTests(ConanClientTestCase):
conanfile = textwrap.dedent(r"""
from conans import ConanFile, tools

class TestConan(ConanFile):
name = "name"
version = "version"
""")

def _get_environ(self, **kwargs):
kwargs = super(YAMLLinterTests, self)._get_environ(**kwargs)
kwargs.update({'CONAN_HOOKS': os.path.join(os.path.dirname(
__file__), '..', '..', 'hooks', 'yaml_linter')})
return kwargs

@parameterized.expand([(False, ), (True, )])
def test_basic(self, yamllint_werr):
conandatafile = textwrap.dedent(r"""
sources:
"version":
url: "https://url.to/name/version.tar.xz"
sha256: "3a530d1b243b5dec00bc54937455471aaa3e56849d2593edb8ded07228202240"
patches:
"version":
- patch_file: "patches/abcdef.diff"
base_path: "source"
patches:
""")
tools.save('conanfile.py', content=self.conanfile)
tools.save('conandata.yml', content=conandatafile)
yamllint_werr_value = "1" if yamllint_werr else None
with environment_append({"CONAN_YAMLLINT_WERR": yamllint_werr_value}):
return_code = ERROR_GENERAL if yamllint_werr else SUCCESS
output = self.conan(['export', '.', 'name/version@'], expected_return_code=return_code)

if yamllint_werr:
self.assertIn("pre_export(): Package recipe has YAML linter errors."
" Please fix them.", output)

self.assertIn("conandata.yml:10:1:"
" [error] duplication of key \"patches\" in mapping (key-duplicates)",
output)

def test_path_with_spaces(self):
conandatafile = textwrap.dedent(r"""
sources:
"version":
url: "https://url.to/name/version.tar.xz"
sha256: "3a530d1b243b5dec00bc54937455471aaa3e56849d2593edb8ded07228202240"
patches:
"version":
- patch_file: "patches/abcdef.diff"
base_path: "source"
""")
tools.save(os.path.join("path spaces", "conanfile.py"), content=self.conanfile)
tools.save(os.path.join("path spaces", "conandata.py"), content=conandatafile)
output = self.conan(['export', 'path spaces/conanfile.py', 'name/version@'])
recipe_path = os.path.join(os.getcwd(), "path spaces", "conanfile.py")
self.assertIn("pre_export(): Lint yaml '{}'".format(recipe_path), output)
self.assertIn("pre_export(): YAML Linter detected '0' errors", output)