Skip to content

Commit 08a8989

Browse files
authored
Add warning message about outdated linter version (#2615)
Checks every 24h if current version is latest and prints warning message if so. If HTTP requests fails, no error is displayed.
1 parent 9be4903 commit 08a8989

File tree

7 files changed

+149
-10
lines changed

7 files changed

+149
-10
lines changed

.config/dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ ungrouped
186186
unignored
187187
unimported
188188
unindented
189+
uninstallation
189190
unjinja
190191
unlex
191192
unnormalized

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ repos:
161161
plugins/.*
162162
)$
163163
- repo: https://github.com/pycqa/pylint
164-
rev: v2.15.3
164+
rev: v2.15.5
165165
hooks:
166166
- id: pylint
167167
additional_dependencies:

.pylintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[MAIN]
2+
extension-pkg-allow-list =
3+
black.parsing,
4+
15
[IMPORTS]
26
preferred-modules =
37
py:pathlib,
@@ -12,6 +16,9 @@ ignore-paths=^src/ansiblelint/_version.*$
1216

1317
[MESSAGES CONTROL]
1418

19+
# increase from default is 50 which is too aggressive
20+
max-statements = 60
21+
1522
disable =
1623
# On purpose disabled as we rely on black
1724
line-too-long,

src/ansiblelint/__main__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from ansiblelint._mockings import _perform_mockings_cleanup
4242
from ansiblelint.app import get_app
4343
from ansiblelint.color import console, console_options, reconfigure, render_yaml
44-
from ansiblelint.config import options
44+
from ansiblelint.config import get_version_warning, options
4545
from ansiblelint.constants import EXIT_CONTROL_C_RC, LOCK_TIMEOUT_RC
4646
from ansiblelint.file_utils import abspath, cwd, normpath
4747
from ansiblelint.skip_utils import normalize_tag
@@ -88,10 +88,6 @@ def initialize_options(arguments: list[str] | None = None) -> None:
8888
new_options = cli.get_config(arguments or [])
8989
new_options.cwd = pathlib.Path.cwd()
9090

91-
if new_options.version:
92-
print(f"ansible-lint {__version__} using ansible {ansible_version()}")
93-
sys.exit(0)
94-
9591
if new_options.colored is None:
9692
new_options.colored = should_do_markup()
9793

@@ -187,6 +183,13 @@ def main(argv: list[str] | None = None) -> int: # noqa: C901
187183
console_options["force_terminal"] = options.colored
188184
reconfigure(console_options)
189185

186+
if options.version:
187+
console.print(
188+
f"ansible-lint [repr.number]{__version__}[/] using ansible [repr.number]{ansible_version()}[/]"
189+
)
190+
console.print(get_version_warning())
191+
sys.exit(0)
192+
190193
initialize_logger(options.verbosity)
191194
_logger.debug("Options: %s", options)
192195
_logger.debug(os.getcwd())

src/ansiblelint/app.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ansiblelint import formatters
1414
from ansiblelint._mockings import _perform_mockings
1515
from ansiblelint.color import console, console_stderr, render_yaml
16-
from ansiblelint.config import PROFILES
16+
from ansiblelint.config import PROFILES, get_version_warning
1717
from ansiblelint.config import options as default_options
1818
from ansiblelint.constants import RULE_DOC_URL, SUCCESS_RC, VIOLATIONS_FOUND_RC
1919
from ansiblelint.errors import MatchError
@@ -204,8 +204,8 @@ def report_outcome(self, result: LintResult, mark_as_success: bool = False) -> i
204204

205205
return SUCCESS_RC if mark_as_success else VIOLATIONS_FOUND_RC
206206

207-
@staticmethod
208207
def report_summary( # pylint: disable=too-many-branches,too-many-locals
208+
self,
209209
summary: SummarizedResults,
210210
changed_files_count: int,
211211
files_count: int,
@@ -290,6 +290,11 @@ def report_summary( # pylint: disable=too-many-branches,too-many-locals
290290
msg += f", and fixed {summary.fixed} issue(s)"
291291
msg += f" on {files_count} files."
292292

293+
if not self.options.offline:
294+
version_warning = get_version_warning()
295+
if version_warning:
296+
msg += f"\n{version_warning}"
297+
293298
console_stderr.print(msg)
294299

295300

src/ansiblelint/config.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
"""Store configuration options as a singleton."""
22
from __future__ import annotations
33

4+
import json
5+
import logging
46
import os
57
import re
8+
import sys
9+
import time
10+
import urllib.request
11+
import warnings
612
from argparse import Namespace
713
from functools import lru_cache
814
from pathlib import Path
915
from typing import Any
16+
from urllib.error import HTTPError, URLError
1017

18+
from packaging.version import Version
19+
20+
from ansiblelint import __version__
1121
from ansiblelint.loaders import yaml_from_file
1222

23+
_logger = logging.getLogger(__name__)
24+
25+
26+
CACHE_DIR = (
27+
os.path.expanduser(os.environ.get("XDG_CONFIG_CACHE", "~/.cache")) + "/ansible-lint"
28+
)
29+
1330
DEFAULT_WARN_LIST = [
1431
"avoid-implicit",
1532
"experimental",
@@ -171,3 +188,106 @@ def parse_ansible_version(stdout: str) -> tuple[str, str | None]:
171188
if match:
172189
return match.group(1), None
173190
return "", f"FATAL: Unable parse ansible cli version: {stdout}"
191+
192+
193+
def in_venv() -> bool:
194+
"""Determine whether Python is running from a venv."""
195+
if hasattr(sys, "real_prefix"):
196+
return True
197+
pfx = getattr(sys, "base_prefix", sys.prefix)
198+
return pfx != sys.prefix
199+
200+
201+
def guess_install_method() -> str:
202+
"""Guess if pip upgrade command should be used."""
203+
pip = ""
204+
if in_venv():
205+
_logger.debug("Found virtualenv, assuming `pip3 install` will work.")
206+
pip = f"pip install --upgrade {__package__}"
207+
elif __file__.startswith(os.path.expanduser("~/.local/lib")):
208+
_logger.debug(
209+
"Found --user installation, assuming `pip3 install --user` will work."
210+
)
211+
pip = f"pip3 install --user --upgrade {__package__}"
212+
213+
# By default we assume pip is not safe to be used
214+
use_pip = False
215+
package_name = "ansible-lint"
216+
try:
217+
# Use pip to detect if is safe to use it to upgrade the package.
218+
# We do imports here to for performance and reasons, and also in order
219+
# to avoid errors if pip internals change. Also we want to avoid having
220+
# to add pip as a dependency, so we make use of it only when present.
221+
222+
# trick to avoid runtime warning from inside pip: _distutils_hack/__init__.py:33: UserWarning: Setuptools is replacing distutils.
223+
with warnings.catch_warnings(record=True):
224+
warnings.simplefilter("always")
225+
# pylint: disable=import-outside-toplevel
226+
from pip._internal.exceptions import UninstallationError
227+
from pip._internal.metadata import get_default_environment
228+
from pip._internal.req.req_uninstall import uninstallation_paths
229+
230+
try:
231+
dist = get_default_environment().get_distribution(package_name)
232+
if dist:
233+
logging.debug("Found %s dist", dist)
234+
for _ in uninstallation_paths(dist):
235+
use_pip = True
236+
else:
237+
logging.debug("Skipping %s as it is not installed.", package_name)
238+
use_pip = False
239+
except UninstallationError as exc:
240+
logging.debug(exc)
241+
use_pip = False
242+
except ImportError:
243+
use_pip = False
244+
245+
# We only want to recommend pip for upgrade if it looks safe to do so.
246+
return pip if use_pip else ""
247+
248+
249+
def get_version_warning() -> str:
250+
"""Display warning if current version is outdated."""
251+
msg = ""
252+
data = {}
253+
current_version = Version(__version__)
254+
if not os.path.exists(CACHE_DIR):
255+
os.makedirs(CACHE_DIR)
256+
cache_file = f"{CACHE_DIR}/latest.json"
257+
refresh = True
258+
if os.path.exists(cache_file):
259+
age = time.time() - os.path.getmtime(cache_file)
260+
if age < 24 * 60 * 60:
261+
refresh = False
262+
with open(cache_file, encoding="utf-8") as f:
263+
data = json.load(f)
264+
265+
if refresh or not data:
266+
release_url = (
267+
"https://api.github.com/repos/ansible/ansible-lint/releases/latest"
268+
)
269+
try:
270+
with urllib.request.urlopen(release_url) as url:
271+
data = json.load(url)
272+
with open(cache_file, "w", encoding="utf-8") as f:
273+
json.dump(data, f)
274+
except (URLError, HTTPError) as exc:
275+
_logger.debug(
276+
"Unable to fetch latest version from %s due to: %s", release_url, exc
277+
)
278+
return ""
279+
280+
html_url = data["html_url"]
281+
new_version = Version(data["tag_name"][1:]) # removing v prefix from tag
282+
# breakpoint()
283+
284+
if current_version > new_version:
285+
msg = "[dim]You are using a pre-release version of ansible-lint.[/]"
286+
elif current_version < new_version:
287+
msg = f"""[warning]A new release of ansible-lint is available: [red]{current_version}[/] → [green][link={html_url}]{new_version}[/][/][/]"""
288+
289+
pip = guess_install_method()
290+
if pip:
291+
msg += f" Upgrade by running: [info]{pip}[/]"
292+
293+
return msg

test/test_strict.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ def test_strict(strict: bool, returncode: int, message: str) -> None:
1919
result = run_ansible_lint(*args)
2020
assert result.returncode == returncode
2121
assert "key-order[task]" in result.stdout
22-
summary_line = result.stderr.splitlines()[-1]
23-
assert message in summary_line
22+
for summary_line in result.stderr.splitlines():
23+
if summary_line.startswith(message):
24+
break
25+
else:
26+
pytest.fail(f"Failed to find {message} inside stderr output")

0 commit comments

Comments
 (0)