|
35 | 35 | import itertools |
36 | 36 | from contextlib import contextmanager |
37 | 37 | from pathlib import Path |
38 | | -from typing import Callable, IO, Iterable, Iterator, Optional, Tuple, Union |
| 38 | +from typing import Callable, IO, Iterable, Iterator, Optional, Tuple, Union, Pattern |
39 | 39 | from google.protobuf import timestamp_pb2 |
40 | 40 |
|
41 | 41 | import psutil |
|
66 | 66 | # it might not work |
67 | 67 | _USE_CP_FILE_RANGE = hasattr(os, "copy_file_range") |
68 | 68 |
|
| 69 | +# The default version guessing pattern for utils.guess_version() |
| 70 | +# |
| 71 | +_DEFAULT_GUESS_PATTERN = re.compile(r"(\d+)\.(\d+)(?:\.(\d+))?") |
| 72 | + |
69 | 73 |
|
70 | 74 | class UtilError(BstError): |
71 | 75 | """Raised by utility functions when system calls fail. |
@@ -697,15 +701,87 @@ def cleanup_tempfile(): |
697 | 701 |
|
698 | 702 | # get_umask(): |
699 | 703 | # |
700 | | -# Get the process's file mode creation mask without changing it. |
| 704 | +# |
701 | 705 | # |
702 | 706 | # Returns: |
703 | | -# (int) The process's file mode creation mask. |
| 707 | +# (int) |
704 | 708 | # |
705 | | -def get_umask(): |
| 709 | +def get_umask() -> int: |
| 710 | + """ |
| 711 | + Get the process's file mode creation mask without changing it. |
| 712 | +
|
| 713 | + Returns: The process's file mode creation mask. |
| 714 | + """ |
706 | 715 | return _UMASK |
707 | 716 |
|
708 | 717 |
|
| 718 | +def guess_version(string: str, *, pattern: Optional[Pattern[str]] = None) -> Optional[str]: |
| 719 | + """ |
| 720 | + Attempt to extract a version from an arbitrary string. |
| 721 | +
|
| 722 | + This function is used by sources who implement |
| 723 | + :func:`Source.get_source_info() <buildstream.source.SourceFetcher.get_source_info>` |
| 724 | + in order to provide a guess at what the version is, given some domain specific |
| 725 | + knowledge such as a git tag or a tarball URL. |
| 726 | +
|
| 727 | + This function will be traverse the provided string for non-overlapping matches, and |
| 728 | + in the case of *optional groups* being specified in the pattern; the match with the |
| 729 | + greatest amount of matched groups will be preferred, allowing for correct handling |
| 730 | + of cases like: ``https://example.com/releases/1.2/release-1.2.3.tgz`` which may |
| 731 | + match the *pattern* multiple times. |
| 732 | +
|
| 733 | + The resulting version will be the captured groups, separated by ``.`` characters. |
| 734 | +
|
| 735 | + Args: |
| 736 | + string: The domain specific string to scan for a version |
| 737 | + pattern: A compiled regex pattern to scan *string*, or None for the default ``(\\d+)\\.(\\d+)(?:\\.(\\d+))?``. |
| 738 | +
|
| 739 | + Returns: |
| 740 | + The guessed version, or None if no match was found. |
| 741 | +
|
| 742 | + .. note:: |
| 743 | +
|
| 744 | + **Specifying a pattern** |
| 745 | +
|
| 746 | + When specifying the pattern, any number of capture groups may be specified, and |
| 747 | + the match containing the most matching groups will be selected. |
| 748 | +
|
| 749 | + The capture groups must contain only the intended result and not any separating |
| 750 | + characters. |
| 751 | +
|
| 752 | + For example, you may parse a string such as ``release-1_2_3-r2`` with the pattern: |
| 753 | + ``(\\d+)_(\\d+)(?:_(\\d+))?(?:\\-(r\\d+))?``, and this would produce the parsed |
| 754 | + version ``1.2.3.r2``. |
| 755 | +
|
| 756 | + **Since: 2.5**. |
| 757 | + """ |
| 758 | + version_guess: Optional[str] = None |
| 759 | + version_guess_groups = 0 |
| 760 | + |
| 761 | + if pattern is None: |
| 762 | + pattern = _DEFAULT_GUESS_PATTERN |
| 763 | + |
| 764 | + # Iterate over non-overlapping matches, and prefer a match which is more qualified (i.e. 1.2.3 is better than 1.2) |
| 765 | + for version_match in pattern.finditer(string): |
| 766 | + |
| 767 | + if not version_match: |
| 768 | + iter_guess = None |
| 769 | + iter_n_groups = 0 |
| 770 | + elif pattern.groups == 0: |
| 771 | + iter_guess = str(version_match.group(0)) |
| 772 | + iter_n_groups = 1 |
| 773 | + else: |
| 774 | + iter_groups = [group for group in version_match.groups() if group is not None] |
| 775 | + iter_n_groups = len(iter_groups) |
| 776 | + iter_guess = ".".join(iter_groups) |
| 777 | + |
| 778 | + if version_guess is None or iter_n_groups > version_guess_groups: |
| 779 | + version_guess = iter_guess |
| 780 | + version_guess_groups = iter_n_groups |
| 781 | + |
| 782 | + return version_guess |
| 783 | + |
| 784 | + |
709 | 785 | # _get_host_tool_internal(): |
710 | 786 | # |
711 | 787 | # Get the full path of a host tool, including tools bundled inside the Python package. |
|
0 commit comments