transformers-mlinter 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlinter/__init__.py +105 -0
- mlinter/__main__.py +18 -0
- mlinter/_helpers.py +194 -0
- mlinter/_version.py +127 -0
- mlinter/mlinter.py +475 -0
- mlinter/rules.toml +230 -0
- mlinter/trf001.py +50 -0
- mlinter/trf002.py +51 -0
- mlinter/trf003.py +65 -0
- mlinter/trf004.py +49 -0
- mlinter/trf005.py +62 -0
- mlinter/trf006.py +59 -0
- mlinter/trf007.py +61 -0
- mlinter/trf008.py +52 -0
- mlinter/trf009.py +90 -0
- mlinter/trf010.py +54 -0
- mlinter/trf011.py +258 -0
- mlinter/trf012.py +60 -0
- mlinter/trf013.py +63 -0
- mlinter/trf014.py +77 -0
- mlinter/trf015.py +263 -0
- transformers_mlinter-0.1.0.dist-info/METADATA +132 -0
- transformers_mlinter-0.1.0.dist-info/RECORD +27 -0
- transformers_mlinter-0.1.0.dist-info/WHEEL +5 -0
- transformers_mlinter-0.1.0.dist-info/entry_points.txt +2 -0
- transformers_mlinter-0.1.0.dist-info/licenses/LICENSE +203 -0
- transformers_mlinter-0.1.0.dist-info/top_level.txt +1 -0
mlinter/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Supported public API for transformers-mlinter.
|
|
16
|
+
|
|
17
|
+
Import public helpers from ``mlinter`` instead of implementation modules such as
|
|
18
|
+
``mlinter.mlinter`` or ``mlinter._helpers``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._helpers import (
|
|
22
|
+
MODELS_ROOT,
|
|
23
|
+
Violation,
|
|
24
|
+
full_name,
|
|
25
|
+
is_self_method_call,
|
|
26
|
+
is_super_method_call,
|
|
27
|
+
iter_pretrained_classes,
|
|
28
|
+
)
|
|
29
|
+
from ._helpers import (
|
|
30
|
+
_collect_class_bases as collect_class_bases,
|
|
31
|
+
)
|
|
32
|
+
from ._helpers import (
|
|
33
|
+
_has_rule_suppression as has_rule_suppression,
|
|
34
|
+
)
|
|
35
|
+
from ._helpers import (
|
|
36
|
+
_inherits_pretrained_model as inherits_pretrained_model,
|
|
37
|
+
)
|
|
38
|
+
from ._helpers import (
|
|
39
|
+
_model_dir_name as model_dir_name,
|
|
40
|
+
)
|
|
41
|
+
from ._version import __version__
|
|
42
|
+
from .mlinter import (
|
|
43
|
+
DEFAULT_ENABLED_TRF_RULES,
|
|
44
|
+
TRF_MODEL_DIR_ALLOWLISTS,
|
|
45
|
+
TRF_RULE_CHECKS,
|
|
46
|
+
TRF_RULE_SPECS,
|
|
47
|
+
TRF_RULES,
|
|
48
|
+
analyze_file,
|
|
49
|
+
colored_error_message,
|
|
50
|
+
emit_violation,
|
|
51
|
+
format_rule_details,
|
|
52
|
+
format_rule_summary,
|
|
53
|
+
format_violation,
|
|
54
|
+
get_changed_modeling_files,
|
|
55
|
+
iter_modeling_files,
|
|
56
|
+
main,
|
|
57
|
+
maybe_handle_rule_docs_cli,
|
|
58
|
+
parse_args,
|
|
59
|
+
render_rules_reference,
|
|
60
|
+
resolve_enabled_rules,
|
|
61
|
+
should_show_progress,
|
|
62
|
+
)
|
|
63
|
+
from .mlinter import (
|
|
64
|
+
_is_rule_allowlisted_for_file as is_rule_allowlisted_for_file,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"DEFAULT_ENABLED_TRF_RULES",
|
|
70
|
+
"MODELS_ROOT",
|
|
71
|
+
"TRF_MODEL_DIR_ALLOWLISTS",
|
|
72
|
+
"TRF_RULE_CHECKS",
|
|
73
|
+
"TRF_RULE_SPECS",
|
|
74
|
+
"TRF_RULES",
|
|
75
|
+
"Violation",
|
|
76
|
+
"__version__",
|
|
77
|
+
"analyze_file",
|
|
78
|
+
"collect_class_bases",
|
|
79
|
+
"colored_error_message",
|
|
80
|
+
"emit_violation",
|
|
81
|
+
"format_rule_details",
|
|
82
|
+
"format_rule_summary",
|
|
83
|
+
"format_violation",
|
|
84
|
+
"full_name",
|
|
85
|
+
"get_changed_modeling_files",
|
|
86
|
+
"has_rule_suppression",
|
|
87
|
+
"inherits_pretrained_model",
|
|
88
|
+
"is_rule_allowlisted_for_file",
|
|
89
|
+
"is_self_method_call",
|
|
90
|
+
"is_super_method_call",
|
|
91
|
+
"iter_modeling_files",
|
|
92
|
+
"iter_pretrained_classes",
|
|
93
|
+
"main",
|
|
94
|
+
"maybe_handle_rule_docs_cli",
|
|
95
|
+
"model_dir_name",
|
|
96
|
+
"parse_args",
|
|
97
|
+
"render_rules_reference",
|
|
98
|
+
"resolve_enabled_rules",
|
|
99
|
+
"should_show_progress",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
for rule_id in sorted(TRF_RULE_CHECKS):
|
|
103
|
+
globals()[rule_id] = rule_id
|
|
104
|
+
__all__.append(rule_id)
|
|
105
|
+
del rule_id
|
mlinter/__main__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from . import main
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
raise SystemExit(main())
|
mlinter/_helpers.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Shared AST helper functions used across mlinter rule modules."""
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MODELS_ROOT = Path("src/transformers/models")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Violation:
|
|
27
|
+
file_path: Path
|
|
28
|
+
line_number: int
|
|
29
|
+
message: str
|
|
30
|
+
rule_id: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def full_name(node: ast.AST):
|
|
34
|
+
"""Return full dotted name from an Attribute or Name node."""
|
|
35
|
+
if isinstance(node, ast.Name):
|
|
36
|
+
return node.id
|
|
37
|
+
elif isinstance(node, ast.Attribute):
|
|
38
|
+
return full_name(node.value) + "." + node.attr
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError("Not a Name or Attribute node")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _simple_name(name: str) -> str:
|
|
44
|
+
return name.split(".")[-1]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _model_dir_name(file_path: Path) -> str | None:
|
|
48
|
+
try:
|
|
49
|
+
relative = file_path.resolve().relative_to(MODELS_ROOT.resolve())
|
|
50
|
+
except ValueError:
|
|
51
|
+
try:
|
|
52
|
+
relative = file_path.relative_to(MODELS_ROOT)
|
|
53
|
+
except ValueError:
|
|
54
|
+
return None
|
|
55
|
+
if len(relative.parts) < 2:
|
|
56
|
+
return None
|
|
57
|
+
return relative.parts[0]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _known_model_dirs() -> set[str]:
|
|
61
|
+
return {path.name for path in MODELS_ROOT.iterdir() if path.is_dir()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_rule_suppression(lines: list[str], rule_id: str, line_number: int) -> bool:
|
|
65
|
+
if line_number <= 0:
|
|
66
|
+
return False
|
|
67
|
+
token = f"trf-ignore: {rule_id}".lower()
|
|
68
|
+
candidate_indexes = (line_number - 1, line_number - 2)
|
|
69
|
+
for idx in candidate_indexes:
|
|
70
|
+
if 0 <= idx < len(lines) and token in lines[idx].lower():
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _collect_class_bases(tree: ast.Module) -> dict[str, list[str]]:
|
|
76
|
+
class_to_bases: dict[str, list[str]] = {}
|
|
77
|
+
for node in tree.body:
|
|
78
|
+
if not isinstance(node, ast.ClassDef):
|
|
79
|
+
continue
|
|
80
|
+
base_names = []
|
|
81
|
+
for base in node.bases:
|
|
82
|
+
try:
|
|
83
|
+
base_names.append(full_name(base))
|
|
84
|
+
except ValueError:
|
|
85
|
+
continue
|
|
86
|
+
class_to_bases[node.name] = base_names
|
|
87
|
+
return class_to_bases
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _inherits_pretrained_model(
|
|
91
|
+
class_name: str, class_to_bases: dict[str, list[str]], visiting: set[str] | None = None
|
|
92
|
+
) -> bool:
|
|
93
|
+
if visiting is None:
|
|
94
|
+
visiting = set()
|
|
95
|
+
if class_name in visiting:
|
|
96
|
+
return False
|
|
97
|
+
visiting.add(class_name)
|
|
98
|
+
|
|
99
|
+
for base_name in class_to_bases.get(class_name, []):
|
|
100
|
+
simple_base_name = _simple_name(base_name)
|
|
101
|
+
if simple_base_name.endswith("PreTrainedModel"):
|
|
102
|
+
return True
|
|
103
|
+
if simple_base_name in class_to_bases and _inherits_pretrained_model(
|
|
104
|
+
simple_base_name, class_to_bases, visiting
|
|
105
|
+
):
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def iter_pretrained_classes(tree: ast.Module, source_lines: list[str], rule_id: str) -> list[ast.ClassDef]:
|
|
111
|
+
"""Yield ClassDef nodes that inherit from PreTrainedModel (transitively), skipping suppressed ones."""
|
|
112
|
+
class_to_bases = _collect_class_bases(tree)
|
|
113
|
+
results = []
|
|
114
|
+
for node in tree.body:
|
|
115
|
+
if not isinstance(node, ast.ClassDef):
|
|
116
|
+
continue
|
|
117
|
+
if not _inherits_pretrained_model(node.name, class_to_bases):
|
|
118
|
+
continue
|
|
119
|
+
if _has_rule_suppression(source_lines, rule_id, node.lineno):
|
|
120
|
+
continue
|
|
121
|
+
results.append(node)
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_class_assignments(class_node: ast.ClassDef) -> dict[str, ast.AST]:
|
|
126
|
+
assignments: dict[str, ast.AST] = {}
|
|
127
|
+
for item in class_node.body:
|
|
128
|
+
if isinstance(item, ast.Assign) and len(item.targets) == 1 and isinstance(item.targets[0], ast.Name):
|
|
129
|
+
assignments[item.targets[0].id] = item.value
|
|
130
|
+
elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name) and item.value is not None:
|
|
131
|
+
assignments[item.target.id] = item.value
|
|
132
|
+
return assignments
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _class_methods(class_node: ast.ClassDef) -> dict[str, ast.FunctionDef]:
|
|
136
|
+
return {item.name: item for item in class_node.body if isinstance(item, ast.FunctionDef)}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _function_argument_names(function_node: ast.FunctionDef) -> set[str]:
|
|
140
|
+
names = {arg.arg for arg in function_node.args.args}
|
|
141
|
+
names.update(arg.arg for arg in function_node.args.kwonlyargs)
|
|
142
|
+
if function_node.args.vararg is not None:
|
|
143
|
+
names.add(function_node.args.vararg.arg)
|
|
144
|
+
if function_node.args.kwarg is not None:
|
|
145
|
+
names.add(function_node.args.kwarg.arg)
|
|
146
|
+
return names
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _function_uses_name(function_node: ast.FunctionDef, name: str) -> bool:
|
|
150
|
+
return any(
|
|
151
|
+
isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)
|
|
152
|
+
for node in ast.walk(function_node)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_self_method_call(node: ast.AST, method: str) -> bool:
|
|
157
|
+
"""Check if `node` is a method call on `self`, such as `self.method(...)`"""
|
|
158
|
+
return (
|
|
159
|
+
isinstance(node, ast.Call)
|
|
160
|
+
and isinstance(node.func, ast.Attribute)
|
|
161
|
+
and isinstance(node.func.value, ast.Name)
|
|
162
|
+
and node.func.value.id == "self"
|
|
163
|
+
and node.func.attr == method
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_super_method_call(node: ast.AST, method: str) -> bool:
|
|
168
|
+
"""Check if `node` is a call to `super().method(...)`"""
|
|
169
|
+
return (
|
|
170
|
+
isinstance(node, ast.Call)
|
|
171
|
+
and isinstance(node.func, ast.Attribute)
|
|
172
|
+
and isinstance(node.func.value, ast.Call)
|
|
173
|
+
and isinstance(node.func.value.func, ast.Name)
|
|
174
|
+
and node.func.value.func.id == "super"
|
|
175
|
+
and node.func.attr == method
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _is_direct_pretrained_config_subclass(class_node: ast.ClassDef) -> bool:
|
|
180
|
+
for base in class_node.bases:
|
|
181
|
+
try:
|
|
182
|
+
if _simple_name(full_name(base)) in {"PreTrainedConfig", "PretrainedConfig"}:
|
|
183
|
+
return True
|
|
184
|
+
except ValueError:
|
|
185
|
+
continue
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _has_strict_decorator(class_node: ast.ClassDef) -> bool:
|
|
190
|
+
for decorator in class_node.decorator_list:
|
|
191
|
+
if isinstance(decorator, ast.Name) and decorator.id == "strict":
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
return False
|
mlinter/_version.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import tomllib # Python >= 3.11
|
|
23
|
+
except ModuleNotFoundError:
|
|
24
|
+
import tomli as tomllib # Python 3.10 fallback
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
PACKAGE_NAME = "transformers-mlinter"
|
|
28
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
DEFAULT_BASE_VERSION = "0.1.0"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_version_from_pyproject() -> str | None:
|
|
33
|
+
pyproject_path = PROJECT_ROOT / "pyproject.toml"
|
|
34
|
+
try:
|
|
35
|
+
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
36
|
+
except (FileNotFoundError, OSError, tomllib.TOMLDecodeError):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
project = data.get("project")
|
|
40
|
+
if not isinstance(project, dict):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
version = project.get("version")
|
|
44
|
+
if not isinstance(version, str) or not version.strip():
|
|
45
|
+
return None
|
|
46
|
+
return version
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _installed_distribution():
|
|
50
|
+
try:
|
|
51
|
+
return distribution(PACKAGE_NAME)
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _short_commit_id(commit_id: str | None) -> str | None:
|
|
57
|
+
if commit_id is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
commit_id = commit_id.strip()
|
|
61
|
+
if not commit_id:
|
|
62
|
+
return None
|
|
63
|
+
return commit_id[:7]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_git_hash_from_direct_url(dist) -> str | None:
|
|
67
|
+
if dist is None:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
direct_url = dist.read_text("direct_url.json")
|
|
71
|
+
if not direct_url:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
direct_url_data = json.loads(direct_url)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
vcs_info = direct_url_data.get("vcs_info")
|
|
80
|
+
if not isinstance(vcs_info, dict) or vcs_info.get("vcs") != "git":
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
commit_id = vcs_info.get("commit_id")
|
|
84
|
+
if not isinstance(commit_id, str):
|
|
85
|
+
return None
|
|
86
|
+
return _short_commit_id(commit_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _read_git_hash_from_checkout() -> str | None:
|
|
90
|
+
if not (PROJECT_ROOT / ".git").exists():
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
["git", "-C", str(PROJECT_ROOT), "rev-parse", "--short", "HEAD"],
|
|
96
|
+
capture_output=True,
|
|
97
|
+
check=False,
|
|
98
|
+
text=True,
|
|
99
|
+
)
|
|
100
|
+
except OSError:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if result.returncode != 0:
|
|
104
|
+
return None
|
|
105
|
+
return _short_commit_id(result.stdout)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _append_git_hash(base_version: str, git_hash: str | None) -> str:
|
|
109
|
+
if git_hash is None or "+" in base_version:
|
|
110
|
+
return base_version
|
|
111
|
+
return f"{base_version}+g{git_hash}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _resolve_base_version(dist) -> str:
|
|
115
|
+
if dist is not None:
|
|
116
|
+
return dist.version
|
|
117
|
+
return _read_version_from_pyproject() or DEFAULT_BASE_VERSION
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _resolve_version() -> str:
|
|
121
|
+
dist = _installed_distribution()
|
|
122
|
+
base_version = _resolve_base_version(dist)
|
|
123
|
+
git_hash = _read_git_hash_from_direct_url(dist) or _read_git_hash_from_checkout()
|
|
124
|
+
return _append_git_hash(base_version, git_hash)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__version__ = _resolve_version()
|