quickpub 4.1.4__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.
- quickpub/__init__.py +10 -0
- quickpub/__main__.py +276 -0
- quickpub/classifiers.py +138 -0
- quickpub/dist_files.py +33 -0
- quickpub/enforcers.py +26 -0
- quickpub/files.py +246 -0
- quickpub/logging_.py +74 -0
- quickpub/package_manager_detection.py +43 -0
- quickpub/proxy.py +35 -0
- quickpub/py.typed +0 -0
- quickpub/qa.py +409 -0
- quickpub/strategies/__init__.py +8 -0
- quickpub/strategies/build_schema.py +21 -0
- quickpub/strategies/constraint_enforcer.py +17 -0
- quickpub/strategies/implementations/__init__.py +6 -0
- quickpub/strategies/implementations/build_schemas/__init__.py +2 -0
- quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +48 -0
- quickpub/strategies/implementations/build_schemas/uv_build_schema.py +50 -0
- quickpub/strategies/implementations/constraint_enforcers/__init__.py +5 -0
- quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +29 -0
- quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +80 -0
- quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +88 -0
- quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +45 -0
- quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +29 -0
- quickpub/strategies/implementations/package_managers/__init__.py +2 -0
- quickpub/strategies/implementations/package_managers/installed_package_parser.py +29 -0
- quickpub/strategies/implementations/package_managers/pip_package_manager.py +31 -0
- quickpub/strategies/implementations/package_managers/uv_package_manager.py +45 -0
- quickpub/strategies/implementations/python_providers/__init__.py +4 -0
- quickpub/strategies/implementations/python_providers/conda_python_provider.py +54 -0
- quickpub/strategies/implementations/python_providers/default_python_provider.py +47 -0
- quickpub/strategies/implementations/python_providers/union_provider.py +52 -0
- quickpub/strategies/implementations/python_providers/uv_python_provider.py +39 -0
- quickpub/strategies/implementations/quality_assurance_runners/__init__.py +4 -0
- quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +98 -0
- quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +106 -0
- quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +188 -0
- quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +138 -0
- quickpub/strategies/implementations/upload_targets/__init__.py +2 -0
- quickpub/strategies/implementations/upload_targets/github_upload_target.py +61 -0
- quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +90 -0
- quickpub/strategies/package_manager.py +54 -0
- quickpub/strategies/python_provider.py +60 -0
- quickpub/strategies/quality_assurance_runner.py +208 -0
- quickpub/strategies/quickpub_strategy.py +14 -0
- quickpub/strategies/upload_target.py +19 -0
- quickpub/structures/__init__.py +3 -0
- quickpub/structures/bound.py +52 -0
- quickpub/structures/dependency.py +82 -0
- quickpub/structures/version.py +41 -0
- quickpub/validators.py +87 -0
- quickpub/worker_pool.py +16 -0
- quickpub-4.1.4.dist-info/METADATA +246 -0
- quickpub-4.1.4.dist-info/RECORD +58 -0
- quickpub-4.1.4.dist-info/WHEEL +5 -0
- quickpub-4.1.4.dist-info/entry_points.txt +2 -0
- quickpub-4.1.4.dist-info/licenses/LICENSE +21 -0
- quickpub-4.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from danielutils import LayeredCommand
|
|
6
|
+
|
|
7
|
+
from ....enforcers import ExitEarlyError
|
|
8
|
+
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PylintRunner(QualityAssuranceRunner):
|
|
14
|
+
"""Quality assurance runner for pylint code analysis. Scores based on pylint rating (0.0 to 10.0)."""
|
|
15
|
+
|
|
16
|
+
def _install_dependencies(self, base: LayeredCommand) -> None:
|
|
17
|
+
logger.info("Installing pylint dependencies")
|
|
18
|
+
with base:
|
|
19
|
+
base(self.package_manager.install_command("pylint"))
|
|
20
|
+
|
|
21
|
+
RATING_PATTERN: re.Pattern = re.compile(r"([\d.]+)/([\d.]+)")
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
bound: str = ">=0.8",
|
|
26
|
+
configuration_path: Optional[str] = None,
|
|
27
|
+
executable_path: Optional[str] = None,
|
|
28
|
+
package_manager=None,
|
|
29
|
+
) -> None:
|
|
30
|
+
QualityAssuranceRunner.__init__(
|
|
31
|
+
self,
|
|
32
|
+
name="pylint",
|
|
33
|
+
bound=bound,
|
|
34
|
+
configuration_path=configuration_path,
|
|
35
|
+
executable_path=executable_path,
|
|
36
|
+
package_manager=package_manager,
|
|
37
|
+
)
|
|
38
|
+
logger.info(
|
|
39
|
+
"Initialized PylintRunner with bound='%s', config='%s', executable='%s'",
|
|
40
|
+
bound,
|
|
41
|
+
configuration_path,
|
|
42
|
+
executable_path,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _build_command(self, target: str, use_system_interpreter: bool = False) -> str:
|
|
46
|
+
command: str = self.get_executable()
|
|
47
|
+
if self.has_config:
|
|
48
|
+
command += f" --rcfile {self.config_path}"
|
|
49
|
+
command += f" {target}"
|
|
50
|
+
return command
|
|
51
|
+
|
|
52
|
+
def _calculate_score(
|
|
53
|
+
self, ret: int, lines: List[str], verbose: bool = False
|
|
54
|
+
) -> float:
|
|
55
|
+
logger.debug("Calculating pylint score from analysis results")
|
|
56
|
+
|
|
57
|
+
if len(lines) == 0:
|
|
58
|
+
logger.debug("No pylint output, returning perfect score: 1.0")
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
for line in reversed(lines):
|
|
62
|
+
match = self.RATING_PATTERN.search(line)
|
|
63
|
+
if match:
|
|
64
|
+
numerator = float(match.group(1))
|
|
65
|
+
denominator = float(match.group(2))
|
|
66
|
+
score = numerator / denominator
|
|
67
|
+
logger.debug(
|
|
68
|
+
"Pylint score calculated: %.3f (%s/%s)",
|
|
69
|
+
score,
|
|
70
|
+
match.group(1),
|
|
71
|
+
match.group(2),
|
|
72
|
+
)
|
|
73
|
+
return score
|
|
74
|
+
|
|
75
|
+
if ret == 0:
|
|
76
|
+
return 1.0
|
|
77
|
+
|
|
78
|
+
joined_output = "\n".join(lines)
|
|
79
|
+
if ret == 1 and "parse-error" in joined_output:
|
|
80
|
+
logger.debug(
|
|
81
|
+
"Pylint reported parse-error with an empty target, returning perfect score: 1.0"
|
|
82
|
+
)
|
|
83
|
+
return 1.0
|
|
84
|
+
|
|
85
|
+
if len(lines) == 1:
|
|
86
|
+
if lines[0].endswith("No module named pylint"):
|
|
87
|
+
logger.error("Pylint module not found")
|
|
88
|
+
raise ExitEarlyError("No module named pylint found")
|
|
89
|
+
|
|
90
|
+
if lines[0].startswith("The config file") and lines[0].endswith(
|
|
91
|
+
"doesn't exist!"
|
|
92
|
+
):
|
|
93
|
+
logger.error("Config file error: %s", lines[0])
|
|
94
|
+
raise ExitEarlyError(lines[0])
|
|
95
|
+
|
|
96
|
+
logger.error("Unexpected pylint error: %s", lines[0])
|
|
97
|
+
raise ExitEarlyError(f"Got an unexpected error: {lines[0]}")
|
|
98
|
+
|
|
99
|
+
msg = f"Failed running Pylint, got exit code {ret}. Try running manually using: {self._build_command('TARGET')}"
|
|
100
|
+
logger.error("Failed to parse pylint rating from output: %s", joined_output)
|
|
101
|
+
raise ExitEarlyError(msg)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"PylintRunner",
|
|
106
|
+
]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Union, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from danielutils import LayeredCommand
|
|
8
|
+
|
|
9
|
+
from ....enforcers import ExitEarlyError
|
|
10
|
+
from ....structures import Bound
|
|
11
|
+
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PytestRunner(QualityAssuranceRunner):
|
|
17
|
+
"""Quality assurance runner for pytest testing. Scores based on the ratio of passed tests to total tests."""
|
|
18
|
+
|
|
19
|
+
PYTEST_SUMMARY_REGEX: re.Pattern = re.compile(
|
|
20
|
+
r"=+ .*?in [\d\.]+s(?: \([^)]+\))? =+"
|
|
21
|
+
)
|
|
22
|
+
PYTEST_QUIET_SUMMARY_REGEX: re.Pattern = re.compile(
|
|
23
|
+
r".*\d+ (?:failed|passed).+in [\d\.]+s(?: \([^)]+\))?"
|
|
24
|
+
)
|
|
25
|
+
PYTEST_FAILED_REGEX: re.Pattern = re.compile(r"(\d+) failed")
|
|
26
|
+
PYTEST_PASSED_REGEX: re.Pattern = re.compile(r"(\d+) passed")
|
|
27
|
+
PYTEST_SKIPPED_REGEX: re.Pattern = re.compile(r"(\d+) skipped")
|
|
28
|
+
ANSI_ESCAPE_REGEX: re.Pattern = re.compile(r"\x1b\[[0-9;]*m")
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
bound: Union[str, Bound] = ">=0.8",
|
|
34
|
+
target: str = "./tests",
|
|
35
|
+
no_output_score: float = 0.0,
|
|
36
|
+
no_tests_score: float = 1.0,
|
|
37
|
+
xdist_workers: Union[int, Literal["auto"]] = "auto",
|
|
38
|
+
configuration_path: Optional[str] = None,
|
|
39
|
+
executable_path: Optional[str] = None,
|
|
40
|
+
package_manager=None,
|
|
41
|
+
) -> None:
|
|
42
|
+
super().__init__(
|
|
43
|
+
name="pytest",
|
|
44
|
+
bound=bound,
|
|
45
|
+
target=target,
|
|
46
|
+
configuration_path=configuration_path,
|
|
47
|
+
executable_path=executable_path,
|
|
48
|
+
package_manager=package_manager,
|
|
49
|
+
)
|
|
50
|
+
if not (0.0 <= no_tests_score <= 1.0):
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"no_tests_score should be between 0.0 and 1.0 (including both)."
|
|
53
|
+
)
|
|
54
|
+
self.no_tests_score = no_tests_score
|
|
55
|
+
|
|
56
|
+
if not (0.0 <= no_output_score <= 1.0):
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"no_output_score should be between 0.0 and 1.0 (including both)."
|
|
59
|
+
)
|
|
60
|
+
self.no_output_score = no_output_score
|
|
61
|
+
if isinstance(xdist_workers, int) and xdist_workers <= 0:
|
|
62
|
+
raise RuntimeError("xdist_workers must be a positive integer or 'auto'.")
|
|
63
|
+
if isinstance(xdist_workers, str) and xdist_workers != "auto":
|
|
64
|
+
raise RuntimeError("xdist_workers must be a positive integer or 'auto'.")
|
|
65
|
+
self.xdist_workers = xdist_workers
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
"Initialized PytestRunner with bound='%s', target='%s', no_tests_score=%s, no_output_score=%s",
|
|
69
|
+
bound,
|
|
70
|
+
target,
|
|
71
|
+
no_tests_score,
|
|
72
|
+
no_output_score,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _is_xdist_installed(self) -> bool:
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
self.package_manager.show_command("pytest-xdist"),
|
|
79
|
+
shell=True,
|
|
80
|
+
stdout=subprocess.PIPE,
|
|
81
|
+
stderr=subprocess.PIPE,
|
|
82
|
+
check=False,
|
|
83
|
+
)
|
|
84
|
+
installed = result.returncode == 0
|
|
85
|
+
logger.debug(
|
|
86
|
+
"pytest-xdist availability check returned code %d (installed=%s)",
|
|
87
|
+
result.returncode,
|
|
88
|
+
installed,
|
|
89
|
+
)
|
|
90
|
+
return installed
|
|
91
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
92
|
+
logger.debug("pytest-xdist availability check failed: %s", exc)
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def _build_command(self, target: str, use_system_interpreter: bool = False) -> str:
|
|
96
|
+
base_command = f"{sys.executable} -m pytest"
|
|
97
|
+
if self.has_config:
|
|
98
|
+
base_command += f" -c {self.config_path}"
|
|
99
|
+
if self._is_xdist_installed():
|
|
100
|
+
logger.debug("pytest-xdist detected; enabling distributed execution")
|
|
101
|
+
return f"{base_command} -n {self.xdist_workers} {self.target}"
|
|
102
|
+
logger.debug("pytest-xdist not detected; running without distribution")
|
|
103
|
+
return f"{base_command} {self.target}"
|
|
104
|
+
|
|
105
|
+
def _strip_ansi(self, text: str) -> str:
|
|
106
|
+
return self.ANSI_ESCAPE_REGEX.sub("", text)
|
|
107
|
+
|
|
108
|
+
def _is_summary_line(self, line: str) -> bool:
|
|
109
|
+
cleaned = self._strip_ansi(line)
|
|
110
|
+
return bool(
|
|
111
|
+
self.PYTEST_SUMMARY_REGEX.match(cleaned)
|
|
112
|
+
or self.PYTEST_QUIET_SUMMARY_REGEX.match(cleaned)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _install_dependencies(self, base: LayeredCommand) -> None:
|
|
116
|
+
logger.info("Installing pytest dependencies")
|
|
117
|
+
with base:
|
|
118
|
+
base(self.package_manager.install_command("pytest"))
|
|
119
|
+
|
|
120
|
+
def _calculate_score(
|
|
121
|
+
self, ret: int, command_output: List[str], *, verbose: bool = False
|
|
122
|
+
) -> float:
|
|
123
|
+
logger.info("Calculating pytest score from test results")
|
|
124
|
+
|
|
125
|
+
if len(command_output) == 0:
|
|
126
|
+
logger.info(
|
|
127
|
+
"No pytest output, returning no_output_score: %s", self.no_output_score
|
|
128
|
+
)
|
|
129
|
+
return self.no_output_score
|
|
130
|
+
|
|
131
|
+
# Find the actual summary line by looking for the regex pattern
|
|
132
|
+
# (pytest output may have warnings or other lines after the summary)
|
|
133
|
+
rating_line = None
|
|
134
|
+
for line in reversed(command_output):
|
|
135
|
+
cleaned_line = self._strip_ansi(line)
|
|
136
|
+
if "no tests ran" in cleaned_line.lower():
|
|
137
|
+
logger.info(
|
|
138
|
+
"No tests ran, returning no_tests_score: %s", self.no_tests_score
|
|
139
|
+
)
|
|
140
|
+
return self.no_tests_score
|
|
141
|
+
if self._is_summary_line(line):
|
|
142
|
+
rating_line = cleaned_line
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if rating_line is None:
|
|
146
|
+
rating_line = self._strip_ansi(command_output[-1])
|
|
147
|
+
if "no tests ran" in rating_line.lower():
|
|
148
|
+
logger.info(
|
|
149
|
+
"No tests ran, returning no_tests_score: %s", self.no_tests_score
|
|
150
|
+
)
|
|
151
|
+
return self.no_tests_score
|
|
152
|
+
|
|
153
|
+
if not (
|
|
154
|
+
self.PYTEST_SUMMARY_REGEX.match(rating_line)
|
|
155
|
+
or self.PYTEST_QUIET_SUMMARY_REGEX.match(rating_line)
|
|
156
|
+
):
|
|
157
|
+
logger.error("Failed to parse pytest output: %s", rating_line)
|
|
158
|
+
raise ExitEarlyError(
|
|
159
|
+
f"Can't calculate score for pytest on the following line: {rating_line}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
failed_match = self.PYTEST_FAILED_REGEX.search(rating_line)
|
|
163
|
+
passed_match = self.PYTEST_PASSED_REGEX.search(rating_line)
|
|
164
|
+
skipped_match = self.PYTEST_SKIPPED_REGEX.search(rating_line)
|
|
165
|
+
failed = int(failed_match.group(1)) if failed_match else 0
|
|
166
|
+
passed = int(passed_match.group(1)) if passed_match else 0
|
|
167
|
+
skipped = int(skipped_match.group(1)) if skipped_match else 0
|
|
168
|
+
assert failed >= 0
|
|
169
|
+
assert passed >= 0
|
|
170
|
+
assert skipped >= 0
|
|
171
|
+
|
|
172
|
+
total_tests = passed + failed + skipped
|
|
173
|
+
if total_tests == 0:
|
|
174
|
+
logger.info(
|
|
175
|
+
"No test results found, returning no_tests_score: %s",
|
|
176
|
+
self.no_tests_score,
|
|
177
|
+
)
|
|
178
|
+
return self.no_tests_score
|
|
179
|
+
|
|
180
|
+
score = passed / total_tests
|
|
181
|
+
logger.info(
|
|
182
|
+
"Pytest score calculated: %.3f (passed: %d, failed: %d, skipped: %d)",
|
|
183
|
+
score,
|
|
184
|
+
passed,
|
|
185
|
+
failed,
|
|
186
|
+
skipped,
|
|
187
|
+
)
|
|
188
|
+
return score
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Any
|
|
6
|
+
from danielutils import LayeredCommand
|
|
7
|
+
|
|
8
|
+
from ....enforcers import ExitEarlyError
|
|
9
|
+
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _removesuffix(string: str, suffix: str) -> str:
|
|
15
|
+
if suffix and string.endswith(suffix):
|
|
16
|
+
return string[: -len(suffix)]
|
|
17
|
+
return string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UnittestRunner(QualityAssuranceRunner):
|
|
21
|
+
"""Quality assurance runner for unittest testing. Scores based on the ratio of passed tests to total tests."""
|
|
22
|
+
|
|
23
|
+
NUM_TESTS_PATTERN: re.Pattern = re.compile(r"Ran (\d+) tests? in \d+\.\d+s")
|
|
24
|
+
NUM_FAILED_PATTERN: re.Pattern = re.compile(
|
|
25
|
+
r"FAILED \((?:failures=(\d+))?(?:, )?(?:errors=(\d+))?(?:, )?(?:skipped=(\d+))?\)|"
|
|
26
|
+
r"FAILED \((?:errors=(\d+))?(?:, )?(?:failures=(\d+))?(?:, )?(?:skipped=(\d+))?\)|"
|
|
27
|
+
r"FAILED \((?:skipped=(\d+))?(?:, )?(?:failures=(\d+))?(?:, )?(?:errors=(\d+))?\)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def _install_dependencies(self, base: LayeredCommand) -> None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def _pre_command(self) -> None:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def _post_command(self) -> None:
|
|
37
|
+
pass
|
|
38
|
+
# set_current_working_directory(self._cwd)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
target: Optional[str] = "./tests",
|
|
43
|
+
bound: str = ">=0.8",
|
|
44
|
+
no_tests_score: float = 0,
|
|
45
|
+
) -> None:
|
|
46
|
+
QualityAssuranceRunner.__init__(
|
|
47
|
+
self, name="unittest", bound=bound, target=target
|
|
48
|
+
)
|
|
49
|
+
self.no_tests_score = no_tests_score
|
|
50
|
+
logger.info(
|
|
51
|
+
"Initialized UnittestRunner with target='%s', bound='%s', no_tests_score=%s",
|
|
52
|
+
target,
|
|
53
|
+
bound,
|
|
54
|
+
no_tests_score,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _build_command(
|
|
58
|
+
self, src: str, *args: Any, use_system_interpreter: bool = False
|
|
59
|
+
) -> str:
|
|
60
|
+
command: str = self.get_executable()
|
|
61
|
+
target = self.target or "./tests"
|
|
62
|
+
rel = _removesuffix(os.path.relpath(src, target), src.lstrip("./\\"))
|
|
63
|
+
command += f" discover -s {rel}"
|
|
64
|
+
normalized_target_path = Path(os.path.join(os.getcwd(), target)).resolve()
|
|
65
|
+
# This is for concurrency reasons
|
|
66
|
+
return f"cd {normalized_target_path} & {command} & cd {Path(os.getcwd()).resolve()}"
|
|
67
|
+
|
|
68
|
+
def _calculate_score(
|
|
69
|
+
self, ret: int, lines: List[str], *, verbose: bool = False
|
|
70
|
+
) -> float:
|
|
71
|
+
logger.info("Calculating unittest score from test results")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
num_tests_ran_line = lines[-3]
|
|
75
|
+
num_tests_failed_line = lines[-1]
|
|
76
|
+
match = self.NUM_TESTS_PATTERN.match(num_tests_ran_line)
|
|
77
|
+
if match is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Failed to parse test count from line: {num_tests_ran_line}"
|
|
80
|
+
)
|
|
81
|
+
num_tests = int(match.group(1))
|
|
82
|
+
if num_tests == 0:
|
|
83
|
+
logger.info(
|
|
84
|
+
"No tests found, returning no_tests_score: %s", self.no_tests_score
|
|
85
|
+
)
|
|
86
|
+
return self.no_tests_score
|
|
87
|
+
|
|
88
|
+
num_failed = 0
|
|
89
|
+
num_errors = 0
|
|
90
|
+
if num_tests_failed_line != "OK":
|
|
91
|
+
if num_tests_failed_line.startswith("FAILED"):
|
|
92
|
+
m = self.NUM_FAILED_PATTERN.match(num_tests_failed_line)
|
|
93
|
+
if m:
|
|
94
|
+
# Handle different group positions based on the pattern that matched
|
|
95
|
+
groups = m.groups()
|
|
96
|
+
if groups[0] is not None: # failures= first pattern
|
|
97
|
+
num_failed = int(groups[0] or "0")
|
|
98
|
+
num_errors = int(groups[1] or "0")
|
|
99
|
+
elif groups[3] is not None: # errors= first pattern
|
|
100
|
+
num_failed = int(groups[4] or "0")
|
|
101
|
+
num_errors = int(groups[3] or "0")
|
|
102
|
+
elif groups[6] is not None: # skipped= first pattern
|
|
103
|
+
num_failed = int(groups[7] or "0")
|
|
104
|
+
num_errors = int(groups[8] or "0")
|
|
105
|
+
else:
|
|
106
|
+
# Fallback to original logic if no groups match
|
|
107
|
+
num_failed = int(m.group(1) or "0")
|
|
108
|
+
num_errors = int(m.group(2) or "0")
|
|
109
|
+
else:
|
|
110
|
+
# If regex doesn't match, treat as malformed
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Failed to parse FAILED line: {num_tests_failed_line}"
|
|
113
|
+
)
|
|
114
|
+
elif num_tests_failed_line.startswith("OK"):
|
|
115
|
+
# 'OK (skipped=3)' could it also be other stuff?
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
score = 1 - ((num_failed + num_errors) / num_tests)
|
|
119
|
+
logger.info(
|
|
120
|
+
"Unittest score calculated: %.3f (tests: %d, failed: %d, errors: %d)",
|
|
121
|
+
score,
|
|
122
|
+
num_tests,
|
|
123
|
+
num_failed,
|
|
124
|
+
num_errors,
|
|
125
|
+
)
|
|
126
|
+
return score
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error("Failed to calculate unittest score: %s", e)
|
|
130
|
+
raise ExitEarlyError(
|
|
131
|
+
f"Failed running Unittest, got exit code {ret}. "
|
|
132
|
+
f"try running manually using: {self._build_command('TARGET')}"
|
|
133
|
+
) from e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"UnittestRunner",
|
|
138
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ...upload_target import UploadTarget
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GithubUploadTarget(UploadTarget):
|
|
10
|
+
"""Upload target implementation for GitHub releases. Commits and pushes changes to the git repository."""
|
|
11
|
+
|
|
12
|
+
def upload(self, name: str, version: str, **kwargs: Any) -> None: # type: ignore[override]
|
|
13
|
+
from quickpub.proxy import cm
|
|
14
|
+
from quickpub.enforcers import exit_if
|
|
15
|
+
|
|
16
|
+
logger.info("Starting GitHub upload for version '%s'", version)
|
|
17
|
+
|
|
18
|
+
if self.verbose:
|
|
19
|
+
logger.debug("Staging files for Git commit")
|
|
20
|
+
|
|
21
|
+
ret, stdout, stderr = cm("git add .")
|
|
22
|
+
if ret != 0:
|
|
23
|
+
logger.error(
|
|
24
|
+
"Git add failed with return code %d: %s",
|
|
25
|
+
ret,
|
|
26
|
+
stderr.decode(encoding="utf8"),
|
|
27
|
+
)
|
|
28
|
+
exit_if(ret != 0, stderr.decode(encoding="utf8"))
|
|
29
|
+
|
|
30
|
+
if self.verbose:
|
|
31
|
+
logger.debug(
|
|
32
|
+
"Committing changes with message 'updated to version %s'", version
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
ret, stdout, stderr = cm(f'git commit -m "updated to version {version}"')
|
|
36
|
+
if ret != 0:
|
|
37
|
+
logger.error(
|
|
38
|
+
"Git commit failed with return code %d: %s",
|
|
39
|
+
ret,
|
|
40
|
+
stderr.decode(encoding="utf8"),
|
|
41
|
+
)
|
|
42
|
+
exit_if(ret != 0, stderr.decode(encoding="utf8"))
|
|
43
|
+
|
|
44
|
+
if self.verbose:
|
|
45
|
+
logger.debug("Pushing changes to GitHub")
|
|
46
|
+
|
|
47
|
+
ret, stdout, stderr = cm("git push")
|
|
48
|
+
if ret != 0:
|
|
49
|
+
logger.error(
|
|
50
|
+
"Git push failed with return code %d: %s",
|
|
51
|
+
ret,
|
|
52
|
+
stderr.decode(encoding="utf8"),
|
|
53
|
+
)
|
|
54
|
+
exit_if(ret != 0, stderr.decode(encoding="utf8"))
|
|
55
|
+
|
|
56
|
+
logger.info("Successfully uploaded version '%s' to GitHub", version)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"GithubUploadTarget",
|
|
61
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from danielutils import file_exists
|
|
7
|
+
|
|
8
|
+
from ....dist_files import find_sdist
|
|
9
|
+
from ..constraint_enforcers import PypircEnforcer
|
|
10
|
+
from ...upload_target import UploadTarget
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PypircUploadTarget(UploadTarget):
|
|
16
|
+
"""Upload target implementation using .pypirc configuration. Uploads packages to PyPI using twine."""
|
|
17
|
+
|
|
18
|
+
REGEX_PATTERN: re.Pattern = PypircEnforcer.PYPIRC_REGEX
|
|
19
|
+
|
|
20
|
+
def upload(self, name: str, version: str, **kwargs: Any) -> None: # type: ignore[override]
|
|
21
|
+
from quickpub.proxy import cm
|
|
22
|
+
from quickpub.enforcers import exit_if
|
|
23
|
+
|
|
24
|
+
logger.info("Starting PyPI upload for package '%s' version '%s'", name, version)
|
|
25
|
+
|
|
26
|
+
self._validate_file_exists()
|
|
27
|
+
self._validate_file_contents()
|
|
28
|
+
|
|
29
|
+
if self.verbose:
|
|
30
|
+
logger.info("Uploading package to PyPI")
|
|
31
|
+
|
|
32
|
+
sdist_path = find_sdist(Path("dist"), name, version)
|
|
33
|
+
ret, stdout, stderr = cm(
|
|
34
|
+
"twine",
|
|
35
|
+
"upload",
|
|
36
|
+
"--config-file",
|
|
37
|
+
".pypirc",
|
|
38
|
+
sdist_path.as_posix(),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if ret != 0:
|
|
42
|
+
logger.error("PyPI upload failed with return code %d: %s", ret, stderr)
|
|
43
|
+
exit_if(
|
|
44
|
+
ret != 0,
|
|
45
|
+
f"Failed uploading the package to pypi. Try running the following command manually:\n"
|
|
46
|
+
f"\ttwine upload --config-file .pypirc {sdist_path.as_posix()}",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logger.info(
|
|
50
|
+
"Successfully uploaded package '%s' version '%s' to PyPI", name, version
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def _validate_file_exists(self) -> None:
|
|
54
|
+
logger.debug("Validating .pypirc file exists at '%s'", self.pypirc_file_path)
|
|
55
|
+
if not file_exists(self.pypirc_file_path):
|
|
56
|
+
logger.error(".pypirc file not found at '%s'", self.pypirc_file_path)
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"{self.__class__.__name__} can't find pypirc file at '{self.pypirc_file_path}'"
|
|
59
|
+
)
|
|
60
|
+
logger.debug(".pypirc file found at '%s'", self.pypirc_file_path)
|
|
61
|
+
|
|
62
|
+
def _validate_file_contents(self) -> None:
|
|
63
|
+
logger.debug("Validating .pypirc file contents at '%s'", self.pypirc_file_path)
|
|
64
|
+
with open(self.pypirc_file_path, "r", encoding="utf8") as f:
|
|
65
|
+
text = f.read()
|
|
66
|
+
if not self.REGEX_PATTERN.match(text):
|
|
67
|
+
logger.error(
|
|
68
|
+
".pypirc file contents validation failed for '%s'",
|
|
69
|
+
self.pypirc_file_path,
|
|
70
|
+
)
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"{self.__class__.__name__} checked the contents of '{self.pypirc_file_path}' and it failed to match the following regex: r\"{self.REGEX_PATTERN.pattern}\""
|
|
73
|
+
)
|
|
74
|
+
logger.debug(
|
|
75
|
+
".pypirc file contents validation passed for '%s'", self.pypirc_file_path
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self, pypirc_file_path: str = "./.pypirc", verbose: bool = False
|
|
80
|
+
) -> None:
|
|
81
|
+
super().__init__(verbose)
|
|
82
|
+
self.pypirc_file_path = pypirc_file_path
|
|
83
|
+
logger.info(
|
|
84
|
+
"Initialized PypircUploadTarget with pypirc_file_path='%s', verbose=%s",
|
|
85
|
+
pypirc_file_path,
|
|
86
|
+
verbose,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = ["PypircUploadTarget"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Dict, Union
|
|
4
|
+
|
|
5
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
6
|
+
|
|
7
|
+
from .quickpub_strategy import QuickpubStrategy
|
|
8
|
+
from ..enforcers import exit_if
|
|
9
|
+
from ..structures import Dependency
|
|
10
|
+
from .implementations.package_managers.installed_package_parser import (
|
|
11
|
+
parse_installed_packages_output,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PackageManager(QuickpubStrategy):
|
|
18
|
+
async def prepare(self, executor: AsyncLayeredCommand) -> None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
async def list_installed(
|
|
23
|
+
self, executor: AsyncLayeredCommand, env_name: str
|
|
24
|
+
) -> Dict[str, Union[str, Dependency]]: ...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def install_command(self, package: str) -> str: ...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def show_command(self, package: str) -> str: ...
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def wrap_command(self, command: str) -> str: ...
|
|
34
|
+
|
|
35
|
+
async def _list_installed_from_command(
|
|
36
|
+
self,
|
|
37
|
+
executor: AsyncLayeredCommand,
|
|
38
|
+
env_name: str,
|
|
39
|
+
list_command: str,
|
|
40
|
+
) -> Dict[str, Union[str, Dependency]]:
|
|
41
|
+
logger.debug(
|
|
42
|
+
"Executing '%s' on environment '%s'", list_command, env_name
|
|
43
|
+
)
|
|
44
|
+
code, out, _ = await executor(list_command)
|
|
45
|
+
exit_if(
|
|
46
|
+
code != 0,
|
|
47
|
+
f"Failed executing '{list_command}' at env '{env_name}'",
|
|
48
|
+
)
|
|
49
|
+
currently_installed = parse_installed_packages_output(out)
|
|
50
|
+
logger.debug("Found %d installed packages", len(currently_installed))
|
|
51
|
+
return currently_installed
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["PackageManager"]
|