| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- """Module for checking and comparing albumentations package versions.
- This module provides utilities for version checking and comparison, including
- the ability to fetch the latest version from PyPI and compare it with the currently
- installed version. It helps users stay informed about available updates and
- encourages keeping the library up-to-date with the latest features and bug fixes.
- """
- from __future__ import annotations
- import json
- import re
- import urllib.request
- from urllib.request import OpenerDirector
- from warnings import warn
- from albumentations import __version__ as current_version
- __version__: str = current_version # type: ignore[has-type, unused-ignore]
- SUCCESS_HTML_CODE = 200
- opener = None
- def get_opener() -> OpenerDirector:
- """Get or create a URL opener for making HTTP requests.
- This function implements a singleton pattern for the opener to avoid
- recreating it on each request. It lazily instantiates a URL opener
- with HTTP and HTTPS handlers.
- Returns:
- OpenerDirector: URL opener instance for making HTTP requests.
- """
- global opener # noqa: PLW0603
- if opener is None:
- opener = urllib.request.build_opener(urllib.request.HTTPHandler(), urllib.request.HTTPSHandler())
- return opener
- def fetch_version_info() -> str:
- """Fetch version information from PyPI for albumentations package.
- This function retrieves JSON data from PyPI containing information about
- the latest available version of albumentations. It handles network errors
- gracefully and returns an empty string if the request fails.
- Returns:
- str: JSON string containing version information if successful,
- empty string otherwise.
- """
- opener = get_opener()
- url = "https://pypi.org/pypi/albumentations/json"
- try:
- with opener.open(url, timeout=2) as response:
- if response.status == SUCCESS_HTML_CODE:
- data = response.read()
- encoding = response.info().get_content_charset("utf-8")
- return data.decode(encoding)
- except Exception as e: # noqa: BLE001
- warn(f"Error fetching version info {e}", stacklevel=2)
- return ""
- def parse_version(data: str) -> str:
- """Parses the version from the given JSON data."""
- if data:
- try:
- json_data = json.loads(data)
- # Use .get() to avoid KeyError if 'version' is not present
- return json_data.get("info", {}).get("version", "")
- except json.JSONDecodeError:
- # This will handle malformed JSON data
- return ""
- return ""
- def compare_versions(v1: tuple[int | str, ...], v2: tuple[int | str, ...]) -> bool:
- """Compare two version tuples.
- Returns True if v1 > v2, False otherwise.
- Special rules:
- 1. Release version > pre-release version (e.g., (1, 4) > (1, 4, 'beta'))
- 2. Numeric parts are compared numerically
- 3. String parts are compared lexicographically
- """
- # First compare common parts
- for p1, p2 in zip(v1, v2):
- if p1 != p2:
- # If both are same type, direct comparison works
- if isinstance(p1, int) and isinstance(p2, int):
- return p1 > p2
- if isinstance(p1, str) and isinstance(p2, str):
- return p1 > p2
- # If types differ, numbers are greater (release > pre-release)
- return isinstance(p1, int)
- # If we get here, all common parts are equal
- # Longer version is greater only if next element is a number
- if len(v1) > len(v2):
- return isinstance(v1[len(v2)], int)
- if len(v2) > len(v1):
- # v2 is longer, so v1 is greater only if v2's next part is a string (pre-release)
- return isinstance(v2[len(v1)], str)
- return False # Versions are equal
- def parse_version_parts(version_str: str) -> tuple[int | str, ...]:
- """Convert version string to tuple of (int | str) parts following PEP 440 conventions.
- Examples:
- "1.4.24" -> (1, 4, 24)
- "1.4beta" -> (1, 4, "beta")
- "1.4.beta2" -> (1, 4, "beta", 2)
- "1.4.alpha2" -> (1, 4, "alpha", 2)
- """
- parts = []
- # First split by dots
- for part in version_str.split("."):
- # Then parse each part for numbers and letters
- segments = re.findall(r"([0-9]+|[a-zA-Z]+)", part)
- for segment in segments:
- if segment.isdigit():
- parts.append(int(segment))
- else:
- parts.append(segment.lower())
- return tuple(parts)
- def check_for_updates() -> None:
- """Check if a newer version of albumentations is available on PyPI.
- This function compares the current installed version with the latest version
- available on PyPI. If a newer version is found, it issues a warning to the user
- with upgrade instructions. All exceptions are caught to ensure this check
- doesn't affect normal package operation.
- The check can be disabled by setting the environment variable
- NO_ALBUMENTATIONS_UPDATE to 1.
- """
- try:
- data = fetch_version_info()
- latest_version = parse_version(data)
- if latest_version:
- latest_parts = parse_version_parts(latest_version)
- current_parts = parse_version_parts(current_version)
- if compare_versions(latest_parts, current_parts):
- warn(
- f"A new version of Albumentations is available: {latest_version!r} (you have {current_version!r}). "
- "Upgrade using: pip install -U albumentations. "
- "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.",
- UserWarning,
- stacklevel=2,
- )
- except Exception as e: # General exception catch to ensure silent failure # noqa: BLE001
- warn(
- f"Failed to check for updates due to error: {e}. "
- "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.",
- UserWarning,
- stacklevel=2,
- )
|