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