Skip to content

Commit b2800bb

Browse files
Use packaging for version parsing (#2694)
Co-authored-by: Malcolm Smith <smith@chaquo.com>
1 parent 8676670 commit b2800bb

17 files changed

Lines changed: 150 additions & 602 deletions

File tree

changes/2693.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Briefcase now accepts more version numbers as valid, including leading zeros in version components (e.g., `25.09.2`) and accepting all forms of capitalization in number suffixes (e.g., accepting both `1.2RC3` and `1.2rc3`).

src/briefcase/commands/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,9 +1296,9 @@ def generate_template(
12961296
) -> None:
12971297
# If a branch wasn't supplied through the --template-branch argument,
12981298
# use the branch derived from the Briefcase version
1299-
version = Version(briefcase.__version__)
1299+
briefcase_version = Version(briefcase.__version__)
13001300
if branch is None:
1301-
template_branch = f"v{version.base_version}"
1301+
template_branch = f"v{briefcase_version.base_version}"
13021302
else:
13031303
template_branch = branch
13041304

@@ -1310,7 +1310,7 @@ def generate_template(
13101310
{
13111311
"template_source": template,
13121312
"template_branch": template_branch,
1313-
"briefcase_version": str(version),
1313+
"briefcase_version": str(briefcase_version),
13141314
}
13151315
)
13161316

@@ -1328,7 +1328,7 @@ def generate_template(
13281328
except InvalidTemplateBranch:
13291329
# Only use the main template if we're on a development branch of briefcase
13301330
# and the user didn't explicitly specify which branch to use.
1331-
if version.dev is None or branch is not None:
1331+
if briefcase_version.dev is None or branch is not None:
13321332
raise
13331333

13341334
# Development branches can use the main template.

src/briefcase/commands/create.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ def generate_app_template(self, app: AppConfig):
243243
{
244244
# Ensure the output format is in the case we expect
245245
"format": self.output_format.lower(),
246+
# Ensure the version number is in string form
247+
"version": str(app.version),
246248
# Properties of the generating environment
247249
# The full Python version string, including minor and dev/a/b/c suffixes
248250
# (e.g., 3.11.0rc2)

src/briefcase/config.py

Lines changed: 24 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import re
66
import sys
77
import unicodedata
8-
from types import SimpleNamespace
98
from urllib.parse import urlparse
109

10+
from packaging.version import InvalidVersion, Version
11+
1112
if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311
1213
import tomllib
1314
else: # pragma: no-cover-if-gte-py311
@@ -17,7 +18,7 @@
1718
from briefcase.platforms import get_output_formats, get_platforms
1819

1920
from .constants import RESERVED_WORDS
20-
from .exceptions import BriefcaseConfigError
21+
from .exceptions import BriefcaseConfigError, InvalidVersionError
2122

2223
# PEP 508 restricts the naming of modules. The PEP defines a regex that uses
2324
# 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):
299300
return VALID_BUNDLE_RE.match(bundle) is not None
300301

301302

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-
345303
def parse_boolean(value: str) -> bool:
346304
"""Takes a string value and attempts to convert to a boolean value."""
347305

@@ -419,13 +377,17 @@ def __init__(
419377
self.license = license
420378
self.requires_python = requires_python
421379

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
429391

430392
def __repr__(self):
431393
return f"<{self.project_name} v{self.version} GlobalConfig>"
@@ -537,14 +499,17 @@ def __init__(
537499
install=self.install_options,
538500
)
539501

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(
543511
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
548513

549514
if self.sources:
550515
# Sources list doesn't include any duplicates

src/briefcase/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ def __str__(self):
8383
return f"Briefcase configuration error: {self.msg}"
8484

8585

86+
class InvalidVersionError(BriefcaseConfigError):
87+
def __init__(self, msg):
88+
super().__init__(
89+
f"{msg}\n\n"
90+
"Version numbers must be PEP440 compliant; see "
91+
"https://packaging.python.org/en/latest/specifications/version-specifiers/"
92+
" for details."
93+
)
94+
95+
8696
class UnsupportedHostError(BriefcaseError):
8797
def __init__(self, reason):
8898
super().__init__(error_code=110, skip_logfile=True)

src/briefcase/integrations/flatpak.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from collections.abc import Collection
55
from pathlib import Path
66

7+
from packaging.version import Version
8+
79
from briefcase.exceptions import BriefcaseCommandError
810
from briefcase.integrations.base import Tool, ToolCache
911
from briefcase.integrations.subprocess import SubprocessArgT
@@ -297,7 +299,7 @@ def bundle(
297299
repo_url: str,
298300
bundle_identifier: str,
299301
app_name: str,
300-
version: str,
302+
version: Version,
301303
build_path: Path,
302304
output_path: Path,
303305
):
@@ -328,7 +330,7 @@ def bundle(
328330
"repo",
329331
output_path,
330332
bundle_identifier,
331-
version,
333+
str(version),
332334
]
333335
+ (["--verbose"] if self.tools.console.is_deep_debug else []),
334336
check=True,

src/briefcase/platforms/android/gradle.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
RunCommand,
1818
UpdateCommand,
1919
)
20-
from briefcase.config import AppConfig, parsed_version
20+
from briefcase.config import AppConfig
2121
from briefcase.console import ANSI_ESC_SEQ_RE_DEF
2222
from briefcase.debuggers.base import (
2323
AppPackagesPathMappings,
@@ -176,9 +176,7 @@ def output_format_template_context(self, app: AppConfig):
176176
try:
177177
version_code = app.version_code
178178
except AttributeError:
179-
parsed = parsed_version(app.version)
180-
181-
v = ([*parsed.release, 0, 0])[:3] # version triple
179+
v = ([*app.version.release, 0, 0])[:3] # version triple
182180
build = int(getattr(app, "build", "0"))
183181
version_code = f"{v[0]:d}{v[1]:02d}{v[2]:02d}{build:02d}".lstrip("0")
184182

src/briefcase/platforms/linux/appimage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def build_app(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-windows
295295
try:
296296
# For some reason, the version has to be passed in as an
297297
# environment variable, *not* in the configuration.
298-
env["LINUXDEPLOY_OUTPUT_VERSION"] = app.version
298+
env["LINUXDEPLOY_OUTPUT_VERSION"] = str(app.version)
299299
# The internals of the binary aren't inherently visible, so
300300
# there's no need to package copyright files. These files
301301
# appear to be missing by default in the OS dev packages anyway,

src/briefcase/platforms/windows/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from zipfile import ZIP_DEFLATED, ZipFile
99

1010
from briefcase.commands import CreateCommand, PackageCommand, RunCommand
11-
from briefcase.config import AppConfig, parsed_version
11+
from briefcase.config import AppConfig
1212
from briefcase.exceptions import BriefcaseCommandError, UnsupportedHostError
1313
from briefcase.integrations.windows_sdk import WindowsSDK
1414
from briefcase.integrations.wix import WiX
@@ -145,9 +145,8 @@ def output_format_template_context(self, app: AppConfig):
145145
try:
146146
version_triple = app.version_triple
147147
except AttributeError:
148-
parsed = parsed_version(app.version)
149148
version_triple = ".".join(
150-
([str(v) for v in parsed.release] + ["0", "0"])[:3]
149+
([str(v) for v in app.version.release] + ["0", "0"])[:3]
151150
)
152151

153152
# The application needs a unique GUID.

src/briefcase/platforms/windows/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def build_app(self, app: BaseConfig, **kwargs):
122122
app.formal_name,
123123
"--set-version-string",
124124
"FileVersion",
125-
app.version,
125+
str(app.version),
126126
"--set-version-string",
127127
"InternalName",
128128
app.module_name,
@@ -134,7 +134,7 @@ def build_app(self, app: BaseConfig, **kwargs):
134134
app.formal_name,
135135
"--set-version-string",
136136
"ProductVersion",
137-
app.version,
137+
str(app.version),
138138
"--set-icon",
139139
"icon.ico",
140140
],

0 commit comments

Comments
 (0)