quickpub 3.1.0__tar.gz → 4.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-3.1.0/quickpub.egg-info → quickpub-4.0.0}/PKG-INFO +1 -1
- {quickpub-3.1.0 → quickpub-4.0.0}/pyproject.toml +1 -1
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/__init__.py +2 -0
- quickpub-4.0.0/quickpub/__main__.py +243 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/classifiers.py +79 -96
- quickpub-4.0.0/quickpub/enforcers.py +26 -0
- quickpub-4.0.0/quickpub/files.py +196 -0
- quickpub-4.0.0/quickpub/logging_.py +74 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/proxy.py +35 -56
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/qa.py +413 -415
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/build_schema.py +3 -9
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/constraint_enforcer.py +3 -9
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +1 -7
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +4 -2
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +6 -14
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +9 -4
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +3 -2
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +3 -3
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/conda_python_provider.py +1 -1
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/default_python_provider.py +6 -13
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +8 -6
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +1 -1
- quickpub-4.0.0/quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +161 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +18 -22
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/github_upload_target.py +3 -2
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +3 -2
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/python_provider.py +4 -16
- quickpub-4.0.0/quickpub/strategies/quality_assurance_runner.py +194 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/quickpub_strategy.py +0 -2
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/upload_target.py +3 -8
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/structures/bound.py +10 -19
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/structures/dependency.py +14 -18
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/structures/version.py +1 -10
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/validators.py +6 -34
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/worker_pool.py +2 -1
- {quickpub-3.1.0 → quickpub-4.0.0/quickpub.egg-info}/PKG-INFO +1 -1
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub.egg-info/SOURCES.txt +2 -2
- quickpub-4.0.0/tests/test_helpers.py +26 -0
- quickpub-3.1.0/quickpub/__main__.py +0 -156
- quickpub-3.1.0/quickpub/enforcers.py +0 -41
- quickpub-3.1.0/quickpub/files.py +0 -127
- quickpub-3.1.0/quickpub/functions.py +0 -120
- quickpub-3.1.0/quickpub/logging_.py +0 -108
- quickpub-3.1.0/quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +0 -140
- quickpub-3.1.0/quickpub/strategies/quality_assurance_runner.py +0 -276
- {quickpub-3.1.0 → quickpub-4.0.0}/LICENSE +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/MANIFEST.in +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/README.md +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/py.typed +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/build_schemas/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub/structures/__init__.py +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub.egg-info/dependency_links.txt +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub.egg-info/entry_points.txt +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub.egg-info/requires.txt +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/quickpub.egg-info/top_level.txt +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/setup.cfg +0 -0
- {quickpub-3.1.0 → quickpub-4.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickpub
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: A local CI/CD simulation tool that runs quality checks, tests, and validations locally before publishing Python packages, ensuring higher build pass rates and faster feedback loops
|
|
5
5
|
Author-email: danielnachumdev <danielnachumdev@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional, Union, List, Any, Dict, Callable, Tuple
|
|
5
|
+
|
|
6
|
+
import fire # type: ignore[import-untyped]
|
|
7
|
+
from danielutils import warning, error
|
|
8
|
+
|
|
9
|
+
from quickpub import ExitEarlyError
|
|
10
|
+
from .strategies import (
|
|
11
|
+
BuildSchema,
|
|
12
|
+
ConstraintEnforcer,
|
|
13
|
+
UploadTarget,
|
|
14
|
+
QualityAssuranceRunner,
|
|
15
|
+
PythonProvider,
|
|
16
|
+
DefaultPythonProvider,
|
|
17
|
+
)
|
|
18
|
+
from .validators import (
|
|
19
|
+
validate_version,
|
|
20
|
+
validate_python_version,
|
|
21
|
+
validate_keywords,
|
|
22
|
+
validate_dependencies,
|
|
23
|
+
validate_source,
|
|
24
|
+
)
|
|
25
|
+
from .structures import Version, Dependency
|
|
26
|
+
from .files import create_toml, create_setup, create_manifest, add_version_to_init
|
|
27
|
+
from .classifiers import *
|
|
28
|
+
from .qa import qa, SupportsProgress
|
|
29
|
+
from .logging_ import setup_logging
|
|
30
|
+
|
|
31
|
+
setup_logging()
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _validate_publish_inputs(
|
|
36
|
+
name: str,
|
|
37
|
+
version: Optional[Union[Version, str]],
|
|
38
|
+
explicit_src_folder_path: Optional[str],
|
|
39
|
+
min_python: Optional[Union[Version, str]],
|
|
40
|
+
keywords: Optional[List[str]],
|
|
41
|
+
dependencies: Optional[List[Union[str, Dependency]]],
|
|
42
|
+
) -> Tuple[Version, str, Version, List[str], List[Dependency]]:
|
|
43
|
+
validated_version = validate_version(version)
|
|
44
|
+
validated_src_path = validate_source(name, explicit_src_folder_path)
|
|
45
|
+
if validated_src_path != f"./{name}":
|
|
46
|
+
warning(
|
|
47
|
+
"The source folder's name is different from the package's name. this may not be currently supported correctly"
|
|
48
|
+
)
|
|
49
|
+
converted_min_python: Optional[Version] = None
|
|
50
|
+
if min_python is not None:
|
|
51
|
+
converted_min_python = (
|
|
52
|
+
min_python
|
|
53
|
+
if isinstance(min_python, Version)
|
|
54
|
+
else Version.from_str(min_python)
|
|
55
|
+
)
|
|
56
|
+
validated_min_python = validate_python_version(converted_min_python)
|
|
57
|
+
validated_keywords = validate_keywords(keywords)
|
|
58
|
+
validated_deps = validate_dependencies(dependencies)
|
|
59
|
+
return (
|
|
60
|
+
validated_version,
|
|
61
|
+
validated_src_path,
|
|
62
|
+
validated_min_python,
|
|
63
|
+
validated_keywords,
|
|
64
|
+
validated_deps,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_constraint_enforcers(
|
|
69
|
+
enforcers: Optional[List[ConstraintEnforcer]],
|
|
70
|
+
name: str,
|
|
71
|
+
version: Version,
|
|
72
|
+
demo: bool,
|
|
73
|
+
) -> None:
|
|
74
|
+
for enforcer in enforcers or []:
|
|
75
|
+
enforcer.enforce(name=name, version=version, demo=demo)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_quality_assurance(
|
|
79
|
+
python_interpreter_provider: PythonProvider,
|
|
80
|
+
global_quality_assurance_runners: Optional[List[QualityAssuranceRunner]],
|
|
81
|
+
name: str,
|
|
82
|
+
explicit_src_folder_path: str,
|
|
83
|
+
validated_dependencies: List[Dependency],
|
|
84
|
+
pbar: Optional[SupportsProgress],
|
|
85
|
+
) -> None:
|
|
86
|
+
try:
|
|
87
|
+
result = asyncio.get_event_loop().run_until_complete(
|
|
88
|
+
qa(
|
|
89
|
+
python_interpreter_provider,
|
|
90
|
+
global_quality_assurance_runners or [],
|
|
91
|
+
name,
|
|
92
|
+
explicit_src_folder_path,
|
|
93
|
+
validated_dependencies,
|
|
94
|
+
pbar,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
if not result:
|
|
98
|
+
error(
|
|
99
|
+
f"quickpub.publish exited early as '{name}' "
|
|
100
|
+
"did not pass quality assurance step, see above "
|
|
101
|
+
"logs to pass this step."
|
|
102
|
+
)
|
|
103
|
+
raise ExitEarlyError("QA step Failed")
|
|
104
|
+
except ExitEarlyError as e:
|
|
105
|
+
raise e
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise RuntimeError("Quality assurance stage has failed", e) from e
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _create_package_files(
|
|
111
|
+
name: str,
|
|
112
|
+
explicit_src_folder_path: str,
|
|
113
|
+
readme_file_path: str,
|
|
114
|
+
license_file_path: str,
|
|
115
|
+
version: Version,
|
|
116
|
+
author: str,
|
|
117
|
+
author_email: str,
|
|
118
|
+
description: str,
|
|
119
|
+
homepage: str,
|
|
120
|
+
keywords: List[str],
|
|
121
|
+
validated_dependencies: List[Dependency],
|
|
122
|
+
min_python: Version,
|
|
123
|
+
scripts: Optional[Dict[str, Callable]],
|
|
124
|
+
) -> None:
|
|
125
|
+
create_setup()
|
|
126
|
+
create_toml(
|
|
127
|
+
name=name,
|
|
128
|
+
src_folder_path=explicit_src_folder_path,
|
|
129
|
+
readme_file_path=readme_file_path,
|
|
130
|
+
license_file_path=license_file_path,
|
|
131
|
+
version=version,
|
|
132
|
+
author=author,
|
|
133
|
+
author_email=author_email,
|
|
134
|
+
description=description,
|
|
135
|
+
homepage=homepage,
|
|
136
|
+
keywords=keywords,
|
|
137
|
+
dependencies=validated_dependencies,
|
|
138
|
+
classifiers=[
|
|
139
|
+
DevelopmentStatusClassifier.Alpha,
|
|
140
|
+
IntendedAudienceClassifier.Developers,
|
|
141
|
+
ProgrammingLanguageClassifier.Python3,
|
|
142
|
+
OperatingSystemClassifier.MicrosoftWindows,
|
|
143
|
+
],
|
|
144
|
+
min_python=min_python,
|
|
145
|
+
scripts=scripts,
|
|
146
|
+
)
|
|
147
|
+
create_manifest(name=name)
|
|
148
|
+
add_version_to_init(
|
|
149
|
+
name=name, src_folder_path=explicit_src_folder_path, version=version
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _build_and_upload_packages(
|
|
154
|
+
build_schemas: List[BuildSchema],
|
|
155
|
+
upload_targets: List[UploadTarget],
|
|
156
|
+
name: str,
|
|
157
|
+
version: Version,
|
|
158
|
+
demo: bool,
|
|
159
|
+
) -> None:
|
|
160
|
+
if not demo:
|
|
161
|
+
for schema in build_schemas:
|
|
162
|
+
schema.build()
|
|
163
|
+
for target in upload_targets:
|
|
164
|
+
target.upload(name=name, version=version)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def publish(
|
|
168
|
+
*,
|
|
169
|
+
name: str,
|
|
170
|
+
author: str,
|
|
171
|
+
author_email: str,
|
|
172
|
+
description: str,
|
|
173
|
+
homepage: str,
|
|
174
|
+
build_schemas: List[BuildSchema],
|
|
175
|
+
upload_targets: List[UploadTarget],
|
|
176
|
+
enforcers: Optional[List[ConstraintEnforcer]] = None,
|
|
177
|
+
global_quality_assurance_runners: Optional[List[QualityAssuranceRunner]] = None,
|
|
178
|
+
python_interpreter_provider: PythonProvider = DefaultPythonProvider(),
|
|
179
|
+
readme_file_path: str = "./README.md",
|
|
180
|
+
license_file_path: str = "./LICENSE",
|
|
181
|
+
version: Optional[Union[Version, str]] = None,
|
|
182
|
+
min_python: Optional[Union[Version, str]] = None,
|
|
183
|
+
dependencies: Optional[List[Union[str, Dependency]]] = None,
|
|
184
|
+
keywords: Optional[List[str]] = None,
|
|
185
|
+
explicit_src_folder_path: Optional[str] = None,
|
|
186
|
+
scripts: Optional[Dict[str, Callable]] = None,
|
|
187
|
+
pbar: Optional[SupportsProgress] = None,
|
|
188
|
+
demo: bool = False,
|
|
189
|
+
config: Optional[Any] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
start_time = time.perf_counter()
|
|
192
|
+
success = False
|
|
193
|
+
try:
|
|
194
|
+
(
|
|
195
|
+
validated_version,
|
|
196
|
+
validated_src_path,
|
|
197
|
+
validated_min_python,
|
|
198
|
+
validated_keywords,
|
|
199
|
+
validated_deps,
|
|
200
|
+
) = _validate_publish_inputs(
|
|
201
|
+
name, version, explicit_src_folder_path, min_python, keywords, dependencies
|
|
202
|
+
)
|
|
203
|
+
_run_constraint_enforcers(enforcers, name, validated_version, demo)
|
|
204
|
+
_run_quality_assurance(
|
|
205
|
+
python_interpreter_provider,
|
|
206
|
+
global_quality_assurance_runners,
|
|
207
|
+
name,
|
|
208
|
+
validated_src_path,
|
|
209
|
+
validated_deps,
|
|
210
|
+
pbar,
|
|
211
|
+
)
|
|
212
|
+
_create_package_files(
|
|
213
|
+
name,
|
|
214
|
+
validated_src_path,
|
|
215
|
+
readme_file_path,
|
|
216
|
+
license_file_path,
|
|
217
|
+
validated_version,
|
|
218
|
+
author,
|
|
219
|
+
author_email,
|
|
220
|
+
description,
|
|
221
|
+
homepage,
|
|
222
|
+
validated_keywords,
|
|
223
|
+
validated_deps,
|
|
224
|
+
validated_min_python,
|
|
225
|
+
scripts,
|
|
226
|
+
)
|
|
227
|
+
_build_and_upload_packages(
|
|
228
|
+
build_schemas, upload_targets, name, validated_version, demo
|
|
229
|
+
)
|
|
230
|
+
success = True
|
|
231
|
+
finally:
|
|
232
|
+
elapsed = time.perf_counter() - start_time
|
|
233
|
+
logger.info("Publish finished in %.3fs (success=%s)", elapsed, success)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main() -> None:
|
|
237
|
+
fire.Fire(publish)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
main()
|
|
242
|
+
|
|
243
|
+
__all__ = ["main", "publish"]
|
|
@@ -1,96 +1,79 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
logger = logging.getLogger(__name__)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Classifier(Enum):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
""
|
|
76
|
-
|
|
77
|
-
""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class OperatingSystemClassifier(Classifier):
|
|
83
|
-
"""
|
|
84
|
-
An enum class for specifying the target operating system
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
MicrosoftWindows = "Microsoft :: Windows"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
__all__ = [
|
|
91
|
-
"Classifier",
|
|
92
|
-
"DevelopmentStatusClassifier",
|
|
93
|
-
"IntendedAudienceClassifier",
|
|
94
|
-
"ProgrammingLanguageClassifier",
|
|
95
|
-
"OperatingSystemClassifier",
|
|
96
|
-
]
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Classifier(Enum):
|
|
8
|
+
|
|
9
|
+
def _str(self) -> str:
|
|
10
|
+
return str(self.value)
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def _split_name(name: str) -> str:
|
|
14
|
+
words = []
|
|
15
|
+
current_word = ""
|
|
16
|
+
|
|
17
|
+
for char in name:
|
|
18
|
+
if char.isupper():
|
|
19
|
+
if current_word:
|
|
20
|
+
words.append(current_word)
|
|
21
|
+
current_word = char
|
|
22
|
+
else:
|
|
23
|
+
current_word += char
|
|
24
|
+
|
|
25
|
+
if current_word:
|
|
26
|
+
words.append(current_word)
|
|
27
|
+
|
|
28
|
+
return " ".join(words[:-1])
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
name = Classifier._split_name(self.__class__.__qualname__)
|
|
32
|
+
value = self._str()
|
|
33
|
+
result = f"{name} :: {value}"
|
|
34
|
+
logger.debug("Classifier string representation: %s", result)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DevelopmentStatusClassifier(Classifier):
|
|
39
|
+
"""Classifier for package development status. Use values like Alpha, Beta, Production, etc."""
|
|
40
|
+
|
|
41
|
+
Planning = 1
|
|
42
|
+
PreAlpha = 2
|
|
43
|
+
Alpha = 3
|
|
44
|
+
Beta = 4
|
|
45
|
+
Production = 5
|
|
46
|
+
Stable = 5
|
|
47
|
+
Mature = 6
|
|
48
|
+
Inactive = 7
|
|
49
|
+
|
|
50
|
+
def _str(self) -> str:
|
|
51
|
+
return f"{self.value} - {self.name}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IntendedAudienceClassifier(Classifier):
|
|
55
|
+
"""Classifier for intended audience of the package."""
|
|
56
|
+
|
|
57
|
+
CustomerService = "CustomerService"
|
|
58
|
+
Developers = "Developers"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProgrammingLanguageClassifier(Classifier):
|
|
62
|
+
"""Classifier for programming languages supported by the package."""
|
|
63
|
+
|
|
64
|
+
Python3 = "Python :: 3"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class OperatingSystemClassifier(Classifier):
|
|
68
|
+
"""Classifier for operating systems supported by the package."""
|
|
69
|
+
|
|
70
|
+
MicrosoftWindows = "Microsoft :: Windows"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"Classifier",
|
|
75
|
+
"DevelopmentStatusClassifier",
|
|
76
|
+
"IntendedAudienceClassifier",
|
|
77
|
+
"ProgrammingLanguageClassifier",
|
|
78
|
+
"OperatingSystemClassifier",
|
|
79
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Union, Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExitEarlyError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def exit_if(
|
|
13
|
+
predicate: Union[bool, Callable[[], bool]],
|
|
14
|
+
msg: str,
|
|
15
|
+
) -> None:
|
|
16
|
+
if (isinstance(predicate, bool) and predicate) or (
|
|
17
|
+
callable(predicate) and predicate()
|
|
18
|
+
):
|
|
19
|
+
logger.error("Exit condition met: %s", msg)
|
|
20
|
+
raise ExitEarlyError(msg)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"exit_if",
|
|
25
|
+
"ExitEarlyError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Dict, Callable
|
|
5
|
+
from danielutils import get_files, file_exists
|
|
6
|
+
|
|
7
|
+
from .classifiers import Classifier
|
|
8
|
+
from .structures import Version, Dependency
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _format_classifiers_string(classifiers: List[Classifier]) -> str:
|
|
14
|
+
classifiers_string = ",\n\t".join([f'"{str(c)}"' for c in classifiers])
|
|
15
|
+
if len(classifiers_string) > 0:
|
|
16
|
+
classifiers_string = f"\n\t{classifiers_string}\n"
|
|
17
|
+
return classifiers_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_py_typed_section(name: str, src_folder_path: str) -> str:
|
|
21
|
+
for file in get_files(src_folder_path):
|
|
22
|
+
if file == "py.typed":
|
|
23
|
+
logger.debug("Found py.typed file, adding package-data configuration")
|
|
24
|
+
return f"""[tool.setuptools.package-data]
|
|
25
|
+
"{name}" = ["py.typed"]"""
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_scripts_section(name: str, scripts: Dict[str, Callable]) -> str:
|
|
30
|
+
logger.debug("Adding [project.scripts] section with %d entries", len(scripts))
|
|
31
|
+
scripts_entries = []
|
|
32
|
+
for script_name, func in scripts.items():
|
|
33
|
+
module = func.__module__
|
|
34
|
+
func_name = func.__name__
|
|
35
|
+
if module == "__main__":
|
|
36
|
+
module = f"{name}.__main__"
|
|
37
|
+
entry_point = f"{module}:{func_name}"
|
|
38
|
+
scripts_entries.append(f' {script_name} = "{entry_point}"')
|
|
39
|
+
return "\n[project.scripts]\n" + "\n".join(scripts_entries) + "\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_toml_content(
|
|
43
|
+
name: str,
|
|
44
|
+
version: Version,
|
|
45
|
+
author: str,
|
|
46
|
+
author_email: str,
|
|
47
|
+
description: str,
|
|
48
|
+
readme_file_path: str,
|
|
49
|
+
license_file_path: str,
|
|
50
|
+
keywords: List[str],
|
|
51
|
+
min_python: Version,
|
|
52
|
+
dependencies: List[Dependency],
|
|
53
|
+
classifiers_string: str,
|
|
54
|
+
scripts_section: str,
|
|
55
|
+
py_typed: str,
|
|
56
|
+
homepage: str,
|
|
57
|
+
) -> str:
|
|
58
|
+
normalized_readme_path = readme_file_path.replace("\\", "/")
|
|
59
|
+
normalized_license_path = license_file_path.replace("\\", "/")
|
|
60
|
+
return f"""[build-system]
|
|
61
|
+
requires = ["setuptools>=61.0"]
|
|
62
|
+
build-backend = "setuptools.build_meta"
|
|
63
|
+
|
|
64
|
+
[project]
|
|
65
|
+
name = "{name}"
|
|
66
|
+
version = "{version}"
|
|
67
|
+
authors = [
|
|
68
|
+
{{ name = "{author}", email = "{author_email}" }},
|
|
69
|
+
]
|
|
70
|
+
dependencies = {[str(dep) for dep in dependencies]}
|
|
71
|
+
keywords = {keywords}
|
|
72
|
+
license = {{ "file" = "{normalized_license_path}" }}
|
|
73
|
+
description = "{description}"
|
|
74
|
+
readme = {{file = "{normalized_readme_path}", content-type = "text/markdown"}}
|
|
75
|
+
requires-python = ">={min_python}"
|
|
76
|
+
classifiers = [{classifiers_string}]{scripts_section}
|
|
77
|
+
[tool.setuptools]
|
|
78
|
+
packages = ["{name}"]
|
|
79
|
+
|
|
80
|
+
{py_typed}
|
|
81
|
+
|
|
82
|
+
[project.urls]
|
|
83
|
+
"Homepage" = "{homepage}"
|
|
84
|
+
"Bug Tracker" = "{homepage}/issues"
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_toml(
|
|
89
|
+
*,
|
|
90
|
+
name: str,
|
|
91
|
+
src_folder_path: str,
|
|
92
|
+
readme_file_path: str,
|
|
93
|
+
license_file_path: str,
|
|
94
|
+
version: Version,
|
|
95
|
+
author: str,
|
|
96
|
+
author_email: str,
|
|
97
|
+
description: str,
|
|
98
|
+
homepage: str,
|
|
99
|
+
keywords: List[str],
|
|
100
|
+
min_python: Version,
|
|
101
|
+
dependencies: List[Dependency],
|
|
102
|
+
classifiers: List[Classifier],
|
|
103
|
+
scripts: Optional[Dict[str, Callable]] = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
logger.info("Creating pyproject.toml for package '%s' version '%s'", name, version)
|
|
106
|
+
classifiers_string = _format_classifiers_string(classifiers)
|
|
107
|
+
py_typed = _build_py_typed_section(name, src_folder_path)
|
|
108
|
+
scripts_section = ""
|
|
109
|
+
if scripts:
|
|
110
|
+
scripts_section = _build_scripts_section(name, scripts)
|
|
111
|
+
toml_content = _build_toml_content(
|
|
112
|
+
name,
|
|
113
|
+
version,
|
|
114
|
+
author,
|
|
115
|
+
author_email,
|
|
116
|
+
description,
|
|
117
|
+
readme_file_path,
|
|
118
|
+
license_file_path,
|
|
119
|
+
keywords,
|
|
120
|
+
min_python,
|
|
121
|
+
dependencies,
|
|
122
|
+
classifiers_string,
|
|
123
|
+
scripts_section,
|
|
124
|
+
py_typed,
|
|
125
|
+
homepage,
|
|
126
|
+
)
|
|
127
|
+
with open("pyproject.toml", "w", encoding="utf8") as f:
|
|
128
|
+
f.write(toml_content)
|
|
129
|
+
logger.info("Successfully created pyproject.toml")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def create_setup() -> None:
|
|
133
|
+
logger.info("Creating setup.py file")
|
|
134
|
+
with open("./setup.py", "w", encoding="utf8") as f:
|
|
135
|
+
f.write("from setuptools import setup\n\nsetup()\n")
|
|
136
|
+
logger.info("Successfully created setup.py")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_manifest(*, name: str) -> None:
|
|
140
|
+
logger.info("Creating MANIFEST.in for package '%s'", name)
|
|
141
|
+
with open("./MANIFEST.in", "w", encoding="utf8") as f:
|
|
142
|
+
f.write(f"recursive-include {name} *.py")
|
|
143
|
+
logger.info("Successfully created MANIFEST.in")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def add_version_to_init(name: str, src_folder_path: str, version: Version) -> None:
|
|
147
|
+
init_file_path = Path(src_folder_path) / "__init__.py"
|
|
148
|
+
|
|
149
|
+
if not file_exists(str(init_file_path)):
|
|
150
|
+
logger.warning("__init__.py not found at '%s', creating it", init_file_path)
|
|
151
|
+
init_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
with open(init_file_path, "w", encoding="utf8") as f:
|
|
153
|
+
f.write(f'__version__ = "{version}"\n')
|
|
154
|
+
logger.info("Created __init__.py with __version__ = '%s'", version)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
with open(init_file_path, "r", encoding="utf8") as f:
|
|
158
|
+
content = f.read()
|
|
159
|
+
|
|
160
|
+
version_line = f'__version__ = "{version}"'
|
|
161
|
+
version_pattern = r'__version__\s*=\s*["\'].*?["\']'
|
|
162
|
+
|
|
163
|
+
if re.search(version_pattern, content):
|
|
164
|
+
new_content = re.sub(version_pattern, version_line, content)
|
|
165
|
+
logger.info("Updated existing __version__ in __init__.py to '%s'", version)
|
|
166
|
+
else:
|
|
167
|
+
lines = content.splitlines()
|
|
168
|
+
if not lines:
|
|
169
|
+
new_content = f"{version_line}\n"
|
|
170
|
+
else:
|
|
171
|
+
import_end_index = 0
|
|
172
|
+
for i, line in enumerate(lines):
|
|
173
|
+
stripped = line.strip()
|
|
174
|
+
if not stripped or stripped.startswith("#"):
|
|
175
|
+
continue
|
|
176
|
+
if stripped.startswith("from") or stripped.startswith("import"):
|
|
177
|
+
import_end_index = i + 1
|
|
178
|
+
else:
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
if import_end_index < len(lines):
|
|
182
|
+
lines.insert(import_end_index, version_line)
|
|
183
|
+
else:
|
|
184
|
+
lines.append(version_line)
|
|
185
|
+
|
|
186
|
+
new_content = "\n".join(lines)
|
|
187
|
+
if not content.endswith("\n"):
|
|
188
|
+
new_content += "\n"
|
|
189
|
+
|
|
190
|
+
logger.info("Added __version__ = '%s' to __init__.py", version)
|
|
191
|
+
|
|
192
|
+
with open(init_file_path, "w", encoding="utf8") as f:
|
|
193
|
+
f.write(new_content)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = ["create_setup", "create_toml", "add_version_to_init"]
|