• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pybuilder / pybuilder / 17148697921

22 Aug 2025 07:10AM UTC coverage: 84.011%. First build
17148697921

Pull #932

github

web-flow
Merge 18e56e93e into 7d30a872e
Pull Request #932: Migrate from `python setup.py sdist/bdist_wheel` to `build`

2170 of 2674 branches covered (81.15%)

Branch coverage included in aggregate %.

11 of 12 new or added lines in 1 file covered. (91.67%)

5533 of 6495 relevant lines covered (85.19%)

36.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.05
/src/main/python/pybuilder/plugins/python/distutils_plugin.py
1
#   -*- coding: utf-8 -*-
2
#
3
#   This file is part of PyBuilder
4
#
5
#   Copyright 2011-2020 PyBuilder Team
6
#
7
#   Licensed under the Apache License, Version 2.0 (the "License");
8
#   you may not use this file except in compliance with the License.
9
#   You may obtain a copy of the License at
10
#
11
#       http://www.apache.org/licenses/LICENSE-2.0
12
#
13
#   Unless required by applicable law or agreed to in writing, software
14
#   distributed under the License is distributed on an "AS IS" BASIS,
15
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
#   See the License for the specific language governing permissions and
17
#   limitations under the License.
18

19
import io
44✔
20
import os
44✔
21
import re
44✔
22
import string
44✔
23
from datetime import datetime
44✔
24
from textwrap import dedent
44✔
25

26
from pybuilder import pip_utils
44✔
27
from pybuilder.core import (after,
44✔
28
                            before,
29
                            use_plugin,
30
                            init,
31
                            task,
32
                            RequirementsFile,
33
                            Dependency)
34
from pybuilder.errors import BuildFailedException, MissingPrerequisiteException
44✔
35
from pybuilder.python_utils import StringIO
44✔
36
from pybuilder.utils import (as_list,
44✔
37
                             is_string,
38
                             is_notstr_iterable,
39
                             get_dist_version_string,
40
                             safe_log_file_name,
41
                             tail_log)
42

43
use_plugin("python.core")
44✔
44

45
LEADING_TAB_RE = re.compile(r'^(\t*)')
44✔
46
DATA_FILES_PROPERTY = "distutils_data_files"
44✔
47
SETUP_TEMPLATE = string.Template("""#!/usr/bin/env python
44✔
48
#   -*- coding: utf-8 -*-
49
$remove_hardlink_capabilities_for_shared_filesystems
50
from $module import setup
51
from $module.command.install import install as _install
52

53
class install(_install):
54
    def pre_install_script(self):
55
$preinstall_script
56

57
    def post_install_script(self):
58
$postinstall_script
59

60
    def run(self):
61
        self.pre_install_script()
62

63
        _install.run(self)
64

65
        self.post_install_script()
66

67
if __name__ == '__main__':
68
    setup(
69
        name = $name,
70
        version = $version,
71
        description = $summary,
72
        long_description = $description,
73
        long_description_content_type = $description_content_type,
74
        classifiers = $classifiers,
75
        keywords = $setup_keywords,
76

77
        author = $author,
78
        author_email = $author_email,
79
        maintainer = $maintainer,
80
        maintainer_email = $maintainer_email,
81

82
        license = $license,
83

84
        url = $url,
85
        project_urls = $project_urls,
86

87
        scripts = $scripts,
88
        packages = $packages,
89
        namespace_packages = $namespace_packages,
90
        py_modules = $modules,
91
        entry_points = $entry_points,
92
        data_files = $data_files,
93
        package_data = $package_data,
94
        install_requires = $dependencies,
95
        dependency_links = $dependency_links,
96
        zip_safe = $zip_safe,
97
        cmdclass = {'install': install},
98
        python_requires = $python_requires,
99
        obsoletes = $obsoletes,
100
    )
101
""")
102

103

104
def default(value, default=""):
44✔
105
    if value is None:
44✔
106
        return default
44✔
107
    return value
44✔
108

109

110
def as_str(value):
44✔
111
    return repr(str(value))
44✔
112

113

114
@init
44✔
115
def initialize_distutils_plugin(project):
44✔
116
    project.plugin_depends_on("pypandoc", "~=1.4")
44✔
117
    project.plugin_depends_on("twine", ">=1.15.0")
44✔
118
    project.plugin_depends_on("setuptools", ">=76.0", eager_update=False)
44✔
119
    project.plugin_depends_on("build", ">=1.3.0", eager_update=False)
44✔
120

121
    project.set_property_if_unset("distutils_commands", ["sdist", "wheel"])
44✔
122
    project.set_property_if_unset("distutils_command_options", None)
44✔
123

124
    # Workaround for http://bugs.python.org/issue8876 , unable to build a bdist
125
    # on a filesystem that does not support hardlinks
126
    project.set_property_if_unset("distutils_issue8876_workaround_enabled", False)
44✔
127
    project.set_property_if_unset("distutils_classifiers", [
44✔
128
        "Development Status :: 3 - Alpha",
129
        "Programming Language :: Python"
130
    ])
131
    project.set_property_if_unset("distutils_fail_on_warnings", False)
44✔
132

133
    project.set_property_if_unset("distutils_upload_register", False)
44✔
134
    project.set_property_if_unset("distutils_upload_repository", None)
44✔
135
    project.set_property_if_unset("distutils_upload_repository_key", None)
44✔
136
    project.set_property_if_unset("distutils_upload_sign", False)
44✔
137
    project.set_property_if_unset("distutils_upload_sign_identity", None)
44✔
138
    project.set_property_if_unset("distutils_upload_skip_existing", False)
44✔
139

140
    project.set_property_if_unset("distutils_readme_description", False)
44✔
141
    project.set_property_if_unset("distutils_readme_file", "README.md")
44✔
142
    project.set_property_if_unset("distutils_readme_file_convert", False)
44✔
143
    project.set_property_if_unset("distutils_readme_file_type", None)
44✔
144
    project.set_property_if_unset("distutils_readme_file_encoding", None)
44✔
145
    project.set_property_if_unset("distutils_readme_file_variant", None)
44✔
146
    project.set_property_if_unset("distutils_summary_overwrite", False)
44✔
147
    project.set_property_if_unset("distutils_description_overwrite", False)
44✔
148

149
    project.set_property_if_unset("distutils_console_scripts", None)
44✔
150
    project.set_property_if_unset("distutils_entry_points", None)
44✔
151
    project.set_property_if_unset("distutils_setup_keywords", None)
44✔
152
    project.set_property_if_unset("distutils_zip_safe", True)
44✔
153

154

155
@after("prepare")
44✔
156
def set_description(project, logger, reactor):
44✔
157
    if project.get_property("distutils_readme_description"):
44✔
158
        description = None
22✔
159
        if project.get_property("distutils_readme_file_convert"):
22!
160
            try:
×
161
                reactor.pybuilder_venv.verify_can_execute(["pandoc", "--version"], "pandoc", "distutils")
×
162
                description = doc_convert(project, logger)
×
163
            except (MissingPrerequisiteException, ImportError):
×
164
                logger.warn("Was unable to find pandoc or pypandoc and did not convert the documentation")
×
165
        else:
166
            with io.open(project.expand_path("$distutils_readme_file"), "rt",
22✔
167
                         encoding=project.get_property("distutils_readme_file_encoding")) as f:
168
                description = f.read()
22✔
169

170
        if description:
22!
171
            if (not hasattr(project, "summary") or
22!
172
                    project.summary is None or
173
                    project.get_property("distutils_summary_overwrite")):
174
                setattr(project, "summary", description.splitlines()[0].strip())
×
175

176
            if (not hasattr(project, "description") or
22!
177
                    project.description is None or
178
                    project.get_property("distutils_description_overwrite")):
179
                setattr(project, "description", description)
22✔
180

181
    if (not hasattr(project, "description") or
44✔
182
            not project.description):
183
        if hasattr(project, "summary") and project.summary:
44!
184
            description = project.summary
×
185
        else:
186
            description = project.name
44✔
187

188
        setattr(project, "description", description)
44✔
189

190
    warn = False
44✔
191
    if len(project.summary) >= 512:
44!
192
        logger.warn("Project summary SHOULD be shorter than 512 characters per PEP-426")
×
193
        warn = True
×
194

195
    if "\n" in project.summary or "\r" in project.summary:
44!
196
        logger.warn("Project summary SHOULD NOT contain new-line characters per PEP-426")
×
197
        warn = True
×
198

199
    if len(project.summary) >= 2048:
44!
200
        raise BuildFailedException("Project summary MUST NOT be shorter than 2048 characters per PEP-426")
×
201

202
    if warn and project.get_property("distutils_fail_on_warnings"):
44!
203
        raise BuildFailedException("Distutil plugin warnings caused a build failure. Please see warnings above.")
×
204

205

206
@after("package")
44✔
207
def write_setup_script(project, logger):
44✔
208
    setup_script = project.expand_path("$dir_dist", "setup.py")
44✔
209
    logger.info("Writing setup.py as %s", setup_script)
44✔
210

211
    with io.open(setup_script, "wt", encoding="utf-8") as setup_file:
44✔
212
        script = render_setup_script(project)
44✔
213
        setup_file.write(script)
44✔
214

215
    os.chmod(setup_script, 0o755)
44✔
216

217

218
def render_setup_script(project):
44✔
219
    author = ", ".join(map(lambda a: a.name, project.authors))
44✔
220
    author_email = ", ".join(map(lambda a: a.email, project.authors))
44✔
221
    maintainer = ", ".join(map(lambda a: a.name, project.maintainers))
44✔
222
    maintainer_email = ",".join(map(lambda a: a.email, project.maintainers))
44✔
223

224
    template_values = {
44✔
225
        "module": "setuptools",
226
        "name": as_str(project.name),
227
        "version": as_str(project.dist_version),
228
        "summary": as_str(default(project.summary)),
229
        "description": as_str(default(project.description)),
230
        "description_content_type": repr(_get_description_content_type(project)),
231
        "author": as_str(author),
232
        "author_email": as_str(author_email),
233
        "maintainer": as_str(maintainer),
234
        "maintainer_email": as_str(maintainer_email),
235
        "license": as_str(default(project.license)),
236
        "url": as_str(default(project.url)),
237
        "project_urls": build_map_string(project.urls),
238
        "scripts": build_scripts_string(project),
239
        "packages": build_packages_string(project),
240
        "namespace_packages": build_namespace_packages_string(project),
241
        "modules": build_modules_string(project),
242
        "classifiers": build_classifiers_string(project),
243
        "entry_points": build_entry_points_string(project),
244
        "data_files": build_data_files_string(project),
245
        "package_data": build_package_data_string(project),
246
        "dependencies": build_install_dependencies_string(project),
247
        "dependency_links": build_dependency_links_string(project),
248
        "remove_hardlink_capabilities_for_shared_filesystems": (
249
            "import os\ndel os.link"
250
            if project.get_property("distutils_issue8876_workaround_enabled")
251
            else ""),
252
        "preinstall_script": _normalize_setup_post_pre_script(project.setup_preinstall_script or "pass"),
253
        "postinstall_script": _normalize_setup_post_pre_script(project.setup_postinstall_script or "pass"),
254
        "setup_keywords": build_setup_keywords(project),
255
        "python_requires": as_str(default(project.requires_python)),
256
        "obsoletes": build_string_from_array(project.obsoletes),
257
        "zip_safe": project.get_property("distutils_zip_safe")
258
    }
259

260
    return SETUP_TEMPLATE.substitute(template_values)
44✔
261

262

263
@after("package")
44✔
264
def write_manifest_file(project, logger):
44✔
265
    if len(project.manifest_included_files) == 0 and len(project.manifest_included_directories) == 0:
44✔
266
        logger.debug("No data to write into MANIFEST.in")
44✔
267
        return
44✔
268

269
    logger.debug("Files included in MANIFEST.in: %s" %
44✔
270
                 project.manifest_included_files)
271

272
    manifest_filename = project.expand_path("$dir_dist", "MANIFEST.in")
44✔
273
    logger.info("Writing MANIFEST.in as %s", manifest_filename)
44✔
274

275
    with open(manifest_filename, "w") as manifest_file:
44✔
276
        manifest_file.write(render_manifest_file(project))
44✔
277

278
    os.chmod(manifest_filename, 0o664)
44✔
279

280

281
@before("publish")
44✔
282
def build_binary_distribution(project, logger, reactor):
44✔
283
    logger.info("Building binary distribution in %s",
44✔
284
                project.expand_path("$dir_dist"))
285

286
    commands = [build_command_with_options(cmd, project.get_property("distutils_command_options"))
44✔
287
                for cmd in as_list(project.get_property("distutils_commands"))]
288
    execute_distutils(project, logger, reactor.pybuilder_venv, commands)
44✔
289
    upload_check(project, logger, reactor)
44✔
290

291

292
@task("install")
44✔
293
def install_distribution(project, logger, reactor):
44✔
294
    logger.info("Installing project %s-%s", project.name, project.version)
44✔
295

296
    _prepare_reports_dir(project)
44✔
297
    outfile_name = project.expand_path("$dir_reports", "distutils",
44✔
298
                                       "pip_install_%s" % datetime.utcnow().strftime("%Y%m%d%H%M%S"))
299
    pip_utils.pip_install(
44✔
300
        install_targets=project.expand_path("$dir_dist"),
301
        python_env=reactor.python_env_registry["system"],
302
        index_url=project.get_property("install_dependencies_index_url"),
303
        extra_index_url=project.get_property("install_dependencies_extra_index_url"),
304
        force_reinstall=True,
305
        logger=logger,
306
        verbose=project.get_property("pip_verbose"),
307
        cwd=".",
308
        outfile_name=outfile_name,
309
        error_file_name=outfile_name)
310

311

312
@task("upload", description="Upload a project to PyPi.")
44✔
313
def upload(project, logger, reactor):
44✔
314
    repository = project.get_property("distutils_upload_repository")
44✔
315
    repository_args = []
44✔
316
    if repository:
44✔
317
        repository_args = ["--repository-url", repository]
44✔
318
    else:
319
        repository_key = project.get_property("distutils_upload_repository_key")
44✔
320
        if repository_key:
44✔
321
            repository_args = ["--repository", repository_key]
44✔
322

323
    upload_sign = project.get_property("distutils_upload_sign")
44✔
324
    sign_identity = project.get_property("distutils_upload_sign_identity")
44✔
325
    upload_sign_args = []
44✔
326
    if upload_sign:
44✔
327
        upload_sign_args = ["--sign"]
44✔
328
        if sign_identity:
44✔
329
            upload_sign_args += ["--identity", sign_identity]
44✔
330

331
    if project.get_property("distutils_upload_register"):
44✔
332
        logger.info("Registering project %s-%s%s", project.name, project.version,
44✔
333
                    (" into repository '%s'" % repository) if repository else "")
334
        execute_twine(project, logger, reactor.pybuilder_venv, repository_args, "register")
44✔
335

336
    skip_existing = project.get_property("distutils_upload_skip_existing")
44✔
337
    logger.info("Uploading project %s-%s%s%s%s%s", project.name, project.version,
44✔
338
                (" to repository '%s'" % repository) if repository else "",
339
                get_dist_version_string(project, " as version %s"),
340
                (" signing%s" % (" with %s" % sign_identity if sign_identity else "")) if upload_sign else "",
341
                (", will skip existing" if skip_existing else ""))
342

343
    upload_cmd_args = repository_args + upload_sign_args
44✔
344
    if skip_existing:
44!
345
        upload_cmd_args.append("--skip-existing")
×
346

347
    execute_twine(project, logger, reactor.pybuilder_venv, upload_cmd_args, "upload")
44✔
348

349

350
def upload_check(project, logger, reactor):
44✔
351
    logger.info("Running Twine check for generated artifacts")
44✔
352
    execute_twine(project, logger, reactor.pybuilder_venv, [], "check")
44✔
353

354

355
def render_manifest_file(project):
44✔
356
    manifest_content = StringIO()
44✔
357

358
    for included_file in project.manifest_included_files:
44✔
359
        manifest_content.write("include %s\n" % included_file)
44✔
360

361
    for directory, pattern_list in project.manifest_included_directories:
44✔
362
        patterns = ' '.join(pattern_list)
44✔
363
        manifest_content.write("recursive-include %s %s\n" % (directory, patterns))
44✔
364

365
    return manifest_content.getvalue()
44✔
366

367

368
def build_command_with_options(command, distutils_command_options=None):
44✔
369
    if command == "bdist_wheel":
44!
NEW
370
        command = "wheel"
×
371
    commands = [f"--{command}"]
44✔
372
    if distutils_command_options:
44✔
373
        try:
44✔
374
            commands.extend([f"-C{cmd}" for cmd in as_list(distutils_command_options[command])])
44✔
375
        except KeyError:
44✔
376
            pass
44✔
377
    return commands
44✔
378

379

380
def execute_distutils(project, logger, python_env, distutils_commands):
44✔
381
    reports_dir = _prepare_reports_dir(project)
44✔
382
    setup_script_dir = project.expand_path("$dir_dist")
44✔
383

384
    for command in distutils_commands:
44✔
385
        if is_string(command):
44✔
386
            out_file = os.path.join(reports_dir, safe_log_file_name(command))
44✔
387
        else:
388
            out_file = os.path.join(reports_dir, safe_log_file_name("__".join(command)))
44✔
389
        with open(out_file, "w") as out_f:
44✔
390
            commands = python_env.executable + ["-c",
44✔
391
                                                "import sys; del sys.path[0]; "
392
                                                "import runpy; runpy.run_module('build.__main__', run_name='__main__')"
393
                                                ]
394
            if project.get_property("verbose"):
44✔
395
                commands.append("-v")
22✔
396
            if is_string(command):
44✔
397
                commands.extend(command.split())
44✔
398
            else:
399
                commands.extend(command)
44✔
400
            commands.append(setup_script_dir)
44✔
401
            logger.debug("Executing distutils command: %s", commands)
44✔
402
            return_code = python_env.run_process_and_wait(commands, project.expand_path("$dir_dist"), out_f)
44✔
403
            if return_code != 0:
44!
404
                raise BuildFailedException(
×
405
                    "Error while executing setup command %s. See %s for full details:\n%s",
406
                    command, out_file, tail_log(out_file))
407

408

409
def execute_twine(project, logger, python_env, command_args, command):
44✔
410
    reports_dir = _prepare_reports_dir(project)
44✔
411
    dist_artifact_dir, artifacts = _get_generated_artifacts(project, logger)
44✔
412

413
    if command == "register":
44✔
414
        for artifact in artifacts:
44✔
415
            out_file = os.path.join(reports_dir,
44✔
416
                                    safe_log_file_name("twine_%s_%s.log" % (command, os.path.basename(artifact))))
417
            _execute_twine(project, logger, python_env,
44✔
418
                           [command] + command_args + [artifact], dist_artifact_dir, out_file)
419
    else:
420
        out_file = os.path.join(reports_dir, safe_log_file_name("twine_%s.log" % command))
44✔
421
        _execute_twine(project, logger, python_env,
44✔
422
                       [command] + command_args + artifacts, dist_artifact_dir, out_file)
423

424

425
def _execute_twine(project, logger, python_env, command, work_dir, out_file):
44✔
426
    with open(out_file, "w") as out_f:
44✔
427
        commands = python_env.executable + ["-m", "twine"] + command
44✔
428
        logger.debug("Executing Twine: %s", commands)
44✔
429
        return_code = python_env.run_process_and_wait(commands, work_dir, out_f)
44✔
430
        if return_code != 0:
44!
431
            raise BuildFailedException(
×
432
                "Error while executing Twine %s. See %s for full details:\n%s", command, out_file, tail_log(out_file))
433

434

435
def strip_comments(requirements):
44✔
436
    return [requirement for requirement in requirements
44✔
437
            if not requirement.strip().startswith("#")]
438

439

440
def quote(requirements):
44✔
441
    return ['"%s"' % requirement for requirement in requirements]
44✔
442

443

444
def is_editable_requirement(requirement):
44✔
445
    return "-e " in requirement or "--editable " in requirement
44✔
446

447

448
def flatten_and_quote(requirements_file):
44✔
449
    with open(requirements_file.name, 'r') as requirements_file:
44✔
450
        requirements = [requirement.strip("\n") for requirement in requirements_file.readlines()]
44✔
451
        requirements = [requirement for requirement in requirements if requirement]
44✔
452
        return quote(strip_comments(requirements))
44✔
453

454

455
def format_single_dependency(dependency):
44✔
456
    return '%s%s' % (dependency.name, pip_utils.build_dependency_version_string(dependency))
44✔
457

458

459
def build_install_dependencies_string(project):
44✔
460
    dependencies = [
44✔
461
        dependency for dependency in project.dependencies
462
        if isinstance(dependency, Dependency) and not dependency.url]
463
    requirements = [
44✔
464
        requirement for requirement in project.dependencies
465
        if isinstance(requirement, RequirementsFile)]
466
    if not dependencies and not requirements:
44✔
467
        return "[]"
44✔
468

469
    dependencies = [format_single_dependency(dependency) for dependency in dependencies]
44✔
470
    requirements = [strip_comments(flatten_and_quote(requirement)) for requirement in requirements]
44✔
471
    flattened_requirements = [dependency for dependency_list in requirements for dependency in dependency_list]
44✔
472
    flattened_requirements_without_editables = [
44✔
473
        requirement for requirement in flattened_requirements if not is_editable_requirement(requirement)]
474

475
    dependencies.extend(flattened_requirements_without_editables)
44✔
476

477
    for i, dep in enumerate(dependencies):
44✔
478
        if dep.startswith('"') and dep.endswith('"'):
44✔
479
            dependencies[i] = dep[1:-1]
44✔
480

481
    return build_string_from_array(dependencies)
44✔
482

483

484
def build_dependency_links_string(project):
44✔
485
    dependency_links = [
44✔
486
        dependency for dependency in project.dependencies
487
        if isinstance(dependency, Dependency) and dependency.url]
488
    requirements = [
44✔
489
        requirement for requirement in project.dependencies
490
        if isinstance(requirement, RequirementsFile)]
491

492
    editable_links_from_requirements = []
44✔
493
    for requirement in requirements:
44✔
494
        editables = [editable for editable in flatten_and_quote(requirement) if is_editable_requirement(editable)]
44✔
495
        editable_links_from_requirements.extend(
44✔
496
            [editable.replace("--editable ", "").replace("-e ", "") for editable in editables])
497

498
    if not dependency_links and not requirements:
44✔
499
        return "[]"
44✔
500

501
    def format_single_dependency(dependency):
44✔
502
        return '%s' % dependency.url
44✔
503

504
    all_dependency_links = [link for link in map(format_single_dependency, dependency_links)]
44✔
505
    all_dependency_links.extend(editable_links_from_requirements)
44✔
506

507
    for i, dep in enumerate(all_dependency_links):
44✔
508
        if dep.startswith('"') and dep.endswith('"'):
44✔
509
            all_dependency_links[i] = dep[1:-1]
44✔
510

511
    return build_string_from_array(all_dependency_links)
44✔
512

513

514
def build_scripts_string(project):
44✔
515
    scripts = [script for script in project.list_scripts()]
44✔
516

517
    scripts_dir = project.get_property("dir_dist_scripts")
44✔
518
    if scripts_dir:
44✔
519
        scripts = list(map(lambda s: '{}/{}'.format(scripts_dir, s), scripts))
44✔
520

521
    return build_string_from_array(scripts)
44✔
522

523

524
def build_data_files_string(project):
44✔
525
    indent = 8
44✔
526
    """
34✔
527
    data_files = [
528
      ('bin', ['foo','bar','hhrm'])
529
    ]
530
    """
531
    data_files = project.files_to_install
44✔
532
    if not len(data_files):
44✔
533
        return '[]'
44✔
534

535
    result = "[\n"
44✔
536

537
    for dataType, dataFiles in data_files:
44✔
538
        result += (" " * (indent + 4)) + "('%s', ['" % dataType
44✔
539
        result += "', '".join(dataFiles)
44✔
540
        result += "']),\n"
44✔
541

542
    result = result[:-2] + "\n"
44✔
543
    result += " " * indent + "]"
44✔
544
    return result
44✔
545

546

547
def build_package_data_string(project):
44✔
548
    package_data = project.package_data
44✔
549
    if not package_data:
44✔
550
        return "{}"
44✔
551

552
    indent = 8
44✔
553

554
    sorted_keys = sorted(project.package_data.keys())
44✔
555

556
    result = "{\n"
44✔
557

558
    for pkgType in sorted_keys:
44✔
559
        result += " " * (indent + 4)
44✔
560
        result += "'%s': " % pkgType
44✔
561
        result += "['"
44✔
562
        result += "', '".join(package_data[pkgType])
44✔
563
        result += "'],\n"
44✔
564

565
    result = result[:-2] + "\n"
44✔
566
    result += " " * indent + "}"
44✔
567

568
    return result
44✔
569

570

571
def build_map_string(m):
44✔
572
    if not m:
44✔
573
        return "{}"
44✔
574

575
    indent = 8
44✔
576

577
    sorted_keys = sorted(m.keys())
44✔
578

579
    result = "{\n"
44✔
580

581
    for k in sorted_keys:
44✔
582
        result += " " * (indent + 4)
44✔
583
        result += "%r: %r,\n" % (k, m[k])
44✔
584

585
    result = result[:-2] + "\n"
44✔
586
    result += " " * indent + "}"
44✔
587

588
    return result
44✔
589

590

591
def build_namespace_packages_string(project):
44✔
592
    return build_string_from_array([pkg for pkg in project.explicit_namespaces])
44✔
593

594

595
def build_packages_string(project):
44✔
596
    return build_string_from_array([pkg for pkg in project.list_packages()])
44✔
597

598

599
def build_modules_string(project):
44✔
600
    return build_string_from_array([mod for mod in project.list_modules()])
44✔
601

602

603
def build_entry_points_string(project):
44✔
604
    console_scripts = project.get_property('distutils_console_scripts')
44✔
605
    entry_points = project.get_property('distutils_entry_points')
44✔
606
    if console_scripts is not None and entry_points is not None:
44✔
607
        raise BuildFailedException("'distutils_console_scripts' cannot be combined with 'distutils_entry_points'")
44✔
608

609
    if entry_points is None:
44✔
610
        entry_points = dict()
44✔
611

612
    if console_scripts is not None:
44✔
613
        entry_points['console_scripts'] = console_scripts
44✔
614

615
    if len(entry_points) == 0:
44✔
616
        return '{}'
44✔
617

618
    indent = 8
44✔
619
    result = "{\n"
44✔
620

621
    for k in sorted(entry_points.keys()):
44✔
622
        result += " " * (indent + 4)
44✔
623
        result += "'%s': %s" % (k, build_string_from_array(as_list(entry_points[k]), indent + 8)) + ",\n"
44✔
624

625
    result = result[:-2] + "\n"
44✔
626
    result += (" " * indent) + "}"
44✔
627

628
    return result
44✔
629

630

631
def build_setup_keywords(project):
44✔
632
    setup_keywords = project.get_property("distutils_setup_keywords")
44✔
633
    if not setup_keywords or not len(setup_keywords):
44✔
634
        return repr("")
44✔
635

636
    if isinstance(setup_keywords, (list, tuple)):
44✔
637
        return repr(" ".join(setup_keywords))
44✔
638

639
    return repr(setup_keywords)
44✔
640

641

642
def build_classifiers_string(project):
44✔
643
    classifiers = project.get_property("distutils_classifiers", [])
44✔
644
    return build_string_from_array(classifiers, indent=12)
44✔
645

646

647
def build_string_from_array(arr, indent=12):
44✔
648
    result = ""
44✔
649

650
    if len(arr) == 1:
44✔
651
        """
652
        arrays with one item contained on one line
653
        """
654
        if len(arr[0]) > 0:
44✔
655
            if is_notstr_iterable(arr[0]):
44✔
656
                result += "[" + build_string_from_array(arr[0], indent + 4) + "]"
44✔
657
            else:
658
                result += "['%s']" % arr[0]
44✔
659
        else:
660
            result = '[[]]'
44✔
661
    elif len(arr) > 1:
44✔
662
        result = "[\n"
44✔
663

664
        for item in arr:
44✔
665
            if is_notstr_iterable(item):
44✔
666
                result += (" " * indent) + build_string_from_array(item, indent + 4) + ",\n"
44✔
667
            else:
668
                result += (" " * indent) + "'" + item + "',\n"
44✔
669
        result = result[:-2] + "\n"
44✔
670
        result += " " * (indent - 4)
44✔
671
        result += "]"
44✔
672
    else:
673
        result = '[]'
44✔
674

675
    return result
44✔
676

677

678
def build_string_from_dict(d, indent=12):
44✔
679
    element_separator = ",\n"
×
680
    element_separator += " " * indent
×
681
    map_elements = []
×
682

683
    for k, v in d.items():
×
684
        map_elements.append("'%s': '%s'" % (k, v))
×
685

686
    result = ""
×
687

688
    if len(map_elements) > 0:
×
689
        result += "{\n"
×
690
        result += " " * indent
×
691
        result += element_separator.join(map_elements)
×
692
        result += "\n"
×
693
        result += " " * (indent - 4)
×
694
        result += "}"
×
695

696
    return result
×
697

698

699
def doc_convert(project, logger):
44✔
700
    import pypandoc
×
701
    readme_file = project.expand_path("$distutils_readme_file")
×
702
    logger.debug("Converting %s into RST format for PyPi documentation...", readme_file)
×
703
    return pypandoc.convert_file(readme_file, "rst")
×
704

705

706
def _expand_leading_tabs(s, indent=4):
44✔
707
    def replace_tabs(match):
44✔
708
        return " " * (len(match.groups(0)) * indent)
44✔
709

710
    return "".join([LEADING_TAB_RE.sub(replace_tabs, line) for line in s.splitlines(True)])
44✔
711

712

713
def _normalize_setup_post_pre_script(s, indent=8):
44✔
714
    indent_str = " " * indent
44✔
715
    return "".join([indent_str + line if len(str.rstrip(line)) > 0 else line for line in
44✔
716
                    dedent(_expand_leading_tabs(s)).splitlines(True)])
717

718

719
def _prepare_reports_dir(project):
44✔
720
    reports_dir = project.expand_path("$dir_reports", "distutils")
44✔
721
    if not os.path.exists(reports_dir):
44✔
722
        os.mkdir(reports_dir)
44✔
723
    return reports_dir
44✔
724

725

726
def _get_description_content_type(project):
44✔
727
    file_type = project.get_property("distutils_readme_file_type")
44✔
728
    file_encoding = project.get_property("distutils_readme_file_encoding")
44✔
729
    file_variant = project.get_property("distutils_readme_file_variant")
44✔
730

731
    if not file_type:
44!
732
        if project.get_property("distutils_readme_description"):
44✔
733
            readme_file_ci = project.get_property("distutils_readme_file").lower()
22✔
734
            if readme_file_ci.endswith("md"):
22!
735
                file_type = "text/markdown"
22✔
736
            elif readme_file_ci.endswith("rst"):
×
737
                file_type = "text/x-rst"
×
738
            else:
739
                file_type = "text/plain"
×
740

741
    if file_encoding:
44!
742
        file_encoding = file_encoding.upper()
×
743

744
    if file_type == "text/markdown":
44✔
745
        if file_variant:
22!
746
            file_variant = file_variant.upper()
×
747

748
    if file_type:
44✔
749
        return "%s%s%s" % (file_type,
22✔
750
                           "; charset=%s" % file_encoding if file_encoding else "",
751
                           "; variant=%s" % file_variant if file_variant else "")
752

753

754
def _get_generated_artifacts(project, logger):
44✔
755
    dist_artifact_dir = project.expand_path("$dir_dist", "dist")
44✔
756

757
    artifacts = [os.path.join(dist_artifact_dir, artifact) for artifact in list(os.walk(dist_artifact_dir))[0][2]]
44✔
758
    return dist_artifact_dir, artifacts
44✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc