pre-commit-crocodile 2.0.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.
- pre_commit_crocodile/__init__.py +5 -0
- pre_commit_crocodile/__main__.py +5 -0
- pre_commit_crocodile/assets/.cz.yaml +188 -0
- pre_commit_crocodile/assets/.pre-commit-config.yaml +68 -0
- pre_commit_crocodile/assets/__init__.py +0 -0
- pre_commit_crocodile/cli/__init__.py +0 -0
- pre_commit_crocodile/cli/entrypoint.py +414 -0
- pre_commit_crocodile/cli/main.py +269 -0
- pre_commit_crocodile/hooks/__init__.py +0 -0
- pre_commit_crocodile/hooks/configurations.py +322 -0
- pre_commit_crocodile/hooks/prepare_commit_message.py +258 -0
- pre_commit_crocodile/package/__init__.py +0 -0
- pre_commit_crocodile/package/bundle.py +34 -0
- pre_commit_crocodile/package/settings.py +148 -0
- pre_commit_crocodile/package/updates.py +228 -0
- pre_commit_crocodile/package/version.py +80 -0
- pre_commit_crocodile/prints/__init__.py +0 -0
- pre_commit_crocodile/prints/boxes.py +82 -0
- pre_commit_crocodile/prints/colors.py +85 -0
- pre_commit_crocodile/system/__init__.py +0 -0
- pre_commit_crocodile/system/commands.py +90 -0
- pre_commit_crocodile/system/platform.py +74 -0
- pre_commit_crocodile/types/__init__.py +0 -0
- pre_commit_crocodile/types/environments.py +92 -0
- pre_commit_crocodile/types/strings.py +152 -0
- pre_commit_crocodile-2.0.0.dist-info/LICENSE +201 -0
- pre_commit_crocodile-2.0.0.dist-info/METADATA +212 -0
- pre_commit_crocodile-2.0.0.dist-info/RECORD +31 -0
- pre_commit_crocodile-2.0.0.dist-info/WHEEL +5 -0
- pre_commit_crocodile-2.0.0.dist-info/entry_points.txt +3 -0
- pre_commit_crocodile-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Standard libraries
|
|
4
|
+
from os import environ
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from re import match, search
|
|
7
|
+
import sys
|
|
8
|
+
from sys import argv, exit as sys_exit, path
|
|
9
|
+
from typing import List, NamedTuple, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
# Bind sources
|
|
12
|
+
sys.dont_write_bytecode = True
|
|
13
|
+
path.append(str(Path(__file__).resolve().parent))
|
|
14
|
+
|
|
15
|
+
# Components, pylint: disable=import-error,wrong-import-position
|
|
16
|
+
from configurations import (
|
|
17
|
+
COMMITS_CHANGES_PATTERN,
|
|
18
|
+
COMMITS_CHANGES_SECTION,
|
|
19
|
+
COMMITS_COMMENTS_PREFIX,
|
|
20
|
+
COMMITS_DEFAULT_BODY,
|
|
21
|
+
COMMITS_DEFAULT_SCOPE,
|
|
22
|
+
COMMITS_DEFAULT_SUBJECT,
|
|
23
|
+
COMMITS_DEFAULT_TYPE,
|
|
24
|
+
CHANGES_EVALUATORS,
|
|
25
|
+
COMMITS_FOOTER_SIGNOFF,
|
|
26
|
+
CHANGES_MATCHERS,
|
|
27
|
+
COMMITS_MESSAGE_EOL,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Parse commit input
|
|
31
|
+
def parse_commit_input(filepath: str) -> Tuple[bool, bool, str, List[str]]:
|
|
32
|
+
|
|
33
|
+
# Variables
|
|
34
|
+
capture: bool = False
|
|
35
|
+
changes: List[str] = []
|
|
36
|
+
context: List[str] = []
|
|
37
|
+
empty: bool = True
|
|
38
|
+
signoff: bool = False
|
|
39
|
+
|
|
40
|
+
# Read commit template
|
|
41
|
+
with open(filepath, encoding='utf8', mode='r') as file:
|
|
42
|
+
for line in file:
|
|
43
|
+
|
|
44
|
+
# Get line content
|
|
45
|
+
content = line.rstrip()
|
|
46
|
+
|
|
47
|
+
# Append context
|
|
48
|
+
context += [content]
|
|
49
|
+
|
|
50
|
+
# Detect signoff
|
|
51
|
+
if content and content.startswith(COMMITS_FOOTER_SIGNOFF):
|
|
52
|
+
signoff = True
|
|
53
|
+
|
|
54
|
+
# Detect contents
|
|
55
|
+
if content and not any(
|
|
56
|
+
content.startswith(prefix) for prefix in [
|
|
57
|
+
COMMITS_COMMENTS_PREFIX,
|
|
58
|
+
COMMITS_FOOTER_SIGNOFF,
|
|
59
|
+
]):
|
|
60
|
+
empty = False
|
|
61
|
+
|
|
62
|
+
# Detect section
|
|
63
|
+
if content.startswith(COMMITS_CHANGES_SECTION):
|
|
64
|
+
capture = True
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Parse section
|
|
68
|
+
if capture:
|
|
69
|
+
matches = match(COMMITS_CHANGES_PATTERN, content)
|
|
70
|
+
if matches:
|
|
71
|
+
changes.append(matches.group(2).strip())
|
|
72
|
+
elif not content.strip() or content.startswith('#'):
|
|
73
|
+
capture = False
|
|
74
|
+
|
|
75
|
+
# Result
|
|
76
|
+
return empty, signoff, COMMITS_MESSAGE_EOL.join(context), changes
|
|
77
|
+
|
|
78
|
+
# Prepare commit title, pylint: disable=too-many-branches,too-many-locals,too-many-nested-blocks
|
|
79
|
+
def prepare_commit_title(changes: List[str]) -> Tuple[str, str]:
|
|
80
|
+
|
|
81
|
+
# Types
|
|
82
|
+
Result = NamedTuple('Result', [
|
|
83
|
+
('priority', int),
|
|
84
|
+
('level', int),
|
|
85
|
+
('commit_type', str),
|
|
86
|
+
('commit_scope', str),
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
# Variables
|
|
90
|
+
commit_scope: str = COMMITS_DEFAULT_SCOPE
|
|
91
|
+
commit_type: str = COMMITS_DEFAULT_TYPE
|
|
92
|
+
level: int
|
|
93
|
+
levels: List[int]
|
|
94
|
+
priority: int = 0
|
|
95
|
+
results: List[Result] = []
|
|
96
|
+
|
|
97
|
+
# Evaluate common changes
|
|
98
|
+
for changes_evaluator in CHANGES_EVALUATORS:
|
|
99
|
+
priority += 1
|
|
100
|
+
for type_change in changes_evaluator.changes:
|
|
101
|
+
for change in changes:
|
|
102
|
+
if not search(type_change, change):
|
|
103
|
+
continue
|
|
104
|
+
for parser in changes_evaluator.parsers:
|
|
105
|
+
matches = search(parser.match, change)
|
|
106
|
+
if not matches or not isinstance(matches.lastindex, int):
|
|
107
|
+
continue
|
|
108
|
+
level = 0
|
|
109
|
+
for group in parser.groups:
|
|
110
|
+
level += 1
|
|
111
|
+
if matches.lastindex >= group:
|
|
112
|
+
results += [
|
|
113
|
+
Result(
|
|
114
|
+
priority=priority,
|
|
115
|
+
level=level,
|
|
116
|
+
commit_type=parser.commit_type,
|
|
117
|
+
commit_scope=matches.group(group),
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Match common changes
|
|
122
|
+
for changes_matcher in CHANGES_MATCHERS:
|
|
123
|
+
priority += 1
|
|
124
|
+
for matcher_type in changes_matcher.types:
|
|
125
|
+
for type_change in matcher_type.changes:
|
|
126
|
+
for change in changes:
|
|
127
|
+
if search(type_change, change):
|
|
128
|
+
results += [
|
|
129
|
+
Result(
|
|
130
|
+
priority=priority,
|
|
131
|
+
level=0,
|
|
132
|
+
commit_type=matcher_type.commit_type,
|
|
133
|
+
commit_scope=changes_matcher.commit_scope,
|
|
134
|
+
)
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Parse results
|
|
138
|
+
if results:
|
|
139
|
+
priority = min((result.priority for result in results))
|
|
140
|
+
results = [result for result in set(results) if result.priority == priority]
|
|
141
|
+
levels = sorted([result.level for result in results])
|
|
142
|
+
level = min((level for level in set(levels) if levels.count(level) == 1),
|
|
143
|
+
default=levels[-1])
|
|
144
|
+
for result in [result for result in set(results) if result.level == level]:
|
|
145
|
+
commit_type = result.commit_type
|
|
146
|
+
commit_scope = result.commit_scope
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# Result
|
|
150
|
+
return commit_type, commit_scope
|
|
151
|
+
|
|
152
|
+
# Prepare commit message
|
|
153
|
+
def prepare_commit_template(
|
|
154
|
+
commit_type: str,
|
|
155
|
+
commit_scope: str,
|
|
156
|
+
commit_subject: str,
|
|
157
|
+
commit_body: str,
|
|
158
|
+
signoff: bool,
|
|
159
|
+
) -> str:
|
|
160
|
+
|
|
161
|
+
# Variables
|
|
162
|
+
commit_lines: List[str] = []
|
|
163
|
+
|
|
164
|
+
# Prepare commit title
|
|
165
|
+
commit_lines += [
|
|
166
|
+
f'{commit_type}({commit_scope}): {commit_subject}',
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# Prepare commit body
|
|
170
|
+
template_body = not ([
|
|
171
|
+
line for line in commit_body.splitlines()
|
|
172
|
+
if not line.startswith(COMMITS_COMMENTS_PREFIX)
|
|
173
|
+
])
|
|
174
|
+
if commit_body:
|
|
175
|
+
commit_lines += [
|
|
176
|
+
'',
|
|
177
|
+
commit_body,
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
# Append commit separator
|
|
181
|
+
if commit_body and signoff:
|
|
182
|
+
commit_lines += [
|
|
183
|
+
f'{COMMITS_COMMENTS_PREFIX} ---' if template_body else '---',
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Append comments separator
|
|
187
|
+
if not signoff:
|
|
188
|
+
commit_lines += [
|
|
189
|
+
'',
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# Result
|
|
193
|
+
return str(COMMITS_MESSAGE_EOL.join(commit_lines))
|
|
194
|
+
|
|
195
|
+
# Main, pylint: disable=too-many-branches,too-many-statements
|
|
196
|
+
def main() -> None:
|
|
197
|
+
|
|
198
|
+
# Variables
|
|
199
|
+
changes: List[str]
|
|
200
|
+
commit_scope: str
|
|
201
|
+
commit_type: str
|
|
202
|
+
context: str
|
|
203
|
+
empty: bool
|
|
204
|
+
filepath: str
|
|
205
|
+
signoff: bool
|
|
206
|
+
source: Optional[str]
|
|
207
|
+
|
|
208
|
+
# Validate arguments
|
|
209
|
+
if len(argv) <= 1:
|
|
210
|
+
sys_exit(1)
|
|
211
|
+
|
|
212
|
+
# Parse arguments
|
|
213
|
+
filepath = argv[1]
|
|
214
|
+
source = argv[2] if len(argv) > 2 else None
|
|
215
|
+
|
|
216
|
+
# Ignore commit source, documentation:
|
|
217
|
+
# - message (if a -m or -F option was given)
|
|
218
|
+
# - template (if a -t option was given or the configuration option commit.template is set)
|
|
219
|
+
# - merge (if the commit is a merge or a .git/MERGE_MSG file exists)
|
|
220
|
+
# - squash (if a .git/SQUASH_MSG file exists)
|
|
221
|
+
# - commit, followed by a commit object name (if a -c, -C or --amend option was given)
|
|
222
|
+
if source and source in ['template', 'squash', 'commit']:
|
|
223
|
+
sys_exit(0)
|
|
224
|
+
|
|
225
|
+
# Parse commit input
|
|
226
|
+
empty, signoff, context, changes = parse_commit_input(filepath)
|
|
227
|
+
|
|
228
|
+
# Existing commit input
|
|
229
|
+
if not empty:
|
|
230
|
+
sys_exit(0)
|
|
231
|
+
|
|
232
|
+
# Prepare commit title
|
|
233
|
+
commit_type, commit_scope = prepare_commit_title(changes)
|
|
234
|
+
|
|
235
|
+
# Prepare commit template
|
|
236
|
+
commit_template = prepare_commit_template(
|
|
237
|
+
commit_type=commit_type,
|
|
238
|
+
commit_scope=commit_scope,
|
|
239
|
+
commit_subject=COMMITS_DEFAULT_SUBJECT,
|
|
240
|
+
commit_body=COMMITS_DEFAULT_BODY,
|
|
241
|
+
signoff=signoff,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Write commit template
|
|
245
|
+
if environ.get('GIT_EXEC_PATH', ''):
|
|
246
|
+
with open(filepath, encoding='utf8', mode='w') as file:
|
|
247
|
+
file.write(commit_template + context)
|
|
248
|
+
|
|
249
|
+
# Dump commit template
|
|
250
|
+
else:
|
|
251
|
+
print(commit_template + context)
|
|
252
|
+
|
|
253
|
+
# Result
|
|
254
|
+
sys_exit(0)
|
|
255
|
+
|
|
256
|
+
# Entrypoint
|
|
257
|
+
if __name__ == '__main__': # pragma: no cover
|
|
258
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Bundle class, pylint: disable=too-few-public-methods
|
|
4
|
+
class Bundle:
|
|
5
|
+
|
|
6
|
+
# Modules
|
|
7
|
+
MODULE: str = 'pre_commit_crocodile'
|
|
8
|
+
|
|
9
|
+
# Names
|
|
10
|
+
NAME: str = 'pre-commit-crocodile'
|
|
11
|
+
|
|
12
|
+
# Packages
|
|
13
|
+
PACKAGE: str = 'pre-commit-crocodile'
|
|
14
|
+
|
|
15
|
+
# Resources
|
|
16
|
+
RESOURCES_ASSETS: str = f'{MODULE}.assets'
|
|
17
|
+
|
|
18
|
+
# Details
|
|
19
|
+
DESCRIPTION: str = 'Git hooks intended for developers using pre-commit'
|
|
20
|
+
|
|
21
|
+
# Sources
|
|
22
|
+
REPOSITORY: str = 'https://gitlab.com/RadianDevCore/tools/pre-commit-crocodile'
|
|
23
|
+
|
|
24
|
+
# Releases
|
|
25
|
+
RELEASE_FIRST_TIMESTAMP: int = 1579337311
|
|
26
|
+
|
|
27
|
+
# Environment
|
|
28
|
+
ENV_DEBUG_REVISION_SHA: str = 'DEBUG_REVISION_SHA'
|
|
29
|
+
ENV_DEBUG_UPDATES_DAILY: str = 'DEBUG_UPDATES_DAILY'
|
|
30
|
+
ENV_DEBUG_UPDATES_DISABLE: str = 'DEBUG_UPDATES_DISABLE'
|
|
31
|
+
ENV_DEBUG_UPDATES_FAKE: str = 'DEBUG_UPDATES_FAKE'
|
|
32
|
+
ENV_DEBUG_UPDATES_OFFLINE: str = 'DEBUG_UPDATES_OFFLINE'
|
|
33
|
+
ENV_DEBUG_VERSION_FAKE: str = 'DEBUG_VERSION_FAKE'
|
|
34
|
+
ENV_NO_COLOR: str = 'NO_COLOR'
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Standard libraries
|
|
4
|
+
from configparser import ConfigParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from sys import stdout
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
# Components
|
|
10
|
+
from ..prints.colors import Colors
|
|
11
|
+
from ..system.platform import Platform
|
|
12
|
+
|
|
13
|
+
# Settings class
|
|
14
|
+
class Settings:
|
|
15
|
+
|
|
16
|
+
# Types
|
|
17
|
+
Value = Union[int, str]
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
SETTINGS_FILE: str = 'settings.ini'
|
|
21
|
+
|
|
22
|
+
# Members
|
|
23
|
+
__folder: Path
|
|
24
|
+
__persistent: bool
|
|
25
|
+
__path: Path
|
|
26
|
+
__settings: ConfigParser
|
|
27
|
+
|
|
28
|
+
# Constructor
|
|
29
|
+
def __init__(self, name: str) -> None:
|
|
30
|
+
|
|
31
|
+
# Prepare paths
|
|
32
|
+
self.__folder = Platform.userspace(name)
|
|
33
|
+
self.__path = self.__folder / Settings.SETTINGS_FILE
|
|
34
|
+
self.__persistent = False
|
|
35
|
+
|
|
36
|
+
# Parse settings
|
|
37
|
+
self.__settings = ConfigParser()
|
|
38
|
+
self.__settings.read(self.__path)
|
|
39
|
+
|
|
40
|
+
# Prepare missing settings
|
|
41
|
+
try:
|
|
42
|
+
if self.get('package', 'name') != name:
|
|
43
|
+
raise ValueError('Missing settings files')
|
|
44
|
+
self.__persistent = True
|
|
45
|
+
except ValueError:
|
|
46
|
+
try:
|
|
47
|
+
self.__prepare()
|
|
48
|
+
self.__reset(name)
|
|
49
|
+
self.__persistent = True
|
|
50
|
+
except PermissionError: # pragma: no cover
|
|
51
|
+
self.__persistent = False
|
|
52
|
+
|
|
53
|
+
# Initialize settings
|
|
54
|
+
if not Path(self.__path).is_file():
|
|
55
|
+
self.__write()
|
|
56
|
+
|
|
57
|
+
# Prepare
|
|
58
|
+
def __prepare(self) -> None:
|
|
59
|
+
|
|
60
|
+
# Prepare folder path
|
|
61
|
+
if not Platform.IS_SIMULATED:
|
|
62
|
+
self.__folder.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
# Reset
|
|
65
|
+
def __reset(self, name: str) -> None:
|
|
66
|
+
|
|
67
|
+
# Prepare barebone settings
|
|
68
|
+
self.__settings = ConfigParser()
|
|
69
|
+
self.set('package', 'name', name)
|
|
70
|
+
|
|
71
|
+
# Writer
|
|
72
|
+
def __write(self) -> None:
|
|
73
|
+
|
|
74
|
+
# Write initial settings
|
|
75
|
+
if self.__persistent and not Platform.IS_SIMULATED:
|
|
76
|
+
with open(self.__path, encoding='utf8', mode='w') as output:
|
|
77
|
+
self.__settings.write(output)
|
|
78
|
+
|
|
79
|
+
# Has
|
|
80
|
+
def has(self, group: str, key: str) -> bool:
|
|
81
|
+
|
|
82
|
+
# Check settings key in group
|
|
83
|
+
return group in self.__settings and key in self.__settings[group]
|
|
84
|
+
|
|
85
|
+
# Get
|
|
86
|
+
def get(self, group: str, key: str) -> Optional[Value]:
|
|
87
|
+
|
|
88
|
+
# Get settings key in group
|
|
89
|
+
if group in self.__settings and key in self.__settings[group]:
|
|
90
|
+
return self.__settings[group][key]
|
|
91
|
+
|
|
92
|
+
# Default fallback
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Get bool
|
|
96
|
+
def get_bool(self, group: str, key: str) -> Optional[bool]:
|
|
97
|
+
|
|
98
|
+
# Get settings key as boolean
|
|
99
|
+
try:
|
|
100
|
+
value: str = str(self.get(group, key))
|
|
101
|
+
return value.lower() == 'true' or int(value) == 1
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Set
|
|
106
|
+
def set(self, group: str, key: str, value: Value) -> None:
|
|
107
|
+
|
|
108
|
+
# Prepare group
|
|
109
|
+
if group not in self.__settings:
|
|
110
|
+
self.__settings[group] = {}
|
|
111
|
+
|
|
112
|
+
# Unset key
|
|
113
|
+
if str(value) == 'UNSET':
|
|
114
|
+
del self.__settings[group][key]
|
|
115
|
+
|
|
116
|
+
# Set key
|
|
117
|
+
else:
|
|
118
|
+
self.__settings[group][key] = str(value)
|
|
119
|
+
|
|
120
|
+
# Write updated settings
|
|
121
|
+
self.__write()
|
|
122
|
+
|
|
123
|
+
# Set bool
|
|
124
|
+
def set_bool(self, group: str, key: str, value: Value) -> None:
|
|
125
|
+
|
|
126
|
+
# Set settings key as boolean
|
|
127
|
+
self.set(group, key, 1 if value else 0)
|
|
128
|
+
|
|
129
|
+
# Show
|
|
130
|
+
def show(self) -> None:
|
|
131
|
+
|
|
132
|
+
# Settings file path
|
|
133
|
+
print(' ')
|
|
134
|
+
print(
|
|
135
|
+
f' {Colors.GREEN}===[ {Colors.YELLOW}Settings:' \
|
|
136
|
+
f' {Colors.BOLD}{self.__path} {Colors.GREEN}]==={Colors.RESET}'
|
|
137
|
+
)
|
|
138
|
+
print(' ')
|
|
139
|
+
|
|
140
|
+
# Settings simulated contents
|
|
141
|
+
if Platform.IS_SIMULATED:
|
|
142
|
+
self.__settings.write(stdout)
|
|
143
|
+
|
|
144
|
+
# Settings file contents
|
|
145
|
+
else:
|
|
146
|
+
with open(self.__path, encoding='utf8', mode='r') as data:
|
|
147
|
+
print(data.read())
|
|
148
|
+
Platform.flush()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Standard libraries
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from os import access, environ, W_OK
|
|
6
|
+
from time import localtime, strftime, time
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
# Modules libraries
|
|
10
|
+
from pkg_resources import parse_version
|
|
11
|
+
|
|
12
|
+
# Components
|
|
13
|
+
from ..prints.boxes import Boxes
|
|
14
|
+
from ..prints.colors import Colors
|
|
15
|
+
from ..system.platform import Platform
|
|
16
|
+
from .bundle import Bundle
|
|
17
|
+
from .settings import Settings
|
|
18
|
+
from .version import Version
|
|
19
|
+
|
|
20
|
+
# Updates class
|
|
21
|
+
class Updates:
|
|
22
|
+
|
|
23
|
+
# Members
|
|
24
|
+
__enabled: bool
|
|
25
|
+
__name: str
|
|
26
|
+
__settings: Settings
|
|
27
|
+
|
|
28
|
+
# Constructor
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
name: str,
|
|
32
|
+
settings: Settings,
|
|
33
|
+
) -> None:
|
|
34
|
+
|
|
35
|
+
# Initialize members
|
|
36
|
+
self.__name = name
|
|
37
|
+
self.__settings = settings
|
|
38
|
+
|
|
39
|
+
# Detect migration
|
|
40
|
+
self.__migration()
|
|
41
|
+
|
|
42
|
+
# Prepare enabled
|
|
43
|
+
if not self.__settings.has('updates', 'enabled'):
|
|
44
|
+
self.__settings.set_bool('updates', 'enabled', True)
|
|
45
|
+
|
|
46
|
+
# Check enabled
|
|
47
|
+
self.__enabled = bool(self.__settings.get_bool('updates', 'enabled')) and \
|
|
48
|
+
not environ.get(Bundle.ENV_DEBUG_UPDATES_DISABLE, '')
|
|
49
|
+
|
|
50
|
+
# Migration
|
|
51
|
+
def __migration(self) -> None:
|
|
52
|
+
|
|
53
|
+
# Acquire versions
|
|
54
|
+
current_version = Version.get()
|
|
55
|
+
package_version = self.__settings.get('package', 'version')
|
|
56
|
+
if not package_version:
|
|
57
|
+
package_version = '0.0.0'
|
|
58
|
+
|
|
59
|
+
# Refresh package version
|
|
60
|
+
if not package_version or current_version != package_version:
|
|
61
|
+
self.__settings.set('package', 'version', current_version)
|
|
62
|
+
|
|
63
|
+
# Checker
|
|
64
|
+
def check(
|
|
65
|
+
self,
|
|
66
|
+
older: bool = False,
|
|
67
|
+
) -> bool:
|
|
68
|
+
|
|
69
|
+
# Reference version
|
|
70
|
+
version = '0.0.0' if older else Version.get()
|
|
71
|
+
|
|
72
|
+
# Fake test updates
|
|
73
|
+
if Bundle.ENV_DEBUG_UPDATES_FAKE in environ:
|
|
74
|
+
available = environ[Bundle.ENV_DEBUG_UPDATES_FAKE]
|
|
75
|
+
if parse_version(available) >= parse_version(version):
|
|
76
|
+
|
|
77
|
+
# Show updates message
|
|
78
|
+
release_date = datetime.utcfromtimestamp(Bundle.RELEASE_FIRST_TIMESTAMP)
|
|
79
|
+
Updates.message(
|
|
80
|
+
name=self.__name,
|
|
81
|
+
older=older,
|
|
82
|
+
available=available,
|
|
83
|
+
date=release_date,
|
|
84
|
+
)
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Check if not offline
|
|
88
|
+
if not environ.get(Bundle.ENV_DEBUG_UPDATES_OFFLINE, ''):
|
|
89
|
+
|
|
90
|
+
# Modules libraries, pylint: disable=import-outside-toplevel
|
|
91
|
+
from update_checker import UpdateChecker
|
|
92
|
+
|
|
93
|
+
# Check for updates
|
|
94
|
+
check = UpdateChecker(bypass_cache=True).check(self.__name, version)
|
|
95
|
+
if check: # pragma: no cover
|
|
96
|
+
|
|
97
|
+
# Show updates message
|
|
98
|
+
Updates.message(
|
|
99
|
+
name=self.__name,
|
|
100
|
+
older=older,
|
|
101
|
+
available=check.available_version,
|
|
102
|
+
date=check.release_date,
|
|
103
|
+
)
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Older offline failure
|
|
107
|
+
if older:
|
|
108
|
+
|
|
109
|
+
# Show offline message
|
|
110
|
+
Updates.message(
|
|
111
|
+
name=self.__name,
|
|
112
|
+
offline=True,
|
|
113
|
+
)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
# Result
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Daily
|
|
120
|
+
@property
|
|
121
|
+
def daily(self) -> bool:
|
|
122
|
+
|
|
123
|
+
# Acquire updates check last timestamp
|
|
124
|
+
last = self.__settings.get('updates', 'last_timestamp')
|
|
125
|
+
|
|
126
|
+
# Fake test updates
|
|
127
|
+
if Bundle.ENV_DEBUG_UPDATES_DAILY in environ:
|
|
128
|
+
last = None
|
|
129
|
+
|
|
130
|
+
# Handle daily checks
|
|
131
|
+
current = int(time())
|
|
132
|
+
if not last or strftime('%Y-%m-%d', localtime(current)) != strftime(
|
|
133
|
+
'%Y-%m-%d', localtime(int(last))):
|
|
134
|
+
self.__settings.set('updates', 'last_timestamp', current)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
# Default fallback
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# Enabled
|
|
141
|
+
@property
|
|
142
|
+
def enabled(self) -> bool:
|
|
143
|
+
return self.__enabled
|
|
144
|
+
|
|
145
|
+
# Message, pylint: disable=too-many-arguments
|
|
146
|
+
@staticmethod
|
|
147
|
+
def message(
|
|
148
|
+
name: str,
|
|
149
|
+
offline: bool = False,
|
|
150
|
+
older: bool = False,
|
|
151
|
+
available: Optional[str] = None,
|
|
152
|
+
date: Optional[datetime] = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
|
|
155
|
+
# Modules libraries, pylint: disable=import-outside-toplevel
|
|
156
|
+
from update_checker import pretty_date
|
|
157
|
+
|
|
158
|
+
# Create message box
|
|
159
|
+
box = Boxes()
|
|
160
|
+
|
|
161
|
+
# Acquire current version
|
|
162
|
+
version = Version.get()
|
|
163
|
+
|
|
164
|
+
# Detect package installer
|
|
165
|
+
package_install: str = ''
|
|
166
|
+
if Platform.PATH_SEPARATOR + 'pipx' + Platform.PATH_SEPARATOR in Version.path():
|
|
167
|
+
package_install = 'pipx upgrade' # pragma: no cover
|
|
168
|
+
else:
|
|
169
|
+
package_install = 'pip3 install -U' # pragma: no cover
|
|
170
|
+
|
|
171
|
+
# Detect package ownership
|
|
172
|
+
writable = access(__file__, W_OK)
|
|
173
|
+
if Platform.IS_USER_SUDO or not writable:
|
|
174
|
+
package_install = 'sudo ' + package_install # pragma: no cover
|
|
175
|
+
|
|
176
|
+
# Prepare package specification
|
|
177
|
+
package_specification: str = ''
|
|
178
|
+
if 'pipx' in package_install:
|
|
179
|
+
package_specification = f'{name}' # pragma: no cover
|
|
180
|
+
else:
|
|
181
|
+
package_specification = f'{name}>={available}' # pragma: no cover
|
|
182
|
+
|
|
183
|
+
# Evaluate same version
|
|
184
|
+
same = available and available == version
|
|
185
|
+
|
|
186
|
+
# Version message prefix
|
|
187
|
+
version_outdated = not offline and available and not older and not same
|
|
188
|
+
version_prefix = f'{Colors.YELLOW_LIGHT}Version: {Colors.BOLD}{name}' \
|
|
189
|
+
f' {Colors.RED if version_outdated else Colors.GREEN}{version}'
|
|
190
|
+
|
|
191
|
+
# Offline version message
|
|
192
|
+
if offline:
|
|
193
|
+
box.add(f'{version_prefix} {Colors.BOLD}not found, network might be down')
|
|
194
|
+
|
|
195
|
+
# Updated version message
|
|
196
|
+
elif same:
|
|
197
|
+
box.add(
|
|
198
|
+
f'{version_prefix} {Colors.BOLD}was released {pretty_date(date)}{Colors.BOLD}!'
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Older version message
|
|
202
|
+
elif older:
|
|
203
|
+
box.add(
|
|
204
|
+
f'{version_prefix} {Colors.BOLD}newer than {Colors.RED}{available}' \
|
|
205
|
+
f' {Colors.BOLD}from {pretty_date(date)}{Colors.BOLD}!'
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Newer version message
|
|
209
|
+
else:
|
|
210
|
+
box.add(
|
|
211
|
+
f'{version_prefix} {Colors.BOLD}updated {pretty_date(date)}' \
|
|
212
|
+
f' to {Colors.GREEN}{available}{Colors.BOLD}!'
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Changelog message
|
|
216
|
+
box.add(
|
|
217
|
+
f'{Colors.YELLOW_LIGHT}Changelog: {Colors.CYAN}{Bundle.REPOSITORY}/-/releases'
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Update message
|
|
221
|
+
if available:
|
|
222
|
+
box.add(
|
|
223
|
+
f'{Colors.YELLOW_LIGHT}Update: {Colors.BOLD}' \
|
|
224
|
+
f"Run {Colors.GREEN}{package_install} '{package_specification}'"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Print message box
|
|
228
|
+
box.print()
|