|
1 | 1 | #!/usr/bin/env python |
2 | 2 |
|
3 | | - |
4 | 3 | import os |
5 | 4 | import re |
6 | 5 | import sys |
| 6 | +import typing as t |
| 7 | + |
| 8 | +# TODO Improve: try using the semantic_version_checker package for semver regex |
| 9 | + |
| 10 | +ExceptionFactory = t.Callable[[str, str, str], Exception] |
| 11 | +ClientCallback = t.Callable[[str, str], t.Tuple] |
| 12 | + |
| 13 | +MatchConverter = t.Callable[[t.Match], t.Tuple] |
| 14 | +MatchData = t.Tuple[str, t.List[t.Any], t.Callable[[t.Match], t.Tuple]] |
| 15 | +# 1st item (str): 'method'/'callable attribute' of the 're' python module) |
| 16 | +# 2nd item (list): zero or more additional runtime arguments |
| 17 | +# 3rd item (Callable): takes a Match object and return a tuple of strings |
7 | 18 |
|
8 | 19 | my_dir = os.path.dirname(os.path.realpath(__file__)) |
9 | 20 |
|
10 | | -SETUP_CFG_FILENAME = 'setup.cfg' |
11 | | -SETUP_CFG = os.path.join(my_dir, '../', SETUP_CFG_FILENAME) |
| 21 | +TOML = 'pyproject.toml' |
| 22 | +TOML_FILE = os.path.abspath(os.path.join(my_dir, '..', TOML)) |
12 | 23 |
|
| 24 | +DEMO_SECTION: str = ( |
| 25 | + "[tool.software-release]\nversion_variable = " |
| 26 | + "src/package_name/__init__.py:__version__" |
| 27 | +) |
13 | 28 |
|
14 | | -def main(): |
15 | | - """Get the package version string provided that the developer has setup indication how to find it. Reads the |
16 | | - [semantic_release] section found in setup.cfg and then determines where is the actual version string |
17 | | - """ |
18 | | - # Automatically compute package version from the [semantic_release] section in setup.cfg |
19 | | - with open(SETUP_CFG, 'r') as _file: |
20 | | - regex = r"\[semantic_release\][\w\s=/\.:\d]+version_variable[\ \t]*=[\ \t]*([\w\.]+(?:/[\w\.]+)*):(\w+)" |
21 | | - match = re.search(regex, _file.read(), re.MULTILINE) |
| 29 | + |
| 30 | +def build_client_callback(data: MatchData, factory: ExceptionFactory) -> ClientCallback: |
| 31 | + |
| 32 | + def client_callback(file_path: str, regex: str) -> t.Tuple: |
| 33 | + with open(file_path, 'r') as _file: |
| 34 | + contents = _file.read() |
| 35 | + match = getattr(re, data[0])(regex, contents, *data[1]) |
22 | 36 | if match: |
23 | | - file_with_version_string = os.path.join(my_dir, '../', match.group(1)) |
24 | | - variable_holding_version_value = match.group(2) |
| 37 | + extracted_tuple = data[2](match) |
| 38 | + return extracted_tuple |
25 | 39 | else: |
26 | | - raise RuntimeError( |
27 | | - f"Expected to find the '[semantic_release]' section, in the '{SETUP_CFG}' file, with key " |
28 | | - f"'version_variable'.\nFor example:\n[semantic_release]\nversion_variable = " |
29 | | - f"src/package_name/__init__.py:__version__\n indicated that the version string should be looked up in " |
30 | | - f"the src/package_name/__init__.py file registered under the __version__ 'name'") |
31 | | - |
32 | | - # (it does not have to be a.py file) |
33 | | - # to indicate that the version is stored in the '__version__' |
34 | | - if not os.path.isfile(file_with_version_string): |
35 | | - raise FileNotFoundError( |
36 | | - f"Path '{file_with_version_string} does not appear to be valid. Please go to the '{SETUP_CFG}' file, at the" |
37 | | - f" [semantic_release] section and set the 'version_variable' key with a valid file path (to look for the " |
38 | | - f"version string)") |
| 40 | + raise factory(file_path, regex, contents) |
| 41 | + return client_callback |
39 | 42 |
|
40 | | - reg_string = r'\s*=\s*[\'\"]([^\'\"]*)[\'\"]' |
41 | 43 |
|
| 44 | +# PARSERS |
42 | 45 |
|
43 | | - with open(file_with_version_string, 'r') as _file: |
44 | | - content = _file.read() |
45 | | - reg = f'^{variable_holding_version_value}' + reg_string |
46 | | - match = re.search(reg, content, re.MULTILINE) |
47 | | - if match: |
48 | | - _version = match.group(1) |
49 | | - return _version |
50 | | - raise AttributeError(f"Could not find a match for regex {reg} when applied to:\n{content}") |
| 46 | +software_release_parser = build_client_callback(( |
| 47 | + 'search', |
| 48 | + [re.MULTILINE,], |
| 49 | + lambda match: (match.group(1), match.group(2)) |
| 50 | +), |
| 51 | + lambda file_path, reg, string: RuntimeError( |
| 52 | + "Expected to find the '[tool.software-release]' section, in " |
| 53 | + f"the '{file_path}' file, with key " |
| 54 | + "'version_variable'.\nFor example:\n" |
| 55 | + f"{DEMO_SECTION}\n " |
| 56 | + "indicates that the version string should be looked up in " |
| 57 | + f"the src/package_name/__init__.py file and specifically " |
| 58 | + "a '__version__ = 1.2.3' kind of line is expected to be found." |
| 59 | + ) |
| 60 | +) |
51 | 61 |
|
52 | 62 |
|
53 | | -if __name__ == '__main__': |
| 63 | +version_file_parser = build_client_callback(( |
| 64 | + 'search', |
| 65 | + [re.MULTILINE,], |
| 66 | + lambda match: (match.group(1),) |
| 67 | +), |
| 68 | + lambda file_path, reg, string: AttributeError( |
| 69 | + "Could not find a match for regex {regex} when applied to:".format( |
| 70 | + regex=reg |
| 71 | + ) + "\n{content}".format(content=string) |
| 72 | + ) |
| 73 | +) |
| 74 | + |
| 75 | + |
| 76 | +def parse_version(software_release_cfg: str) -> str: |
| 77 | + """Detect, parse and return the version (string) from python source code. |
| 78 | +
|
| 79 | + Get the package version (string) provided that the developer has setup |
| 80 | + indication how to find it. |
| 81 | +
|
| 82 | + Reads the [tool.software-release] section found in pyproject.toml and then |
| 83 | + determines where is the actual version string. |
| 84 | + """ |
| 85 | + header = r'\[tool\.software-release\]' |
| 86 | + sep = r'[\w\s=/\.:\d]+' # in some cases accounts for miss-typed characters! |
| 87 | + version_specification = \ |
| 88 | + r"version_variable[\ \t]*=[\ \t]*['\"]?([\w\.]+(?:/[\w\.]+)*):(\w+)['\"]?" |
| 89 | + regex = f"{header}{sep}{version_specification}" |
| 90 | + |
| 91 | + file_name_with_version, version_variable_name = \ |
| 92 | + software_release_parser(software_release_cfg, regex) |
| 93 | + |
| 94 | + file_with_version_string = \ |
| 95 | + os.path.abspath(os.path.join(my_dir, '../', file_name_with_version)) |
| 96 | + |
| 97 | + if not os.path.isfile(file_with_version_string): |
| 98 | + raise FileNotFoundError( |
| 99 | + f"Path '{file_with_version_string} does not appear to be valid. " |
| 100 | + f"Please go to the '{software_release_cfg}' file, at the" |
| 101 | + " [tool.software-release] section and set the 'version_variable' " |
| 102 | + "key with a valid file path (to look for the version string). " |
| 103 | + f"For example:\n{DEMO_SECTION}\n" |
| 104 | + ) |
| 105 | + |
| 106 | + reg = f'^{version_variable_name}' + r'\s*=\s*[\'\"]([^\'\"]*)[\'\"]' |
| 107 | + version, = version_file_parser(file_with_version_string, reg) |
| 108 | + return version |
| 109 | + |
| 110 | + |
| 111 | +def _main(): |
54 | 112 | try: |
55 | | - version_string = main() |
| 113 | + version_string = parse_version(TOML_FILE) |
56 | 114 | print(version_string) |
| 115 | + return 0 |
57 | 116 | except (RuntimeError, FileNotFoundError, AttributeError) as exception: |
58 | 117 | print(exception) |
59 | | - sys.exit(1) |
| 118 | + return 1 |
| 119 | + |
| 120 | + |
| 121 | +def main(): |
| 122 | + sys.exit(_main()) |
| 123 | + |
| 124 | + |
| 125 | +if __name__ == '__main__': |
| 126 | + main() |
0 commit comments