diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..26cea6c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,52 @@ +--- + +name: CI + +on: # yamllint disable-line rule:truthy + push: + pull_request: + branches: + - master + +jobs: + lint: + name: Linters + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - run: python -m pip install flake8 flake8-import-order doc8 sphinx + - run: python -m pip install . + - run: flake8 . + - run: doc8 $(git ls-files '*.rst') + - run: yamllint --strict $(git ls-files '*.yaml' '*.yml') + - run: python setup.py build_sphinx + + test: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Append GitHub Actions system path + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - run: pip install coveralls + - run: pip install . + - run: coverage run --source=yamllint -m unittest discover + - name: Coveralls + uses: AndreMiras/coveralls-python-action@develop diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c7c311c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069) -language: python -python: - - 2.7 - - 3.5 - - 3.6 - - 3.7 - - 3.8 - - nightly -env: - - REMOVE_LOCALES=false - - REMOVE_LOCALES=true -install: - - pip install pyyaml coveralls flake8 flake8-import-order doc8 - - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then pip install sphinx; fi - - pip install . - - if [[ $REMOVE_LOCALES = "true" ]]; then sudo rm -rf /usr/lib/locale/*; fi -script: - - if [[ $TRAVIS_PYTHON_VERSION != nightly ]]; then flake8 .; fi - - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then doc8 $(git ls-files '*.rst'); fi - - yamllint --strict $(git ls-files '*.yaml' '*.yml') - - coverage run --source=yamllint -m unittest discover - - if [[ $TRAVIS_PYTHON_VERSION != 2* ]]; then - python setup.py build_sphinx; - fi -after_success: - coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 39fce9c..876ccd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,32 @@ Changelog ========= +1.26.3 (2021-08-21) +------------------- + +- Restore runtime dependency ``setuptools`` for Python < 3.8 + +1.26.2 (2021-08-03) +------------------- + +- Fix ``python_requires`` to comply with PEP 345 and PEP 440 + +1.26.1 (2021-04-06) +------------------- + +- Remove runtime dependency ``setuptools`` for Python < 3.8 +- Fix ``line_length`` to skip all hash signs starting comment + +1.26.0 (2021-01-29) +------------------- + +- End support for Python 2 and Python 3.4, add support for Python 3.9 +- Add ``forbid: non-empty`` option to ``braces`` and ``brackets`` rules +- Fix ``quoted-strings`` for explicit octal recognition +- Add documentation for integration with Arcanist +- Fix typos in changelog and README +- Stop using deprecated ``python setup.py test`` in tests + 1.25.0 (2020-09-29) ------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f57ec5e..312c6d8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -42,5 +42,7 @@ Pull Request Process 6. Write a `good commit message `_. + If the pull request has multiple commits, each must be atomic (single + irreducible change that makes sense on its own). 7. Then, open a pull request. diff --git a/README.rst b/README.rst index 496b152..3383ec4 100644 --- a/README.rst +++ b/README.rst @@ -19,11 +19,7 @@ indentation, etc. :target: https://yamllint.readthedocs.io/en/latest/?badge=latest :alt: Documentation status -Written in Python (compatible with Python 2 & 3). - -⚠ Python 2 upstream support stopped on January 1, 2020. yamllint will keep -best-effort support for Python 2.7 until January 1, 2021. Past that date, -yamllint will drop all Python 2-related code. +Written in Python (compatible with Python 3 only). Documentation ------------- diff --git a/docs/conf.py b/docs/conf.py index c651cf9..f5d317e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,7 @@ htmlhelp_basename = 'yamllintdoc' # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'yamllint', '', [u'Adrien Vergé'], 1) + ('index', 'yamllint', 'Linter for YAML files', [u'Adrien Vergé'], 1) ] # -- Build with sphinx automodule without needing to install third-party libs diff --git a/docs/configuration.rst b/docs/configuration.rst index ba455c9..3c1e0c8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -14,11 +14,11 @@ To use a custom configuration file, use the ``-c`` option: If ``-c`` is not provided, yamllint will look for a configuration file in the following locations (by order of preference): -- ``.yamllint``, ``.yamllint.yaml`` or ``.yamllint.yml`` in the current working - directory -- the file referenced by ``$YAMLLINT_CONFIG_FILE``, if set -- ``$XDG_CONFIG_HOME/yamllint/config`` -- ``~/.config/yamllint/config`` +- a file named ``.yamllint``, ``.yamllint.yaml``, or ``.yamllint.yml`` in the + current working directory +- a filename referenced by ``$YAMLLINT_CONFIG_FILE``, if set +- a file named ``$XDG_CONFIG_HOME/yamllint/config`` or + ``~/.config/yamllint/config``, if present Finally if no config file is found, the default configuration is applied. diff --git a/docs/integration.rst b/docs/integration.rst index b094edc..7b349ff 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -22,9 +22,9 @@ Integration with GitHub Actions ------------------------------- yamllint auto-detects when it's running inside of `GitHub -Actions` and automatically uses the suited -output format to decorate code with linting errors automatically. You can also -force the GitHub Actions output with ``yamllint --format github``. +Actions `_ and automatically uses the suited +output format to decorate code with linting errors. You can also force the +GitHub Actions output with ``yamllint --format github``. An example workflow using GitHub Actions: @@ -51,3 +51,22 @@ An example workflow using GitHub Actions: - name: Lint YAML files run: yamllint . + +Integration with Arcanist +------------------------- + +You can configure yamllint to run on ``arc lint``. Here is an example +``.arclint`` file that makes use of this configuration. + +.. code:: json + + { + "linters": { + "yamllint": { + "type": "script-and-regex", + "script-and-regex.script": "yamllint", + "script-and-regex.regex": "/^(?P\\d+):(?P\\d+) +(?Pwarning|error) +(?P.*) +\\((?P.*)\\)$/m", + "include": "(\\.(yml|yaml)$)" + } + } + } diff --git a/setup.cfg b/setup.cfg index 246c468..03ff8eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,14 +26,12 @@ classifiers = Environment :: Console Intended Audience :: Developers License :: OSI Approved :: GNU General Public License v3 (GPLv3) - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Software Development Topic :: Software Development :: Debuggers Topic :: Software Development :: Quality Assurance @@ -48,7 +46,7 @@ project_urls = [options] packages = find: -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +python_requires = >=3.6 include_package_data = True install_requires = @@ -69,3 +67,6 @@ yamllint = conf/*.yaml [options.entry_points] console_scripts = yamllint = yamllint.cli:run + +[coverage:run] +relative_files = True diff --git a/tests/rules/test_braces.py b/tests/rules/test_braces.py index 69c5461..cae7014 100644 --- a/tests/rules/test_braces.py +++ b/tests/rules/test_braces.py @@ -61,6 +61,30 @@ class ColonTestCase(RuleTestCase): ' a: 1\n' '}\n', conf, problem=(2, 8)) + conf = ('braces:\n' + ' forbid: non-empty\n') + self.check('---\n' + 'dict:\n' + ' a: 1\n', conf) + self.check('---\n' + 'dict: {}\n', conf) + self.check('---\n' + 'dict: {\n' + '}\n', conf) + self.check('---\n' + 'dict: {\n' + '# commented: value\n' + '# another: value2\n' + '}\n', conf) + self.check('---\n' + 'dict: {a}\n', conf, problem=(2, 8)) + self.check('---\n' + 'dict: {a: 1}\n', conf, problem=(2, 8)) + self.check('---\n' + 'dict: {\n' + ' a: 1\n' + '}\n', conf, problem=(2, 8)) + def test_min_spaces(self): conf = ('braces:\n' ' max-spaces-inside: -1\n' diff --git a/tests/rules/test_brackets.py b/tests/rules/test_brackets.py index a35eff9..41f20a7 100644 --- a/tests/rules/test_brackets.py +++ b/tests/rules/test_brackets.py @@ -60,6 +60,29 @@ class ColonTestCase(RuleTestCase): ' b\n' ']\n', conf, problem=(2, 9)) + conf = ('brackets:\n' + ' forbid: non-empty\n') + self.check('---\n' + 'array:\n' + ' - a\n' + ' - b\n', conf) + self.check('---\n' + 'array: []\n', conf) + self.check('---\n' + 'array: [\n\n' + ']\n', conf) + self.check('---\n' + 'array: [\n' + '# a comment\n' + ']\n', conf) + self.check('---\n' + 'array: [a, b]\n', conf, problem=(2, 9)) + self.check('---\n' + 'array: [\n' + ' a,\n' + ' b\n' + ']\n', conf, problem=(2, 9)) + def test_min_spaces(self): conf = ('brackets:\n' ' max-spaces-inside: -1\n' diff --git a/tests/rules/test_comments.py b/tests/rules/test_comments.py index 2c3f399..7a7bc8a 100644 --- a/tests/rules/test_comments.py +++ b/tests/rules/test_comments.py @@ -97,7 +97,7 @@ class CommentsTestCase(RuleTestCase): '#!/bin/env my-interpreter\n' '', conf, problem1=(1, 2), problem2=(3, 2), problem3=(4, 2)) - self.check('#! not a shebang\n', + self.check('#! is a valid shebang too\n', conf, problem1=(1, 2)) self.check('key: #!/not/a/shebang\n', conf, problem1=(1, 8)) @@ -117,8 +117,7 @@ class CommentsTestCase(RuleTestCase): '#comment\n' '#!/bin/env my-interpreter\n', conf, problem2=(3, 2), problem3=(4, 2)) - self.check('#! not a shebang\n', - conf, problem1=(1, 2)) + self.check('#! is a valid shebang too\n', conf) self.check('key: #!/not/a/shebang\n', conf, problem1=(1, 8)) diff --git a/tests/rules/test_line_length.py b/tests/rules/test_line_length.py index c1865f1..22a439e 100644 --- a/tests/rules/test_line_length.py +++ b/tests/rules/test_line_length.py @@ -14,9 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -import unittest - from tests.common import RuleTestCase @@ -119,6 +116,27 @@ class LineLengthTestCase(RuleTestCase): 'long_line: http://localhost/very/very/long/url\n' '...\n', conf, problem=(2, 21)) + conf = 'line-length: {max: 20, allow-non-breakable-words: true}' + self.check('---\n' + '# http://www.verylongurlurlurlurlurlurlurlurl.com\n' + 'key:\n' + ' subkey: value\n', conf) + self.check('---\n' + '## http://www.verylongurlurlurlurlurlurlurlurl.com\n' + 'key:\n' + ' subkey: value\n', conf) + self.check('---\n' + '# # http://www.verylongurlurlurlurlurlurlurlurl.com\n' + 'key:\n' + ' subkey: value\n', conf, + problem=(2, 21)) + self.check('---\n' + '#A http://www.verylongurlurlurlurlurlurlurlurl.com\n' + 'key:\n' + ' subkey: value\n', conf, + problem1=(2, 2, 'comments'), + problem2=(2, 21, 'line-length')) + conf = ('line-length: {max: 20, allow-non-breakable-words: true}\n' 'trailing-spaces: disable') self.check('---\n' @@ -159,18 +177,17 @@ class LineLengthTestCase(RuleTestCase): ' {% this line is' + 99 * ' really' + ' long %}\n', conf, problem=(3, 81)) - @unittest.skipIf(sys.version_info < (3, 0), 'Python 2 not supported') def test_unicode(self): conf = 'line-length: {max: 53}' self.check('---\n' '# This is a test to check if “line-length” works nice\n' - 'with: “unicode characters” that span accross bytes! ↺\n', + 'with: “unicode characters” that span across bytes! ↺\n', conf) - conf = 'line-length: {max: 52}' + conf = 'line-length: {max: 51}' self.check('---\n' '# This is a test to check if “line-length” works nice\n' - 'with: “unicode characters” that span accross bytes! ↺\n', - conf, problem1=(2, 53), problem2=(3, 53)) + 'with: “unicode characters” that span across bytes! ↺\n', + conf, problem1=(2, 52), problem2=(3, 52)) def test_with_dos_newlines(self): conf = ('line-length: {max: 10}\n' diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 1ee86a1..79f36bc 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -436,3 +436,21 @@ class QuotedTestCase(RuleTestCase): '- foo bar\n' '- "foo bar"\n', conf, problem1=(3, 3), problem2=(7, 3), problem3=(11, 3)) + + def test_octal_values(self): + conf = 'quoted-strings: {required: true}\n' + + self.check('---\n' + '- 100\n' + '- 0100\n' + '- 0o100\n' + '- 777\n' + '- 0777\n' + '- 0o777\n' + '- 800\n' + '- 0800\n' + '- 0o800\n' + '- "0800"\n' + '- "0o800"\n', + conf, + problem1=(9, 3), problem2=(10, 3)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 020b371..95e3fc7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,10 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO +from io import StringIO import fcntl import locale import os @@ -246,19 +243,19 @@ class CommandLineTestCase(unittest.TestCase): cli.run(()) self.assertNotEqual(ctx.returncode, 0) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'^usage') + self.assertRegex(ctx.stderr, r'^usage') with RunContext(self) as ctx: cli.run(('--unknown-arg', )) self.assertNotEqual(ctx.returncode, 0) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'^usage') + self.assertRegex(ctx.stderr, r'^usage') with RunContext(self) as ctx: cli.run(('-c', './conf.yaml', '-d', 'relaxed', 'file')) self.assertNotEqual(ctx.returncode, 0) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches( + self.assertRegex( ctx.stderr.splitlines()[-1], r'^yamllint: error: argument -d\/--config-data: ' r'not allowed with argument -c\/--config-file$' @@ -269,21 +266,21 @@ class CommandLineTestCase(unittest.TestCase): cli.run(('-', 'file')) self.assertNotEqual(ctx.returncode, 0) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'^usage') + self.assertRegex(ctx.stderr, r'^usage') def test_run_with_bad_config(self): with RunContext(self) as ctx: cli.run(('-d', 'rules: {a: b}', 'file')) self.assertEqual(ctx.returncode, -1) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'^invalid config: no such rule') + self.assertRegex(ctx.stderr, r'^invalid config: no such rule') def test_run_with_empty_config(self): with RunContext(self) as ctx: cli.run(('-d', '', 'file')) self.assertEqual(ctx.returncode, -1) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'^invalid config: not a dict') + self.assertRegex(ctx.stderr, r'^invalid config: not a dict') def test_run_with_config_file(self): with open(os.path.join(self.wd, 'config'), 'w') as f: @@ -300,6 +297,7 @@ 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') @@ -386,7 +384,7 @@ class CommandLineTestCase(unittest.TestCase): with RunContext(self) as ctx: cli.run(('--version', )) self.assertEqual(ctx.returncode, 0) - self.assertRegexpMatches(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+') + self.assertRegex(ctx.stdout + ctx.stderr, r'yamllint \d+\.\d+') def test_run_non_existing_file(self): path = os.path.join(self.wd, 'i-do-not-exist.yaml') @@ -395,7 +393,7 @@ class CommandLineTestCase(unittest.TestCase): cli.run(('-f', 'parsable', path)) self.assertEqual(ctx.returncode, -1) self.assertEqual(ctx.stdout, '') - self.assertRegexpMatches(ctx.stderr, r'No such file or directory') + self.assertRegex(ctx.stderr, r'No such file or directory') def test_run_one_problem_file(self): path = os.path.join(self.wd, 'a.yaml') @@ -555,11 +553,13 @@ class CommandLineTestCase(unittest.TestCase): with RunContext(self) as ctx: cli.run((path, '--format', 'github')) expected_out = ( - '::error file=%s,line=2,col=4::[trailing-spaces] trailing' + '::group::%s\n' + '::error file=%s,line=2,col=4::2:4 [trailing-spaces] trailing' ' spaces\n' - '::error file=%s,line=3,col=4::[new-line-at-end-of-file] no' + '::error file=%s,line=3,col=4::3:4 [new-line-at-end-of-file] no' ' new line character at the end of file\n' - % (path, path)) + '::endgroup::\n\n' + % (path, path, path)) self.assertEqual( (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) @@ -573,11 +573,13 @@ class CommandLineTestCase(unittest.TestCase): os.environ['GITHUB_WORKFLOW'] = 'something' cli.run((path, )) expected_out = ( - '::error file=%s,line=2,col=4::[trailing-spaces] trailing' + '::group::%s\n' + '::error file=%s,line=2,col=4::2:4 [trailing-spaces] trailing' ' spaces\n' - '::error file=%s,line=3,col=4::[new-line-at-end-of-file] no' + '::error file=%s,line=3,col=4::3:4 [new-line-at-end-of-file] no' ' new line character at the end of file\n' - % (path, path)) + '::endgroup::\n\n' + % (path, path, path)) self.assertEqual( (ctx.returncode, ctx.stdout, ctx.stderr), (1, expected_out, '')) diff --git a/tests/test_config.py b/tests/test_config.py index b48546c..70a73e0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -14,10 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO +from io import StringIO import os import shutil import sys @@ -48,7 +45,7 @@ class SimpleConfigTestCase(unittest.TestCase): config.YamlLintConfig('not: valid: yaml') def test_unknown_rule(self): - with self.assertRaisesRegexp( + with self.assertRaisesRegex( config.YamlLintConfigError, 'invalid config: no such rule: "this-one-does-not-exist"'): config.YamlLintConfig('rules:\n' @@ -67,7 +64,7 @@ class SimpleConfigTestCase(unittest.TestCase): self.assertEqual(c.rules['colons']['max-spaces-after'], 1) def test_unknown_option(self): - with self.assertRaisesRegexp( + with self.assertRaisesRegex( config.YamlLintConfigError, 'invalid config: unknown option "abcdef" for rule "colons"'): config.YamlLintConfig('rules:\n' @@ -105,7 +102,7 @@ class SimpleConfigTestCase(unittest.TestCase): self.assertEqual(c.rules['indentation']['check-multi-line-strings'], False) - with self.assertRaisesRegexp( + with self.assertRaisesRegex( config.YamlLintConfigError, 'invalid config: option "indent-sequences" of "indentation" ' 'should be in '): diff --git a/tests/test_module.py b/tests/test_module.py index e1bf066..570992d 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -47,16 +47,15 @@ class ModuleTestCase(unittest.TestCase): subprocess.check_output([PYTHON, '-m', 'yamllint'], stderr=subprocess.STDOUT) self.assertEqual(ctx.exception.returncode, 2) - self.assertRegexpMatches(ctx.exception.output.decode(), - r'^usage: yamllint') + self.assertRegex(ctx.exception.output.decode(), r'^usage: yamllint') def test_run_module_on_bad_dir(self): with self.assertRaises(subprocess.CalledProcessError) as ctx: subprocess.check_output([PYTHON, '-m', 'yamllint', '/does/not/exist'], stderr=subprocess.STDOUT) - self.assertRegexpMatches(ctx.exception.output.decode(), - r'No such file or directory') + self.assertRegex(ctx.exception.output.decode(), + r'No such file or directory') def test_run_module_on_file(self): out = subprocess.check_output( diff --git a/yamllint/__init__.py b/yamllint/__init__.py index bd04714..da49d9a 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.25.0' +APP_VERSION = '1.26.3' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' diff --git a/yamllint/cli.py b/yamllint/cli.py index cb87350..4d94342 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -93,6 +93,10 @@ class Format(object): line += 'line=' + format(problem.line) + ',' line += 'col=' + format(problem.column) line += '::' + line += format(problem.line) + line += ':' + line += format(problem.column) + line += ' ' if problem.rule: line += '[' + problem.rule + '] ' line += problem.desc @@ -103,18 +107,25 @@ def show_problems(problems, file, args_format, no_warn): max_level = 0 first = True + if args_format == 'auto': + if ('GITHUB_ACTIONS' in os.environ and + 'GITHUB_WORKFLOW' in os.environ): + args_format = 'github' + elif supports_color(): + args_format = 'colored' + for problem in problems: max_level = max(max_level, PROBLEM_LEVELS[problem.level]) if no_warn and (problem.level != 'error'): continue if args_format == 'parsable': print(Format.parsable(problem, file)) - elif args_format == 'github' or (args_format == 'auto' and - 'GITHUB_ACTIONS' in os.environ and - 'GITHUB_WORKFLOW' in os.environ): + elif args_format == 'github': + if first: + print('::group::%s' % file) + first = False print(Format.github(problem, file)) - elif args_format == 'colored' or \ - (args_format == 'auto' and supports_color()): + elif args_format == 'colored': if first: print('\033[4m%s\033[0m' % file) first = False @@ -125,6 +136,9 @@ def show_problems(problems, file, args_format, no_warn): first = False print(Format.standard(problem, file)) + if not first and args_format == 'github': + print('::endgroup::') + if not first and args_format != 'parsable': print('') diff --git a/yamllint/linter.py b/yamllint/linter.py index bcc99f1..b3dc887 100644 --- a/yamllint/linter.py +++ b/yamllint/linter.py @@ -242,7 +242,7 @@ def run(input, conf, filepath=None): if conf.is_file_ignored(filepath): return () - if isinstance(input, (type(b''), type(u''))): # compat with Python 2 & 3 + if isinstance(input, (bytes, str)): return _run(input, conf, filepath) elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase # We need to have everything in memory to parse correctly diff --git a/yamllint/rules/braces.py b/yamllint/rules/braces.py index 759306e..d3c03fb 100644 --- a/yamllint/rules/braces.py +++ b/yamllint/rules/braces.py @@ -22,7 +22,8 @@ braces (``{`` and ``}``). * ``forbid`` is used to forbid the use of flow mappings which are denoted by surrounding braces (``{`` and ``}``). Use ``true`` to forbid the use of flow - mappings completely. + mappings completely. Use ``non-empty`` to forbid the use of all flow + mappings except for empty ones. * ``min-spaces-inside`` defines the minimal number of spaces required inside braces. * ``max-spaces-inside`` defines the maximal number of spaces allowed inside @@ -60,6 +61,18 @@ braces (``{`` and ``}``). object: { key1: 4, key2: 8 } +#. With ``braces: {forbid: non-empty}`` + + the following code snippet would **PASS**: + :: + + object: {} + + the following code snippet would **FAIL**: + :: + + object: { key1: 4, key2: 8 } + #. With ``braces: {min-spaces-inside: 0, max-spaces-inside: 0}`` the following code snippet would **PASS**: @@ -128,7 +141,7 @@ from yamllint.rules.common import spaces_after, spaces_before ID = 'braces' TYPE = 'token' -CONF = {'forbid': bool, +CONF = {'forbid': (bool, 'non-empty'), 'min-spaces-inside': int, 'max-spaces-inside': int, 'min-spaces-inside-empty': int, @@ -141,7 +154,15 @@ DEFAULT = {'forbid': False, def check(conf, token, prev, next, nextnext, context): - if conf['forbid'] and isinstance(token, yaml.FlowMappingStartToken): + if (conf['forbid'] is True and + isinstance(token, yaml.FlowMappingStartToken)): + yield LintProblem(token.start_mark.line + 1, + token.end_mark.column + 1, + 'forbidden flow mapping') + + elif (conf['forbid'] == 'non-empty' and + isinstance(token, yaml.FlowMappingStartToken) and + not isinstance(next, yaml.FlowMappingEndToken)): yield LintProblem(token.start_mark.line + 1, token.end_mark.column + 1, 'forbidden flow mapping') diff --git a/yamllint/rules/brackets.py b/yamllint/rules/brackets.py index 6ab02df..01fee87 100644 --- a/yamllint/rules/brackets.py +++ b/yamllint/rules/brackets.py @@ -22,7 +22,8 @@ inside brackets (``[`` and ``]``). * ``forbid`` is used to forbid the use of flow sequences which are denoted by surrounding brackets (``[`` and ``]``). Use ``true`` to forbid the use of - flow sequences completely. + flow sequences completely. Use ``non-empty`` to forbid the use of all flow + sequences except for empty ones. * ``min-spaces-inside`` defines the minimal number of spaces required inside brackets. * ``max-spaces-inside`` defines the maximal number of spaces allowed inside @@ -61,6 +62,18 @@ inside brackets (``[`` and ``]``). object: [ 1, 2, abc ] +#. With ``brackets: {forbid: non-empty}`` + + the following code snippet would **PASS**: + :: + + object: [] + + the following code snippet would **FAIL**: + :: + + object: [ 1, 2, abc ] + #. With ``brackets: {min-spaces-inside: 0, max-spaces-inside: 0}`` the following code snippet would **PASS**: @@ -129,7 +142,7 @@ from yamllint.rules.common import spaces_after, spaces_before ID = 'brackets' TYPE = 'token' -CONF = {'forbid': bool, +CONF = {'forbid': (bool, 'non-empty'), 'min-spaces-inside': int, 'max-spaces-inside': int, 'min-spaces-inside-empty': int, @@ -142,7 +155,15 @@ DEFAULT = {'forbid': False, def check(conf, token, prev, next, nextnext, context): - if conf['forbid'] and isinstance(token, yaml.FlowSequenceStartToken): + if (conf['forbid'] is True and + isinstance(token, yaml.FlowSequenceStartToken)): + yield LintProblem(token.start_mark.line + 1, + token.end_mark.column + 1, + 'forbidden flow sequence') + + elif (conf['forbid'] == 'non-empty' and + isinstance(token, yaml.FlowSequenceStartToken) and + not isinstance(next, yaml.FlowSequenceEndToken)): yield LintProblem(token.start_mark.line + 1, token.end_mark.column + 1, 'forbidden flow sequence') diff --git a/yamllint/rules/comments.py b/yamllint/rules/comments.py index 70ae250..02eddad 100644 --- a/yamllint/rules/comments.py +++ b/yamllint/rules/comments.py @@ -74,8 +74,6 @@ Use this rule to control the position and formatting of comments. """ -import re - from yamllint.linter import LintProblem @@ -105,7 +103,7 @@ def check(conf, comment): if (conf['ignore-shebangs'] and comment.line_no == 1 and comment.column_no == 1 and - re.match(r'^!\S', comment.buffer[text_start:])): + comment.buffer[text_start] == '!'): return # We can test for both \r and \r\n just by checking first char # \r itself is a valid newline on some older OS. diff --git a/yamllint/rules/key_ordering.py b/yamllint/rules/key_ordering.py index eca38ab..c5cb45a 100644 --- a/yamllint/rules/key_ordering.py +++ b/yamllint/rules/key_ordering.py @@ -18,8 +18,8 @@ Use this rule to enforce alphabetical ordering of keys in mappings. The sorting order uses the Unicode code point number as a default. As a result, the ordering is case-sensitive and not accent-friendly (see examples below). -This can be changed by setting the global ``locale`` option. This allows to -sort case and accents properly. +This can be changed by setting the global ``locale`` option. This allows one +to sort case and accents properly. .. rubric:: Examples diff --git a/yamllint/rules/line_length.py b/yamllint/rules/line_length.py index b329817..cfc328c 100644 --- a/yamllint/rules/line_length.py +++ b/yamllint/rules/line_length.py @@ -17,10 +17,6 @@ """ Use this rule to set a limit to lines length. -Note: with Python 2, the ``line-length`` rule may not work properly with -unicode characters because of the way strings are represented in bytes. We -recommend running yamllint with Python 3. - .. rubric:: Options * ``max`` defines the maximal (inclusive) length of lines. @@ -144,7 +140,11 @@ def check(conf, line): start += 1 if start != line.end: - if line.buffer[start] in ('#', '-'): + if line.buffer[start] == '#': + while line.buffer[start] == '#': + start += 1 + start += 1 + elif line.buffer[start] == '-': start += 2 if line.buffer.find(' ', start, line.end) == -1: diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 279cd6a..1ef18db 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -144,6 +144,17 @@ def VALIDATE(conf): DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' +# https://stackoverflow.com/a/36514274 +yaml.resolver.Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:int', + re.compile(r'''^(?:[-+]?0b[0-1_]+ + |[-+]?0o?[0-7_]+ + |[-+]?0[0-7_]+ + |[-+]?(?:0|[1-9][0-9_]*) + |[-+]?0x[0-9a-fA-F_]+ + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), + list('-+0123456789')) + def _quote_match(quote_type, token_style): return ((quote_type == 'any') or