check_version.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. """Module for checking and comparing albumentations package versions.
  2. This module provides utilities for version checking and comparison, including
  3. the ability to fetch the latest version from PyPI and compare it with the currently
  4. installed version. It helps users stay informed about available updates and
  5. encourages keeping the library up-to-date with the latest features and bug fixes.
  6. """
  7. from __future__ import annotations
  8. import json
  9. import re
  10. import urllib.request
  11. from urllib.request import OpenerDirector
  12. from warnings import warn
  13. from albumentations import __version__ as current_version
  14. __version__: str = current_version # type: ignore[has-type, unused-ignore]
  15. SUCCESS_HTML_CODE = 200
  16. opener = None
  17. def get_opener() -> OpenerDirector:
  18. """Get or create a URL opener for making HTTP requests.
  19. This function implements a singleton pattern for the opener to avoid
  20. recreating it on each request. It lazily instantiates a URL opener
  21. with HTTP and HTTPS handlers.
  22. Returns:
  23. OpenerDirector: URL opener instance for making HTTP requests.
  24. """
  25. global opener # noqa: PLW0603
  26. if opener is None:
  27. opener = urllib.request.build_opener(urllib.request.HTTPHandler(), urllib.request.HTTPSHandler())
  28. return opener
  29. def fetch_version_info() -> str:
  30. """Fetch version information from PyPI for albumentations package.
  31. This function retrieves JSON data from PyPI containing information about
  32. the latest available version of albumentations. It handles network errors
  33. gracefully and returns an empty string if the request fails.
  34. Returns:
  35. str: JSON string containing version information if successful,
  36. empty string otherwise.
  37. """
  38. opener = get_opener()
  39. url = "https://pypi.org/pypi/albumentations/json"
  40. try:
  41. with opener.open(url, timeout=2) as response:
  42. if response.status == SUCCESS_HTML_CODE:
  43. data = response.read()
  44. encoding = response.info().get_content_charset("utf-8")
  45. return data.decode(encoding)
  46. except Exception as e: # noqa: BLE001
  47. warn(f"Error fetching version info {e}", stacklevel=2)
  48. return ""
  49. def parse_version(data: str) -> str:
  50. """Parses the version from the given JSON data."""
  51. if data:
  52. try:
  53. json_data = json.loads(data)
  54. # Use .get() to avoid KeyError if 'version' is not present
  55. return json_data.get("info", {}).get("version", "")
  56. except json.JSONDecodeError:
  57. # This will handle malformed JSON data
  58. return ""
  59. return ""
  60. def compare_versions(v1: tuple[int | str, ...], v2: tuple[int | str, ...]) -> bool:
  61. """Compare two version tuples.
  62. Returns True if v1 > v2, False otherwise.
  63. Special rules:
  64. 1. Release version > pre-release version (e.g., (1, 4) > (1, 4, 'beta'))
  65. 2. Numeric parts are compared numerically
  66. 3. String parts are compared lexicographically
  67. """
  68. # First compare common parts
  69. for p1, p2 in zip(v1, v2):
  70. if p1 != p2:
  71. # If both are same type, direct comparison works
  72. if isinstance(p1, int) and isinstance(p2, int):
  73. return p1 > p2
  74. if isinstance(p1, str) and isinstance(p2, str):
  75. return p1 > p2
  76. # If types differ, numbers are greater (release > pre-release)
  77. return isinstance(p1, int)
  78. # If we get here, all common parts are equal
  79. # Longer version is greater only if next element is a number
  80. if len(v1) > len(v2):
  81. return isinstance(v1[len(v2)], int)
  82. if len(v2) > len(v1):
  83. # v2 is longer, so v1 is greater only if v2's next part is a string (pre-release)
  84. return isinstance(v2[len(v1)], str)
  85. return False # Versions are equal
  86. def parse_version_parts(version_str: str) -> tuple[int | str, ...]:
  87. """Convert version string to tuple of (int | str) parts following PEP 440 conventions.
  88. Examples:
  89. "1.4.24" -> (1, 4, 24)
  90. "1.4beta" -> (1, 4, "beta")
  91. "1.4.beta2" -> (1, 4, "beta", 2)
  92. "1.4.alpha2" -> (1, 4, "alpha", 2)
  93. """
  94. parts = []
  95. # First split by dots
  96. for part in version_str.split("."):
  97. # Then parse each part for numbers and letters
  98. segments = re.findall(r"([0-9]+|[a-zA-Z]+)", part)
  99. for segment in segments:
  100. if segment.isdigit():
  101. parts.append(int(segment))
  102. else:
  103. parts.append(segment.lower())
  104. return tuple(parts)
  105. def check_for_updates() -> None:
  106. """Check if a newer version of albumentations is available on PyPI.
  107. This function compares the current installed version with the latest version
  108. available on PyPI. If a newer version is found, it issues a warning to the user
  109. with upgrade instructions. All exceptions are caught to ensure this check
  110. doesn't affect normal package operation.
  111. The check can be disabled by setting the environment variable
  112. NO_ALBUMENTATIONS_UPDATE to 1.
  113. """
  114. try:
  115. data = fetch_version_info()
  116. latest_version = parse_version(data)
  117. if latest_version:
  118. latest_parts = parse_version_parts(latest_version)
  119. current_parts = parse_version_parts(current_version)
  120. if compare_versions(latest_parts, current_parts):
  121. warn(
  122. f"A new version of Albumentations is available: {latest_version!r} (you have {current_version!r}). "
  123. "Upgrade using: pip install -U albumentations. "
  124. "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.",
  125. UserWarning,
  126. stacklevel=2,
  127. )
  128. except Exception as e: # General exception catch to ensure silent failure # noqa: BLE001
  129. warn(
  130. f"Failed to check for updates due to error: {e}. "
  131. "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.",
  132. UserWarning,
  133. stacklevel=2,
  134. )