quickpub 2.0.4__tar.gz → 3.0.0__tar.gz
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-2.0.4/quickpub.egg-info → quickpub-3.0.0}/PKG-INFO +7 -3
- {quickpub-2.0.4 → quickpub-3.0.0}/README.md +1 -1
- {quickpub-2.0.4 → quickpub-3.0.0}/pyproject.toml +1 -1
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/__init__.py +2 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/__main__.py +18 -10
- quickpub-3.0.0/quickpub/enforcers.py +27 -0
- quickpub-3.0.0/quickpub/qa.py +265 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/constraint_enforcer.py +2 -1
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +4 -3
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +1 -1
- quickpub-3.0.0/quickpub/strategies/implementations/python_providers/conda_python_provider.py +35 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/python_providers/default_python_provider.py +12 -3
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +11 -4
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +7 -2
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +3 -2
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +10 -11
- quickpub-3.0.0/quickpub/strategies/python_provider.py +55 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/quality_assurance_runner.py +14 -10
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/quickpub_strategy.py +3 -1
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/validators.py +5 -3
- {quickpub-2.0.4 → quickpub-3.0.0/quickpub.egg-info}/PKG-INFO +7 -3
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub.egg-info/SOURCES.txt +2 -0
- quickpub-2.0.4/quickpub/enforcers.py +0 -17
- quickpub-2.0.4/quickpub/qa.py +0 -180
- quickpub-2.0.4/quickpub/strategies/implementations/python_providers/conda_python_provider.py +0 -32
- quickpub-2.0.4/quickpub/strategies/python_provider.py +0 -42
- {quickpub-2.0.4 → quickpub-3.0.0}/LICENSE +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/MANIFEST.in +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/classifiers.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/files.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/functions.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/proxy.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/py.typed +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/build_schema.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/build_schemas/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/python_providers/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/quality_assurance_runners/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/upload_targets/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/upload_targets/github_upload_target.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/upload_target.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/structures/__init__.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/structures/bound.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/structures/dependency.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub/structures/version.py +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub.egg-info/dependency_links.txt +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub.egg-info/requires.txt +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/quickpub.egg-info/top_level.txt +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/setup.cfg +0 -0
- {quickpub-2.0.4 → quickpub-3.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quickpub
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A python package to quickly configure and publish a new package
|
|
5
5
|
Author-email: danielnachumdev <danielnachumdev@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -33,8 +33,12 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
33
33
|
Requires-Python: >=3.8.0
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
|
+
Requires-Dist: danielutils>=1.0.0
|
|
37
|
+
Requires-Dist: requests
|
|
38
|
+
Requires-Dist: fire
|
|
39
|
+
Dynamic: license-file
|
|
36
40
|
|
|
37
|
-
# quickpub
|
|
41
|
+
# quickpub V3.0.0
|
|
38
42
|
**Tested python versions**: `3.8.0`, `3.9.0`, `3.10.13`,
|
|
39
43
|
|
|
40
44
|
Example usage of how this package was published
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional, Union, List, Any, Callable
|
|
2
3
|
|
|
3
4
|
import fire
|
|
4
5
|
from danielutils import warning, error
|
|
5
6
|
|
|
7
|
+
from quickpub import ExitEarlyError
|
|
6
8
|
from .strategies import BuildSchema, ConstraintEnforcer, UploadTarget, QualityAssuranceRunner, PythonProvider, \
|
|
7
9
|
DefaultPythonProvider
|
|
8
10
|
from .validators import validate_version, validate_python_version, validate_keywords, validate_dependencies, \
|
|
@@ -10,7 +12,7 @@ from .validators import validate_version, validate_python_version, validate_keyw
|
|
|
10
12
|
from .structures import Version, Dependency
|
|
11
13
|
from .files import create_toml, create_setup, create_manifest
|
|
12
14
|
from .classifiers import *
|
|
13
|
-
from .qa import qa
|
|
15
|
+
from .qa import qa, SupportsProgress
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def publish(
|
|
@@ -34,12 +36,16 @@ def publish(
|
|
|
34
36
|
dependencies: Optional[List[Union[str, Dependency]]] = None,
|
|
35
37
|
keywords: Optional[List[str]] = None,
|
|
36
38
|
explicit_src_folder_path: Optional[str] = None,
|
|
39
|
+
# ========== QA Parameters ==========
|
|
40
|
+
log: Optional[Callable[[Any], None]] = None,
|
|
41
|
+
pbar: Optional[SupportsProgress] = None, # tqdm
|
|
37
42
|
|
|
38
43
|
demo: bool = False,
|
|
39
44
|
config: Optional[Any] = None,
|
|
40
45
|
) -> None:
|
|
41
46
|
"""The main function for publishing a package. It performs all necessary steps to prepare and publish the package.
|
|
42
47
|
|
|
48
|
+
|
|
43
49
|
:param name: The name of the package.
|
|
44
50
|
:param author: The name of the author.
|
|
45
51
|
:param author_email: The email of the author.
|
|
@@ -56,6 +62,8 @@ def publish(
|
|
|
56
62
|
:param min_python: The minimum Python version required for the package. Defaults to the Python version running this script.
|
|
57
63
|
:param keywords: A list of keywords describing areas of interest for the package. Defaults to None.
|
|
58
64
|
:param dependencies: A list of dependencies for the package. Defaults to None.
|
|
65
|
+
:param log: A function to receive log statements about the process and print them (or do something else idk)
|
|
66
|
+
:param pbar: and object that can be notified about an update of progress like a tqdm progress bar.
|
|
59
67
|
:param demo: Whether to perform checks without making any changes. Defaults to False.
|
|
60
68
|
:param config: Reserved for future use. Defaults to None.
|
|
61
69
|
|
|
@@ -70,27 +78,27 @@ def publish(
|
|
|
70
78
|
min_python = validate_python_version(min_python) # type:ignore
|
|
71
79
|
keywords = validate_keywords(keywords)
|
|
72
80
|
validated_dependencies: List[Dependency] = validate_dependencies(dependencies)
|
|
73
|
-
|
|
74
81
|
for enforcer in enforcers or []:
|
|
75
82
|
enforcer.enforce(name=name, version=version, demo=demo)
|
|
76
|
-
|
|
77
83
|
try:
|
|
78
|
-
res = qa(
|
|
84
|
+
res = asyncio.get_event_loop().run_until_complete(qa(
|
|
79
85
|
python_interpreter_provider,
|
|
80
86
|
global_quality_assurance_runners or [],
|
|
81
87
|
name,
|
|
82
88
|
explicit_src_folder_path,
|
|
83
|
-
validated_dependencies
|
|
84
|
-
|
|
89
|
+
validated_dependencies,
|
|
90
|
+
log,
|
|
91
|
+
pbar
|
|
92
|
+
))
|
|
85
93
|
if not res:
|
|
86
94
|
error(f"quickpub.publish exited early as '{name}' "
|
|
87
95
|
"did not pass quality assurance step, see above "
|
|
88
96
|
"logs to pass this step.")
|
|
89
|
-
raise
|
|
90
|
-
except
|
|
97
|
+
raise ExitEarlyError("QA step Failed")
|
|
98
|
+
except ExitEarlyError as e:
|
|
91
99
|
raise e
|
|
92
100
|
except Exception as e:
|
|
93
|
-
raise RuntimeError("Quality assurance stage has failed") from e
|
|
101
|
+
raise RuntimeError("Quality assurance stage has failed", e) from e
|
|
94
102
|
|
|
95
103
|
create_setup()
|
|
96
104
|
create_toml(
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Union, Callable
|
|
3
|
+
|
|
4
|
+
from danielutils import error
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExitEarlyError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def exit_if(
|
|
12
|
+
predicate: Union[bool, Callable[[], bool]],
|
|
13
|
+
msg: str,
|
|
14
|
+
*,
|
|
15
|
+
verbose: bool = True,
|
|
16
|
+
err_func: Callable[[str], None] = error
|
|
17
|
+
) -> None:
|
|
18
|
+
if (isinstance(predicate, bool) and predicate) or (callable(predicate) and predicate()):
|
|
19
|
+
if verbose:
|
|
20
|
+
err_func(msg)
|
|
21
|
+
raise ExitEarlyError(msg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"exit_if",
|
|
26
|
+
"ExitEarlyError",
|
|
27
|
+
]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import ContextManager, List, Callable, Tuple, Dict, Union, Any, Literal, Optional, Protocol, \
|
|
6
|
+
runtime_checkable
|
|
7
|
+
from danielutils import TemporaryFile, AsyncWorkerPool, RandomDataGenerator
|
|
8
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
9
|
+
|
|
10
|
+
from .enforcers import ExitEarlyError
|
|
11
|
+
from .strategies import PythonProvider, QualityAssuranceRunner # pylint: disable=relative-beyond-top-level
|
|
12
|
+
from .structures import Dependency, Version # pylint: disable=relative-beyond-top-level
|
|
13
|
+
from .enforcers import exit_if # pylint: disable=relative-beyond-top-level
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from danielutils import MultiContext # type:ignore
|
|
17
|
+
except ImportError:
|
|
18
|
+
class MultiContext(ContextManager): # type: ignore # pylint: disable=missing-class-docstring
|
|
19
|
+
def __init__(self, *contexts: ContextManager):
|
|
20
|
+
self.contexts = contexts
|
|
21
|
+
|
|
22
|
+
def __enter__(self):
|
|
23
|
+
for context in self.contexts:
|
|
24
|
+
context.__enter__()
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
28
|
+
for context in self.contexts:
|
|
29
|
+
context.__exit__(exc_type, exc_val, exc_tb)
|
|
30
|
+
|
|
31
|
+
def __getitem__(self, index):
|
|
32
|
+
return self.contexts[index]
|
|
33
|
+
|
|
34
|
+
ASYNC_POOL_NAME: str = "Quickpub QA"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class SupportsProgress(Protocol):
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def update(self, amount: int) -> None:
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def total(self) -> int: ...
|
|
46
|
+
|
|
47
|
+
@total.setter
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def total(self, amount: int) -> None: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def global_import_sanity_check(
|
|
53
|
+
package_name: str,
|
|
54
|
+
executor: AsyncLayeredCommand,
|
|
55
|
+
is_system_interpreter: bool,
|
|
56
|
+
env_name: str,
|
|
57
|
+
pbar: Optional[SupportsProgress] = None
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Will check that importing from the package works as a sanity check.
|
|
61
|
+
:param package_name: Name of the package
|
|
62
|
+
:param executor: the previously ued AsyncLayeredCommand executor
|
|
63
|
+
:param is_system_interpreter: whether or not the system interpreter is used
|
|
64
|
+
:param env_name: The name of the currently tested environment
|
|
65
|
+
:return: None
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
p = sys.executable if is_system_interpreter else "python"
|
|
69
|
+
file_name = f"./{RandomDataGenerator().name(15)}__sanity_check_main.py"
|
|
70
|
+
with TemporaryFile(file_name) as f:
|
|
71
|
+
f.writelines([f"from {package_name} import *"])
|
|
72
|
+
cmd = f"{p} {file_name}"
|
|
73
|
+
code, stdout, stderr = await executor(cmd)
|
|
74
|
+
msg = f"Env '{env_name}' failed sanity check."
|
|
75
|
+
if stderr:
|
|
76
|
+
if stderr[0] == 'Traceback (most recent call last):':
|
|
77
|
+
msg += f" Got error '{stderr[-1]}' when tried 'from {package_name} import *'"
|
|
78
|
+
else:
|
|
79
|
+
msg += f" Try manually running the following script 'from {package_name} import *'"
|
|
80
|
+
exit_if(
|
|
81
|
+
code != 0, msg,
|
|
82
|
+
verbose=True,
|
|
83
|
+
err_func=lambda msg: None # TODO remove
|
|
84
|
+
)
|
|
85
|
+
finally:
|
|
86
|
+
if pbar is not None:
|
|
87
|
+
pbar.update(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
VERSION_REGEX: re.Pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def validate_dependencies(
|
|
94
|
+
validation_exit_on_fail: bool,
|
|
95
|
+
required_dependencies: List[Dependency],
|
|
96
|
+
executor: AsyncLayeredCommand,
|
|
97
|
+
env_name: str,
|
|
98
|
+
pbar: Optional[SupportsProgress] = None
|
|
99
|
+
) -> None:
|
|
100
|
+
"""
|
|
101
|
+
will check if all the dependencies of the package are installed on current env.
|
|
102
|
+
:param validation_exit_on_fail:
|
|
103
|
+
:param required_dependencies: the dependencies to check
|
|
104
|
+
:param executor: the current AsyncLayeredCommand executor
|
|
105
|
+
:param env_name: name of the currently checked environment
|
|
106
|
+
:return: None
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
if validation_exit_on_fail:
|
|
110
|
+
code, out, err = await executor("pip list")
|
|
111
|
+
exit_if(code != 0, f"Failed executing 'pip list' at env '{env_name}'",
|
|
112
|
+
err_func=lambda msg: None # TODO remove
|
|
113
|
+
)
|
|
114
|
+
split_lines = (line.split(' ') for line in out[2:])
|
|
115
|
+
version_tuples = [(s[0], s[-1].strip()) for s in split_lines]
|
|
116
|
+
filtered_tuples = [t for t in version_tuples if VERSION_REGEX.match(t[1])]
|
|
117
|
+
currently_installed: Dict[str, Union[str, Dependency]] = {
|
|
118
|
+
s[0]: Dependency(s[0], "==", Version.from_str(s[-1]))
|
|
119
|
+
for s in filtered_tuples}
|
|
120
|
+
currently_installed.update(**{t[0]: t[1] for t in version_tuples if not VERSION_REGEX.match(t[1])})
|
|
121
|
+
not_installed_properly: List[Tuple[Dependency, str]] = []
|
|
122
|
+
for req in required_dependencies:
|
|
123
|
+
if req.name not in currently_installed:
|
|
124
|
+
not_installed_properly.append((req, "dependency not found"))
|
|
125
|
+
else:
|
|
126
|
+
v = currently_installed[req.name]
|
|
127
|
+
if isinstance(v, str):
|
|
128
|
+
not_installed_properly.append(
|
|
129
|
+
(req, "Version format of dependency is not currently supported by quickpub"))
|
|
130
|
+
elif isinstance(v, Dependency):
|
|
131
|
+
if not req.is_satisfied_by(v.ver):
|
|
132
|
+
not_installed_properly.append((req, "Invalid version installed"))
|
|
133
|
+
|
|
134
|
+
exit_if(bool(not_installed_properly),
|
|
135
|
+
f"On env '{env_name}' the following dependencies have problems: {(not_installed_properly)}",
|
|
136
|
+
err_func=lambda msg: None # TODO remove
|
|
137
|
+
)
|
|
138
|
+
finally:
|
|
139
|
+
if pbar is not None:
|
|
140
|
+
pbar.update(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
is_config_run_success: List[bool] = []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def run_config(
|
|
147
|
+
env_name: str,
|
|
148
|
+
async_executor: AsyncLayeredCommand,
|
|
149
|
+
runner: QualityAssuranceRunner,
|
|
150
|
+
config_id: int,
|
|
151
|
+
*,
|
|
152
|
+
is_system_interpreter: bool,
|
|
153
|
+
validation_exit_on_fail: bool,
|
|
154
|
+
src_folder_path: str,
|
|
155
|
+
pbar: Optional[SupportsProgress] = None
|
|
156
|
+
) -> None:
|
|
157
|
+
try:
|
|
158
|
+
await runner.run(
|
|
159
|
+
src_folder_path,
|
|
160
|
+
async_executor,
|
|
161
|
+
use_system_interpreter=is_system_interpreter,
|
|
162
|
+
env_name=env_name
|
|
163
|
+
)
|
|
164
|
+
except ExitEarlyError as e:
|
|
165
|
+
raise e
|
|
166
|
+
except Exception as e:
|
|
167
|
+
if validation_exit_on_fail:
|
|
168
|
+
raise RuntimeError(e) from e
|
|
169
|
+
return
|
|
170
|
+
finally:
|
|
171
|
+
if pbar is not None:
|
|
172
|
+
pbar.update(1)
|
|
173
|
+
is_config_run_success[config_id] = True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def qa(
|
|
177
|
+
python_provider: PythonProvider,
|
|
178
|
+
quality_assurance_strategies: List[QualityAssuranceRunner],
|
|
179
|
+
package_name: str,
|
|
180
|
+
src_folder_path: str,
|
|
181
|
+
dependencies: list,
|
|
182
|
+
log: Optional[Callable[[Any], None]] = None,
|
|
183
|
+
pbar: Optional[SupportsProgress] = None
|
|
184
|
+
) -> bool:
|
|
185
|
+
is_config_run_success.clear()
|
|
186
|
+
from .strategies import DefaultPythonProvider
|
|
187
|
+
is_system_interpreter = isinstance(python_provider, DefaultPythonProvider)
|
|
188
|
+
|
|
189
|
+
class MyAsyncWorkerPool(AsyncWorkerPool):
|
|
190
|
+
@classmethod
|
|
191
|
+
def log(
|
|
192
|
+
self,
|
|
193
|
+
level: Literal["INFO", "WARNING", "ERROR"],
|
|
194
|
+
message: str,
|
|
195
|
+
order: Optional[List[str]] = AsyncWorkerPool.DEFAULT_ORDER_IF_KEY_EXISTS,
|
|
196
|
+
**kwargs
|
|
197
|
+
) -> None:
|
|
198
|
+
kwargs["level"] = level
|
|
199
|
+
kwargs["message"] = message
|
|
200
|
+
kwargs["timestamp"] = datetime.now().isoformat()
|
|
201
|
+
ordered_kwargs = kwargs
|
|
202
|
+
if order:
|
|
203
|
+
ordered_kwargs = {key: kwargs[key] for key in order if key in kwargs}
|
|
204
|
+
ordered_kwargs.update(kwargs)
|
|
205
|
+
|
|
206
|
+
if log:
|
|
207
|
+
log(ordered_kwargs)
|
|
208
|
+
|
|
209
|
+
pool = MyAsyncWorkerPool(ASYNC_POOL_NAME, num_workers=5)
|
|
210
|
+
total = 0
|
|
211
|
+
i = 0
|
|
212
|
+
with AsyncLayeredCommand() as base:
|
|
213
|
+
async for env_name, async_executor in python_provider:
|
|
214
|
+
with async_executor:
|
|
215
|
+
async_executor.prev = base
|
|
216
|
+
await pool.submit(
|
|
217
|
+
validate_dependencies,
|
|
218
|
+
args=[
|
|
219
|
+
python_provider.exit_on_fail,
|
|
220
|
+
dependencies,
|
|
221
|
+
async_executor,
|
|
222
|
+
env_name,
|
|
223
|
+
pbar
|
|
224
|
+
],
|
|
225
|
+
name=f"Validate dependencies for env '{env_name}'",
|
|
226
|
+
)
|
|
227
|
+
total += 1
|
|
228
|
+
await pool.submit(
|
|
229
|
+
global_import_sanity_check,
|
|
230
|
+
args=[
|
|
231
|
+
package_name,
|
|
232
|
+
async_executor,
|
|
233
|
+
is_system_interpreter,
|
|
234
|
+
env_name,
|
|
235
|
+
pbar
|
|
236
|
+
],
|
|
237
|
+
name=f"Global Import Sanity Check for env '{env_name}'",
|
|
238
|
+
)
|
|
239
|
+
total += 1
|
|
240
|
+
for runner in quality_assurance_strategies:
|
|
241
|
+
await pool.submit(
|
|
242
|
+
run_config,
|
|
243
|
+
args=[env_name, async_executor, runner, i],
|
|
244
|
+
kwargs=dict(src_folder_path=src_folder_path,
|
|
245
|
+
is_system_interpreter=is_system_interpreter,
|
|
246
|
+
validation_exit_on_fail=python_provider.exit_on_fail,
|
|
247
|
+
pbar=pbar),
|
|
248
|
+
name=f"Run config for '{env_name}' + '{runner.__class__.__qualname__}'",
|
|
249
|
+
)
|
|
250
|
+
total += 1
|
|
251
|
+
i += 1
|
|
252
|
+
if pbar is not None:
|
|
253
|
+
pbar.total = total
|
|
254
|
+
for _ in range(i):
|
|
255
|
+
is_config_run_success.append(False)
|
|
256
|
+
await pool.start()
|
|
257
|
+
await pool.join()
|
|
258
|
+
|
|
259
|
+
return all(is_config_run_success)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
__all__ = [
|
|
263
|
+
'qa',
|
|
264
|
+
"SupportsProgress"
|
|
265
|
+
]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from typing import Type
|
|
3
3
|
|
|
4
|
+
from ..enforcers import ExitEarlyError
|
|
4
5
|
from .quickpub_strategy import QuickpubStrategy
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class ConstraintEnforcer(QuickpubStrategy):
|
|
8
|
-
EXCEPTION_TYPE: Type[
|
|
9
|
+
EXCEPTION_TYPE: Type[Exception] = ExitEarlyError
|
|
9
10
|
|
|
10
11
|
@abstractmethod
|
|
11
12
|
def enforce(self, **kwargs) -> None: ...
|
|
@@ -22,15 +22,16 @@ class PypiRemoteVersionEnforcer(ConstraintEnforcer):
|
|
|
22
22
|
executor: RetryExecutor[Response] = RetryExecutor(ConstantBackOffStrategy(1))
|
|
23
23
|
response = executor.execute(wrapper, 5)
|
|
24
24
|
if response is None:
|
|
25
|
-
raise
|
|
25
|
+
raise self.EXCEPTION_TYPE(self._HTTP_FAILED_MESSAGE)
|
|
26
26
|
html = response.content.decode()
|
|
27
27
|
i = html.index(f" {name} ")
|
|
28
28
|
try:
|
|
29
29
|
remote_version = Version.from_str(html[i:i + 50].splitlines()[0].strip().split()[1])
|
|
30
30
|
except Exception as e:
|
|
31
|
-
raise
|
|
31
|
+
raise self.EXCEPTION_TYPE(f"{self.__class__.__name__} encountered an unexpected error while parsing.",
|
|
32
|
+
e) from e
|
|
32
33
|
if not version > remote_version:
|
|
33
|
-
raise
|
|
34
|
+
raise self.EXCEPTION_TYPE(
|
|
34
35
|
f"Specified version is '{version}' but (remotely available) latest existing is '{remote_version}'")
|
|
35
36
|
|
|
36
37
|
|
|
@@ -15,7 +15,7 @@ class PypircEnforcer(ConstraintEnforcer):
|
|
|
15
15
|
|
|
16
16
|
def enforce(self, **kwargs) -> None:
|
|
17
17
|
if not file_exists(self.path):
|
|
18
|
-
raise
|
|
18
|
+
raise self.EXCEPTION_TYPE(f"Couldn't find '{self.path}'")
|
|
19
19
|
if self.should_enforce_expected_format:
|
|
20
20
|
with open(self.path, "r") as f:
|
|
21
21
|
text = f.read()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Tuple, Optional, Set, List
|
|
2
|
+
from danielutils import AsyncLayeredCommand
|
|
3
|
+
|
|
4
|
+
from ....enforcers import ExitEarlyError
|
|
5
|
+
from ...python_provider import PythonProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CondaPythonProvider(PythonProvider):
|
|
9
|
+
def get_python_executable_name(self) -> str:
|
|
10
|
+
return "python"
|
|
11
|
+
|
|
12
|
+
def __init__(self, env_names: List[str]) -> None:
|
|
13
|
+
PythonProvider.__init__(self, requested_envs=env_names, explicit_versions=[], exit_on_fail=True)
|
|
14
|
+
self._cached_available_envs: Optional[Set[str]] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
async def _get_available_envs_impl(cls) -> Set[str]:
|
|
18
|
+
with AsyncLayeredCommand() as base:
|
|
19
|
+
code, out, err = await base("conda env list")
|
|
20
|
+
return set([line.split(' ')[0] for line in out[2:] if len(line.split(' ')) > 1])
|
|
21
|
+
|
|
22
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]:
|
|
23
|
+
if self.aiter_index >= len(self.requested_envs):
|
|
24
|
+
raise StopAsyncIteration
|
|
25
|
+
available_envs = await self.get_available_envs()
|
|
26
|
+
self.aiter_index += 1
|
|
27
|
+
name = self.requested_envs[self.aiter_index - 1]
|
|
28
|
+
if name not in available_envs:
|
|
29
|
+
raise ExitEarlyError(f"Can't find env '{name}' in list of conda environments, try 'conda env list'")
|
|
30
|
+
return name, AsyncLayeredCommand(f"conda activate {name}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'CondaPythonProvider',
|
|
35
|
+
]
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from typing import Set, Tuple, Iterator
|
|
2
|
+
from typing import Set, Tuple, Iterator, AsyncIterator, Iterable
|
|
3
3
|
|
|
4
4
|
from danielutils import LayeredCommand
|
|
5
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
5
6
|
|
|
6
7
|
from ...python_provider import PythonProvider
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
async def cast_aiter(itr: Iterable) -> AsyncIterator:
|
|
11
|
+
for x in itr:
|
|
12
|
+
yield x
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
class DefaultPythonProvider(PythonProvider):
|
|
10
16
|
def get_python_executable_name(self) -> str:
|
|
11
17
|
return sys.executable
|
|
@@ -13,8 +19,11 @@ class DefaultPythonProvider(PythonProvider):
|
|
|
13
19
|
def __init__(self) -> None:
|
|
14
20
|
PythonProvider.__init__(self, requested_envs=["system"], explicit_versions=[], exit_on_fail=True)
|
|
15
21
|
|
|
16
|
-
def
|
|
17
|
-
|
|
22
|
+
async def __anext__(self):
|
|
23
|
+
if self.aiter_index == 0:
|
|
24
|
+
self.aiter_index += 1
|
|
25
|
+
return "system", AsyncLayeredCommand()
|
|
26
|
+
raise StopAsyncIteration
|
|
18
27
|
|
|
19
28
|
@classmethod
|
|
20
29
|
def _get_available_envs_impl(cls) -> Set[str]:
|
|
@@ -3,6 +3,7 @@ from typing import Optional, List
|
|
|
3
3
|
|
|
4
4
|
from danielutils import LayeredCommand
|
|
5
5
|
|
|
6
|
+
from ....enforcers import ExitEarlyError
|
|
6
7
|
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
7
8
|
|
|
8
9
|
|
|
@@ -34,14 +35,20 @@ class MypyRunner(QualityAssuranceRunner):
|
|
|
34
35
|
return 0.0
|
|
35
36
|
|
|
36
37
|
if rating_line.endswith("No module named mypy"):
|
|
37
|
-
raise
|
|
38
|
+
raise ExitEarlyError("Mypy is not installed.")
|
|
39
|
+
|
|
40
|
+
if rating_line.startswith("mypy: error: Cannot find config file"):
|
|
41
|
+
raise ExitEarlyError(rating_line)
|
|
38
42
|
|
|
39
43
|
if rating_line.startswith("Success"):
|
|
40
44
|
return 0.0
|
|
41
45
|
|
|
42
|
-
exit_if(
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
exit_if(
|
|
47
|
+
not (m := self.RATING_PATTERN.match(rating_line)),
|
|
48
|
+
f"Failed running MyPy, got exit code {ret}. try running manually using: {self._build_command('TARGET')}",
|
|
49
|
+
verbose=verbose,
|
|
50
|
+
err_func=lambda msg: None # TODO remove
|
|
51
|
+
)
|
|
45
52
|
num_failed = float(m.group(1)) # type :ignore
|
|
46
53
|
# active_files = float(m.group(2)) # type :ignore
|
|
47
54
|
# total_files = float(m.group(3)) # type :ignore
|
|
@@ -3,6 +3,7 @@ from typing import Optional, List
|
|
|
3
3
|
|
|
4
4
|
from danielutils import LayeredCommand
|
|
5
5
|
|
|
6
|
+
from ....enforcers import ExitEarlyError
|
|
6
7
|
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
7
8
|
|
|
8
9
|
|
|
@@ -31,8 +32,12 @@ class PylintRunner(QualityAssuranceRunner):
|
|
|
31
32
|
return 1
|
|
32
33
|
if len(lines) == 1:
|
|
33
34
|
if lines[0].endswith("No module named pylint"):
|
|
34
|
-
raise
|
|
35
|
-
|
|
35
|
+
raise ExitEarlyError("No module named pylint found")
|
|
36
|
+
|
|
37
|
+
if lines[0].startswith("The config file") and lines[0].endswith("doesn't exist!"):
|
|
38
|
+
raise ExitEarlyError(lines[0])
|
|
39
|
+
|
|
40
|
+
raise ExitEarlyError(f"Got an unexpected error: {lines[0]}")
|
|
36
41
|
index = -2
|
|
37
42
|
if lines[-1] == '\x1b[0m':
|
|
38
43
|
index += -1
|
|
@@ -4,7 +4,8 @@ from typing import List, Union
|
|
|
4
4
|
|
|
5
5
|
from danielutils import LayeredCommand
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from ....enforcers import ExitEarlyError
|
|
8
|
+
from ....structures import Bound
|
|
8
9
|
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
9
10
|
|
|
10
11
|
|
|
@@ -85,7 +86,7 @@ class PytestRunner(QualityAssuranceRunner):
|
|
|
85
86
|
if "no tests ran" in rating_line:
|
|
86
87
|
return self.no_tests_score
|
|
87
88
|
if not (m := self.PYTEST_REGEX.match(rating_line)):
|
|
88
|
-
raise
|
|
89
|
+
raise ExitEarlyError(f"Can't calculate score for pytest on the following line: {rating_line}")
|
|
89
90
|
|
|
90
91
|
dct = m.groupdict()
|
|
91
92
|
failed = int(dct["failed"] or "0")
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import os
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Optional, List
|
|
4
5
|
from danielutils import get_current_working_directory, set_current_working_directory, LayeredCommand, warning
|
|
5
6
|
|
|
7
|
+
from ....enforcers import ExitEarlyError
|
|
6
8
|
from ...quality_assurance_runner import QualityAssuranceRunner
|
|
7
9
|
|
|
8
10
|
|
|
@@ -24,26 +26,23 @@ class UnittestRunner(QualityAssuranceRunner):
|
|
|
24
26
|
def _install_dependencies(self, base: LayeredCommand) -> None:
|
|
25
27
|
return None
|
|
26
28
|
|
|
27
|
-
def _pre_command(self):
|
|
28
|
-
|
|
29
|
-
if self.target is None:
|
|
30
|
-
self.target = ""
|
|
31
|
-
warning("This is not supposed to happen. See quickpub's UnitestRunner._pre_command")
|
|
32
|
-
set_current_working_directory(os.path.join(self._cwd, self.target))
|
|
29
|
+
def _pre_command(self) -> None:
|
|
30
|
+
pass
|
|
33
31
|
|
|
34
32
|
def _post_command(self):
|
|
35
|
-
|
|
33
|
+
pass
|
|
34
|
+
# set_current_working_directory(self._cwd)
|
|
36
35
|
|
|
37
36
|
def __init__(self, target: Optional[str] = "./tests", bound: str = ">=0.8", no_tests_score: float = 0) -> None:
|
|
38
37
|
QualityAssuranceRunner.__init__(self, name="unittest", bound=bound, target=target)
|
|
39
|
-
self._cwd = ""
|
|
40
38
|
self.no_tests_score = no_tests_score
|
|
41
39
|
|
|
42
40
|
def _build_command(self, src: str, *args, use_system_interpreter: bool = False) -> str:
|
|
43
41
|
command: str = self.get_executable()
|
|
44
42
|
rel = _removesuffix(os.path.relpath(src, self.target), src.lstrip("./\\"))
|
|
45
43
|
command += f" discover -s {rel}"
|
|
46
|
-
|
|
44
|
+
normalized_target_path = Path(os.path.join(os.getcwd(), self.target)).resolve()
|
|
45
|
+
return f"cd {normalized_target_path} & {command} & cd {Path(os.getcwd()).resolve()}" # This is for concurrency reasons
|
|
47
46
|
|
|
48
47
|
def _calculate_score(self, ret: int, lines: List[str], *, verbose: bool = False) -> float:
|
|
49
48
|
num_tests_ran_line = lines[-3]
|
|
@@ -61,8 +60,8 @@ class UnittestRunner(QualityAssuranceRunner):
|
|
|
61
60
|
return 1 - ((num_failed + num_errors) / num_tests)
|
|
62
61
|
|
|
63
62
|
except Exception as e:
|
|
64
|
-
raise
|
|
65
|
-
|
|
63
|
+
raise ExitEarlyError(f"Failed running Unittest, got exit code {ret}. "
|
|
64
|
+
f"try running manually using: {self._build_command('TARGET')}") from e
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
__all__ = [
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .quickpub_strategy import QuickpubStrategy
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import Tuple, Set, List, AsyncIterator, Iterator, Iterable
|
|
6
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PythonProvider(AsyncIterator, QuickpubStrategy):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
auto_install_dependencies: bool = True,
|
|
13
|
+
*,
|
|
14
|
+
requested_envs: List[str],
|
|
15
|
+
explicit_versions: List[str],
|
|
16
|
+
exit_on_fail: bool = False
|
|
17
|
+
) -> None:
|
|
18
|
+
self.auto_install_dependencies = auto_install_dependencies
|
|
19
|
+
self.requested_envs = requested_envs
|
|
20
|
+
self.explicit_versions = explicit_versions
|
|
21
|
+
self.exit_on_fail = exit_on_fail
|
|
22
|
+
self.aiter_index = 0
|
|
23
|
+
|
|
24
|
+
def __aiter__(self) -> AsyncIterator[Tuple[str, AsyncLayeredCommand]]:
|
|
25
|
+
self.aiter_index = 0
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]: ...
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
async def get_available_envs(cls) -> Set[str]:
|
|
33
|
+
KEY = "__available_envs__"
|
|
34
|
+
if (res := getattr(cls, KEY, None)) is not None:
|
|
35
|
+
return res
|
|
36
|
+
|
|
37
|
+
setattr(cls, KEY, res := await cls._get_available_envs_impl())
|
|
38
|
+
return res
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def _get_available_envs_impl(cls) -> Set[str]:
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def __len__(self) -> int:
|
|
46
|
+
return len(self.requested_envs)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_python_executable_name(self) -> str:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
'PythonProvider'
|
|
55
|
+
]
|
|
@@ -2,6 +2,7 @@ import sys
|
|
|
2
2
|
from abc import abstractmethod
|
|
3
3
|
from typing import Union, List, Optional, cast, Dict, Tuple
|
|
4
4
|
from danielutils import LayeredCommand, get_os, OSType, file_exists
|
|
5
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
5
6
|
|
|
6
7
|
from quickpub import Bound
|
|
7
8
|
|
|
@@ -124,8 +125,8 @@ class QualityAssuranceRunner(Configurable, HasOptionalExecutable):
|
|
|
124
125
|
"""
|
|
125
126
|
pass
|
|
126
127
|
|
|
127
|
-
def run(self, target: str, executor:
|
|
128
|
-
|
|
128
|
+
async def run(self, target: str, executor: AsyncLayeredCommand, *, verbose: bool = True, # type: ignore
|
|
129
|
+
use_system_interpreter: bool = False, env_name: str) -> None:
|
|
129
130
|
"""
|
|
130
131
|
Runs the QA process on the specified target.
|
|
131
132
|
|
|
@@ -133,31 +134,34 @@ class QualityAssuranceRunner(Configurable, HasOptionalExecutable):
|
|
|
133
134
|
:param executor: The executor object to run the command.
|
|
134
135
|
:param verbose: Whether to output verbose logs, default is True.
|
|
135
136
|
:param use_system_interpreter: Whether to use the system interpreter, default is False.
|
|
136
|
-
:param print_func: The function to use for printing output.
|
|
137
137
|
:param env_name: The name of the environment in which the QA runner is executed.
|
|
138
138
|
"""
|
|
139
139
|
from quickpub.proxy import os_system # pylint: disable=import-error
|
|
140
140
|
from quickpub.enforcers import exit_if # pylint: disable=import-error
|
|
141
141
|
# =====================================
|
|
142
142
|
# IMPORTANT: need to explicitly override it here
|
|
143
|
-
executor._executor = os_system # pylint: disable=protected-access
|
|
143
|
+
# executor._executor = os_system # pylint: disable=protected-access #TODO re-fix this for the tests because now this is not working with the async variant
|
|
144
144
|
# =====================================
|
|
145
145
|
command = self._build_command(target, use_system_interpreter)
|
|
146
146
|
self._pre_command()
|
|
147
147
|
try:
|
|
148
|
-
ret, out, err = executor(command, command_raise_on_fail=False)
|
|
148
|
+
ret, out, err = await executor(command, command_raise_on_fail=False)
|
|
149
149
|
if ret in SPEICLA_EXIT_CODES:
|
|
150
150
|
title, explanation = SPEICLA_EXIT_CODES[ret]
|
|
151
151
|
unsigned_integer_ret = ret + 2 ** 32
|
|
152
152
|
raise RuntimeError(
|
|
153
153
|
title + "\n\t" + explanation.format(command=command, ret=ret, hex=hex(unsigned_integer_ret)))
|
|
154
|
-
score = self._calculate_score(ret,
|
|
155
|
-
exit_if(
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
score = self._calculate_score(ret, out + err, verbose=verbose)
|
|
155
|
+
exit_if(
|
|
156
|
+
not self.bound.compare_against(score),
|
|
157
|
+
f"On env '{env_name}' runner '{self.__class__.__name__}' failed to pass its defined bound. Got a score of {score} but expected {self.bound}",
|
|
158
|
+
verbose=verbose,
|
|
159
|
+
err_func=lambda msg: None # TODO remove
|
|
160
|
+
)
|
|
158
161
|
except Exception as e:
|
|
159
162
|
raise RuntimeError(
|
|
160
|
-
f"On env {env_name}, failed to run {self.__class__.__name__}. Try running manually:\n{executor._build_command(command)}"
|
|
163
|
+
f"On env {env_name}, failed to run {self.__class__.__name__}. Try running manually:\n{executor._build_command(command)}",
|
|
164
|
+
e) from e
|
|
161
165
|
finally:
|
|
162
166
|
self._post_command()
|
|
163
167
|
|
|
@@ -2,9 +2,11 @@ from typing import Type
|
|
|
2
2
|
|
|
3
3
|
from danielutils.university.oop.strategy import Strategy
|
|
4
4
|
|
|
5
|
+
from ..enforcers import ExitEarlyError
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class QuickpubStrategy(Strategy):
|
|
7
|
-
EXCEPTION_TYPE: Type[Exception] =
|
|
9
|
+
EXCEPTION_TYPE: Type[Exception] = ExitEarlyError
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
__all__ = [
|
|
@@ -2,12 +2,13 @@ from typing import Optional, Union, List
|
|
|
2
2
|
|
|
3
3
|
from danielutils import get_python_version
|
|
4
4
|
|
|
5
|
+
from .enforcers import ExitEarlyError
|
|
5
6
|
from .structures import Version, Dependency
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def validate_version(version: Optional[Union[str, Version]] = None) -> Version:
|
|
9
|
-
if version
|
|
10
|
-
|
|
10
|
+
if not bool(version):
|
|
11
|
+
raise ExitEarlyError(f"Must supply a version number. got '{version}'")
|
|
11
12
|
return version if isinstance(version, Version) else Version.from_str(version) # type: ignore
|
|
12
13
|
|
|
13
14
|
|
|
@@ -45,5 +46,6 @@ __all__ = [
|
|
|
45
46
|
"validate_version",
|
|
46
47
|
"validate_python_version",
|
|
47
48
|
"validate_keywords",
|
|
48
|
-
"validate_dependencies"
|
|
49
|
+
"validate_dependencies",
|
|
50
|
+
"validate_source"
|
|
49
51
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quickpub
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A python package to quickly configure and publish a new package
|
|
5
5
|
Author-email: danielnachumdev <danielnachumdev@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -33,8 +33,12 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
33
33
|
Requires-Python: >=3.8.0
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
|
+
Requires-Dist: danielutils>=1.0.0
|
|
37
|
+
Requires-Dist: requests
|
|
38
|
+
Requires-Dist: fire
|
|
39
|
+
Dynamic: license-file
|
|
36
40
|
|
|
37
|
-
# quickpub
|
|
41
|
+
# quickpub V3.0.0
|
|
38
42
|
**Tested python versions**: `3.8.0`, `3.9.0`, `3.10.13`,
|
|
39
43
|
|
|
40
44
|
Example usage of how this package was published
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from typing import Union, Callable
|
|
3
|
-
|
|
4
|
-
from danielutils import error
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def exit_if(predicate: Union[bool, Callable[[], bool]], msg: str, *, verbose: bool = True,
|
|
8
|
-
err_func: Callable[[str], None] = error) -> None:
|
|
9
|
-
if (isinstance(predicate, bool) and predicate) or (callable(predicate) and predicate()):
|
|
10
|
-
if verbose:
|
|
11
|
-
err_func(msg)
|
|
12
|
-
sys.exit(1)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
__all__ = [
|
|
16
|
-
"exit_if"
|
|
17
|
-
]
|
quickpub-2.0.4/quickpub/qa.py
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import sys
|
|
3
|
-
from functools import wraps
|
|
4
|
-
from typing import Optional, ContextManager, List, Callable, Tuple, Dict, Union
|
|
5
|
-
from danielutils import AttrContext, LayeredCommand, AsciiProgressBar, ColoredText, ProgressBarPool, TemporaryFile
|
|
6
|
-
|
|
7
|
-
from .strategies import PythonProvider, QualityAssuranceRunner # pylint: disable=relative-beyond-top-level
|
|
8
|
-
from .structures import Dependency, Version, Bound # pylint: disable=relative-beyond-top-level
|
|
9
|
-
from .enforcers import exit_if # pylint: disable=relative-beyond-top-level
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
from danielutils import MultiContext # type:ignore
|
|
13
|
-
except ImportError:
|
|
14
|
-
class MultiContext(ContextManager): # type: ignore # pylint: disable=missing-class-docstring
|
|
15
|
-
def __init__(self, *contexts: ContextManager):
|
|
16
|
-
self.contexts = contexts
|
|
17
|
-
|
|
18
|
-
def __enter__(self):
|
|
19
|
-
for context in self.contexts:
|
|
20
|
-
context.__enter__()
|
|
21
|
-
return self
|
|
22
|
-
|
|
23
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
24
|
-
for context in self.contexts:
|
|
25
|
-
context.__exit__(exc_type, exc_val, exc_tb)
|
|
26
|
-
|
|
27
|
-
def __getitem__(self, index):
|
|
28
|
-
return self.contexts[index]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def global_import_sanity_check(package_name: str, executor: LayeredCommand, is_system_interpreter: bool,
|
|
32
|
-
env_name: str, err_print_func) -> None:
|
|
33
|
-
"""
|
|
34
|
-
Will check that importing from the package works as a sanity check.
|
|
35
|
-
:param package_name: Name of the package
|
|
36
|
-
:param executor: the previously ued LayeredCommand executor
|
|
37
|
-
:param is_system_interpreter: whether or not the system interpreter is used
|
|
38
|
-
:param env_name: The name of the currently tested environment
|
|
39
|
-
:param err_print_func: the function to print our errors
|
|
40
|
-
:return: None
|
|
41
|
-
"""
|
|
42
|
-
p = sys.executable if is_system_interpreter else "python"
|
|
43
|
-
file_name = "./__sanity_check_main.py"
|
|
44
|
-
with TemporaryFile(file_name) as f:
|
|
45
|
-
f.writelines([f"from {package_name} import *"])
|
|
46
|
-
code, _, _ = executor(f"{p} {file_name}")
|
|
47
|
-
exit_if(code != 0,
|
|
48
|
-
f"Env '{env_name}' failed sanity check. "
|
|
49
|
-
f"Try manually running the following script 'from {package_name} import *'",
|
|
50
|
-
verbose=True, err_func=err_print_func)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
VERSION_REGEX: re.Pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def validate_dependencies(python_manager: PythonProvider, required_dependencies: List[Dependency],
|
|
57
|
-
executor: LayeredCommand,
|
|
58
|
-
env_name: str, err_print_func: Callable) -> None:
|
|
59
|
-
"""
|
|
60
|
-
will check if all the dependencies of the package are installed on current env.
|
|
61
|
-
:param python_manager: the manager to use
|
|
62
|
-
:param required_dependencies: the dependencies to check
|
|
63
|
-
:param executor: the current LayeredCommand executor
|
|
64
|
-
:param env_name: name of the currently checked environment
|
|
65
|
-
:param err_print_func: function to print errors
|
|
66
|
-
:return: None
|
|
67
|
-
"""
|
|
68
|
-
if python_manager.exit_on_fail:
|
|
69
|
-
code, out, err = executor("pip list")
|
|
70
|
-
exit_if(code != 0, f"Failed executing 'pip list' at env '{env_name}'", err_func=err_print_func)
|
|
71
|
-
split_lines = (line.split(' ') for line in out[2:])
|
|
72
|
-
version_tuples = [(s[0], s[-1].strip()) for s in split_lines]
|
|
73
|
-
filtered_tuples = [t for t in version_tuples if VERSION_REGEX.match(t[1])]
|
|
74
|
-
currently_installed: Dict[str, Union[str, Dependency]] = {s[0]: Dependency(s[0], "==", Version.from_str(s[-1]))
|
|
75
|
-
for s in filtered_tuples}
|
|
76
|
-
currently_installed.update(**{t[0]: t[1] for t in version_tuples if not VERSION_REGEX.match(t[1])})
|
|
77
|
-
not_installed_properly: List[Tuple[Dependency, str]] = []
|
|
78
|
-
for req in required_dependencies:
|
|
79
|
-
if req.name not in currently_installed:
|
|
80
|
-
not_installed_properly.append((req, "dependency not found"))
|
|
81
|
-
else:
|
|
82
|
-
v = currently_installed[req.name]
|
|
83
|
-
if isinstance(v, str):
|
|
84
|
-
not_installed_properly.append(
|
|
85
|
-
(req, "Verion format of dependecy is not currently supported by quickpub"))
|
|
86
|
-
elif isinstance(v, Dependency):
|
|
87
|
-
if not req.is_satisfied_by(v.ver):
|
|
88
|
-
not_installed_properly.append((req, "Invalid version installed"))
|
|
89
|
-
|
|
90
|
-
exit_if(bool(not_installed_properly),
|
|
91
|
-
f"On env '{env_name}' the following dependencies have problems: {(not_installed_properly)}",
|
|
92
|
-
err_func=err_print_func)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def create_progress_bar_pool(python_version_manager: PythonProvider,
|
|
96
|
-
quality_assurance_strategies: List[QualityAssuranceRunner]) -> ProgressBarPool:
|
|
97
|
-
return ProgressBarPool(
|
|
98
|
-
AsciiProgressBar,
|
|
99
|
-
2,
|
|
100
|
-
individual_options=[
|
|
101
|
-
dict(
|
|
102
|
-
iterator=python_version_manager,
|
|
103
|
-
desc="Envs",
|
|
104
|
-
total=len(python_version_manager.requested_envs)
|
|
105
|
-
),
|
|
106
|
-
dict(
|
|
107
|
-
iterator=quality_assurance_strategies or [],
|
|
108
|
-
desc="Runners",
|
|
109
|
-
total=len(quality_assurance_strategies or [])
|
|
110
|
-
),
|
|
111
|
-
]
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def create_pool_print_error(pool: ProgressBarPool):
|
|
116
|
-
@wraps(pool.write)
|
|
117
|
-
def func(*args, **kwargs):
|
|
118
|
-
msg = "".join([ColoredText.red("ERROR"), ": ", *args])
|
|
119
|
-
pool.write(msg, **kwargs)
|
|
120
|
-
|
|
121
|
-
return func
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def qa(
|
|
125
|
-
python_provider: PythonProvider,
|
|
126
|
-
quality_assurance_strategies: List[QualityAssuranceRunner],
|
|
127
|
-
package_name: str,
|
|
128
|
-
src_folder_path: str,
|
|
129
|
-
dependencies: list
|
|
130
|
-
) -> bool:
|
|
131
|
-
from .strategies import DefaultPythonProvider
|
|
132
|
-
result = True
|
|
133
|
-
is_system_interpreter = isinstance(python_provider, DefaultPythonProvider)
|
|
134
|
-
pool = create_progress_bar_pool(python_provider, quality_assurance_strategies)
|
|
135
|
-
pool_err = create_pool_print_error(pool)
|
|
136
|
-
with LayeredCommand() as base:
|
|
137
|
-
for env_name, executor in pool[0]:
|
|
138
|
-
pool[0].desc = f"Env '{env_name}'"
|
|
139
|
-
pool[0].update(0, refresh=True)
|
|
140
|
-
with executor:
|
|
141
|
-
executor._prev_instance = base
|
|
142
|
-
try:
|
|
143
|
-
validate_dependencies(python_provider, dependencies, executor, env_name, pool_err)
|
|
144
|
-
except SystemExit:
|
|
145
|
-
result = False
|
|
146
|
-
continue
|
|
147
|
-
try:
|
|
148
|
-
global_import_sanity_check(package_name, executor, is_system_interpreter, env_name, pool_err)
|
|
149
|
-
except SystemExit:
|
|
150
|
-
result = False
|
|
151
|
-
continue
|
|
152
|
-
for runner in pool[1]:
|
|
153
|
-
pool[1].desc = f"Runner '{runner.__class__.__name__}'"
|
|
154
|
-
pool[1].update(0, refresh=True)
|
|
155
|
-
try:
|
|
156
|
-
runner.run(
|
|
157
|
-
src_folder_path,
|
|
158
|
-
executor,
|
|
159
|
-
use_system_interpreter=is_system_interpreter,
|
|
160
|
-
print_func=pool_err,
|
|
161
|
-
env_name=env_name
|
|
162
|
-
)
|
|
163
|
-
except SystemExit:
|
|
164
|
-
result = False
|
|
165
|
-
continue
|
|
166
|
-
except Exception as e:
|
|
167
|
-
result = False
|
|
168
|
-
manual_command = executor._build_command(runner._build_command(src_folder_path))
|
|
169
|
-
pool_err(
|
|
170
|
-
f"Failed running '{runner.__class__.__name__}' on env '{env_name}'. "
|
|
171
|
-
f"Try manually: '{manual_command}'.")
|
|
172
|
-
pool.write(f"\tCaused by '{e.__cause__ or e}'")
|
|
173
|
-
if python_provider.exit_on_fail:
|
|
174
|
-
raise RuntimeError() from e
|
|
175
|
-
return result
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
__all__ = [
|
|
179
|
-
'qa'
|
|
180
|
-
]
|
quickpub-2.0.4/quickpub/strategies/implementations/python_providers/conda_python_provider.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from typing import Tuple, Optional, Set, Iterator, List
|
|
2
|
-
from danielutils import LayeredCommand, warning
|
|
3
|
-
|
|
4
|
-
from ...python_provider import PythonProvider
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class CondaPythonProvider(PythonProvider):
|
|
8
|
-
def get_python_executable_name(self) -> str:
|
|
9
|
-
return "python"
|
|
10
|
-
|
|
11
|
-
def __init__(self, env_names: List[str]) -> None:
|
|
12
|
-
PythonProvider.__init__(self, requested_envs=env_names, explicit_versions=[], exit_on_fail=True)
|
|
13
|
-
self._cached_available_envs: Optional[Set[str]] = None
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def _get_available_envs_impl(cls) -> Set[str]:
|
|
17
|
-
with LayeredCommand() as base:
|
|
18
|
-
code, out, err = base("conda env list")
|
|
19
|
-
return set([line.split(' ')[0] for line in out[2:] if len(line.split(' ')) > 1])
|
|
20
|
-
|
|
21
|
-
def __iter__(self) -> Iterator[Tuple[str, LayeredCommand]]:
|
|
22
|
-
available_envs = self.get_available_envs()
|
|
23
|
-
for name in self.requested_envs:
|
|
24
|
-
if name not in available_envs:
|
|
25
|
-
warning(f"Couldn't find env '{name}'")
|
|
26
|
-
continue
|
|
27
|
-
yield name, LayeredCommand(f"conda activate {name}")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
__all__ = [
|
|
31
|
-
'CondaPythonProvider',
|
|
32
|
-
]
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from .quickpub_strategy import QuickpubStrategy
|
|
2
|
-
from .quality_assurance_runner import QualityAssuranceRunner
|
|
3
|
-
from abc import abstractmethod
|
|
4
|
-
from typing import Tuple, Set, Iterator, List
|
|
5
|
-
from danielutils import LayeredCommand
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class PythonProvider(QuickpubStrategy):
|
|
9
|
-
def __init__(self, auto_install_dependencies: bool = True, *, requested_envs: List[str],
|
|
10
|
-
explicit_versions: List[str],
|
|
11
|
-
exit_on_fail: bool = False) -> None:
|
|
12
|
-
self.auto_install_dependencies = auto_install_dependencies
|
|
13
|
-
self.requested_envs = requested_envs
|
|
14
|
-
self.explicit_versions = explicit_versions
|
|
15
|
-
self.exit_on_fail = exit_on_fail
|
|
16
|
-
|
|
17
|
-
@abstractmethod
|
|
18
|
-
def __iter__(self) -> Iterator[Tuple[str, LayeredCommand]]: ...
|
|
19
|
-
|
|
20
|
-
@classmethod
|
|
21
|
-
def get_available_envs(cls) -> Set[str]:
|
|
22
|
-
KEY = "__available_envs__"
|
|
23
|
-
if (res := getattr(cls, KEY, None)) is not None:
|
|
24
|
-
return res
|
|
25
|
-
|
|
26
|
-
setattr(cls, KEY, res := cls._get_available_envs_impl())
|
|
27
|
-
return res
|
|
28
|
-
|
|
29
|
-
@classmethod
|
|
30
|
-
@abstractmethod
|
|
31
|
-
def _get_available_envs_impl(cls) -> Set[str]: ...
|
|
32
|
-
|
|
33
|
-
def __len__(self) -> int:
|
|
34
|
-
return len(self.requested_envs)
|
|
35
|
-
|
|
36
|
-
@abstractmethod
|
|
37
|
-
def get_python_executable_name(self) -> str: ...
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
__all__ = [
|
|
41
|
-
'PythonProvider'
|
|
42
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/build_schemas/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/python_providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{quickpub-2.0.4 → quickpub-3.0.0}/quickpub/strategies/implementations/upload_targets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|