quickpub 3.0.61__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.
Files changed (68) hide show
  1. {quickpub-3.0.61/quickpub.egg-info → quickpub-4.0.0}/PKG-INFO +2 -2
  2. {quickpub-3.0.61 → quickpub-4.0.0}/pyproject.toml +5 -2
  3. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/__init__.py +3 -1
  4. quickpub-4.0.0/quickpub/__main__.py +243 -0
  5. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/classifiers.py +79 -92
  6. quickpub-4.0.0/quickpub/enforcers.py +26 -0
  7. quickpub-4.0.0/quickpub/files.py +196 -0
  8. quickpub-4.0.0/quickpub/logging_.py +74 -0
  9. quickpub-4.0.0/quickpub/proxy.py +35 -0
  10. quickpub-4.0.0/quickpub/qa.py +413 -0
  11. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/build_schema.py +7 -14
  12. quickpub-4.0.0/quickpub/strategies/constraint_enforcer.py +17 -0
  13. quickpub-4.0.0/quickpub/strategies/implementations/build_schemas/__init__.py +1 -0
  14. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +14 -11
  15. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +5 -2
  16. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +24 -22
  17. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +29 -16
  18. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +9 -4
  19. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +3 -3
  20. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/conda_python_provider.py +10 -8
  21. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/default_python_provider.py +14 -16
  22. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +36 -16
  23. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +31 -12
  24. quickpub-4.0.0/quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +161 -0
  25. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +57 -35
  26. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/github_upload_target.py +21 -11
  27. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +37 -26
  28. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/python_provider.py +13 -27
  29. quickpub-4.0.0/quickpub/strategies/quality_assurance_runner.py +194 -0
  30. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/quickpub_strategy.py +1 -2
  31. quickpub-4.0.0/quickpub/strategies/upload_target.py +19 -0
  32. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/structures/bound.py +15 -23
  33. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/structures/dependency.py +29 -24
  34. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/structures/version.py +11 -14
  35. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/validators.py +10 -36
  36. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/worker_pool.py +3 -2
  37. {quickpub-3.0.61 → quickpub-4.0.0/quickpub.egg-info}/PKG-INFO +2 -2
  38. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub.egg-info/SOURCES.txt +3 -2
  39. quickpub-4.0.0/quickpub.egg-info/entry_points.txt +2 -0
  40. quickpub-4.0.0/tests/test_helpers.py +26 -0
  41. quickpub-3.0.61/quickpub/__main__.py +0 -133
  42. quickpub-3.0.61/quickpub/enforcers.py +0 -39
  43. quickpub-3.0.61/quickpub/files.py +0 -110
  44. quickpub-3.0.61/quickpub/functions.py +0 -125
  45. quickpub-3.0.61/quickpub/logging_.py +0 -111
  46. quickpub-3.0.61/quickpub/proxy.py +0 -56
  47. quickpub-3.0.61/quickpub/qa.py +0 -346
  48. quickpub-3.0.61/quickpub/strategies/constraint_enforcer.py +0 -24
  49. quickpub-3.0.61/quickpub/strategies/implementations/build_schemas/__init__.py +0 -1
  50. quickpub-3.0.61/quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +0 -117
  51. quickpub-3.0.61/quickpub/strategies/quality_assurance_runner.py +0 -239
  52. quickpub-3.0.61/quickpub/strategies/upload_target.py +0 -23
  53. {quickpub-3.0.61 → quickpub-4.0.0}/LICENSE +0 -0
  54. {quickpub-3.0.61 → quickpub-4.0.0}/MANIFEST.in +0 -0
  55. {quickpub-3.0.61 → quickpub-4.0.0}/README.md +0 -0
  56. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/py.typed +0 -0
  57. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/__init__.py +0 -0
  58. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/__init__.py +0 -0
  59. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/constraint_enforcers/__init__.py +0 -0
  60. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/python_providers/__init__.py +0 -0
  61. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/quality_assurance_runners/__init__.py +0 -0
  62. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/strategies/implementations/upload_targets/__init__.py +0 -0
  63. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub/structures/__init__.py +0 -0
  64. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub.egg-info/dependency_links.txt +0 -0
  65. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub.egg-info/requires.txt +0 -0
  66. {quickpub-3.0.61 → quickpub-4.0.0}/quickpub.egg-info/top_level.txt +0 -0
  67. {quickpub-3.0.61 → quickpub-4.0.0}/setup.cfg +0 -0
  68. {quickpub-3.0.61 → quickpub-4.0.0}/setup.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickpub
3
- Version: 3.0.61
4
- Summary: A python package to quickly configure and publish a new package
3
+ Version: 4.0.0
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
7
7
 
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quickpub"
7
- version = "3.0.61"
7
+ version = "4.0.0"
8
8
  authors = [
9
9
  { name = "danielnachumdev", email = "danielnachumdev@gmail.com" },
10
10
  ]
11
11
  dependencies = ['danielutils>=1.0.0', 'requests', 'fire']
12
12
  keywords = []
13
13
  license = { "file" = "./LICENSE" }
14
- description = "A python package to quickly configure and publish a new package"
14
+ description = "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"
15
15
  readme = {file = "./README.md", content-type = "text/markdown"}
16
16
  requires-python = ">=3.8.0"
17
17
  classifiers = [
@@ -20,9 +20,12 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3",
21
21
  "Operating System :: Microsoft :: Windows"
22
22
  ]
23
+ [project.scripts]
24
+ quickpub = "quickpub.__main__:main"
23
25
 
24
26
  [tool.setuptools]
25
27
  packages = ["quickpub"]
28
+
26
29
  [tool.setuptools.package-data]
27
30
  "quickpub" = ["py.typed"]
28
31
 
@@ -3,4 +3,6 @@ from .strategies import *
3
3
  from .enforcers import ExitEarlyError
4
4
  from .qa import SupportsProgress
5
5
  from .logging_ import set_log_level
6
- from .__main__ import publish
6
+ from .__main__ import publish, main
7
+
8
+ __version__ = "4.0.0"
@@ -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,92 +1,79 @@
1
- import logging
2
- from enum import Enum
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
-
7
- class Classifier(Enum):
8
- """
9
- A base enum class for all classifiers.
10
- """
11
-
12
- def _str(self) -> str:
13
- return str(self.value)
14
-
15
- @staticmethod
16
- def _split_name(name: str) -> str:
17
- words = []
18
- current_word = ''
19
-
20
- for char in name:
21
- # Check if the character is uppercase
22
- if char.isupper():
23
- # If current_word is not empty, add it to words list
24
- if current_word:
25
- words.append(current_word)
26
- # Start a new word with the uppercase character
27
- current_word = char
28
- else:
29
- # Add lowercase character to current_word
30
- current_word += char
31
-
32
- # Add the last word to the list
33
- if current_word:
34
- words.append(current_word)
35
-
36
- return " ".join(words[:-1])
37
-
38
- def __str__(self) -> str:
39
- name = Classifier._split_name(self.__class__.__qualname__)
40
- value = self._str()
41
- result = f"{name} :: {value}"
42
- logger.debug("Classifier string representation: %s", result)
43
- return result
44
-
45
-
46
- class DevelopmentStatusClassifier(Classifier):
47
- """
48
- An enum class for specifying the development state of the package
49
- """
50
- # https://pypi.org/classifiers/
51
- Planning = 1
52
- PreAlpha = 2
53
- Alpha = 3
54
- Beta = 4
55
- Production = 5
56
- Stable = 5
57
- Mature = 6
58
- Inactive = 7
59
-
60
- def _str(self) -> str:
61
- return f"{self.value} - {self.name}"
62
-
63
-
64
- class IntendedAudienceClassifier(Classifier):
65
- """
66
- An enum class for specifying the intended audience
67
- """
68
- CustomerService = "CustomerService"
69
- Developers = "Developers"
70
-
71
-
72
- class ProgrammingLanguageClassifier(Classifier):
73
- """
74
- An enum class for specifying the target language level
75
- """
76
- Python3 = "Python :: 3"
77
-
78
-
79
- class OperatingSystemClassifier(Classifier):
80
- """
81
- An enum class for specifying the target operating system
82
- """
83
- MicrosoftWindows = "Microsoft :: Windows"
84
-
85
-
86
- __all__ = [
87
- "Classifier",
88
- "DevelopmentStatusClassifier",
89
- "IntendedAudienceClassifier",
90
- "ProgrammingLanguageClassifier",
91
- "OperatingSystemClassifier"
92
- ]
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"]