Coverage for pyTooling/Packaging/__init__.py: 81%

330 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ __ _ ___| | ____ _ __ _(_)_ __ __ _ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) / _` |/ __| |/ / _` |/ _` | | '_ \ / _` | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/ (_| | (__| < (_| | (_| | | | | | (_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| \__,_|\___|_|\_\__,_|\__, |_|_| |_|\__, | # 

7# |_| |___/ |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2021-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32A set of helper functions to describe a Python package for setuptools. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <PACKAGING>` for explanations and usage examples. 

37 

38.. seealso:: 

39 

40 :mod:`pyTooling.Versioning` 

41 |rarr| The version numbers read from a package's dunder variables. 

42 :mod:`pyTooling.Licensing` 

43 |rarr| The license names translated for setuptools. 

44 :mod:`pyTooling.Testing` 

45 |rarr| Testing the console scripts a package installs. 

46""" 

47from ast import parse as ast_parse, get_docstring as ast_get_docstring, iter_child_nodes, Assign, \ 

48 Constant, Name, List as ast_List 

49from collections.abc import Sized 

50from os import scandir as os_scandir 

51from pathlib import Path 

52from re import split as re_split 

53from sys import version_info 

54from typing import Iterable, Sequence, Any, Optional as Nullable, Union 

55from pyTooling.Decorators import export, readonly 

56from pyTooling.Documentation import splitDocString 

57from pyTooling.Exceptions import ToolingException, MissingDependencyError 

58from pyTooling.MetaClasses import ExtendedType 

59from pyTooling.Common import __version__, getFullyQualifiedName, firstElement 

60from pyTooling.Licensing import License, Apache_2_0_License 

61 

62 

63__all__ = [ 

64 "STATUS", "DEFAULT_LICENSE", "DEFAULT_PY_VERSIONS", "DEFAULT_CLASSIFIERS", "DEFAULT_README", "DEFAULT_REQUIREMENTS", 

65 "DEFAULT_DOCUMENTATION_REQUIREMENTS", "DEFAULT_TEST_REQUIREMENTS", "DEFAULT_PACKAGING_REQUIREMENTS", 

66 "DEFAULT_VERSION_FILE" 

67] 

68 

69 

70@export 

71class PackagingError(ToolingException): 

72 """Base-exception of all exceptions raised by :mod:`pyTooling.Packaging`.""" 

73 

74 

75@export 

76class Readme: 

77 """Encapsulates the READMEs file content and MIME type.""" 

78 

79 _content: str #: Content of the README file 

80 _mimeType: str #: MIME type of the README content 

81 

82 def __init__(self, content: str, mimeType: str) -> None: 

83 """ 

84 Initializes a README file wrapper. 

85 

86 :param content: Raw content of the README file. 

87 :param mimeType: MIME type of the README file. 

88 """ 

89 self._content = content 

90 self._mimeType = mimeType 

91 

92 @readonly 

93 def Content(self) -> str: 

94 """ 

95 Read-only property to access the README's content. 

96 

97 :returns: Raw content of the README file. 

98 """ 

99 return self._content 

100 

101 @readonly 

102 def MimeType(self) -> str: 

103 """ 

104 Read-only property to access the README's MIME type. 

105 

106 :returns: The MIME type of the README file. 

107 """ 

108 return self._mimeType 

109 

110 

111@export 

112def loadReadmeFile(readmeFile: Path) -> Readme: 

113 """ 

114 Read the README file (e.g. in Markdown format), so it can be used as long description for the package. 

115 

116 Supported formats: 

117 

118 * Plain text (``*.txt``) 

119 * Markdown (``*.md``) 

120 * ReStructured Text (``*.rst``) 

121 

122 :param readmeFile: Optional, path to the `README` file as an instance of :class:`Path`. 

123 :returns: A tuple containing the file content and the MIME type. 

124 :raises TypeError: If parameter 'readmeFile' is not of type :class:`~pathlib.Path`. 

125 :raises ValueError: If README file has an unsupported format. 

126 :raises FileNotFoundError: If README file does not exist. 

127 """ 

128 if not isinstance(readmeFile, Path): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

129 ex = TypeError("Parameter 'readmeFile' is not of type 'Path'.") 

130 ex.add_note(f"Got type '{getFullyQualifiedName(readmeFile)}'.") 

131 raise ex 

132 

133 if readmeFile.suffix == ".txt": 

134 mimeType = "text/plain" 

135 elif readmeFile.suffix == ".md": 

136 mimeType = "text/markdown" 

137 elif readmeFile.suffix == ".rst": 

138 mimeType = "text/x-rst" 

139 else: # pragma: no cover 

140 raise ValueError("Unsupported README format.") 

141 

142 try: 

143 with readmeFile.open("r", encoding="utf-8") as file: 

144 return Readme( 

145 content=file.read(), 

146 mimeType=mimeType 

147 ) 

148 except FileNotFoundError as ex: 

149 raise FileNotFoundError(f"README file '{readmeFile}' not found in '{Path.cwd()}'.") from ex 

150 

151 

152@export 

153def loadRequirementsFile(requirementsFile: Path, indent: int = 0, debug: bool = False) -> list[str]: 

154 """ 

155 Reads a `requirements.txt` file (recursively) and extracts all specified dependencies into an array. 

156 

157 Special dependency entries like Git repository references are translates to match the syntax expected by setuptools. 

158 

159 .. hint:: 

160 

161 Duplicates should be removed by converting the result to a :class:`set` and back to a :class:`list`. 

162 

163 .. code-block:: Python 

164 

165 requirements = list(set(loadRequirementsFile(requirementsFile))) 

166 

167 :param requirementsFile: Optional, path to the ``requirements.txt`` file as an instance of :class:`Path`. 

168 :param indent: Optional, indentation level used for the debug output of nested requirements files. 

169 :param debug: Optional, if ``True``, print found dependencies and recursion. 

170 :returns: A list of dependencies. 

171 :raises TypeError: If parameter 'requirementsFile' is not of type :class:`~pathlib.Path`. 

172 :raises FileNotFoundError: If requirements file does not exist. 

173 """ 

174 if not isinstance(requirementsFile, Path): 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true

175 ex = TypeError(f"Parameter '{requirementsFile}' is not of type 'Path'.") 

176 ex.add_note(f"Got type '{getFullyQualifiedName(requirementsFile)}'.") 

177 raise ex 

178 

179 def _loadRequirementsFile(requirementsFile: Path, indent: int) -> list[str]: 

180 """ 

181 Recursive variant of :func:`loadRequirementsFile`. 

182 

183 :param requirementsFile: Optional, path to the requirements file to read. 

184 :param indent: Optional, indentation level used for the debug output of nested requirements files. 

185 :returns: List of requirements read from that file and every file it includes. 

186 :raises FileNotFoundError: If the requirements file doesn't exist. 

187 """ 

188 requirements = [] 

189 try: 

190 with requirementsFile.open("r", encoding="utf-8") as file: 

191 if debug: 

192 print(f"[pyTooling.Packaging]{' ' * indent} Extracting requirements from '{requirementsFile}'.") 

193 

194 for line in file.readlines(): 

195 line = line.strip() 

196 if line.startswith("#") or line == "": 

197 continue 

198 elif line.startswith("-r"): 

199 # Remove the first word/argument (-r) 

200 filename = line[2:].lstrip() 

201 requirements += _loadRequirementsFile(requirementsFile.parent / filename, indent + 1) 

202 elif line.startswith("https"): 

203 if debug: 

204 print(f"[pyTooling.Packaging]{' ' * indent} Found URL '{line}'.") 

205 

206 # Convert 'URL#NAME' to 'NAME @ URL' 

207 splitItems = line.split("#") 

208 requirements.append(f"{splitItems[1]} @ {splitItems[0]}") 

209 else: 

210 if debug: 

211 print(f"[pyTooling.Packaging]{' ' * indent} - {line}") 

212 

213 requirements.append(line) 

214 except FileNotFoundError as ex: 

215 raise FileNotFoundError(f"Requirements file '{requirementsFile}' not found in '{Path.cwd()}'.") from ex 

216 

217 return requirements 

218 

219 return _loadRequirementsFile(requirementsFile, 0) 

220 

221 

222@export 

223class VersionInformation(metaclass=ExtendedType, slots=True): 

224 """Encapsulates version information extracted from a Python source file.""" 

225 

226 _author: str #: Author name(s). 

227 _copyright: str #: Copyright information. 

228 _email: str #: Author's email address. 

229 _keywords: list[str] #: Keywords. 

230 _license: str #: License name. 

231 _description: str #: Description of the package. 

232 _version: str #: Version number. 

233 

234 def __init__( 

235 self, 

236 author: str, 

237 email: str, 

238 copyright: str, 

239 license: str, 

240 version: str, 

241 description: str, 

242 keywords: Iterable[str] 

243 ) -> None: 

244 """ 

245 Initializes a Python package (version) information instance. 

246 

247 :param author: Author of the Python package. 

248 :param email: The author's email address 

249 :param copyright: The copyright notice of the Package. 

250 :param license: Optional, the Python package's license. 

251 :param version: The Python package's version. 

252 :param description: The Python package's short description. 

253 :param keywords: Optional, the Python package's list of keywords. 

254 """ 

255 self._author = author 

256 self._email = email 

257 self._copyright = copyright 

258 self._license = license 

259 self._version = version 

260 self._description = description 

261 self._keywords = [k for k in keywords] 

262 

263 @readonly 

264 def Author(self) -> str: 

265 """ 

266 Read-only property to access the name(s) of the package author(s) (:attr:`_author`). 

267 

268 :returns: Name(s) of the package author(s). 

269 """ 

270 return self._author 

271 

272 @readonly 

273 def Copyright(self) -> str: 

274 """ 

275 Read-only property to access the package's copyright information (:attr:`_copyright`). 

276 

277 :returns: Copyright information. 

278 """ 

279 return self._copyright 

280 

281 @readonly 

282 def Description(self) -> str: 

283 """ 

284 Read-only property to access the package description (:attr:`_description`). 

285 

286 :returns: Package description text. 

287 """ 

288 return self._description 

289 

290 @readonly 

291 def Email(self) -> str: 

292 """ 

293 Read-only property to access the author's email address (:attr:`_email`). 

294 

295 :returns: Email address of the author. 

296 """ 

297 return self._email 

298 

299 @readonly 

300 def Keywords(self) -> list[str]: 

301 """ 

302 Read-only property to access the package's keywords (:attr:`_keywords`). 

303 

304 :returns: List of keywords. 

305 """ 

306 return self._keywords 

307 

308 @readonly 

309 def License(self) -> str: 

310 """ 

311 Read-only property to access the package's license (:attr:`_license`). 

312 

313 :returns: License name. 

314 """ 

315 return self._license 

316 

317 @readonly 

318 def Version(self) -> str: 

319 """ 

320 Read-only property to access the package's version number (:attr:`_version`). 

321 

322 :returns: Version number. 

323 """ 

324 return self._version 

325 

326 def __str__(self) -> str: 

327 """ 

328 Return a string representation of this version information. 

329 

330 :returns: The version number. 

331 """ 

332 return f"{self._version}" 

333 

334 

335def _extractDescription(docString: Nullable[str]) -> str: 

336 """ 

337 Read a package's short description from the first paragraph of its module doc-string. 

338 

339 A package's short description **is** the summary of its module doc-string - the first paragraph - so the two are 

340 one text in two places, and :func:`~pyTooling.Documentation.splitDocString` reads it. 

341 

342 It is folded into a single line, because a short description is one line of plain text while a doc-string is 

343 wrapped to the source file's line length. Strong emphasis around the whole paragraph is removed for the same 

344 reason: ``**An abstract VHDL language model.**`` is markup for the rendered documentation, and nothing renders it 

345 where a short description is displayed. 

346 

347 :param docString: The module's doc-string, or ``None`` if it has none. 

348 :returns: The description as a single line, or an empty string if there is no doc-string. 

349 :raises DocumentationError: If the first paragraph is too long to be a description. 

350 

351 .. seealso:: 

352 

353 :func:`~pyTooling.Documentation.splitDocString` 

354 |rarr| Reads the first paragraph, and rejects one that is too long to be a summary. 

355 """ 

356 description, _ = splitDocString(docString) 

357 description = " ".join(description.split()) 

358 

359 if description.startswith("**") and description.endswith("**") and "**" not in description[2:-2]: 

360 description = description[2:-2] 

361 

362 return description 

363 

364 

365@export 

366def extractVersionInformation(sourceFile: Path) -> VersionInformation: 

367 """ 

368 Extract double underscored variables from a Python source file, so these can be used for single-sourcing information. 

369 

370 Supported variables: 

371 

372 * ``__author__`` 

373 * ``__copyright__`` 

374 * ``__email__`` 

375 * ``__keywords__`` 

376 * ``__license__`` 

377 * ``__version__`` 

378 

379 The package's short description is not a dunder variable - it is the first paragraph of the source file's module 

380 doc-string, which is where a package already describes itself. 

381 

382 :param sourceFile: Path to a Python source file as an instance of :class:`Path`. 

383 :returns: An instance of :class:`VersionInformation` with gathered variable contents. 

384 :raises TypeError: If parameter 'sourceFile' is not of type :class:`~pathlib.Path`. 

385 :raises PackagingError: If the given file doesn't exist. 

386 :raises PackagingError: If the given file can't be parsed as Python source code. 

387 :raises PackagingError: If a dunder variable has an unexpected format. 

388 :raises AssertionError: If a dunder variable is missing in the given file. 

389 """ 

390 if not isinstance(sourceFile, Path): 

391 ex = TypeError("Parameter 'sourceFile' is not of type 'Path'.") 

392 ex.add_note(f"Got type '{getFullyQualifiedName(sourceFile)}'.") 

393 raise ex 

394 

395 author = None 

396 copyright = None 

397 email = None 

398 keywords = [] 

399 license = None 

400 version = None 

401 

402 try: 

403 with sourceFile.open("r", encoding="utf-8") as file: 

404 content = file.read() 

405 except OSError as ex: 

406 raise PackagingError(f"Source file '{sourceFile}' couldn't be read.") from ex 

407 

408 try: 

409 ast = ast_parse(content) 

410 except Exception as ex: # pragma: no cover 

411 raise PackagingError(f"Source file '{sourceFile}' couldn't be parsed as Python source code.") from ex 

412 

413 description = _extractDescription(ast_get_docstring(ast)) 

414 

415 for item in iter_child_nodes(ast): 

416 if isinstance(item, Assign) and len(item.targets) == 1: 

417 target = item.targets[0] 

418 value = item.value 

419 if isinstance(target, Name) and target.id == "__author__": 

420 if isinstance(value, Constant) and isinstance(value.value, str): 420 ↛ 423line 420 didn't jump to line 423 because the condition on line 420 was always true

421 author = value.value 

422 

423 if isinstance(target, Name) and target.id == "__copyright__": 

424 if isinstance(value, Constant) and isinstance(value.value, str): 424 ↛ 427line 424 didn't jump to line 427 because the condition on line 424 was always true

425 copyright = value.value 

426 

427 if isinstance(target, Name) and target.id == "__email__": 

428 if isinstance(value, Constant) and isinstance(value.value, str): 428 ↛ 431line 428 didn't jump to line 431 because the condition on line 428 was always true

429 email = value.value 

430 

431 if isinstance(target, Name) and target.id == "__keywords__": 

432 if isinstance(value, Constant) and isinstance(value.value, str): 

433 cause = TypeError("Variable '__keywords__' should be a list of strings.") 

434 raise PackagingError(f"Couldn't extract '__keywords__' from '{sourceFile}'.") from cause 

435 elif isinstance(value, ast_List): 

436 for const in value.elts: 

437 if isinstance(const, Constant) and isinstance(const.value, str): 

438 keywords.append(const.value) 

439 else: 

440 cause = TypeError("List elements in '__keywords__' should be strings.") 

441 raise PackagingError(f"Couldn't extract '__keywords__' from '{sourceFile}'.") from cause 

442 else: 

443 cause = TypeError(f"Used unsupported type '{getFullyQualifiedName(value)}' for variable '__keywords__'.") 

444 raise PackagingError(f"Couldn't extract '__keywords__' from '{sourceFile}'.") from cause 

445 

446 if isinstance(target, Name) and target.id == "__license__": 

447 if isinstance(value, Constant) and isinstance(value.value, str): 447 ↛ 450line 447 didn't jump to line 450 because the condition on line 447 was always true

448 license = value.value 

449 

450 if isinstance(target, Name) and target.id == "__version__": 

451 if isinstance(value, Constant) and isinstance(value.value, str): 451 ↛ 415line 451 didn't jump to line 415 because the condition on line 451 was always true

452 version = value.value 

453 

454 if author is None: 

455 raise AssertionError(f"Could not extract '__author__' from '{sourceFile}'.") # pragma: no cover 

456 

457 if copyright is None: 

458 raise AssertionError(f"Could not extract '__copyright__' from '{sourceFile}'.") # pragma: no cover 

459 

460 if email is None: 

461 raise AssertionError(f"Could not extract '__email__' from '{sourceFile}'.") # pragma: no cover 

462 

463 if license is None: 

464 raise AssertionError(f"Could not extract '__license__' from '{sourceFile}'.") # pragma: no cover 

465 

466 if version is None: 

467 raise AssertionError(f"Could not extract '__version__' from '{sourceFile}'.") # pragma: no cover 

468 

469 return VersionInformation(author, email, copyright, license, version, description, keywords) 

470 

471 

472STATUS: dict[str, str] = { 

473 "planning": "1 - Planning", 

474 "pre-alpha": "2 - Pre-Alpha", 

475 "alpha": "3 - Alpha", 

476 "beta": "4 - Beta", 

477 "stable": "5 - Production/Stable", 

478 "mature": "6 - Mature", 

479 "inactive": "7 - Inactive" 

480} 

481""" 

482A dictionary of supported development status values. 

483 

484The mapping's value will be appended to ``Development Status :: `` to form a package classifier. 

485 

4861. Planning 

4872. Pre-Alpha 

4883. Alpha 

4894. Beta 

4905. Production/Stable 

4916. Mature 

4927. Inactive 

493 

494.. seealso:: 

495 

496 `Python package classifiers <https://pypi.org/classifiers/>`__ 

497""" 

498 

499DEFAULT_LICENSE = Apache_2_0_License 

500""" 

501Default license (Apache License, 2.0) used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

502if parameter ``license`` is not assigned. 

503""" 

504 

505DEFAULT_PY_VERSIONS = ("3.10", "3.11", "3.12", "3.13", "3.14") 

506""" 

507A tuple of supported CPython versions used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

508if parameter ``pythonVersions`` is not assigned. 

509 

510.. seealso:: 

511 

512 `Status of Python versions <https://devguide.python.org/versions/>`__ 

513""" 

514 

515DEFAULT_CLASSIFIERS = ( 

516 "Operating System :: OS Independent", 

517 "Intended Audience :: Developers", 

518 "Topic :: Utilities" 

519 ) 

520""" 

521A list of Python package classifiers used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

522if parameter ``classifiers`` is not assigned. 

523 

524.. seealso:: 

525 

526 `Python package classifiers <https://pypi.org/classifiers/>`__ 

527""" 

528 

529DEFAULT_README = Path("README.md") 

530""" 

531Path to the README file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

532if parameter ``readmeFile`` is not assigned. 

533""" 

534 

535DEFAULT_REQUIREMENTS = Path("requirements.txt") 

536""" 

537Path to the requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

538if parameter ``requirementsFile`` is not assigned. 

539""" 

540 

541DEFAULT_DOCUMENTATION_REQUIREMENTS = Path("doc/requirements.txt") 

542""" 

543Path to the README requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

544if parameter ``documentationRequirementsFile`` is not assigned. 

545""" 

546 

547DEFAULT_TEST_REQUIREMENTS = Path("tests/requirements.txt") 

548""" 

549Path to the README requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

550if parameter ``unittestRequirementsFile`` is not assigned. 

551""" 

552 

553DEFAULT_PACKAGING_REQUIREMENTS = Path("build/requirements.txt") 

554""" 

555Path to the package requirements file used by :func:`DescribePythonPackage` and :func:`DescribePythonPackageHostedOnGitHub` 

556if parameter ``packagingRequirementsFile`` is not assigned. 

557""" 

558 

559DEFAULT_VERSION_FILE = Path("__init__.py") 

560 

561 

562def _collectEntryPoints( 

563 consoleScripts: Nullable[dict[str, str]], 

564 guiScripts: Nullable[dict[str, str]], 

565 pytestPlugins: Nullable[dict[str, str]] 

566) -> Nullable[dict[str, list[str]]]: 

567 """ 

568 Collect what a package advertises into setuptools' entry point groups. 

569 

570 Each parameter names one kind of thing a package can offer, so a caller says *what* it provides and this 

571 function knows *how* it is declared - which group it belongs to, and setuptools' ``"name = entry.point"`` 

572 spelling. 

573 

574 :param consoleScripts: Optional, command line programs, declared as ``console_scripts``. 

575 :param guiScripts: Optional, windowed programs, declared as ``gui_scripts``. 

576 :param pytestPlugins: Optional, pytest plugins, declared as ``pytest11``. 

577 :returns: A dictionary mapping a group to its entry point lines, or ``None`` if a package 

578 advertises nothing. 

579 """ 

580 groups = { 

581 "console_scripts": consoleScripts, 

582 "gui_scripts": guiScripts, 

583 "pytest11": pytestPlugins, 

584 } 

585 

586 entryPoints = { 

587 groupName: [f"{name} = {entryPoint}" for name, entryPoint in group.items()] 

588 for groupName, group in groups.items() 

589 if group is not None and len(group) > 0 

590 } 

591 

592 return entryPoints if len(entryPoints) > 0 else None 

593 

594 

595@export 

596def DescribePythonPackage( 

597 packageName: str, 

598 projectURL: str, 

599 sourceCodeURL: str, 

600 documentationURL: str, 

601 issueTrackerCodeURL: str, 

602 description: Nullable[str] = None, 

603 keywords: Nullable[Iterable[str]] = None, 

604 license: License = DEFAULT_LICENSE, 

605 readmeFile: Path = DEFAULT_README, 

606 requirementsFile: Path = DEFAULT_REQUIREMENTS, 

607 documentationRequirementsFile: Path = DEFAULT_DOCUMENTATION_REQUIREMENTS, 

608 unittestRequirementsFile: Path = DEFAULT_TEST_REQUIREMENTS, 

609 packagingRequirementsFile: Path = DEFAULT_PACKAGING_REQUIREMENTS, 

610 additionalRequirements: Nullable[dict[str, list[str]]] = None, 

611 sourceFileWithVersion: Nullable[Path] = DEFAULT_VERSION_FILE, 

612 classifiers: Iterable[str] = DEFAULT_CLASSIFIERS, 

613 developmentStatus: str = "stable", 

614 pythonVersions: Sequence[str] = DEFAULT_PY_VERSIONS, 

615 consoleScripts: Nullable[dict[str, str]] = None, 

616 guiScripts: Nullable[dict[str, str]] = None, 

617 pytestPlugins: Nullable[dict[str, str]] = None, 

618 dataFiles: Nullable[dict[str, list[str]]] = None, 

619 debug: bool = False 

620) -> dict[str, Any]: 

621 """ 

622 Helper function to describe a Python package. 

623 

624 .. hint:: 

625 

626 Some information will be gathered automatically from well-known files. 

627 

628 Examples: ``README.md``, ``requirements.txt``, ``__init__.py`` 

629 

630 .. topic:: Handling of namespace packages 

631 

632 If parameter ``packageName`` contains a dot, a namespace package is assumed. Then 

633 :func:`setuptools.find_namespace_packages` is used to discover package files. |br| 

634 Otherwise, the package is considered a normal package and :func:`setuptools.find_packages` is used. 

635 

636 In both cases, the following packages (directories) are excluded from search: 

637 

638 * ``build``, ``build.*`` 

639 * ``dist``, ``dist.*`` 

640 * ``doc``, ``doc.*`` 

641 * ``tests``, ``tests.*`` 

642 

643 .. topic:: Handling of minimal Python version 

644 

645 The minimal required Python version is selected from parameter ``pythonVersions``. 

646 

647 .. topic:: Handling of dunder variables 

648 

649 A Python source file specified by parameter ``sourceFileWithVersion`` will be analyzed with Pythons parser and the 

650 resulting AST will be searched for the following dunder variables: 

651 

652 * ``__author__``: :class:`str` 

653 * ``__copyright__``: :class:`str` 

654 * ``__email__``: :class:`str` 

655 * ``__keywords__``: :class:`typing.Iterable`[:class:`str`] 

656 * ``__license__``: :class:`str` 

657 * ``__version__``: :class:`str` 

658 

659 The gathered information be used to add further mappings in the result dictionary. 

660 

661 .. topic:: Handling of package classifiers 

662 

663 To reduce redundantly provided parameters to this function (e.g. supported ``pythonVersions``), only additional 

664 classifiers should be provided via parameter ``classifiers``. The supported Python versions will be implicitly 

665 converted to package classifiers, so no need to specify them in parameter ``classifiers``. 

666 

667 The following classifiers are implicitly handled: 

668 

669 license 

670 The license specified by parameter ``license`` is translated into a classifier. |br| 

671 See also :meth:`pyTooling.Licensing.License.PythonClassifier` 

672 

673 Python versions 

674 Always add ``Programming Language :: Python :: 3 :: Only``. |br| 

675 For each value in ``pythonVersions``, one ``Programming Language :: Python :: Major.Minor`` is added. 

676 

677 Development status 

678 The development status specified by parameter ``developmentStatus`` is translated to a classifier and added. 

679 

680 .. topic:: Handling of extra requirements 

681 

682 If additional requirement files are provided, e.g. requirements to build the documentation, then *extra* 

683 requirements are defined. These can be installed via ``pip install packageName[extraName]``. If so, an extra called 

684 ``all`` is added, so developers can install all dependencies needed for package development. 

685 

686 ``doc`` 

687 If parameter ``documentationRequirementsFile`` is present, an extra requirements called ``doc`` will be defined. 

688 ``test`` 

689 If parameter ``unittestRequirementsFile`` is present, an extra requirements called ``test`` will be defined. 

690 ``build`` 

691 If parameter ``packagingRequirementsFile`` is present, an extra requirements called ``build`` will be defined. 

692 User-defined 

693 If parameter ``additionalRequirements`` is present, an extra requirements for every mapping entry in the 

694 dictionary will be added. 

695 ``all`` 

696 If any of the above was added, an additional extra requirement called ``all`` will be added, summarizing all 

697 extra requirements. 

698 

699 .. topic:: Handling of keywords 

700 

701 If parameter ``keywords`` is not specified, the dunder variable ``__keywords__`` from ``sourceFileWithVersion`` 

702 will be used. Otherwise, the content of the parameter, if not None or empty. 

703 

704 .. topic:: Handling of the description 

705 

706 If parameter ``description`` is not specified, the first paragraph of the module doc-string in 

707 ``sourceFileWithVersion`` is used, so a package describes itself in one place. An explicitly passed description 

708 always wins - including an empty one. 

709 

710 :param packageName: Name of the Python package. 

711 :param projectURL: URL to the Python project. 

712 :param sourceCodeURL: URL to the Python source code. 

713 :param documentationURL: URL to the package's documentation. 

714 :param issueTrackerCodeURL: URL to the projects issue tracker (ticket system). 

715 :param description: Optional, short description of the package. (Default: the first paragraph 

716 of the module doc-string in ``sourceFileWithVersion``.) The long description 

717 is read from the README file. 

718 :param keywords: Optional, a list of keywords. 

719 :param license: Optional, the package's license. (Default: ``Apache License, 2.0``, see 

720 :const:`DEFAULT_LICENSE`) 

721 :param readmeFile: Optional, the path to the README file. (Default: ``README.md``, see 

722 :const:`DEFAULT_README`) 

723 :param requirementsFile: Optional, the path to the project's requirements file. (Default: 

724 ``requirements.txt``, see :const:`DEFAULT_REQUIREMENTS`) 

725 :param documentationRequirementsFile: Optional, the path to the project's requirements file for documentation. 

726 (Default: ``doc/requirements.txt``, see 

727 :const:`DEFAULT_DOCUMENTATION_REQUIREMENTS`) 

728 :param unittestRequirementsFile: Optional, the path to the project's requirements file for unit tests. (Default: 

729 ``tests/requirements.txt``, see :const:`DEFAULT_TEST_REQUIREMENTS`) 

730 :param packagingRequirementsFile: Optional, the path to the project's requirements file for packaging. (Default: 

731 ``build/requirements.txt``, see :const:`DEFAULT_PACKAGING_REQUIREMENTS`) 

732 :param additionalRequirements: Optional, a dictionary of a lists with additional requirements. (default: None) 

733 :param sourceFileWithVersion: Optional, the path to the project's source file containing dunder variables like 

734 ``__version__``. (Default: ``__init__.py``, see :const:`DEFAULT_VERSION_FILE`) 

735 :param classifiers: Optional, a list of package classifiers. (Default: 3 classifiers, see 

736 :const:`DEFAULT_CLASSIFIERS`) 

737 :param developmentStatus: Optional, development status of the package. (Default: stable, see 

738 :const:`STATUS` for supported status values) 

739 :param pythonVersions: Optional, a list of supported Python 3 version. (Default: all currently 

740 maintained CPython versions, see :const:`DEFAULT_PY_VERSIONS`) 

741 :param consoleScripts: Optional, a dictionary mapping command line names to entry points, declared 

742 as ``console_scripts``. (Default: None) 

743 :param guiScripts: Optional, like ``consoleScripts``, but declared as ``gui_scripts`` - on 

744 Windows such a program starts without a console window. (Default: None) 

745 :param pytestPlugins: Optional, a dictionary mapping plugin names to entry points, declared as 

746 ``pytest11``. The classifier ``Framework :: Pytest`` is added with it. 

747 (Default: None) 

748 :param dataFiles: Optional, a dictionary mapping package names to lists of additional data files. 

749 :param debug: Optional, if ``True``, enable extended outputs for debugging. 

750 :returns: A dictionary suitable for :func:`setuptools.setup`. 

751 :raises MissingDependencyError: If package 'setuptools' is not available. 

752 :raises TypeError: If parameter 'readmeFile' is not of type :class:`~pathlib.Path`. 

753 :raises FileNotFoundError: If README file doesn't exist. 

754 :raises TypeError: If parameter 'requirementsFile' is not of type :class:`~pathlib.Path`. 

755 :raises FileNotFoundError: If requirements file doesn't exist. 

756 :raises TypeError: If parameter 'documentationRequirementsFile' is not of type :class:`~pathlib.Path`. 

757 :raises TypeError: If parameter 'unittestRequirementsFile' is not of type :class:`~pathlib.Path`. 

758 :raises TypeError: If parameter 'packagingRequirementsFile' is not of type :class:`~pathlib.Path`. 

759 :raises TypeError: If parameter 'sourceFileWithVersion' is not of type :class:`~pathlib.Path`. 

760 :raises FileNotFoundError: If package file with dunder variables doesn't exist. 

761 :raises TypeError: If parameter 'license' is not of type :class:`~pyTooling.Licensing.License`. 

762 :raises ValueError: If developmentStatus uses an unsupported value. (See :const:`STATUS`) 

763 :raises ValueError: If the content type of the README file is not supported. (See :func:`loadReadmeFile`) 

764 :raises FileNotFoundError: If the README file doesn't exist. (See :func:`loadReadmeFile`) 

765 :raises FileNotFoundError: If the requirements file doesn't exist. (See :func:`loadRequirementsFile`) 

766 :raises Exception: If the package's directory doesn't exist, or if a requirements file is 

767 malformed. 

768 :raises PackagingError: If no description was given and the package file has no module doc-string. 

769 """ 

770 try: 

771 from setuptools import find_packages, find_namespace_packages 

772 except ImportError as ex: 

773 raise MissingDependencyError(dependency="setuptools", extra="packaging") from ex 

774 

775 print(f"[pyTooling.Packaging] Python: {version_info.major}.{version_info.minor}.{version_info.micro}, pyTooling: {__version__}") 

776 

777 # Read README for upload to PyPI 

778 if not isinstance(readmeFile, Path): 778 ↛ 779line 778 didn't jump to line 779 because the condition on line 778 was never true

779 ex = TypeError("Parameter 'readmeFile' is not of type 'Path'.") 

780 ex.add_note(f"Got type '{getFullyQualifiedName(readmeFile)}'.") 

781 raise ex 

782 elif not readmeFile.exists(): 782 ↛ 783line 782 didn't jump to line 783 because the condition on line 782 was never true

783 raise FileNotFoundError(f"README file '{readmeFile}' not found in '{Path.cwd()}'.") 

784 else: 

785 readme = loadReadmeFile(readmeFile) 

786 

787 # Read requirements file and add them to package dependency list (remove duplicates) 

788 if not isinstance(requirementsFile, Path): 788 ↛ 789line 788 didn't jump to line 789 because the condition on line 788 was never true

789 ex = TypeError("Parameter 'requirementsFile' is not of type 'Path'.") 

790 ex.add_note(f"Got type '{getFullyQualifiedName(requirementsFile)}'.") 

791 raise ex 

792 elif not requirementsFile.exists(): 792 ↛ 793line 792 didn't jump to line 793 because the condition on line 792 was never true

793 raise FileNotFoundError(f"Requirements file '{requirementsFile}' not found in '{Path.cwd()}'.") 

794 else: 

795 requirements = list(set(loadRequirementsFile(requirementsFile, debug=debug))) 

796 

797 extraRequirements: dict[str, list[str]] = {} 

798 if documentationRequirementsFile is not None: 798 ↛ 811line 798 didn't jump to line 811 because the condition on line 798 was always true

799 if not isinstance(documentationRequirementsFile, Path): 799 ↛ 800line 799 didn't jump to line 800 because the condition on line 799 was never true

800 ex = TypeError("Parameter 'documentationRequirementsFile' is not of type 'Path'.") 

801 ex.add_note(f"Got type '{getFullyQualifiedName(documentationRequirementsFile)}'.") 

802 raise ex 

803 elif not documentationRequirementsFile.exists(): 803 ↛ 804line 803 didn't jump to line 804 because the condition on line 803 was never true

804 if debug: 

805 print(f"[pyTooling.Packaging] Documentation requirements file '{documentationRequirementsFile}' not found in '{Path.cwd()}'.") 

806 print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") 

807 # raise FileNotFoundError(f"Documentation requirements file '{documentationRequirementsFile}' not found in '{Path.cwd()}'.") 

808 else: 

809 extraRequirements["doc"] = list(set(loadRequirementsFile(documentationRequirementsFile, debug=debug))) 

810 

811 if unittestRequirementsFile is not None: 811 ↛ 824line 811 didn't jump to line 824 because the condition on line 811 was always true

812 if not isinstance(unittestRequirementsFile, Path): 812 ↛ 813line 812 didn't jump to line 813 because the condition on line 812 was never true

813 ex = TypeError("Parameter 'unittestRequirementsFile' is not of type 'Path'.") 

814 ex.add_note(f"Got type '{getFullyQualifiedName(unittestRequirementsFile)}'.") 

815 raise ex 

816 elif not unittestRequirementsFile.exists(): 816 ↛ 817line 816 didn't jump to line 817 because the condition on line 816 was never true

817 if debug: 

818 print(f"[pyTooling.Packaging] Unit testing requirements file '{unittestRequirementsFile}' not found in '{Path.cwd()}'.") 

819 print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") 

820 # raise FileNotFoundError(f"Unit testing requirements file '{unittestRequirementsFile}' not found in '{Path.cwd()}'.") 

821 else: 

822 extraRequirements["test"] = list(set(loadRequirementsFile(unittestRequirementsFile, debug=debug))) 

823 

824 if packagingRequirementsFile is not None: 824 ↛ 837line 824 didn't jump to line 837 because the condition on line 824 was always true

825 if not isinstance(packagingRequirementsFile, Path): 825 ↛ 826line 825 didn't jump to line 826 because the condition on line 825 was never true

826 ex = TypeError("Parameter 'packagingRequirementsFile' is not of type 'Path'.") 

827 ex.add_note(f"Got type '{getFullyQualifiedName(packagingRequirementsFile)}'.") 

828 raise ex 

829 elif not packagingRequirementsFile.exists(): 

830 if debug: 830 ↛ 831line 830 didn't jump to line 831 because the condition on line 830 was never true

831 print(f"[pyTooling.Packaging] Packaging requirements file '{packagingRequirementsFile}' not found in '{Path.cwd()}'.") 

832 print( "[pyTooling.Packaging] No section added to 'extraRequirements'.") 

833 # raise FileNotFoundError(f"Packaging requirements file '{packagingRequirementsFile}' not found in '{Path.cwd()}'.") 

834 else: 

835 extraRequirements["build"] = list(set(loadRequirementsFile(packagingRequirementsFile, debug=debug))) 

836 

837 if additionalRequirements is not None: 

838 for key, value in additionalRequirements.items(): 

839 extraRequirements[key] = value 

840 

841 if len(extraRequirements) > 0: 841 ↛ 845line 841 didn't jump to line 845 because the condition on line 841 was always true

842 extraRequirements["all"] = list(set([dep for deps in extraRequirements.values() for dep in deps])) 

843 

844 # Read __author__, __email__, __version__ from source file 

845 if not isinstance(sourceFileWithVersion, Path): 845 ↛ 846line 845 didn't jump to line 846 because the condition on line 845 was never true

846 ex = TypeError("Parameter 'sourceFileWithVersion' is not of type 'Path'.") 

847 ex.add_note(f"Got type '{getFullyQualifiedName(sourceFileWithVersion)}'.") 

848 raise ex 

849 elif not sourceFileWithVersion.exists(): 849 ↛ 850line 849 didn't jump to line 850 because the condition on line 849 was never true

850 raise FileNotFoundError(f"Package file '{sourceFileWithVersion}' with dunder variables not found in '{Path.cwd()}'.") 

851 else: 

852 versionInformation = extractVersionInformation(sourceFileWithVersion) 

853 

854 # Scan for packages and source files 

855 if debug: 855 ↛ 856line 855 didn't jump to line 856 because the condition on line 855 was never true

856 print("[pyTooling.Packaging] Exclude list for find_(namespace_)packages:") 

857 exclude = [] 

858 rootNamespace = firstElement(packageName.split(".")) 

859 for dirName in (dirItem.name for dirItem in os_scandir(Path.cwd()) if dirItem.is_dir() and "." not in dirItem.name and dirItem.name != rootNamespace): 

860 exclude.append(f"{dirName}") 

861 exclude.append(f"{dirName}.*") 

862 if debug: 862 ↛ 863line 862 didn't jump to line 863 because the condition on line 862 was never true

863 print(f"[pyTooling.Packaging] - {dirName}, {dirName}.*") 

864 

865 if "." in packageName: 

866 exclude.append(rootNamespace) 

867 packages = find_namespace_packages(exclude=exclude) 

868 if packageName.endswith(".*"): 868 ↛ 869line 868 didn't jump to line 869 because the condition on line 868 was never true

869 packageName = packageName[:-2] 

870 else: 

871 packages = find_packages(exclude=exclude) 

872 

873 if debug: 873 ↛ 874line 873 didn't jump to line 874 because the condition on line 873 was never true

874 print(f"[pyTooling.Packaging] Found packages: ({getFullyQualifiedName(packages)})") 

875 for package in packages: 

876 print(f"[pyTooling.Packaging] - {package}") 

877 

878 if keywords is None or isinstance(keywords, Sized) and len(keywords) == 0: 

879 keywords = versionInformation.Keywords 

880 

881 if description is None: 

882 description = versionInformation.Description 

883 

884 if len(description) == 0: 

885 ex = PackagingError(f"Package '{packageName}' has no description.") 

886 ex.add_note(f"Neither was parameter 'description' given, nor has '{sourceFileWithVersion}' a module doc-string.") 

887 ex.add_note("Describe the package in the first paragraph of that doc-string, or pass parameter 'description'.") 

888 raise ex 

889 

890 # Assemble classifiers 

891 classifiers = list(classifiers) 

892 

893 # Check the license; it reaches setuptools as an SPDX expression, not as a classifier 

894 if not isinstance(license, License): 

895 ex = TypeError("Parameter 'license' is not of type 'License'.") 

896 ex.add_note(f"Got type '{getFullyQualifiedName(license)}'.") 

897 raise ex 

898 

899 if pytestPlugins is not None and "Framework :: Pytest" not in classifiers: 

900 classifiers.append("Framework :: Pytest") 

901 

902 if (deprecated := [classifier for classifier in classifiers if classifier.startswith("License ::")]) != []: 

903 names = "', '".join(deprecated) 

904 print(f"[pyTooling.Packaging] License classifiers are deprecated: '{names}'.") 

905 print( "[pyTooling.Packaging] Remove them; the 'license' parameter becomes the SPDX expression setuptools wants.") 

906 

907 def _naturalSorting(array: Iterable[str]) -> list[str]: 

908 """ 

909 A simple natural sorting implementation. 

910 

911 :param array: The strings to sort. 

912 :returns: The strings, sorted with embedded numbers compared numerically. 

913 """ 

914 # See http://nedbatchelder.com/blog/200712/human_sorting.html 

915 def _toInt(text: str) -> Union[str, int]: 

916 """ 

917 Try to convert a :class:`str` to :class:`int` if possible, otherwise preserve the string. 

918 

919 :param text: The text to convert. 

920 :returns: The converted integer, or the unchanged string. 

921 """ 

922 return int(text) if text.isdigit() else text 

923 

924 def _createKey(text: str) -> tuple[Union[str, float], ...]: 

925 """ 

926 Split the text into a tuple of multiple :class:`str` and :class:`int` fields, so embedded numbers can be sorted by 

927 their value. 

928 

929 :param text: The text to split. 

930 :returns: Tuple of string and integer fields, usable as a sort key. 

931 """ 

932 return tuple(_toInt(part) for part in re_split(r"(\d+)", text)) 

933 

934 sortedArray = list(array) 

935 sortedArray.sort(key=_createKey) 

936 return sortedArray 

937 

938 pythonVersions = _naturalSorting(pythonVersions) 

939 

940 # Translate Python versions to classifiers 

941 classifiers.append("Programming Language :: Python :: 3 :: Only") 

942 for v in pythonVersions: 

943 classifiers.append(f"Programming Language :: Python :: {v}") 

944 

945 # Translate status to classifier 

946 try: 

947 classifiers.append(f"Development Status :: {STATUS[developmentStatus.lower()]}") 

948 except KeyError: # pragma: no cover 

949 raise ValueError(f"Unsupported development status '{developmentStatus}'.") 

950 

951 # Assemble all package information 

952 parameters = { 

953 "name": packageName, 

954 "version": versionInformation.Version, 

955 "author": versionInformation.Author, 

956 "author_email": versionInformation.Email, 

957 "license": license.SPDXIdentifier, 

958 "description": description, 

959 "long_description": readme.Content, 

960 "long_description_content_type": readme.MimeType, 

961 "url": projectURL, 

962 "project_urls": { 

963 'Documentation': documentationURL, 

964 'Source Code': sourceCodeURL, 

965 'Issue Tracker': issueTrackerCodeURL 

966 }, 

967 "packages": packages, 

968 "classifiers": classifiers, 

969 "keywords": keywords, 

970 "python_requires": f">={pythonVersions[0]}", 

971 "install_requires": requirements, 

972 } 

973 

974 if len(extraRequirements) > 0: 974 ↛ 977line 974 didn't jump to line 977 because the condition on line 974 was always true

975 parameters["extras_require"] = extraRequirements 

976 

977 if (groups := _collectEntryPoints(consoleScripts, guiScripts, pytestPlugins)) is not None: 

978 parameters["entry_points"] = groups 

979 

980 if dataFiles: 980 ↛ 981line 980 didn't jump to line 981 because the condition on line 980 was never true

981 parameters["package_data"] = dataFiles 

982 

983 return parameters 

984 

985 

986@export 

987def DescribePythonPackageHostedOnGitHub( 

988 packageName: str, 

989 gitHubNamespace: str, 

990 gitHubRepository: Nullable[str] = None, 

991 projectURL: Nullable[str] = None, 

992 description: Nullable[str] = None, 

993 keywords: Nullable[Iterable[str]] = None, 

994 license: License = DEFAULT_LICENSE, 

995 readmeFile: Path = DEFAULT_README, 

996 requirementsFile: Path = DEFAULT_REQUIREMENTS, 

997 documentationRequirementsFile: Path = DEFAULT_DOCUMENTATION_REQUIREMENTS, 

998 unittestRequirementsFile: Path = DEFAULT_TEST_REQUIREMENTS, 

999 packagingRequirementsFile: Path = DEFAULT_PACKAGING_REQUIREMENTS, 

1000 additionalRequirements: Nullable[dict[str, list[str]]] = None, 

1001 sourceFileWithVersion: Path = DEFAULT_VERSION_FILE, 

1002 classifiers: Iterable[str] = DEFAULT_CLASSIFIERS, 

1003 developmentStatus: str = "stable", 

1004 pythonVersions: Sequence[str] = DEFAULT_PY_VERSIONS, 

1005 consoleScripts: Nullable[dict[str, str]] = None, 

1006 guiScripts: Nullable[dict[str, str]] = None, 

1007 pytestPlugins: Nullable[dict[str, str]] = None, 

1008 dataFiles: Nullable[dict[str, list[str]]] = None, 

1009 debug: bool = False 

1010) -> dict[str, Any]: 

1011 """ 

1012 Helper function to describe a Python package when the source code is hosted on GitHub. 

1013 

1014 This is a wrapper for :func:`DescribePythonPackage`, because some parameters can be simplified by knowing the GitHub 

1015 namespace and repository name: issue tracker URL, source code URL, ... 

1016 

1017 :param packageName: Name of the Python package. 

1018 :param gitHubNamespace: Name of the GitHub namespace (organization or user). 

1019 :param gitHubRepository: Optional, name of the GitHub repository. 

1020 :param projectURL: Optional, URL to the Python project. 

1021 :param description: Optional, short description of the package. (Default: the first paragraph 

1022 of the module doc-string in ``sourceFileWithVersion``.) The long description 

1023 is read from the README file. 

1024 :param keywords: Optional, a list of keywords. 

1025 :param license: Optional, the package's license. (Default: ``Apache License, 2.0``, see 

1026 :const:`DEFAULT_LICENSE`) 

1027 :param readmeFile: Optional, the path to the README file. (Default: ``README.md``, see 

1028 :const:`DEFAULT_README`) 

1029 :param requirementsFile: Optional, the path to the project's requirements file. (Default: 

1030 ``requirements.txt``, see :const:`DEFAULT_REQUIREMENTS`) 

1031 :param documentationRequirementsFile: Optional, the path to the project's requirements file for documentation. 

1032 (Default: ``doc/requirements.txt``, see 

1033 :const:`DEFAULT_DOCUMENTATION_REQUIREMENTS`) 

1034 :param unittestRequirementsFile: Optional, the path to the project's requirements file for unit tests. (Default: 

1035 ``tests/requirements.txt``, see :const:`DEFAULT_TEST_REQUIREMENTS`) 

1036 :param packagingRequirementsFile: Optional, the path to the project's requirements file for packaging. (Default: 

1037 ``build/requirements.txt``, see :const:`DEFAULT_PACKAGING_REQUIREMENTS`) 

1038 :param additionalRequirements: Optional, a dictionary of a lists with additional requirements. (default: None) 

1039 :param sourceFileWithVersion: Optional, the path to the project's source file containing dunder variables like 

1040 ``__version__``. (Default: ``__init__.py``, see :const:`DEFAULT_VERSION_FILE`) 

1041 :param classifiers: Optional, a list of package classifiers. (Default: 3 classifiers, see 

1042 :const:`DEFAULT_CLASSIFIERS`) 

1043 :param developmentStatus: Optional, development status of the package. (Default: stable, see 

1044 :const:`STATUS` for supported status values) 

1045 :param pythonVersions: Optional, a list of supported Python 3 version. (Default: all currently 

1046 maintained CPython versions, see :const:`DEFAULT_PY_VERSIONS`) 

1047 :param consoleScripts: Optional, a dictionary mapping command line names to entry points, declared 

1048 as ``console_scripts``. (Default: None) 

1049 :param guiScripts: Optional, like ``consoleScripts``, but declared as ``gui_scripts`` - on 

1050 Windows such a program starts without a console window. (Default: None) 

1051 :param pytestPlugins: Optional, a dictionary mapping plugin names to entry points, declared as 

1052 ``pytest11``. The classifier ``Framework :: Pytest`` is added with it. 

1053 (Default: None) 

1054 :param dataFiles: Optional, a dictionary mapping package names to lists of additional data files. 

1055 :param debug: Optional, if ``True``, enable extended outputs for debugging. 

1056 :returns: A dictionary suitable for :func:`setuptools.setup`. 

1057 :raises MissingDependencyError: If package 'setuptools' is not available. 

1058 :raises TypeError: If parameter 'readmeFile' is not of type :class:`~pathlib.Path`. 

1059 :raises FileNotFoundError: If README file doesn't exist. 

1060 :raises TypeError: If parameter 'requirementsFile' is not of type :class:`~pathlib.Path`. 

1061 :raises FileNotFoundError: If requirements file doesn't exist. 

1062 :raises TypeError: If parameter 'documentationRequirementsFile' is not of type :class:`~pathlib.Path`. 

1063 :raises TypeError: If parameter 'unittestRequirementsFile' is not of type :class:`~pathlib.Path`. 

1064 :raises TypeError: If parameter 'packagingRequirementsFile' is not of type :class:`~pathlib.Path`. 

1065 :raises TypeError: If parameter 'sourceFileWithVersion' is not of type :class:`~pathlib.Path`. 

1066 :raises FileNotFoundError: If package file with dunder variables doesn't exist. 

1067 :raises TypeError: If parameter 'license' is not of type :class:`~pyTooling.Licensing.License`. 

1068 :raises ValueError: If developmentStatus uses an unsupported value. (See :const:`STATUS`) 

1069 :raises ValueError: If the content type of the README file is not supported. (See :func:`loadReadmeFile`) 

1070 :raises FileNotFoundError: If the README file doesn't exist. (See :func:`loadReadmeFile`) 

1071 :raises FileNotFoundError: If the requirements file doesn't exist. (See :func:`loadRequirementsFile`) 

1072 :raises PackagingError: If no description was given and the package file has no module doc-string. 

1073 """ 

1074 if gitHubRepository is None: 1074 ↛ 1076line 1074 didn't jump to line 1076 because the condition on line 1074 was never true

1075 # Assign GitHub repository name without '.*', if derived from Python package name. 

1076 if packageName.endswith(".*"): 

1077 gitHubRepository = packageName[:-2] 

1078 else: 

1079 gitHubRepository = packageName 

1080 

1081 # Derive URLs 

1082 sourceCodeURL = f"https://GitHub.com/{gitHubNamespace}/{gitHubRepository}" 

1083 documentationURL = f"https://{gitHubNamespace}.GitHub.io/{gitHubRepository}" 

1084 issueTrackerCodeURL = f"{sourceCodeURL}/issues" 

1085 

1086 projectURL = projectURL if projectURL is not None else sourceCodeURL 

1087 

1088 return DescribePythonPackage( 

1089 packageName=packageName, 

1090 description=description, 

1091 keywords=keywords, 

1092 projectURL=projectURL, 

1093 sourceCodeURL=sourceCodeURL, 

1094 documentationURL=documentationURL, 

1095 issueTrackerCodeURL=issueTrackerCodeURL, 

1096 license=license, 

1097 readmeFile=readmeFile, 

1098 requirementsFile=requirementsFile, 

1099 documentationRequirementsFile=documentationRequirementsFile, 

1100 unittestRequirementsFile=unittestRequirementsFile, 

1101 packagingRequirementsFile=packagingRequirementsFile, 

1102 additionalRequirements=additionalRequirements, 

1103 sourceFileWithVersion=sourceFileWithVersion, 

1104 classifiers=classifiers, 

1105 developmentStatus=developmentStatus, 

1106 pythonVersions=pythonVersions, 

1107 consoleScripts=consoleScripts, 

1108 guiScripts=guiScripts, 

1109 pytestPlugins=pytestPlugins, 

1110 dataFiles=dataFiles, 

1111 debug=debug, 

1112 )