Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix cylc lint U013/U015 missing info #6214

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes.d/6214.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`cylc lint` rules U013 & U015 now tell you which deprecated variables you are using
44 changes: 29 additions & 15 deletions cylc/flow/scripts/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
}


LIST_ITEM = ' * '
LIST_ITEM = '\n * '


deprecated_string_templates = {
Expand Down Expand Up @@ -264,7 +264,12 @@
def check_for_deprecated_environment_variables(
line: str
) -> Union[bool, dict]:
"""Warn that environment variables with SUITE in are deprecated"""
"""Warn that environment variables with SUITE in are deprecated

Examples:
>>> check_for_deprecated_environment_variables('CYLC_SUITE_HOST')
{'vars': ['CYLC_SUITE_HOST: CYLC_WORKFLOW_HOST']}
"""
vars_found = [
f'{k}: {v}' for k, v in DEPRECATED_ENV_VARS.items()
if k in line
Expand All @@ -277,16 +282,21 @@
return False


def check_for_obsolete_environment_variables(line: str) -> List[str]:
def check_for_obsolete_environment_variables(line: str) -> Dict[str, List]:
"""Warn that environment variables are obsolete.

Examples:

>>> this = check_for_obsolete_environment_variables
>>> this('CYLC_SUITE_DEF_PATH')
['CYLC_SUITE_DEF_PATH']
>>> this('script = echo $CYLC_SUITE_DEF_PATH')
{'vars': ['CYLC_SUITE_DEF_PATH']}
>>> this('script = echo "irrelevent"')
{}
"""
return [i for i in OBSOLETE_ENV_VARS if i in line]
vars_found = [i for i in OBSOLETE_ENV_VARS if i in line]
if vars_found:
return {'vars': vars_found}

Check warning on line 298 in cylc/flow/scripts/lint.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/scripts/lint.py#L298

Added line #L298 was not covered by tests
return {}


def check_for_deprecated_task_event_template_vars(
Expand All @@ -298,13 +308,10 @@
>>> this = check_for_deprecated_task_event_template_vars

>>> this('hello = "My name is %(suite)s"')
{'list': ' * %(suite)s ⇒ %(workflow)s'}
{'suggest': '\\n * %(suite)s ⇒ %(workflow)s'}

>>> expect = {'list': (
... ' * %(suite)s ⇒ %(workflow)s * %(task_url)s'
... ' - get ``URL`` (if set in :cylc:conf:`[meta]URL`)')}
>>> this('hello = "My name is %(suite)s, %(task_url)s"') == expect
True
>>> this('hello = "My name is %(suite)s, %(task_url)s"')
{'suggest': '\\n * %(suite)s ⇒ %(workf...cylc:conf:`[meta]URL`)'}
"""
result = []
for key, (regex, replacement) in deprecated_string_templates.items():
Expand All @@ -315,7 +322,7 @@
result.append(f'%({key})s ⇒ %({replacement})s')

if result:
return {'list': LIST_ITEM + LIST_ITEM.join(result)}
return {'suggest': LIST_ITEM + LIST_ITEM.join(result)}

Check warning on line 325 in cylc/flow/scripts/lint.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/scripts/lint.py#L325

Added line #L325 was not covered by tests
return None


Expand Down Expand Up @@ -370,7 +377,14 @@


def check_lowercase_family_names(line: str) -> bool:
"""Check for lowercase in family names."""
"""Check for lowercase in family names.

Examples:
>>> check_lowercase_family_names(' inherit = FOO')
False
wxtim marked this conversation as resolved.
Show resolved Hide resolved
>>> check_lowercase_family_names(' inherit = foo')
True
"""
match = INHERIT_REGEX.match(line)
if not match:
return False
Expand Down Expand Up @@ -673,7 +687,7 @@
},
'U015': {
'short': (
'Deprecated template variables.'),
'Deprecated template variables: {suggest}'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than have to rely on variable names matching here (suggest in this case but vars elsewhere), why not just make the check functions return a list of strings, and add the joined list to the end of the short message?

Copy link
Member Author

@wxtim wxtim Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows us to insert multiple variables

msg = check_meta['short'].format(**check)

It's used by one check.

I've re-drafted this PR - I want to have a think about where I'm going with this....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Un drafted - I think that my long term goal is to have a better defined check API so that others can add them, but I think I want this fixed sooner, and will worry about that later.

'rst': (
'The following template variables, mostly used in event handlers,'
'are deprecated, and should be replaced:'
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/scripts/test_lint_checkers.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Separate - I have in my mind that at some point I'm going to refactor out those aspects of Cylc lint which are data rather than infrastructure.

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# 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/>.
"""Test check functions in the `cylc lint` CLI Utility."""

import doctest
import json
import pytest
import re

from cylc.flow.scripts import lint

VARS = re.compile(r'\{(.*)\}')

# Functions in Cylc Lint defined with "check_*
CHECKERS = [
getattr(lint, i) for i in lint.__dir__() if i.startswith('check_')]
# List of checks defined as checks by Cylc Lint
ALL_CHECKS = [
*lint.MANUAL_DEPRECATIONS.values(),
*lint.STYLE_CHECKS.values(),
]

finder = doctest.DocTestFinder()


@pytest.mark.parametrize(
'check',
# Those checks that have custom checker functions
# and a short message with variables to insert:
[
pytest.param(c, id=c.get('function').__name__) for c in ALL_CHECKS
if c.get('function') in CHECKERS
]
)
def test_custom_checker_doctests(check):
"""All check functions have at least one failure doctest

By forcing each check function to have valid doctests
for the case that linting has failed we are able to
check that the function outputs the correct information
for formatting the short formats.
"""
doctests = finder.find(check['function'])[0]

msg = f'{check["function"].__name__}: No failure examples in doctest'
assert any(i.want for i in doctests.examples if i.want), msg


@pytest.mark.parametrize(
'ref, check',
# Those checks that have custom checker functions
# and a short message with variables to insert:
[
(c.get('function'), c) for c in ALL_CHECKS
if c.get('function') in CHECKERS
and VARS.findall(c['short'])
]
)
def test_custom_checkers_short_formatters(ref, check):
"""If a check message has a format string assert that the checker
function will return a dict to be used in
``check['short'].format(**kwargs)``, based on doctest output.

ref is useful to allow us to identify the check, even
though not used in the test.
"""
doctests = finder.find(check['function'])[0]

# Filter doctest examples for cases where there is a json parsable
# want.
examples = [
eg for eg in [
json.loads(e.want.replace("'", '"'))
for e in doctests.examples if e.want
]
if eg
]

# Formatting using the example output changes the check short text:
for example in examples:
assert check['short'].format(**example) != check['short']
Loading