diff options
49 files changed, 1449 insertions, 183 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7e55e6a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,62 @@ +--- + +name: CI + +on: # yamllint disable-line rule:truthy + push: + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + lint: + name: Linters + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + - run: + pip install flake8 flake8-import-order sphinx sphinx_rtd_theme + rstcheck[sphinx] doc8 + - run: pip install . + - run: flake8 . + - run: doc8 $(git ls-files '*.rst') + - run: rstcheck --ignore-directives automodule $(git ls-files '*.rst') + - run: yamllint --strict $(git ls-files '*.yaml' '*.yml') + - run: make -C docs html + - name: Check for broken links in documentation + run: make -C docs linkcheck + + test: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Append GitHub Actions system path + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - run: pip install coverage + - run: pip install . + # https://github.com/AndreMiras/coveralls-python-action/issues/18 + - run: echo -e "[run]\nrelative_files = True" > .coveragerc + - run: coverage run -m unittest discover + - name: Coveralls + uses: AndreMiras/coveralls-python-action@develop diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58d609b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ +*.py[cod] +/docs/_build +/dist +/yamllint.egg-info +/build +/.eggs diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..4d6a254 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,11 @@ +--- +version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.12" +sphinx: + configuration: docs/conf.py +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0b847d2..c73e882 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,29 @@ Changelog ========= +1.35.1 (2024-02-16) +------------------- + +- Restore ignoration of files passed as command-line arguments +- Revert API change from version 1.35.0 + +1.35.0 (2024-02-15) +------------------- + +- Fix failure on broken symlinks that should be ignored +- API change: ``linter.run(stream, config)`` doesn't filter files anymore +- Docs: Restore official Read the Docs theme + +1.34.0 (2024-02-06) +------------------- + +- Config: validate ``ignore-from-file`` inside rules +- Rule ``quoted-strings``: fix ``only-when-needed`` in flow maps and sequences +- Rule ``key-duplicates``: add ``forbid-duplicated-merge-keys`` option +- Rule ``quoted-strings``: add ``check-keys`` option +- Docs: add GitLab CI example +- Rule ``truthy``: adapt forbidden values based on YAML version + 1.33.0 (2023-11-09) ------------------- diff --git a/docs/conf.py b/docs/conf.py index 2c40177..8a03b2f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,17 +1,17 @@ # yamllint documentation build configuration file, created by # sphinx-quickstart on Thu Jan 21 21:18:52 2016. -import sys import os +import sys from unittest.mock import MagicMock sys.path.insert(0, os.path.abspath('..')) - -from yamllint import __copyright__, APP_NAME, APP_VERSION # noqa +from yamllint import __copyright__, APP_NAME, APP_VERSION # noqa: I001, E402 # -- General configuration ------------------------------------------------ extensions = [ + 'sphinx_rtd_theme', 'sphinx.ext.autodoc', ] @@ -29,7 +29,7 @@ pygments_style = 'sphinx' # -- Options for HTML output ---------------------------------------------- -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' htmlhelp_basename = 'yamllintdoc' diff --git a/docs/disable_with_comments.rst b/docs/disable_with_comments.rst index a973da6..6fcd221 100644 --- a/docs/disable_with_comments.rst +++ b/docs/disable_with_comments.rst @@ -12,11 +12,11 @@ line above. For instance: # The following mapping contains the same key twice, # but I know what I'm doing: - key: value 1 - key: value 2 # yamllint disable-line rule:key-duplicates + - key: value 1 + key: value 2 # yamllint disable-line rule:key-duplicates - This line is waaaaaaaaaay too long but yamllint will not report anything about it. # yamllint disable-line rule:line-length - This line will be checked by yamllint. + - This line will be checked by yamllint. or: @@ -24,13 +24,13 @@ or: # The following mapping contains the same key twice, # but I know what I'm doing: - key: value 1 - # yamllint disable-line rule:key-duplicates - key: value 2 + - key: value 1 + # yamllint disable-line rule:key-duplicates + key: value 2 # yamllint disable-line rule:line-length - This line is waaaaaaaaaay too long but yamllint will not report anything about it. - This line will be checked by yamllint. + - This line will be checked by yamllint. It is possible, although not recommend, to disabled **all** rules for a specific line: @@ -90,8 +90,8 @@ For instance: # yamllint disable-file # The following mapping contains the same key twice, but I know what I'm doing: - key: value 1 - key: value 2 + - key: value 1 + key: value 2 - This line is waaaaaaaaaay too long but yamllint will not report anything about it. diff --git a/docs/integration.rst b/docs/integration.rst index 9a6a935..e805667 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -9,15 +9,15 @@ Here is an example, to add in your .pre-commit-config.yaml .. code:: yaml - --- - # Update the rev variable with the release version that you want, from the yamllint repo - # You can pass your custom .yamllint with args attribute. - repos: - - repo: https://github.com/adrienverge/yamllint.git - rev: v1.29.0 - hooks: - - id: yamllint - args: [--strict, -c=/path/to/.yamllint] + --- + # Update the rev variable with the release version that you want, from the yamllint repo + # You can pass your custom .yamllint with args attribute. + repos: + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.29.0 + hooks: + - id: yamllint + args: [--strict, -c=/path/to/.yamllint] Integration with GitHub Actions @@ -32,20 +32,62 @@ A minimal example workflow using GitHub Actions: .. code:: yaml - --- - on: push # yamllint disable-line rule:truthy + --- + on: push # yamllint disable-line rule:truthy - jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - - name: Install yamllint - run: pip install yamllint + - name: Install yamllint + run: pip install yamllint - - name: Lint YAML files - run: yamllint . + - name: Lint YAML files + run: yamllint . + +Integration with GitLab +----------------------- + +You can use the following GitLab CI/CD stage to run yamllint and get the +results as a `Code quality (Code Climate) +<https://docs.gitlab.com/ee/ci/testing/code_quality.html>` report. + +.. code:: yaml + + --- + lint: + stage: lint + script: + - pip install yamllint + - mkdir reports + - > + yamllint -f parsable . | tee >(awk ' + BEGIN {FS = ":"; ORS="\n"; first=1} + { + gsub(/^[ \t]+|[ \t]+$|"/, "", $4); + match($4, /^\[(warning|error)\](.*)\((.*)\)$/, a); + sev = (a[1] == "error" ? "major" : "minor"); + if (first) { + first=0; + printf("["); + } else { + printf(","); + } + printf("{\"location\":{\"path\":\"%s\",\"lines\":{\"begin\":%s",\ + "\"end\":%s}},\"severity\":\"%s\",\"check_name\":\"%s\","\ + "\"categories\":[\"Style\"],\"type\":\"issue\","\ + "\"description\":\"%s\"}", $1, $2, $3, sev, a[3], a[2]); + } + END { if (!first) printf("]\n"); }' > reports/codequality.json) + artifacts: + when: always + paths: + - reports + expire_in: 1 week + reports: + codequality: reports/codequality.json Integration with Arcanist ------------------------- @@ -55,13 +97,13 @@ You can configure yamllint to run on ``arc lint``. Here is an example .. code:: json - { - "linters": { - "yamllint": { - "type": "script-and-regex", - "script-and-regex.script": "yamllint", - "script-and-regex.regex": "/^(?P<line>\\d+):(?P<offset>\\d+) +(?P<severity>warning|error) +(?P<message>.*) +\\((?P<name>.*)\\)$/m", - "include": "(\\.(yml|yaml)$)" - } - } - } + { + "linters": { + "yamllint": { + "type": "script-and-regex", + "script-and-regex.script": "yamllint", + "script-and-regex.regex": "/^(?P<line>\\d+):(?P<offset>\\d+) +(?P<severity>warning|error) +(?P<message>.*) +\\((?P<name>.*)\\)$/m", + "include": "(\\.(yml|yaml)$)" + } + } + } diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..f5a3564 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx-rtd-theme >=2.0.0 diff --git a/tests/__init__.py b/tests/__init__.py index da1cd75..d8c46fc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -15,5 +15,4 @@ import locale - locale.setlocale(locale.LC_ALL, 'C') diff --git a/tests/common.py b/tests/common.py index 65af63b..29dcfb9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,15 +14,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import contextlib +from io import StringIO import os import shutil +import sys import tempfile import unittest import yaml -from yamllint.config import YamlLintConfig from yamllint import linter +from yamllint.config import YamlLintConfig class RuleTestCase(unittest.TestCase): @@ -54,6 +56,33 @@ class RuleTestCase(unittest.TestCase): self.assertEqual(real_problems, expected_problems) +class RunContext: + """Context manager for ``cli.run()`` to capture exit code and streams.""" + + def __init__(self, case): + self.stdout = self.stderr = None + self._raises_ctx = case.assertRaises(SystemExit) + + def __enter__(self): + self._raises_ctx.__enter__() + self.old_sys_stdout = sys.stdout + self.old_sys_stderr = sys.stderr + sys.stdout = self.outstream = StringIO() + sys.stderr = self.errstream = StringIO() + return self + + def __exit__(self, *exc_info): + self.stdout = self.outstream.getvalue() + self.stderr = self.errstream.getvalue() + sys.stdout = self.old_sys_stdout + sys.stderr = self.old_sys_stderr + return self._raises_ctx.__exit__(*exc_info) + + @property + def returncode(self): + return self._raises_ctx.exception.code + + def build_temp_workspace(files): tempdir = tempfile.mkdtemp(prefix='yamllint-tests-') @@ -62,8 +91,10 @@ def build_temp_workspace(files): if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) - if type(content) is list: + if isinstance(content, list): os.mkdir(path) + elif isinstance(content, str) and content.startswith('symlink://'): + os.symlink(content[10:], path) else: mode = 'wb' if isinstance(content, bytes) else 'w' with open(path, mode) as f: diff --git a/tests/rules/test_indentation.py b/tests/rules/test_indentation.py index 1c6eddb..b08ada7 100644 --- a/tests/rules/test_indentation.py +++ b/tests/rules/test_indentation.py @@ -15,7 +15,7 @@ from tests.common import RuleTestCase -from yamllint.parser import token_or_comment_generator, Comment +from yamllint.parser import Comment, token_or_comment_generator from yamllint.rules.indentation import check @@ -50,8 +50,8 @@ class IndentationStackTestCase(RuleTestCase): .replace('Mapping', 'Map')) if token_type in ('StreamStart', 'StreamEnd'): continue - output += '{:>9} {}\n'.format(token_type, - self.format_stack(context['stack'])) + stack = self.format_stack(context['stack']) + output += f'{token_type:>9} {stack}\n' return output def test_simple_mapping(self): @@ -192,7 +192,7 @@ class IndentationStackTestCase(RuleTestCase): 'BMapStart B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n' ' Key B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n' ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2\n' - ' Value B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:4\n' # noqa + ' Value B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2 KEY:2 VAL:4\n' # noqa: E501 ' Scalar B_MAP:0 KEY:0 VAL:2 B_SEQ:0 B_ENT:2 B_MAP:2\n' ' BEnd B_MAP:0\n' # missing BEnd here diff --git a/tests/rules/test_key_duplicates.py b/tests/rules/test_key_duplicates.py index 3f8a9e6..c1704e6 100644 --- a/tests/rules/test_key_duplicates.py +++ b/tests/rules/test_key_duplicates.py @@ -179,3 +179,57 @@ class KeyDuplicatesTestCase(RuleTestCase): '[\n' ' flow: sequence, with, key: value, mappings\n' ']\n', conf) + + def test_forbid_duplicated_merge_keys(self): + conf = 'key-duplicates: {forbid-duplicated-merge-keys: true}' + self.check('---\n' + 'Multiple Merge Keys are NOT OK:\n' + 'anchor_one: &anchor_one\n' + ' one: one\n' + 'anchor_two: &anchor_two\n' + ' two: two\n' + 'anchor_reference:\n' + ' <<: *anchor_one\n' + ' <<: *anchor_two\n', conf, problem=(9, 3)) + self.check('---\n' + 'Multiple Merge Keys are NOT OK:\n' + 'anchor_one: &anchor_one\n' + ' one: one\n' + 'anchor_two: &anchor_two\n' + ' two: two\n' + 'anchor_three: &anchor_three\n' + ' two: three\n' + 'anchor_reference:\n' + ' <<: *anchor_one\n' + ' <<: *anchor_two\n' + ' <<: *anchor_three\n', conf, + problem1=(11, 3), problem2=(12, 3)) + self.check('---\n' + 'Multiple Merge Keys are NOT OK:\n' + 'anchor_one: &anchor_one\n' + ' one: one\n' + 'anchor_two: &anchor_two\n' + ' two: two\n' + 'anchor_reference:\n' + ' a: 1\n' + ' <<: *anchor_one\n' + ' b: 2\n' + ' <<: *anchor_two\n', conf, problem=(11, 3)) + self.check('---\n' + 'Single Merge Key is OK:\n' + 'anchor_one: &anchor_one\n' + ' one: one\n' + 'anchor_two: &anchor_two\n' + ' two: two\n' + 'anchor_reference:\n' + ' <<: [*anchor_one, *anchor_two]\n', conf) + self.check('---\n' + 'Duplicate keys without Merge Keys:\n' + ' key: a\n' + ' otherkey: b\n' + ' key: c\n', conf, + problem=(5, 3)) + self.check('---\n' + 'No Merge Keys:\n' + ' key: a\n' + ' otherkey: b\n', conf) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 543cc0d..1bcb6f8 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -18,7 +18,7 @@ from tests.common import RuleTestCase from yamllint import config -class QuotedTestCase(RuleTestCase): +class QuotedValuesTestCase(RuleTestCase): rule_id = 'quoted-strings' def test_disabled(self): @@ -57,9 +57,14 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails - conf, problem1=(4, 10), problem2=(17, 5), - problem3=(19, 12), problem4=(20, 15)) + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', + conf, problem1=(4, 10), problem2=(17, 5), problem3=(19, 12), + problem4=(20, 15), problem5=(21, 13), problem6=(22, 16), + problem7=(23, 19), problem8=(23, 28), problem9=(24, 20)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -97,11 +102,19 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' # fails ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(4, 10), problem2=(5, 10), problem3=(6, 10), problem4=(7, 10), problem5=(17, 5), problem6=(18, 5), problem7=(19, 12), problem8=(19, 17), problem9=(20, 15), - problem10=(20, 23)) + problem10=(20, 23), problem11=(21, 13), problem12=(21, 18), + problem13=(21, 29), problem14=(21, 41), problem15=(22, 16), + problem16=(22, 24), problem17=(23, 19), problem18=(23, 28), + problem19=(23, 33), problem20=(24, 20), problem21=(24, 30), + problem22=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -139,9 +152,15 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5), - problem4=(19, 12), problem5=(20, 15)) + problem4=(19, 12), problem5=(20, 15), problem6=(21, 13), + problem7=(22, 16), problem8=(23, 19), problem9=(23, 28), + problem10=(24, 20)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -179,7 +198,11 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf) self.check('---\n' 'multiline string 1: |\n' @@ -218,9 +241,16 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), - problem4=(18, 5), problem5=(19, 17), problem6=(20, 23)) + problem4=(18, 5), problem5=(19, 17), problem6=(20, 23), + problem7=(21, 18), problem8=(21, 29), problem9=(21, 41), + problem10=(22, 24), problem11=(23, 33), problem12=(24, 30), + problem13=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -258,7 +288,11 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5), problem4=(19, 17), problem5=(20, 23)) self.check('---\n' @@ -299,10 +333,15 @@ class QuotedTestCase(RuleTestCase): ' - foo\n' ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar"]\n' # fails + 'flow-map2: {a: foo, b: "foo,bar"}\n' # fails + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), problem4=(8, 10), problem5=(18, 5), problem6=(19, 17), - problem7=(20, 23)) + problem7=(20, 23), problem8=(21, 18), problem9=(22, 24), + problem10=(23, 33), problem11=(24, 30), problem12=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -556,3 +595,722 @@ class QuotedTestCase(RuleTestCase): "foo1: '[barbaz]'\n" "foo2: '[bar\"baz]'\n", conf) + + +class QuotedKeysTestCase(RuleTestCase): + rule_id = 'quoted-strings' + + def test_disabled(self): + conf_disabled = "quoted-strings: {}" + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf_disabled) + + def test_default(self): + # Default configuration, but with check-keys + conf_default = ("quoted-strings:\n" + " check-keys: true\n") + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf_default, problem1=(4, 1), + problem3=(20, 3), problem4=(23, 2), problem5=(33, 3)) + + def test_quote_type_any(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: any\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(4, 1), problem2=(20, 3), problem3=(23, 2), + problem4=(33, 3)) + + def test_quote_type_single(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(4, 1), problem2=(5, 1), problem3=(6, 1), + problem4=(7, 1), problem5=(20, 3), problem6=(21, 3), + problem7=(23, 2), problem8=(23, 10), problem9=(33, 3), + problem10=(37, 3)) + + def test_quote_type_double(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(4, 1), problem2=(8, 1), problem3=(20, 3), + problem4=(23, 2), problem5=(33, 3)) + + def test_any_quotes_not_required(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: any\n' + ' required: false\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf) + + def test_single_quotes_not_required(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: false\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(5, 1), problem2=(6, 1), problem3=(7, 1), + problem4=(21, 3), problem5=(23, 10), problem6=(37, 3)) + + def test_only_when_needed(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: only-when-needed\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(5, 1), problem2=(8, 1), problem3=(21, 3), + problem4=(23, 10), problem5=(37, 3)) + + def test_only_when_needed_single_quotes(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: only-when-needed\n') + + key_strings = ('---\n' + 'true: 2\n' + '123: 3\n' + 'foo1: 4\n' + '"foo2": 5\n' + '"false": 6\n' + '"234": 7\n' + '\'bar\': 8\n' + '!!str generic_string: 9\n' + '!!str 456: 10\n' + '!!str "quoted_generic_string": 11\n' + '!!binary binstring: 12\n' + '!!int int_string: 13\n' + '!!bool bool_string: 14\n' + '!!bool "quoted_bool_string": 15\n' + # Sequences and mappings + '? - 16\n' + ' - 17\n' + ': 18\n' + '[119, 219]: 19\n' + '? a: 20\n' + ' "b": 21\n' + ': 22\n' + '{a: 123, "b": 223}: 23\n' + # Multiline strings + '? |\n' + ' line 1\n' + ' line 2\n' + ': 27\n' + '? >\n' + ' line 1\n' + ' line 2\n' + ': 31\n' + '?\n' + ' line 1\n' + ' line 2\n' + ': 35\n' + '?\n' + ' "line 1\\\n' + ' line 2"\n' + ': 39\n') + self.check(key_strings, conf, + problem1=(5, 1), problem2=(6, 1), problem3=(7, 1), + problem4=(8, 1), problem5=(21, 3), problem6=(23, 10), + problem7=(37, 3)) + + def test_only_when_needed_corner_cases(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: only-when-needed\n') + + self.check('---\n' + '"": 2\n' + '"- item": 3\n' + '"key: value": 4\n' + '"%H:%M:%S": 5\n' + '"%wheel ALL=(ALL) NOPASSWD: ALL": 6\n' + '\'"quoted"\': 7\n' + '"\'foo\' == \'bar\'": 8\n' + '"\'Mac\' in ansible_facts.product_name": 9\n' + '\'foo # bar\': 10\n', + conf) + self.check('---\n' + '"": 2\n' + '"- item": 3\n' + '"key: value": 4\n' + '"%H:%M:%S": 5\n' + '"%wheel ALL=(ALL) NOPASSWD: ALL": 6\n' + '\'"quoted"\': 7\n' + '"\'foo\' == \'bar\'": 8\n' + '"\'Mac\' in ansible_facts.product_name": 9\n', + conf) + + self.check('---\n' + '---: 2\n' + '"----": 3\n' # fails + '---------: 4\n' + '"----------": 5\n' # fails + ':wq: 6\n' + '":cw": 7\n', # fails + conf, problem1=(3, 1), problem2=(5, 1), problem3=(7, 1)) + + def test_only_when_needed_extras(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: true\n' + ' extra-allowed: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: true\n' + ' extra-required: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: false\n' + ' extra-allowed: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: true\n') + self.check('---\n' + '123: 2\n' + '"234": 3\n' + 'localhost: 4\n' # fails + '"host.local": 5\n' + 'http://localhost: 6\n' # fails + '"http://host.local": 7\n' + 'ftp://localhost: 8\n' # fails + '"ftp://host.local": 9\n', + conf, problem1=(4, 1), problem2=(6, 1), problem3=(8, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: only-when-needed\n' + ' extra-allowed: [^ftp://]\n' + ' extra-required: [^http://]\n') + self.check('---\n' + '123: 2\n' + '"234": 3\n' + 'localhost: 4\n' + '"host.local": 5\n' # fails + 'http://localhost: 6\n' # fails + '"http://host.local": 7\n' + 'ftp://localhost: 8\n' + '"ftp://host.local": 9\n', + conf, problem1=(5, 1), problem2=(6, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: false\n' + ' extra-required: [^http://, ^ftp://]\n') + self.check('---\n' + '123: 2\n' + '"234": 3\n' + 'localhost: 4\n' + '"host.local": 5\n' + 'http://localhost: 6\n' # fails + '"http://host.local": 7\n' + 'ftp://localhost: 8\n' # fails + '"ftp://host.local": 9\n', + conf, problem1=(6, 1), problem2=(8, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: only-when-needed\n' + ' extra-allowed: [^ftp://, ";$", " "]\n') + self.check('---\n' + 'localhost: 2\n' + '"host.local": 3\n' # fails + 'ftp://localhost: 4\n' + '"ftp://host.local": 5\n' + 'i=i+1: 6\n' + '"i=i+2": 7\n' # fails + 'i=i+3;: 8\n' + '"i=i+4;": 9\n' + 'foo1: 10\n' + '"foo2": 11\n' # fails + 'foo bar1: 12\n' + '"foo bar2": 13\n', + conf, problem1=(3, 1), problem2=(7, 1), problem3=(11, 1)) + + def test_octal_values(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' required: true\n') + + self.check('---\n' + '100: 2\n' + '0100: 3\n' + '0o100: 4\n' + '777: 5\n' + '0777: 6\n' + '0o777: 7\n' + '800: 8\n' + '0800: 9\n' # fails + '0o800: 10\n' # fails + '"0900": 11\n' + '"0o900": 12\n', + conf, + problem1=(9, 1), problem2=(10, 1)) + + def test_allow_quoted_quotes(self): + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: false\n' + ' allow-quoted-quotes: false\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: false\n' + ' allow-quoted-quotes: true\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: true\n' + ' allow-quoted-quotes: false\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: true\n' + ' allow-quoted-quotes: true\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: only-when-needed\n' + ' allow-quoted-quotes: false\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: single\n' + ' required: only-when-needed\n' + ' allow-quoted-quotes: true\n') + self.check('---\n' + '"[barbaz]": 2\n' # fails + '"[bar\'baz]": 3\n', + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: false\n' + ' allow-quoted-quotes: false\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: false\n' + ' allow-quoted-quotes: true\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: true\n' + ' allow-quoted-quotes: false\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: true\n' + ' allow-quoted-quotes: true\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: only-when-needed\n' + ' allow-quoted-quotes: false\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", # fails + conf, problem1=(2, 1), problem2=(3, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: double\n' + ' required: only-when-needed\n' + ' allow-quoted-quotes: true\n') + self.check("---\n" + "'[barbaz]': 2\n" # fails + "'[bar\"baz]': 3\n", + conf, problem1=(2, 1)) + + conf = ('quoted-strings:\n' + ' check-keys: true\n' + ' quote-type: any\n') + self.check("---\n" + "'[barbaz]': 2\n" + "'[bar\"baz]': 3\n", + conf) diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index c8c8b7a..e485d07 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -27,7 +27,8 @@ class TruthyTestCase(RuleTestCase): 'True: 1\n', conf) def test_enabled(self): - conf = 'truthy: enable\n' + conf = ('truthy: enable\n' + 'document-start: disable\n') self.check('---\n' '1: True\n' 'True: 1\n', @@ -35,7 +36,8 @@ class TruthyTestCase(RuleTestCase): self.check('---\n' '1: "True"\n' '"True": 1\n', conf) - self.check('---\n' + self.check('%YAML 1.1\n' + '---\n' '[\n' ' true, false,\n' ' "false", "FALSE",\n' @@ -44,9 +46,47 @@ class TruthyTestCase(RuleTestCase): ' on, OFF,\n' ' NO, Yes\n' ']\n', conf, - problem1=(6, 3), problem2=(6, 9), - problem3=(7, 3), problem4=(7, 7), - problem5=(8, 3), problem6=(8, 7)) + problem1=(7, 3), problem2=(7, 9), + problem3=(8, 3), problem4=(8, 7), + problem5=(9, 3), problem6=(9, 7)) + self.check('y: 1\n' + 'yes: 2\n' + 'on: 3\n' + 'true: 4\n' + 'True: 5\n' + '...\n' + '%YAML 1.2\n' + '---\n' + 'y: 1\n' + 'yes: 2\n' + 'on: 3\n' + 'true: 4\n' + 'True: 5\n' + '...\n' + '%YAML 1.1\n' + '---\n' + 'y: 1\n' + 'yes: 2\n' + 'on: 3\n' + 'true: 4\n' + 'True: 5\n' + '---\n' + 'y: 1\n' + 'yes: 2\n' + 'on: 3\n' + 'true: 4\n' + 'True: 5\n', + conf, + problem1=(2, 1), + problem2=(3, 1), + problem3=(5, 1), + problem4=(13, 1), + problem5=(18, 1), + problem6=(19, 1), + problem7=(21, 1), + problem8=(24, 1), + problem9=(25, 1), + problem10=(27, 1)) def test_different_allowed_values(self): conf = ('truthy:\n' @@ -56,15 +96,16 @@ class TruthyTestCase(RuleTestCase): 'key2: yes\n' 'key3: bar\n' 'key4: no\n', conf) - self.check('---\n' + self.check('%YAML 1.1\n' + '---\n' 'key1: true\n' 'key2: Yes\n' 'key3: false\n' 'key4: no\n' 'key5: yes\n', conf, - problem1=(2, 7), problem2=(3, 7), - problem3=(4, 7)) + problem1=(3, 7), problem2=(4, 7), + problem3=(5, 7)) def test_combined_allowed_values(self): conf = ('truthy:\n' @@ -81,6 +122,22 @@ class TruthyTestCase(RuleTestCase): 'key4: no\n' 'key5: yes\n', conf, problem1=(3, 7)) + self.check('%YAML 1.1\n' + '---\n' + 'key1: true\n' + 'key2: Yes\n' + 'key3: false\n' + 'key4: no\n' + 'key5: yes\n', + conf, problem1=(4, 7)) + self.check('%YAML 1.2\n' + '---\n' + 'key1: true\n' + 'key2: Yes\n' + 'key3: false\n' + 'key4: no\n' + 'key5: yes\n', + conf) def test_no_allowed_values(self): conf = ('truthy:\n' @@ -95,6 +152,21 @@ class TruthyTestCase(RuleTestCase): 'key4: no\n', conf, problem1=(2, 7), problem2=(3, 7), problem3=(4, 7), problem4=(5, 7)) + self.check('%YAML 1.1\n' + '---\n' + 'key1: true\n' + 'key2: yes\n' + 'key3: false\n' + 'key4: no\n', conf, + problem1=(3, 7), problem2=(4, 7), + problem3=(5, 7), problem4=(6, 7)) + self.check('%YAML 1.2\n' + '---\n' + 'key1: true\n' + 'key2: yes\n' + 'key3: false\n' + 'key4: no\n', conf, + problem1=(3, 7), problem2=(5, 7)) def test_explicit_types(self): conf = 'truthy: enable\n' diff --git a/tests/test_cli.py b/tests/test_cli.py index 444f2f9..e0ae0fe 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from io import StringIO import fcntl import locale import os @@ -22,34 +21,11 @@ import shutil import sys import tempfile import unittest +from io import StringIO -from tests.common import build_temp_workspace, temp_workspace - -from yamllint import cli -from yamllint import config - - -class RunContext: - """Context manager for ``cli.run()`` to capture exit code and streams.""" - - def __init__(self, case): - self.stdout = self.stderr = None - self._raises_ctx = case.assertRaises(SystemExit) - - def __enter__(self): - self._raises_ctx.__enter__() - sys.stdout = self.outstream = StringIO() - sys.stderr = self.errstream = StringIO() - return self - - def __exit__(self, *exc_info): - self.stdout, sys.stdout = self.outstream.getvalue(), sys.__stdout__ - self.stderr, sys.stderr = self.errstream.getvalue(), sys.__stderr__ - return self._raises_ctx.__exit__(*exc_info) +from tests.common import build_temp_workspace, RunContext, temp_workspace - @property - def returncode(self): - return self._raises_ctx.exception.code +from yamllint import cli, config # Check system's UTF-8 availability @@ -57,9 +33,30 @@ def utf8_available(): try: locale.setlocale(locale.LC_ALL, 'C.UTF-8') locale.setlocale(locale.LC_ALL, (None, None)) - return True except locale.Error: # pragma: no cover return False + else: + return True + + +def setUpModule(): + # yamllint uses these environment variables to find a config file. + env_vars_that_could_interfere = ( + 'YAMLLINT_CONFIG_FILE', + 'XDG_CONFIG_HOME', + # These variables are used to determine where the user’s home + # directory is. See + # https://docs.python.org/3/library/os.path.html#os.path.expanduser + 'HOME', + 'USERPROFILE', + 'HOMEPATH', + 'HOMEDRIVE' + ) + for name in env_vars_that_could_interfere: + try: + del os.environ[name] + except KeyError: + pass class CommandLineTestCase(unittest.TestCase): @@ -88,6 +85,9 @@ class CommandLineTestCase(unittest.TestCase): 'key: other value\n', # empty dir 'empty-dir': [], + # symbolic link + 'symlinks/file-without-yaml-extension': '42\n', + 'symlinks/link.yaml': 'symlink://file-without-yaml-extension', # non-YAML file 'no-yaml.json': '---\n' 'key: value\n', @@ -116,8 +116,6 @@ class CommandLineTestCase(unittest.TestCase): shutil.rmtree(cls.wd) - @unittest.skipIf(not utf8_available() and sys.version_info < (3, 7), - 'UTF-8 paths not supported') def test_find_files_recursively(self): conf = config.YamlLintConfig('extends: default') self.assertEqual( @@ -130,6 +128,7 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 'sub/directory.yaml/empty.yml'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')], ) @@ -167,6 +166,7 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 'en.yaml'), os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')] ) @@ -204,6 +204,8 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 'sub/directory.yaml/empty.yml'), os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/file-without-yaml-extension'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')] ) @@ -225,6 +227,8 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 'sub/directory.yaml/empty.yml'), os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/file-without-yaml-extension'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')] ) @@ -306,14 +310,13 @@ class CommandLineTestCase(unittest.TestCase): cli.run(('-c', f.name, os.path.join(self.wd, 'a.yaml'))) self.assertEqual(ctx.returncode, 1) - @unittest.skipIf(os.environ.get('GITHUB_RUN_ID'), '$HOME not overridable') def test_run_with_user_global_config_file(self): home = os.path.join(self.wd, 'fake-home') dir = os.path.join(home, '.config', 'yamllint') os.makedirs(dir) config = os.path.join(dir, 'config') - self.addCleanup(os.environ.update, HOME=os.environ['HOME']) + self.addCleanup(os.environ.__delitem__, 'HOME') os.environ['HOME'] = home with open(config, 'w') as f: @@ -690,6 +693,7 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 'sub/directory.yaml/empty.yml'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')] ) @@ -706,9 +710,29 @@ class CommandLineTestCase(unittest.TestCase): os.path.join(self.wd, 's/s/s/s/s/s/s/s/s/s/s/s/s/s/s/file.yaml'), os.path.join(self.wd, 'sub/directory.yaml/not-yaml.txt'), os.path.join(self.wd, 'sub/ok.yaml'), + os.path.join(self.wd, 'symlinks/link.yaml'), os.path.join(self.wd, 'warn.yaml')] ) + config = 'ignore: ["*.yaml", "*.yml", "!a.yaml"]' + with RunContext(self) as ctx: + cli.run(('--list-files', '-d', config, self.wd)) + self.assertEqual(ctx.returncode, 0) + self.assertEqual( + sorted(ctx.stdout.splitlines()), + [os.path.join(self.wd, 'a.yaml')] + ) + with RunContext(self) as ctx: + cli.run(('--list-files', '-d', config, + os.path.join(self.wd, 'a.yaml'), + os.path.join(self.wd, 'en.yaml'), + os.path.join(self.wd, 'c.yaml'))) + self.assertEqual(ctx.returncode, 0) + self.assertEqual( + sorted(ctx.stdout.splitlines()), + [os.path.join(self.wd, 'a.yaml')] + ) + class CommandLineConfigTestCase(unittest.TestCase): def test_config_file(self): diff --git a/tests/test_config.py b/tests/test_config.py index 8e90246..fb570c6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,18 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from io import StringIO import os import shutil import sys import tempfile import unittest +from io import StringIO -from tests.common import build_temp_workspace +from tests.common import build_temp_workspace, RunContext +from yamllint import cli, config from yamllint.config import YamlLintConfigError -from yamllint import cli -from yamllint import config class SimpleConfigTestCase(unittest.TestCase): @@ -212,6 +211,14 @@ class SimpleConfigTestCase(unittest.TestCase): ' colons:\n' ' ignore: yes\n') + def test_invalid_rule_ignore_from_file(self): + self.assertRaises( + config.YamlLintConfigError, + config.YamlLintConfig, + 'rules:\n' + ' colons:\n' + ' ignore-from-file: 1337\n') + def test_invalid_locale(self): with self.assertRaisesRegex( config.YamlLintConfigError, @@ -660,12 +667,19 @@ class IgnoreConfigTestCase(unittest.TestCase): def test_run_with_ignore_from_file(self): with open(os.path.join(self.wd, '.yamllint'), 'w') as f: f.write('extends: default\n' - 'ignore-from-file: .gitignore\n') + 'ignore-from-file: .gitignore\n' + 'rules:\n' + ' key-duplicates:\n' + ' ignore-from-file: .ignore-key-duplicates\n') + with open(os.path.join(self.wd, '.gitignore'), 'w') as f: f.write('*.dont-lint-me.yaml\n' '/bin/\n' '!/bin/*.lint-me-anyway.yaml\n') + with open(os.path.join(self.wd, '.ignore-key-duplicates'), 'w') as f: + f.write('/ign-dup\n') + sys.stdout = StringIO() with self.assertRaises(SystemExit): cli.run(('-f', 'parsable', '.')) @@ -686,10 +700,8 @@ class IgnoreConfigTestCase(unittest.TestCase): './file-at-root.yaml:3:3: ' + keydup, './file-at-root.yaml:4:17: ' + trailing, './file-at-root.yaml:5:5: ' + hyphen, - './ign-dup/file.yaml:3:3: ' + keydup, './ign-dup/file.yaml:4:17: ' + trailing, './ign-dup/file.yaml:5:5: ' + hyphen, - './ign-dup/sub/dir/file.yaml:3:3: ' + keydup, './ign-dup/sub/dir/file.yaml:4:17: ' + trailing, './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen, './ign-trail/file.yaml:3:3: ' + keydup, @@ -761,3 +773,50 @@ class IgnoreConfigTestCase(unittest.TestCase): './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing, './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen, ))) + + def test_run_with_ignore_with_broken_symlink(self): + wd = build_temp_workspace({ + 'file-without-yaml-extension': '42\n', + 'link.yaml': 'symlink://file-without-yaml-extension', + 'link-404.yaml': 'symlink://file-that-does-not-exist', + }) + backup_wd = os.getcwd() + os.chdir(wd) + + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', '.')) + self.assertNotEqual(ctx.returncode, 0) + + with open(os.path.join(wd, '.yamllint'), 'w') as f: + f.write('extends: default\n' + 'ignore: |\n' + ' *404.yaml\n') + with RunContext(self) as ctx: + cli.run(('-f', 'parsable', '.')) + self.assertEqual(ctx.returncode, 0) + docstart = '[warning] missing document start "---" (document-start)' + out = '\n'.join(sorted(ctx.stdout.splitlines())) + self.assertEqual(out, '\n'.join(( + './.yamllint:1:1: ' + docstart, + './link.yaml:1:1: ' + docstart, + ))) + + os.chdir(backup_wd) + shutil.rmtree(wd) + + def test_run_with_ignore_on_ignored_file(self): + with open(os.path.join(self.wd, '.yamllint'), 'w') as f: + f.write('ignore: file.dont-lint-me.yaml\n' + 'rules:\n' + ' trailing-spaces: enable\n' + ' key-duplicates:\n' + ' ignore: file-at-root.yaml\n') + + sys.stdout = StringIO() + with self.assertRaises(SystemExit): + cli.run(('-f', 'parsable', 'file.dont-lint-me.yaml', + 'file-at-root.yaml')) + self.assertEqual( + sys.stdout.getvalue().strip(), + 'file-at-root.yaml:4:17: [error] trailing spaces (trailing-spaces)' + ) diff --git a/tests/test_linter.py b/tests/test_linter.py index 9855120..ea509d8 100644 --- a/tests/test_linter.py +++ b/tests/test_linter.py @@ -16,8 +16,8 @@ import io import unittest -from yamllint.config import YamlLintConfig from yamllint import linter +from yamllint.config import YamlLintConfig class LinterTestCase(unittest.TestCase): diff --git a/tests/test_module.py b/tests/test_module.py index 299e153..7f4f62b 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -16,11 +16,10 @@ import os import shutil import subprocess -import tempfile import sys +import tempfile import unittest - PYTHON = sys.executable or 'python' diff --git a/tests/test_parser.py b/tests/test_parser.py index dbeb36b..c2b598a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -17,9 +17,14 @@ import unittest import yaml -from yamllint.parser import (line_generator, token_or_comment_generator, - token_or_comment_or_line_generator, - Line, Token, Comment) +from yamllint.parser import ( + Comment, + Line, + Token, + line_generator, + token_or_comment_generator, + token_or_comment_or_line_generator, +) class ParserTestCase(unittest.TestCase): diff --git a/tests/test_spec_examples.py b/tests/test_spec_examples.py index ac68e12..87a57f6 100644 --- a/tests/test_spec_examples.py +++ b/tests/test_spec_examples.py @@ -17,7 +17,6 @@ import os from tests.common import RuleTestCase - # This file checks examples from YAML 1.2 specification [1] against yamllint. # # [1]: http://www.yaml.org/spec/1.2/spec.html @@ -43,6 +42,7 @@ from tests.common import RuleTestCase # encoding='utf-8') as g: # g.write(text) + class SpecificationTestCase(RuleTestCase): rule_id = None diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 907328e..ff98de5 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -21,7 +21,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.33.0' +APP_VERSION = '1.35.1' APP_DESCRIPTION = __doc__ __author__ = 'Adrien Vergé' diff --git a/yamllint/__main__.py b/yamllint/__main__.py index bc16534..529ac74 100644 --- a/yamllint/__main__.py +++ b/yamllint/__main__.py @@ -1,3 +1,18 @@ +# Copyright (C) 2016 Adrien Vergé +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + from yamllint.cli import run if __name__ == '__main__': diff --git a/yamllint/cli.py b/yamllint/cli.py index 604e594..9a39bd8 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -19,8 +19,7 @@ import os import platform import sys -from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION -from yamllint import linter +from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION, linter from yamllint.config import YamlLintConfig, YamlLintConfigError from yamllint.linter import PROBLEM_LEVELS @@ -28,10 +27,11 @@ from yamllint.linter import PROBLEM_LEVELS def find_files_recursively(items, conf): for item in items: if os.path.isdir(item): - for root, dirnames, filenames in os.walk(item): + for root, _dirnames, filenames in os.walk(item): for f in filenames: filepath = os.path.join(root, f) - if conf.is_yaml_file(filepath): + if (conf.is_yaml_file(filepath) and + not conf.is_file_ignored(filepath)): yield filepath else: yield item @@ -79,9 +79,9 @@ class Format: @staticmethod def github(problem, filename): - line = f'::{problem.level} file={format(filename)},' \ - f'line={format(problem.line)},col={format(problem.column)}' \ - f'::{format(problem.line)}:{format(problem.column)} ' + line = f'::{problem.level} file={filename},' \ + f'line={problem.line},col={problem.column}' \ + f'::{problem.line}:{problem.column} ' if problem.rule: line += f'[{problem.rule}] ' line += problem.desc diff --git a/yamllint/config.py b/yamllint/config.py index 47a61a8..9ce6254 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -76,7 +76,7 @@ class YamlLintConfig: try: conf = yaml.safe_load(raw_content) except Exception as e: - raise YamlLintConfigError(f'invalid config: {e}') + raise YamlLintConfigError(f'invalid config: {e}') from e if not isinstance(conf, dict): raise YamlLintConfigError('invalid config: not a dict') @@ -95,7 +95,7 @@ class YamlLintConfig: try: self.extend(base) except Exception as e: - raise YamlLintConfigError(f'invalid config: {e}') + raise YamlLintConfigError(f'invalid config: {e}') from e if 'ignore' in conf and 'ignore-from-file' in conf: raise YamlLintConfigError( @@ -143,7 +143,7 @@ class YamlLintConfig: try: rule = yamllint.rules.get(id) except Exception as e: - raise YamlLintConfigError(f'invalid config: {e}') + raise YamlLintConfigError(f'invalid config: {e}') from e self.rules[id] = validate_rule_conf(rule, self.rules[id]) @@ -153,8 +153,21 @@ def validate_rule_conf(rule, conf): return False if isinstance(conf, dict): - if ('ignore' in conf and - not isinstance(conf['ignore'], pathspec.pathspec.PathSpec)): + if ('ignore-from-file' in conf and not isinstance( + conf['ignore-from-file'], pathspec.pathspec.PathSpec)): + if isinstance(conf['ignore-from-file'], str): + conf['ignore-from-file'] = [conf['ignore-from-file']] + if not (isinstance(conf['ignore-from-file'], list) + and all(isinstance(line, str) + for line in conf['ignore-from-file'])): + raise YamlLintConfigError( + 'invalid config: ignore-from-file should contain ' + 'valid filename(s), either as a list or string') + with fileinput.input(conf['ignore-from-file']) as f: + conf['ignore'] = pathspec.PathSpec.from_lines( + 'gitwildmatch', f) + elif ('ignore' in conf and not isinstance( + conf['ignore'], pathspec.pathspec.PathSpec)): if isinstance(conf['ignore'], str): conf['ignore'] = pathspec.PathSpec.from_lines( 'gitwildmatch', conf['ignore'].splitlines()) @@ -192,7 +205,7 @@ def validate_rule_conf(rule, conf): # Example: CONF = {option: ['flag1', 'flag2', int]} # → {option: [flag1]} → {option: [42, flag1, flag2]} elif isinstance(options[optkey], list): - if (type(conf[optkey]) is not list or + if (not isinstance(conf[optkey], list) or any(flag not in options[optkey] and type(flag) not in options[optkey] for flag in conf[optkey])): diff --git a/yamllint/linter.py b/yamllint/linter.py index 0de1f71..a2faa06 100644 --- a/yamllint/linter.py +++ b/yamllint/linter.py @@ -13,14 +13,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import re import io +import re import yaml from yamllint import parser - PROBLEM_LEVELS = { 0: None, 1: 'warning', diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py index 606b37a..815d4bc 100644 --- a/yamllint/rules/__init__.py +++ b/yamllint/rules/__init__.py @@ -25,6 +25,7 @@ from yamllint.rules import ( document_start, empty_lines, empty_values, + float_values, hyphens, indentation, key_duplicates, @@ -33,7 +34,6 @@ from yamllint.rules import ( new_line_at_end_of_file, new_lines, octal_values, - float_values, quoted_strings, trailing_spaces, truthy, diff --git a/yamllint/rules/anchors.py b/yamllint/rules/anchors.py index 343f38b..d968461 100644 --- a/yamllint/rules/anchors.py +++ b/yamllint/rules/anchors.py @@ -108,7 +108,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'anchors' TYPE = 'token' CONF = {'forbid-undeclared-aliases': bool, diff --git a/yamllint/rules/braces.py b/yamllint/rules/braces.py index e77cda4..19f870a 100644 --- a/yamllint/rules/braces.py +++ b/yamllint/rules/braces.py @@ -137,7 +137,6 @@ import yaml from yamllint.linter import LintProblem from yamllint.rules.common import spaces_after, spaces_before - ID = 'braces' TYPE = 'token' CONF = {'forbid': (bool, 'non-empty'), diff --git a/yamllint/rules/brackets.py b/yamllint/rules/brackets.py index 47d2ad4..f1c08d5 100644 --- a/yamllint/rules/brackets.py +++ b/yamllint/rules/brackets.py @@ -138,7 +138,6 @@ import yaml from yamllint.linter import LintProblem from yamllint.rules.common import spaces_after, spaces_before - ID = 'brackets' TYPE = 'token' CONF = {'forbid': (bool, 'non-empty'), diff --git a/yamllint/rules/colons.py b/yamllint/rules/colons.py index 7390e51..2a181c6 100644 --- a/yamllint/rules/colons.py +++ b/yamllint/rules/colons.py @@ -82,7 +82,6 @@ import yaml from yamllint.rules.common import is_explicit_key, spaces_after, spaces_before - ID = 'colons' TYPE = 'token' CONF = {'max-spaces-before': int, diff --git a/yamllint/rules/commas.py b/yamllint/rules/commas.py index e87c8f9..baa8b7d 100644 --- a/yamllint/rules/commas.py +++ b/yamllint/rules/commas.py @@ -106,7 +106,6 @@ import yaml from yamllint.linter import LintProblem from yamllint.rules.common import spaces_after, spaces_before - ID = 'commas' TYPE = 'token' CONF = {'max-spaces-before': int, diff --git a/yamllint/rules/comments.py b/yamllint/rules/comments.py index 1259dea..7e4f04c 100644 --- a/yamllint/rules/comments.py +++ b/yamllint/rules/comments.py @@ -75,7 +75,6 @@ Use this rule to control the position and formatting of comments. from yamllint.linter import LintProblem - ID = 'comments' TYPE = 'comment' CONF = {'require-starting-space': bool, diff --git a/yamllint/rules/comments_indentation.py b/yamllint/rules/comments_indentation.py index 569abee..8bcda4d 100644 --- a/yamllint/rules/comments_indentation.py +++ b/yamllint/rules/comments_indentation.py @@ -79,7 +79,6 @@ import yaml from yamllint.linter import LintProblem from yamllint.rules.common import get_line_indent - ID = 'comments-indentation' TYPE = 'comment' diff --git a/yamllint/rules/document_end.py b/yamllint/rules/document_end.py index 2337484..e1ce2a1 100644 --- a/yamllint/rules/document_end.py +++ b/yamllint/rules/document_end.py @@ -85,7 +85,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'document-end' TYPE = 'token' CONF = {'present': bool} diff --git a/yamllint/rules/document_start.py b/yamllint/rules/document_start.py index f1d6667..225d7c3 100644 --- a/yamllint/rules/document_start.py +++ b/yamllint/rules/document_start.py @@ -75,7 +75,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'document-start' TYPE = 'token' CONF = {'present': bool} diff --git a/yamllint/rules/empty_lines.py b/yamllint/rules/empty_lines.py index eca7812..7ccbedf 100644 --- a/yamllint/rules/empty_lines.py +++ b/yamllint/rules/empty_lines.py @@ -61,7 +61,6 @@ Use this rule to set a maximal number of allowed consecutive blank lines. from yamllint.linter import LintProblem - ID = 'empty-lines' TYPE = 'line' CONF = {'max': int, diff --git a/yamllint/rules/empty_values.py b/yamllint/rules/empty_values.py index 6c8328b..c1ff4f2 100644 --- a/yamllint/rules/empty_values.py +++ b/yamllint/rules/empty_values.py @@ -105,7 +105,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'empty-values' TYPE = 'token' CONF = {'forbid-in-block-mappings': bool, diff --git a/yamllint/rules/float_values.py b/yamllint/rules/float_values.py index a5852c5..77a243b 100644 --- a/yamllint/rules/float_values.py +++ b/yamllint/rules/float_values.py @@ -90,7 +90,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'float-values' TYPE = 'token' CONF = { diff --git a/yamllint/rules/hyphens.py b/yamllint/rules/hyphens.py index 50e4d6d..54a96bf 100644 --- a/yamllint/rules/hyphens.py +++ b/yamllint/rules/hyphens.py @@ -79,7 +79,6 @@ import yaml from yamllint.rules.common import spaces_after - ID = 'hyphens' TYPE = 'token' CONF = {'max-spaces-after': int} diff --git a/yamllint/rules/indentation.py b/yamllint/rules/indentation.py index d839d5a..bde6fa3 100644 --- a/yamllint/rules/indentation.py +++ b/yamllint/rules/indentation.py @@ -204,7 +204,6 @@ import yaml from yamllint.linter import LintProblem from yamllint.rules.common import get_real_end_line, is_explicit_key - ID = 'indentation' TYPE = 'token' CONF = {'spaces': (int, 'consistent'), diff --git a/yamllint/rules/key_duplicates.py b/yamllint/rules/key_duplicates.py index 771a8e2..638ac1e 100644 --- a/yamllint/rules/key_duplicates.py +++ b/yamllint/rules/key_duplicates.py @@ -16,6 +16,19 @@ """ Use this rule to prevent multiple entries with the same key in mappings. +.. rubric:: Options + +* Use ``forbid-duplicated-merge-keys`` to forbid the usage of + multiple merge keys ``<<``. + +.. rubric:: Default values (when enabled) + +.. code-block:: yaml + + rules: + key-duplicates: + forbid-duplicated-merge-keys: false + .. rubric:: Examples #. With ``key-duplicates: {}`` @@ -51,15 +64,39 @@ Use this rule to prevent multiple entries with the same key in mappings. other duplication : 2 + +#. With `key-duplicates`: {forbid-duplicated-merge-keys: true}`` + + the following code snippet would **PASS**: + :: + + anchor_one: &anchor_one + one: one + anchor_two: &anchor_two + two: two + anchor_reference: + <<: [*anchor_one, *anchor_two] + + the following code snippet would **FAIL**: + :: + + anchor_one: &anchor_one + one: one + anchor_two: &anchor_two + two: two + anchor_reference: + <<: *anchor_one + <<: *anchor_two """ import yaml from yamllint.linter import LintProblem - ID = 'key-duplicates' TYPE = 'token' +CONF = {'forbid-duplicated-merge-keys': bool} +DEFAULT = {'forbid-duplicated-merge-keys': False} MAP, SEQ = range(2) @@ -92,7 +129,8 @@ def check(conf, token, prev, next, nextnext, context): if len(context['stack']) > 0 and context['stack'][-1].type == MAP: if (next.value in context['stack'][-1].keys and # `<<` is "merge key", see http://yaml.org/type/merge.html - next.value != '<<'): + (next.value != '<<' or + conf['forbid-duplicated-merge-keys'])): yield LintProblem( next.start_mark.line + 1, next.start_mark.column + 1, f'duplication of key "{next.value}" in mapping') diff --git a/yamllint/rules/key_ordering.py b/yamllint/rules/key_ordering.py index 7fa9597..ec0716d 100644 --- a/yamllint/rules/key_ordering.py +++ b/yamllint/rules/key_ordering.py @@ -86,7 +86,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'key-ordering' TYPE = 'token' diff --git a/yamllint/rules/line_length.py b/yamllint/rules/line_length.py index e7cc8bc..8214d74 100644 --- a/yamllint/rules/line_length.py +++ b/yamllint/rules/line_length.py @@ -101,7 +101,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'line-length' TYPE = 'line' CONF = {'max': int, diff --git a/yamllint/rules/new_line_at_end_of_file.py b/yamllint/rules/new_line_at_end_of_file.py index 302cfe6..f49edb9 100644 --- a/yamllint/rules/new_line_at_end_of_file.py +++ b/yamllint/rules/new_line_at_end_of_file.py @@ -25,7 +25,6 @@ this convention too. from yamllint.linter import LintProblem - ID = 'new-line-at-end-of-file' TYPE = 'line' diff --git a/yamllint/rules/new_lines.py b/yamllint/rules/new_lines.py index b3f018a..2ee5512 100644 --- a/yamllint/rules/new_lines.py +++ b/yamllint/rules/new_lines.py @@ -37,7 +37,6 @@ from os import linesep from yamllint.linter import LintProblem - ID = 'new-lines' TYPE = 'line' CONF = {'type': ('unix', 'dos', 'platform')} @@ -54,6 +53,6 @@ def check(conf, line): if line.start == 0 and len(line.buffer) > line.end: if line.buffer[line.end:line.end + len(newline_char)] != newline_char: + c = repr(newline_char).strip('\'') yield LintProblem(1, line.end - line.start + 1, - 'wrong new line character: expected {}' - .format(repr(newline_char).strip('\''))) + f'wrong new line character: expected {c}') diff --git a/yamllint/rules/octal_values.py b/yamllint/rules/octal_values.py index eb24c81..e94e4bf 100644 --- a/yamllint/rules/octal_values.py +++ b/yamllint/rules/octal_values.py @@ -76,7 +76,6 @@ import yaml from yamllint.linter import LintProblem - ID = 'octal-values' TYPE = 'token' CONF = {'forbid-implicit-octal': bool, diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 9380ae5..b38f6ed 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -32,6 +32,9 @@ used. even if ``required: only-when-needed`` is set. * ``allow-quoted-quotes`` allows (``true``) using disallowed quotes for strings with allowed quotes inside. Default ``false``. +* ``check-keys`` defines whether to apply the rules to keys in mappings. By + default, ``quoted-strings`` rules apply only to values. Set this option to + ``true`` to apply the rules to keys as well. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. @@ -46,6 +49,7 @@ used. extra-required: [] extra-allowed: [] allow-quoted-quotes: false + check-keys: false .. rubric:: Examples @@ -135,6 +139,18 @@ used. foo: 'bar"baz' +#. With ``quoted-strings: {required: only-when-needed, check-keys: true, + extra-required: ["[:]"]}`` + + the following code snippet would **FAIL**: + :: + + foo:bar: baz + + the following code snippet would **PASS**: + :: + + "foo:bar": baz """ import re @@ -149,12 +165,14 @@ CONF = {'quote-type': ('any', 'single', 'double'), 'required': (True, False, 'only-when-needed'), 'extra-required': [str], 'extra-allowed': [str], - 'allow-quoted-quotes': bool} + 'allow-quoted-quotes': bool, + 'check-keys': bool} DEFAULT = {'quote-type': 'any', 'required': True, 'extra-required': [], 'extra-allowed': [], - 'allow-quoted-quotes': False} + 'allow-quoted-quotes': False, + 'check-keys': False} def VALIDATE(conf): @@ -186,7 +204,11 @@ def _quote_match(quote_type, token_style): (quote_type == 'double' and token_style == '"')) -def _quotes_are_needed(string): +def _quotes_are_needed(string, is_inside_a_flow): + # Quotes needed on strings containing flow tokens + if is_inside_a_flow and set(string) & {',', '[', ']', '{', '}'}: + return True + loader = yaml.BaseLoader('key: ' + string) # Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken, # BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken) @@ -194,12 +216,13 @@ def _quotes_are_needed(string): loader.get_token() try: a, b = loader.get_token(), loader.get_token() + except yaml.scanner.ScannerError: + return True + else: if (isinstance(a, yaml.ScalarToken) and a.style is None and isinstance(b, yaml.BlockEndToken) and a.value == string): return False return True - except yaml.scanner.ScannerError: - return True def _has_quoted_quotes(token): @@ -209,11 +232,25 @@ def _has_quoted_quotes(token): def check(conf, token, prev, next, nextnext, context): + if 'flow_nest_count' not in context: + context['flow_nest_count'] = 0 + + if isinstance(token, (yaml.FlowMappingStartToken, + yaml.FlowSequenceStartToken)): + context['flow_nest_count'] += 1 + elif isinstance(token, (yaml.FlowMappingEndToken, + yaml.FlowSequenceEndToken)): + context['flow_nest_count'] -= 1 + if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken, yaml.FlowSequenceStartToken, yaml.TagToken, - yaml.ValueToken))): + yaml.ValueToken, yaml.KeyToken))): + + return + node = 'key' if isinstance(prev, yaml.KeyToken) else 'value' + if node == 'key' and not conf['check-keys']: return # Ignore explicit types, e.g. !!str testtest or !!int 42 @@ -240,7 +277,7 @@ def check(conf, token, prev, next, nextnext, context): if (token.style is None or not (_quote_match(quote_type, token.style) or (conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))): - msg = f"string value is not quoted with {quote_type} quotes" + msg = f"string {node} is not quoted with {quote_type} quotes" elif conf['required'] is False: @@ -249,38 +286,39 @@ def check(conf, token, prev, next, nextnext, context): not _quote_match(quote_type, token.style) and not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))): - msg = f"string value is not quoted with {quote_type} quotes" + msg = f"string {node} is not quoted with {quote_type} quotes" elif not token.style: is_extra_required = any(re.search(r, token.value) for r in conf['extra-required']) if is_extra_required: - msg = "string value is not quoted" + msg = f"string {node} is not quoted" elif conf['required'] == 'only-when-needed': # Quotes are not strictly needed here if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and - not _quotes_are_needed(token.value)): + not _quotes_are_needed(token.value, + context['flow_nest_count'] > 0)): is_extra_required = any(re.search(r, token.value) for r in conf['extra-required']) is_extra_allowed = any(re.search(r, token.value) for r in conf['extra-allowed']) if not (is_extra_required or is_extra_allowed): - msg = f"string value is redundantly quoted with " \ + msg = f"string {node} is redundantly quoted with " \ f"{quote_type} quotes" # But when used need to match config elif (token.style and not _quote_match(quote_type, token.style) and not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))): - msg = f"string value is not quoted with {quote_type} quotes" + msg = f"string {node} is not quoted with {quote_type} quotes" elif not token.style: is_extra_required = len(conf['extra-required']) and any( re.search(r, token.value) for r in conf['extra-required']) if is_extra_required: - msg = "string value is not quoted" + msg = f"string {node} is not quoted" if msg is not None: yield LintProblem( diff --git a/yamllint/rules/trailing_spaces.py b/yamllint/rules/trailing_spaces.py index 2295714..b5455f3 100644 --- a/yamllint/rules/trailing_spaces.py +++ b/yamllint/rules/trailing_spaces.py @@ -40,7 +40,6 @@ import string from yamllint.linter import LintProblem - ID = 'trailing-spaces' TYPE = 'line' diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py index d19f6ea..ff47a83 100644 --- a/yamllint/rules/truthy.py +++ b/yamllint/rules/truthy.py @@ -21,6 +21,13 @@ This can be useful to prevent surprises from YAML parsers transforming ``[yes, FALSE, Off]`` into ``[true, false, false]`` or ``{y: 1, yes: 2, on: 3, true: 4, True: 5}`` into ``{y: 1, true: 5}``. +Depending on the YAML specification version used by the YAML document, the list +of truthy values can differ. In YAML 1.2, only capitalized / uppercased +combinations of ``true`` and ``false`` are considered truthy, whereas in YAML +1.1 combinations of ``yes``, ``no``, ``on`` and ``off`` are too. To make the +YAML specification version explicit in a YAML document, a ``%YAML 1.2`` +directive can be used (see example below). + .. rubric:: Options * ``allowed-values`` defines the list of truthy values which will be ignored @@ -80,10 +87,21 @@ This can be useful to prevent surprises from YAML parsers transforming the following code snippet would **FAIL**: :: + %YAML 1.1 + --- yes: 1 on: 2 True: 3 + the following code snippet would **PASS**: + :: + + %YAML 1.2 + --- + yes: 1 + on: 2 + true: 3 + #. With ``truthy: {allowed-values: ["yes", "no"]}`` the following code snippet would **PASS**: @@ -125,22 +143,35 @@ import yaml from yamllint.linter import LintProblem - -TRUTHY = ['YES', 'Yes', 'yes', - 'NO', 'No', 'no', - 'TRUE', 'True', 'true', - 'FALSE', 'False', 'false', - 'ON', 'On', 'on', - 'OFF', 'Off', 'off'] +TRUTHY_1_1 = ['YES', 'Yes', 'yes', + 'NO', 'No', 'no', + 'TRUE', 'True', 'true', + 'FALSE', 'False', 'false', + 'ON', 'On', 'on', + 'OFF', 'Off', 'off'] +TRUTHY_1_2 = ['TRUE', 'True', 'true', + 'FALSE', 'False', 'false'] ID = 'truthy' TYPE = 'token' -CONF = {'allowed-values': TRUTHY.copy(), 'check-keys': bool} +CONF = {'allowed-values': TRUTHY_1_1.copy(), 'check-keys': bool} DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True} +def yaml_spec_version_for_document(context): + if 'yaml_spec_version' in context: + return context['yaml_spec_version'] + return (1, 1) + + def check(conf, token, prev, next, nextnext, context): + if isinstance(token, yaml.tokens.DirectiveToken) and token.name == 'YAML': + context['yaml_spec_version'] = token.value + elif isinstance(token, yaml.tokens.DocumentEndToken): + context.pop('yaml_spec_version', None) + context.pop('bad_truthy_values', None) + if prev and isinstance(prev, yaml.tokens.TagToken): return @@ -148,9 +179,14 @@ def check(conf, token, prev, next, nextnext, context): isinstance(token, yaml.tokens.ScalarToken)): return - if isinstance(token, yaml.tokens.ScalarToken): - if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and - token.style is None): + if isinstance(token, yaml.tokens.ScalarToken) and token.style is None: + if 'bad_truthy_values' not in context: + context['bad_truthy_values'] = set( + TRUTHY_1_2 if yaml_spec_version_for_document(context) == (1, 2) + else TRUTHY_1_1) + context['bad_truthy_values'] -= set(conf['allowed-values']) + + if token.value in context['bad_truthy_values']: yield LintProblem(token.start_mark.line + 1, token.start_mark.column + 1, "truthy value should be one of [" + |