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,88 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from danielutils import RetryExecutor, MultiplicativeBackoff, ConstantBackOffStrategy
|
|
6
|
+
from requests import Response
|
|
7
|
+
|
|
8
|
+
from quickpub import Version
|
|
9
|
+
from quickpub.proxy import get # type: ignore
|
|
10
|
+
from ...constraint_enforcer import ConstraintEnforcer
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PypiRemoteVersionEnforcer(ConstraintEnforcer):
|
|
16
|
+
"""Enforces that the new version is greater than the latest version published on PyPI."""
|
|
17
|
+
|
|
18
|
+
_HTTP_FAILED_MESSAGE: str = "Failed to send http request"
|
|
19
|
+
|
|
20
|
+
def enforce(
|
|
21
|
+
self, name: str, version: Version, demo: bool = False, **kwargs: Any
|
|
22
|
+
) -> None: # type: ignore[override]
|
|
23
|
+
if demo:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
logger.info(
|
|
27
|
+
"Checking remote version for package '%s' against version '%s'",
|
|
28
|
+
name,
|
|
29
|
+
version,
|
|
30
|
+
)
|
|
31
|
+
url = f"https://pypi.org/simple/{name}/"
|
|
32
|
+
|
|
33
|
+
timeout_strategy = MultiplicativeBackoff(2)
|
|
34
|
+
|
|
35
|
+
def wrapper() -> Response:
|
|
36
|
+
return get(url, timeout=timeout_strategy.get_backoff())
|
|
37
|
+
|
|
38
|
+
executor: RetryExecutor[Response] = RetryExecutor(ConstantBackOffStrategy(1))
|
|
39
|
+
response = executor.execute(wrapper, 5)
|
|
40
|
+
if response is None:
|
|
41
|
+
logger.error("Failed to fetch package information from PyPI for '%s'", name)
|
|
42
|
+
raise self.EXCEPTION_TYPE(self._HTTP_FAILED_MESSAGE)
|
|
43
|
+
html = response.content.decode()
|
|
44
|
+
|
|
45
|
+
# Parse version information from href attributes in anchor tags
|
|
46
|
+
# Pattern to match: <a href="...">package-name-version.tar.gz</a>
|
|
47
|
+
# <a href=.*?>(({re.escape(name)})-([^<]+)\.tar\.gz)<\/a><br \/>
|
|
48
|
+
version_pattern = re.compile(
|
|
49
|
+
rf"<a href=.*?>(({re.escape(name)})-([^<]+)\.tar\.gz)<\/a><br \/>"
|
|
50
|
+
)
|
|
51
|
+
matches = version_pattern.findall(html)
|
|
52
|
+
|
|
53
|
+
if not matches:
|
|
54
|
+
logger.error("No versions found for package '%s' on PyPI", name)
|
|
55
|
+
raise self.EXCEPTION_TYPE(f"No versions found for package '{name}' on PyPI")
|
|
56
|
+
|
|
57
|
+
# Extract all versions and find the latest
|
|
58
|
+
versions = []
|
|
59
|
+
for _, _, version_str in matches:
|
|
60
|
+
try:
|
|
61
|
+
# version_str already contains just the version (e.g., "0.0.0")
|
|
62
|
+
versions.append(Version.from_str(version_str))
|
|
63
|
+
except Exception:
|
|
64
|
+
# Skip invalid version strings
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if not versions:
|
|
68
|
+
logger.error("No valid versions found for package '%s' on PyPI", name)
|
|
69
|
+
raise self.EXCEPTION_TYPE(
|
|
70
|
+
f"No valid versions found for package '{name}' on PyPI"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
remote_version = max(versions)
|
|
74
|
+
|
|
75
|
+
if not version > remote_version:
|
|
76
|
+
logger.error(
|
|
77
|
+
"Version conflict: specified '%s' is not greater than remote '%s'",
|
|
78
|
+
version,
|
|
79
|
+
remote_version,
|
|
80
|
+
)
|
|
81
|
+
raise self.EXCEPTION_TYPE(
|
|
82
|
+
f"Specified version is '{version}' but (remotely available) latest existing is '{remote_version}'"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
logger.info("Version check passed: '%s' > '%s'", version, remote_version)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["PypiRemoteVersionEnforcer"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from danielutils import file_exists
|
|
6
|
+
|
|
7
|
+
from ...constraint_enforcer import ConstraintEnforcer
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PypircEnforcer(ConstraintEnforcer):
|
|
13
|
+
"""Enforces the presence and validity of a .pypirc configuration file for PyPI uploads."""
|
|
14
|
+
|
|
15
|
+
PYPIRC_REGEX: re.Pattern = re.compile(
|
|
16
|
+
r"\[distutils\]\nindex-servers =\n\s*pypi\n\s*testpypi\n\n\[pypi\]\n\s*username = __token__\n\s*password = .+\n\n\[testpypi\]\n\s*username = __token__\n\s*password = .+\n?"
|
|
17
|
+
) # pylint: disable=line-too-long
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, path: str = "./.pypirc", should_enforce_expected_format: bool = True
|
|
21
|
+
) -> None:
|
|
22
|
+
self.path = path
|
|
23
|
+
self.should_enforce_expected_format = should_enforce_expected_format
|
|
24
|
+
|
|
25
|
+
def enforce(self, **kwargs: Any) -> None:
|
|
26
|
+
logger.info("Validating .pypirc file at '%s'", self.path)
|
|
27
|
+
|
|
28
|
+
if not file_exists(self.path):
|
|
29
|
+
logger.error("Could not find .pypirc file at '%s'", self.path)
|
|
30
|
+
raise self.EXCEPTION_TYPE(f"Couldn't find '{self.path}'")
|
|
31
|
+
|
|
32
|
+
if self.should_enforce_expected_format:
|
|
33
|
+
with open(self.path, "r") as f:
|
|
34
|
+
text = f.read()
|
|
35
|
+
|
|
36
|
+
if not self.PYPIRC_REGEX.match(text):
|
|
37
|
+
logger.error("Invalid .pypirc format at '%s'", self.path)
|
|
38
|
+
raise self.EXCEPTION_TYPE(f"'{self.path}' has an invalid format.")
|
|
39
|
+
|
|
40
|
+
logger.info(".pypirc file validation passed for '%s'", self.path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"PypircEnforcer",
|
|
45
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from danielutils import file_exists
|
|
5
|
+
|
|
6
|
+
from ...constraint_enforcer import ConstraintEnforcer
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReadmeEnforcer(ConstraintEnforcer):
|
|
12
|
+
"""Enforces the presence of a README file at the specified path."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, path: str = "./README.md") -> None:
|
|
15
|
+
self.path = path
|
|
16
|
+
|
|
17
|
+
def enforce(self, **kwargs: Any) -> None:
|
|
18
|
+
logger.info("Checking for readme file at '%s'", self.path)
|
|
19
|
+
|
|
20
|
+
if not file_exists(self.path):
|
|
21
|
+
logger.error("Readme file not found at '%s'", self.path)
|
|
22
|
+
raise self.EXCEPTION_TYPE(f"Could not find readme file at '{self.path}'")
|
|
23
|
+
|
|
24
|
+
logger.info("Readme file found at '%s'", self.path)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ReadmeEnforcer",
|
|
29
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Union
|
|
3
|
+
|
|
4
|
+
from ....structures import Dependency, Version
|
|
5
|
+
|
|
6
|
+
VERSION_REGEX: re.Pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_installed_packages_output(
|
|
10
|
+
output_lines: List[str],
|
|
11
|
+
) -> Dict[str, Union[str, Dependency]]:
|
|
12
|
+
split_lines = (line.split(" ") for line in output_lines[2:])
|
|
13
|
+
version_tuples = [(s[0], s[-1].strip()) for s in split_lines if s[0]]
|
|
14
|
+
filtered_tuples = [t for t in version_tuples if VERSION_REGEX.match(t[1])]
|
|
15
|
+
currently_installed: Dict[str, Union[str, Dependency]] = {
|
|
16
|
+
name: Dependency(name, "==", Version.from_str(version))
|
|
17
|
+
for name, version in filtered_tuples
|
|
18
|
+
}
|
|
19
|
+
currently_installed.update(
|
|
20
|
+
**{
|
|
21
|
+
name: version
|
|
22
|
+
for name, version in version_tuples
|
|
23
|
+
if not VERSION_REGEX.match(version)
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
return currently_installed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["parse_installed_packages_output", "VERSION_REGEX"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Union
|
|
3
|
+
|
|
4
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
5
|
+
|
|
6
|
+
from ...package_manager import PackageManager
|
|
7
|
+
from ....structures import Dependency
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PipPackageManager(PackageManager):
|
|
13
|
+
async def prepare(self, executor: AsyncLayeredCommand) -> None:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
async def list_installed(
|
|
17
|
+
self, executor: AsyncLayeredCommand, env_name: str
|
|
18
|
+
) -> Dict[str, Union[str, Dependency]]:
|
|
19
|
+
return await self._list_installed_from_command(executor, env_name, "pip list")
|
|
20
|
+
|
|
21
|
+
def install_command(self, package: str) -> str:
|
|
22
|
+
return f"pip install {package}"
|
|
23
|
+
|
|
24
|
+
def show_command(self, package: str) -> str:
|
|
25
|
+
return f"pip show {package}"
|
|
26
|
+
|
|
27
|
+
def wrap_command(self, command: str) -> str:
|
|
28
|
+
return command
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["PipPackageManager"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, Union
|
|
4
|
+
|
|
5
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
6
|
+
|
|
7
|
+
from ...package_manager import PackageManager
|
|
8
|
+
from ....enforcers import exit_if
|
|
9
|
+
from ....structures import Dependency
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UvPackageManager(PackageManager):
|
|
15
|
+
def __init__(self, project_root: Path) -> None:
|
|
16
|
+
self.project_root = project_root
|
|
17
|
+
|
|
18
|
+
async def prepare(self, executor: AsyncLayeredCommand) -> None:
|
|
19
|
+
lockfile = self.project_root / "uv.lock"
|
|
20
|
+
exit_if(
|
|
21
|
+
not lockfile.exists(),
|
|
22
|
+
f"uv.lock not found at '{lockfile}'. Run 'uv lock' before using UvPackageManager.",
|
|
23
|
+
)
|
|
24
|
+
logger.debug("Running 'uv sync' for project at '%s'", self.project_root)
|
|
25
|
+
code, _, _ = await executor("uv sync")
|
|
26
|
+
exit_if(code != 0, "Failed executing 'uv sync'")
|
|
27
|
+
|
|
28
|
+
async def list_installed(
|
|
29
|
+
self, executor: AsyncLayeredCommand, env_name: str
|
|
30
|
+
) -> Dict[str, Union[str, Dependency]]:
|
|
31
|
+
return await self._list_installed_from_command(
|
|
32
|
+
executor, env_name, "uv pip list"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def install_command(self, package: str) -> str:
|
|
36
|
+
return f"uv add --dev {package}"
|
|
37
|
+
|
|
38
|
+
def show_command(self, package: str) -> str:
|
|
39
|
+
return f"uv pip show {package}"
|
|
40
|
+
|
|
41
|
+
def wrap_command(self, command: str) -> str:
|
|
42
|
+
return f"uv run -- {command}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["UvPackageManager"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Tuple, Optional, Set, List
|
|
3
|
+
from danielutils import AsyncLayeredCommand
|
|
4
|
+
|
|
5
|
+
from ....enforcers import ExitEarlyError
|
|
6
|
+
from ...python_provider import PythonProvider
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CondaPythonProvider(PythonProvider):
|
|
12
|
+
"""Python provider implementation using conda environments. Iterates over specified conda environment names."""
|
|
13
|
+
|
|
14
|
+
def get_python_executable_name(self) -> str:
|
|
15
|
+
return "python"
|
|
16
|
+
|
|
17
|
+
def __init__(self, env_names: List[str]) -> None:
|
|
18
|
+
PythonProvider.__init__(
|
|
19
|
+
self, requested_envs=env_names, explicit_versions=[], exit_on_fail=True
|
|
20
|
+
)
|
|
21
|
+
self._cached_available_envs: Optional[Set[str]] = None
|
|
22
|
+
logger.info("Initialized CondaPythonProvider with environments: %s", env_names)
|
|
23
|
+
|
|
24
|
+
async def _get_available_envs_impl(self) -> Set[str]:
|
|
25
|
+
logger.info("Fetching available conda environments")
|
|
26
|
+
with AsyncLayeredCommand() as base:
|
|
27
|
+
code, out, err = await base("conda env list")
|
|
28
|
+
return set([line.split(" ")[0] for line in out[2:] if len(line.split(" ")) > 1])
|
|
29
|
+
|
|
30
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]:
|
|
31
|
+
if self.aiter_index >= len(self.requested_envs):
|
|
32
|
+
raise StopAsyncIteration
|
|
33
|
+
|
|
34
|
+
available_envs = await self._get_available_envs()
|
|
35
|
+
self.aiter_index += 1
|
|
36
|
+
name = self.requested_envs[self.aiter_index - 1]
|
|
37
|
+
|
|
38
|
+
logger.debug("Activating conda environment: %s", name)
|
|
39
|
+
|
|
40
|
+
if name not in available_envs:
|
|
41
|
+
logger.error(
|
|
42
|
+
"Environment '%s' not found in available conda environments", name
|
|
43
|
+
)
|
|
44
|
+
raise ExitEarlyError(
|
|
45
|
+
f"Can't find env '{name}' in list of conda environments, try 'conda env list'"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger.debug("Successfully activated conda environment: %s", name)
|
|
49
|
+
return name, AsyncLayeredCommand(f"conda activate {name}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"CondaPythonProvider",
|
|
54
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Set, Tuple, AsyncIterator, Iterable, Any
|
|
4
|
+
|
|
5
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
6
|
+
|
|
7
|
+
from ...python_provider import PythonProvider
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def cast_aiter(itr: Iterable[Any]) -> AsyncIterator[Any]:
|
|
13
|
+
for x in itr:
|
|
14
|
+
yield x
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DefaultPythonProvider(PythonProvider):
|
|
18
|
+
"""Default Python provider using the system Python interpreter. Provides a single 'system' environment."""
|
|
19
|
+
|
|
20
|
+
def get_python_executable_name(self) -> str:
|
|
21
|
+
return sys.executable
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
PythonProvider.__init__(
|
|
25
|
+
self, requested_envs=["system"], explicit_versions=[], exit_on_fail=True
|
|
26
|
+
)
|
|
27
|
+
logger.info(
|
|
28
|
+
"Initialized DefaultPythonProvider with system Python: %s", sys.executable
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]:
|
|
32
|
+
if self.aiter_index == 0:
|
|
33
|
+
self.aiter_index += 1
|
|
34
|
+
logger.info("Using system Python environment")
|
|
35
|
+
executor = AsyncLayeredCommand()
|
|
36
|
+
executor.prev = None
|
|
37
|
+
return "system", executor
|
|
38
|
+
raise StopAsyncIteration
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
async def _get_available_envs_impl(cls) -> Set[str]:
|
|
42
|
+
return {"system"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DefaultPythonProvider",
|
|
47
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Set, Tuple
|
|
3
|
+
|
|
4
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
5
|
+
|
|
6
|
+
from ...python_provider import PythonProvider
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnionProvider(PythonProvider):
|
|
12
|
+
def __init__(self, providers: List[PythonProvider]) -> None:
|
|
13
|
+
requested_envs = [env for provider in providers for env in provider.requested_envs]
|
|
14
|
+
PythonProvider.__init__(
|
|
15
|
+
self,
|
|
16
|
+
requested_envs=requested_envs,
|
|
17
|
+
explicit_versions=[],
|
|
18
|
+
exit_on_fail=any(provider.exit_on_fail for provider in providers),
|
|
19
|
+
)
|
|
20
|
+
self.providers = providers
|
|
21
|
+
self._child_index = 0
|
|
22
|
+
self._child_iterator = None
|
|
23
|
+
logger.info(
|
|
24
|
+
"Initialized UnionProvider with %d child providers", len(providers)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def get_python_executable_name(self) -> str:
|
|
28
|
+
if not self.providers:
|
|
29
|
+
raise RuntimeError("UnionProvider has no child providers")
|
|
30
|
+
return self.providers[0].get_python_executable_name()
|
|
31
|
+
|
|
32
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]:
|
|
33
|
+
while self._child_index < len(self.providers):
|
|
34
|
+
if self._child_iterator is None:
|
|
35
|
+
self._child_iterator = self.providers[self._child_index].__aiter__()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
return await self._child_iterator.__anext__()
|
|
39
|
+
except StopAsyncIteration:
|
|
40
|
+
self._child_index += 1
|
|
41
|
+
self._child_iterator = None
|
|
42
|
+
|
|
43
|
+
raise StopAsyncIteration
|
|
44
|
+
|
|
45
|
+
async def _get_available_envs_impl(self) -> Set[str]:
|
|
46
|
+
available_envs: Set[str] = set()
|
|
47
|
+
for provider in self.providers:
|
|
48
|
+
available_envs.update(await provider._get_available_envs())
|
|
49
|
+
return available_envs
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["UnionProvider"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Set, Tuple
|
|
3
|
+
|
|
4
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
5
|
+
|
|
6
|
+
from ...python_provider import PythonProvider
|
|
7
|
+
from ..package_managers.uv_package_manager import UvPackageManager
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UvPythonProvider(PythonProvider):
|
|
15
|
+
def __init__(self, project_root: Path) -> None:
|
|
16
|
+
PythonProvider.__init__(
|
|
17
|
+
self, requested_envs=["uv"], explicit_versions=[], exit_on_fail=True
|
|
18
|
+
)
|
|
19
|
+
self.project_root = project_root
|
|
20
|
+
self.package_manager = UvPackageManager(project_root=project_root)
|
|
21
|
+
logger.info("Initialized UvPythonProvider for project at '%s'", project_root)
|
|
22
|
+
|
|
23
|
+
def get_python_executable_name(self) -> str:
|
|
24
|
+
return str(self.project_root / ".venv" / "bin" / "python")
|
|
25
|
+
|
|
26
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]:
|
|
27
|
+
if self.aiter_index == 0:
|
|
28
|
+
self.aiter_index += 1
|
|
29
|
+
logger.info("Using uv-managed Python environment")
|
|
30
|
+
executor = AsyncLayeredCommand()
|
|
31
|
+
executor.prev = None
|
|
32
|
+
return "uv", executor
|
|
33
|
+
raise StopAsyncIteration
|
|
34
|
+
|
|
35
|
+
async def _get_available_envs_impl(self) -> Set[str]:
|
|
36
|
+
return {"uv"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["UvPythonProvider"]
|
|
@@ -0,0 +1,98 @@
|
|
|
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 MypyRunner(QualityAssuranceRunner):
|
|
14
|
+
"""Quality assurance runner for mypy type checking. Scores based on the number of type errors found."""
|
|
15
|
+
|
|
16
|
+
NO_TESTS_PATTERN: re.Pattern = re.compile(
|
|
17
|
+
r"There are no \.py\[i\] files in directory '[\w\.\\\/]+'"
|
|
18
|
+
)
|
|
19
|
+
RATING_PATTERN: re.Pattern = re.compile(
|
|
20
|
+
r"Found (\d+(?:\.\d+)?) errors? in (\d+(?:\.\d+)?) files? \(checked (\d+(?:\.\d+)?) source files?\)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _install_dependencies(self, base: LayeredCommand) -> None:
|
|
24
|
+
logger.info("Installing mypy dependencies")
|
|
25
|
+
with base:
|
|
26
|
+
base(self.package_manager.install_command("mypy"))
|
|
27
|
+
|
|
28
|
+
def _build_command(self, target: str, use_system_interpreter: bool = False) -> str:
|
|
29
|
+
command: str = self.get_executable(use_system_interpreter)
|
|
30
|
+
if self.has_config:
|
|
31
|
+
command += f" --config-file {self.config_path}"
|
|
32
|
+
command += f" {target}"
|
|
33
|
+
return command
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
bound: str = "<15",
|
|
38
|
+
configuration_path: Optional[str] = None,
|
|
39
|
+
executable_path: Optional[str] = None,
|
|
40
|
+
package_manager=None,
|
|
41
|
+
) -> None:
|
|
42
|
+
QualityAssuranceRunner.__init__(
|
|
43
|
+
self,
|
|
44
|
+
name="mypy",
|
|
45
|
+
bound=bound,
|
|
46
|
+
configuration_path=configuration_path,
|
|
47
|
+
executable_path=executable_path,
|
|
48
|
+
package_manager=package_manager,
|
|
49
|
+
)
|
|
50
|
+
logger.info(
|
|
51
|
+
"Initialized MypyRunner with bound='%s', config='%s', executable='%s'",
|
|
52
|
+
bound,
|
|
53
|
+
configuration_path,
|
|
54
|
+
executable_path,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _calculate_score(
|
|
58
|
+
self, ret: int, lines: List[str], verbose: bool = False
|
|
59
|
+
) -> float:
|
|
60
|
+
from quickpub.enforcers import exit_if
|
|
61
|
+
|
|
62
|
+
logger.debug("Calculating mypy score from type checking results")
|
|
63
|
+
|
|
64
|
+
rating_line = lines[-1]
|
|
65
|
+
if self.NO_TESTS_PATTERN.match(rating_line):
|
|
66
|
+
logger.debug(
|
|
67
|
+
"No Python files found for type checking, returning score: 0.0"
|
|
68
|
+
)
|
|
69
|
+
return 0.0
|
|
70
|
+
|
|
71
|
+
if rating_line.endswith("No module named mypy"):
|
|
72
|
+
logger.error("Mypy module not found")
|
|
73
|
+
raise ExitEarlyError("Mypy is not installed.")
|
|
74
|
+
|
|
75
|
+
if rating_line.startswith("mypy: error: Cannot find config file"):
|
|
76
|
+
logger.error("Config file error: %s", rating_line)
|
|
77
|
+
raise ExitEarlyError(rating_line)
|
|
78
|
+
|
|
79
|
+
if rating_line.startswith("Success"):
|
|
80
|
+
logger.debug("Mypy type checking successful, returning score: 0.0")
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
m = self.RATING_PATTERN.match(rating_line)
|
|
84
|
+
exit_if(
|
|
85
|
+
m is None,
|
|
86
|
+
f"Failed running MyPy, got exit code {ret}. try running manually using: {self._build_command('TARGET')}",
|
|
87
|
+
)
|
|
88
|
+
assert m is not None # For type checker
|
|
89
|
+
num_failed = float(m.group(1))
|
|
90
|
+
# active_files = float(m.group(2)) # type :ignore
|
|
91
|
+
# total_files = float(m.group(3)) # type :ignore
|
|
92
|
+
logger.debug("Mypy score calculated: %s type errors found", num_failed)
|
|
93
|
+
return num_failed
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"MypyRunner",
|
|
98
|
+
]
|