Test Failed
Push — dev ( 071855...3757e3 )
by Konstantinos
06:23 queued 04:11
created

parse_version   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 129
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 70
dl 0
loc 129
rs 10
c 0
b 0
f 0
wmc 13

4 Functions

Rating   Name   Duplication   Size   Complexity  
A main() 0 8 2
A parse_version() 0 36 2
A get_arguments() 0 6 4
A build_client_callback() 0 16 5
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))
0 ignored issues
show
introduced by
The variable project_dir does not seem to be defined in case len(sys_args) == 1 on line 110 is False. Are you sure this can never be the case?
Loading history...
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