|
7 | 7 | # By accessing, using, copying or modifying this work you indicate your |
8 | 8 | # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights |
9 | 9 | # not expressly granted therein are reserved by Shotgun Software Inc. |
10 | | -import warnings |
11 | 10 | import contextlib |
12 | | -import sys |
13 | | - |
14 | | -LooseVersion = None |
15 | | -try: |
16 | | - import packaging.version |
17 | | -except ModuleNotFoundError: |
18 | | - try: |
19 | | - # Try importing from setuptools. |
20 | | - # If it fails, then we can't do much at the moment |
21 | | - # The DCC should have either setuptools or packaging installed. |
22 | | - from setuptools._distutils.version import LooseVersion |
23 | | - except ModuleNotFoundError: |
24 | | - try: |
25 | | - # DCCs with older versions of Python 3.12 |
26 | | - from distutils.version import LooseVersion |
27 | | - except ModuleNotFoundError: |
28 | | - pass |
| 11 | +import warnings |
| 12 | + |
| 13 | +from tank_vendor.packaging.version import parse as version_parse |
29 | 14 |
|
30 | | -from . import sgre as re |
31 | 15 | from .. import LogManager |
32 | 16 | from ..errors import TankError |
33 | | - |
| 17 | +from . import sgre as re |
34 | 18 |
|
35 | 19 | logger = LogManager.get_logger(__name__) |
36 | 20 | GITHUB_HASH_RE = re.compile("^[0-9a-fA-F]{7,40}$") |
37 | 21 |
|
| 22 | +# Normalize non-standard version formats |
| 23 | +# into PEP 440–compliant forms ("1.2.3") to ensure compatibility with |
| 24 | +# Python’s version parsing utilities (e.g., packaging.version.parse). |
| 25 | +# Reference: https://peps.python.org/pep-0440/ |
| 26 | +_VERSION_PATTERNS = [ |
| 27 | + ( # Extract version from software names: "Software Name 21.0" -> "21.0" |
| 28 | + re.compile(r"^[a-zA-Z\s]+(\d+(?:\.\d+)*(?:v\d+(?:\.\d+)*)?)$"), |
| 29 | + r"\1", |
| 30 | + ), |
| 31 | + ( # Dot-v format: "6.3v6" -> "6.3.6" |
| 32 | + re.compile(r"^(\d+)\.(\d+)v(\d+)$"), |
| 33 | + r"\1.\2.\3", |
| 34 | + ), |
| 35 | + ( # Simple v format: "2019v0.1" -> "2019.0.1" |
| 36 | + re.compile(r"^(\d+)v(\d+(?:\.\d+)*)$"), |
| 37 | + r"\1.\2", |
| 38 | + ), |
| 39 | + ( # Service pack with/without dot: "2017.2sp1" or "2017.2.sp1" -> "2017.2.post1" |
| 40 | + re.compile(r"^(\d+(?:\.\d+)*)\.?(sp|hotfix|hf)(\d+)$"), |
| 41 | + r"\1.post\3", |
| 42 | + ), |
| 43 | +] |
| 44 | + |
38 | 45 |
|
39 | 46 | def is_version_head(version): |
40 | 47 | """ |
@@ -150,28 +157,29 @@ def suppress_known_deprecation(): |
150 | 157 | yield ctx |
151 | 158 |
|
152 | 159 |
|
153 | | -def version_parse(version_string): |
| 160 | +def normalize_version_format(version: str) -> str: |
154 | 161 | """ |
155 | | - Parse a version string into a Version object. We also support LooseVersion |
156 | | - for compatibility with older versions of Python. |
| 162 | + Normalize version strings by applying common format transformations. |
| 163 | +
|
| 164 | + This function exists because packaging.version.parse() follows PEP 440 |
| 165 | + and cannot handle non-standard version formats like "v1.2.3" or "6.3v6", |
| 166 | + which are commonly found in various software tools and DCCs but don't |
| 167 | + conform to the PEP 440 specification. |
157 | 168 |
|
158 | | - :param str version_string: The version string to parse. |
| 169 | + Transformations applied: |
| 170 | + - Extract version numbers from software names: "Software Name 21.0" -> "21.0" |
| 171 | + - Convert dot-v format: "6.3v6" -> "6.3.6" |
| 172 | + - Convert simple v format: "2019v0.1" -> "2019.0.1" |
| 173 | + - Convert service pack formats: "2017.2sp1" -> "2017.2.post1", "2017.2.sp1" -> "2017.2.post1" |
159 | 174 |
|
160 | | - :rtype: packaging.version.Version, LooseVersion or str as fallback. |
| 175 | + :param str version: Version string to normalize |
| 176 | + :return str: Normalized version string compatible with PEP 440 |
161 | 177 | """ |
162 | | - if "packaging" in sys.modules: |
163 | | - try: |
164 | | - return packaging.version.parse(version_string) |
165 | | - except packaging.version.InvalidVersion: |
166 | | - # Version cannot be parsed with packaging.version (SG-40480) |
167 | | - pass |
168 | 178 |
|
169 | | - if LooseVersion: |
170 | | - with suppress_known_deprecation(): |
171 | | - return LooseVersion(version_string) |
| 179 | + for compiled_pattern, replacement in _VERSION_PATTERNS: |
| 180 | + version = compiled_pattern.sub(replacement, version) |
172 | 181 |
|
173 | | - # Fallback to string comparison |
174 | | - return version_string |
| 182 | + return version |
175 | 183 |
|
176 | 184 |
|
177 | 185 | def _compare_versions(a, b): |
@@ -205,79 +213,12 @@ def _compare_versions(a, b): |
205 | 213 | # comparing against HEAD - our version is always old |
206 | 214 | return False |
207 | 215 |
|
208 | | - if a.startswith("v"): |
209 | | - a = a[1:] |
210 | | - if b.startswith("v"): |
211 | | - b = b[1:] |
212 | | - |
213 | | - if "packaging" in sys.modules: |
214 | | - version_a = version_parse(a) |
215 | | - version_b = version_parse(b) |
216 | | - if isinstance(version_a, str) or isinstance(version_b, str): |
217 | | - return a > b |
218 | | - |
219 | | - return version_a > version_b |
220 | | - |
221 | | - if LooseVersion: |
222 | | - # In Python 3, LooseVersion comparisons between versions where a non-numeric |
223 | | - # version component is compared to a numeric one fail. We'll work around this |
224 | | - # as follows: |
225 | | - # First, try to use LooseVersion for comparison. This should work in |
226 | | - # most cases. |
227 | | - try: |
228 | | - with suppress_known_deprecation(): |
229 | | - # Supress `distutils Version classes are deprecated.` for Python 3.10 |
230 | | - version_a = LooseVersion(a).version |
231 | | - version_b = LooseVersion(b).version |
232 | | - |
233 | | - version_num_a = [] |
234 | | - version_num_b = [] |
235 | | - # taking only the integers of the version to make comparison |
236 | | - for version in version_a: |
237 | | - if isinstance(version, (int)): |
238 | | - version_num_a.append(version) |
239 | | - elif version == "-": |
240 | | - break |
241 | | - for version in version_b: |
242 | | - if isinstance(version, (int)): |
243 | | - version_num_b.append(version) |
244 | | - elif version == "-": |
245 | | - break |
246 | | - |
247 | | - # Comparing equal number versions with with one of them with '-' appended, if a version |
248 | | - # has '-' appended it's older than the same version with '-' at the end |
249 | | - if version_num_a == version_num_b: |
250 | | - if "-" in a and "-" not in b: |
251 | | - return False # False, version a is older than b |
252 | | - elif "-" in b and "-" not in a: |
253 | | - return True # True, version a is older than b |
254 | | - else: |
255 | | - return LooseVersion(a) > LooseVersion( |
256 | | - b |
257 | | - ) # If both has '-' compare '-rcx' versions |
258 | | - else: |
259 | | - return LooseVersion(a) > LooseVersion( |
260 | | - b |
261 | | - ) # If they are different numeric versions |
262 | | - except TypeError: |
263 | | - version_expr = re.compile(r"^((?:\d+)(?:\.\d+)*)(.+)$") |
264 | | - match_a = version_expr.match(a) |
265 | | - match_b = version_expr.match(b) |
266 | | - if match_a and match_b: |
267 | | - # If we could get two numeric versions, generate LooseVersions for |
268 | | - # them. |
269 | | - ver_a = LooseVersion(match_a.group(1)) |
270 | | - ver_b = LooseVersion(match_b.group(1)) |
271 | | - if ver_a != ver_b: |
272 | | - # If they're not identical, return based on this comparison |
273 | | - return ver_a > ver_b |
274 | | - else: |
275 | | - # If the numeric versions do match, do a string comparsion for |
276 | | - # the rest. |
277 | | - return match_a.group(2) > match_b.group(2) |
278 | | - elif match_a or match_b: |
279 | | - # If only one had a numeric version, treat that as the newer version. |
280 | | - return bool(match_a) |
281 | | - |
282 | | - # In the case that both versions are non-numeric, do a string comparison. |
283 | | - return a > b |
| 216 | + a = normalize_version_format(a) |
| 217 | + b = normalize_version_format(b) |
| 218 | + |
| 219 | + # Use packaging.version (either system or vendored) |
| 220 | + # This is now guaranteed to be available |
| 221 | + version_a = version_parse(a) |
| 222 | + version_b = version_parse(b) |
| 223 | + |
| 224 | + return version_a > version_b |
0 commit comments