|
5 | 5 | import re |
6 | 6 | import sys |
7 | 7 | import unicodedata |
8 | | -from types import SimpleNamespace |
9 | 8 | from urllib.parse import urlparse |
10 | 9 |
|
| 10 | +from packaging.version import InvalidVersion, Version |
| 11 | + |
11 | 12 | if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 |
12 | 13 | import tomllib |
13 | 14 | else: # pragma: no-cover-if-gte-py311 |
|
17 | 18 | from briefcase.platforms import get_output_formats, get_platforms |
18 | 19 |
|
19 | 20 | from .constants import RESERVED_WORDS |
20 | | -from .exceptions import BriefcaseConfigError |
| 21 | +from .exceptions import BriefcaseConfigError, InvalidVersionError |
21 | 22 |
|
22 | 23 | # PEP 508 restricts the naming of modules. The PEP defines a regex that uses |
23 | 24 | # re.IGNORECASE; but in in practice, packaging uses a version that rolls out the lower |
@@ -299,49 +300,6 @@ def is_valid_bundle_identifier(bundle): |
299 | 300 | return VALID_BUNDLE_RE.match(bundle) is not None |
300 | 301 |
|
301 | 302 |
|
302 | | -# This is the canonical definition from PEP440, modified to include named groups |
303 | | -PEP440_CANONICAL_VERSION_PATTERN_RE = re.compile( |
304 | | - r"^((?P<epoch>[1-9][0-9]*)!)?" |
305 | | - r"(?P<release>(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*)" |
306 | | - r"((?P<pre_tag>a|b|rc)(?P<pre_value>0|[1-9][0-9]*))?" |
307 | | - r"(\.post(?P<post>0|[1-9][0-9]*))?" |
308 | | - r"(\.dev(?P<dev>0|[1-9][0-9]*))?$" |
309 | | -) |
310 | | - |
311 | | - |
312 | | -def is_pep440_canonical_version(version): |
313 | | - """Determine if the string describes a valid PEP440 canonical version specifier. |
314 | | -
|
315 | | - This implementation comes directly from PEP440 itself. |
316 | | -
|
317 | | - :returns: True if the version string is valid; false otherwise. |
318 | | - """ |
319 | | - return PEP440_CANONICAL_VERSION_PATTERN_RE.match(version) is not None |
320 | | - |
321 | | - |
322 | | -def parsed_version(version): |
323 | | - """Return a parsed version string. |
324 | | -
|
325 | | - :param version: The parsed version string |
326 | | - """ |
327 | | - groupdict = PEP440_CANONICAL_VERSION_PATTERN_RE.match(version).groupdict() |
328 | | - |
329 | | - # Convert dot separated string of integers to tuple of integers |
330 | | - groupdict["release"] = tuple(int(p) for p in groupdict.pop("release").split(".")) |
331 | | - |
332 | | - # Convert strings to values |
333 | | - for key in ("epoch", "pre_value", "post", "dev"): |
334 | | - try: |
335 | | - groupdict[key] = int(groupdict[key]) |
336 | | - except TypeError: |
337 | | - pass |
338 | | - |
339 | | - tag = groupdict.pop("pre_tag") |
340 | | - value = groupdict.pop("pre_value") |
341 | | - groupdict["pre"] = (tag, value) if tag is not None else None |
342 | | - return SimpleNamespace(**groupdict) |
343 | | - |
344 | | - |
345 | 303 | def parse_boolean(value: str) -> bool: |
346 | 304 | """Takes a string value and attempts to convert to a boolean value.""" |
347 | 305 |
|
@@ -419,13 +377,17 @@ def __init__( |
419 | 377 | self.license = license |
420 | 378 | self.requires_python = requires_python |
421 | 379 |
|
422 | | - # Version number is PEP440 compliant: |
423 | | - if not is_pep440_canonical_version(self.version): |
424 | | - raise BriefcaseConfigError( |
425 | | - f"Version number ({self.version}) is not valid.\n\n" |
426 | | - "Version numbers must be PEP440 compliant; " |
427 | | - "see https://www.python.org/dev/peps/pep-0440/ for details." |
428 | | - ) |
| 380 | + # Version number is compliant with PEP440 (and related updates): |
| 381 | + try: |
| 382 | + # If input is already a version object (can happen by copying), use as-is |
| 383 | + if isinstance(version, Version): |
| 384 | + self.version = version |
| 385 | + else: |
| 386 | + self.version = Version(version) |
| 387 | + except InvalidVersion: |
| 388 | + raise InvalidVersionError( |
| 389 | + f"Version number ({self.version}) is not valid." |
| 390 | + ) from None |
429 | 391 |
|
430 | 392 | def __repr__(self): |
431 | 393 | return f"<{self.project_name} v{self.version} GlobalConfig>" |
@@ -537,14 +499,17 @@ def __init__( |
537 | 499 | install=self.install_options, |
538 | 500 | ) |
539 | 501 |
|
540 | | - # Version number is PEP440 compliant: |
541 | | - if not is_pep440_canonical_version(self.version): |
542 | | - raise BriefcaseConfigError( |
| 502 | + # Version number is compliant with PEP440 (and related updates): |
| 503 | + try: |
| 504 | + # If input is already a version object (can happen by copying), use as-is |
| 505 | + if isinstance(version, Version): |
| 506 | + self.version = version |
| 507 | + else: |
| 508 | + self.version = Version(version) |
| 509 | + except InvalidVersion: |
| 510 | + raise InvalidVersionError( |
543 | 511 | f"Version number for {self.app_name!r} ({self.version}) is not valid." |
544 | | - f"\n\n" |
545 | | - "Version numbers must be PEP440 compliant; " |
546 | | - "see https://www.python.org/dev/peps/pep-0440/ for details." |
547 | | - ) |
| 512 | + ) from None |
548 | 513 |
|
549 | 514 | if self.sources: |
550 | 515 | # Sources list doesn't include any duplicates |
|
0 commit comments