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

kivy / python-for-android / 3770697291

pending completion
3770697291

push

github

GitHub
Merge pull request #2718 from kivy/release-2022.12.20

877 of 2011 branches covered (43.61%)

Branch coverage included in aggregate %.

38 of 86 new or added lines in 16 files covered. (44.19%)

10 existing lines in 4 files now uncovered.

4515 of 6886 relevant lines covered (65.57%)

2.59 hits per line

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

57.23
/pythonforandroid/pythonpackage.py
1
""" This module offers highlevel functions to get package metadata
2
    like the METADATA file, the name, or a list of dependencies.
3

4
    Usage examples:
5

6
       # Getting package name from pip reference:
7
       from pytonforandroid.pythonpackage import get_package_name
8
       print(get_package_name("pillow"))
9
       # Outputs: "Pillow" (note the spelling!)
10

11
       # Getting package dependencies:
12
       from pytonforandroid.pythonpackage import get_package_dependencies
13
       print(get_package_dependencies("pep517"))
14
       # Outputs: "['pytoml']"
15

16
       # Get package name from arbitrary package source:
17
       from pytonforandroid.pythonpackage import get_package_name
18
       print(get_package_name("/some/local/project/folder/"))
19
       # Outputs package name
20

21
    NOTE:
22

23
    Yes, this module doesn't fit well into python-for-android, but this
24
    functionality isn't available ANYWHERE ELSE, and upstream (pip, ...)
25
    currently has no interest in taking this over, so it has no other place
26
    to go.
27
    (Unless someone reading this puts it into yet another packaging lib)
28

29
    Reference discussion/upstream inclusion attempt:
30

31
    https://github.com/pypa/packaging-problems/issues/247
32

33
"""
34

35

36
import functools
4✔
37
import os
4✔
38
import shutil
4✔
39
import subprocess
4✔
40
import sys
4✔
41
import tarfile
4✔
42
import tempfile
4✔
43
import textwrap
4✔
44
import time
4✔
45
import zipfile
4✔
46
from io import open  # needed for python 2
4✔
47
from urllib.parse import unquote as urlunquote
4✔
48
from urllib.parse import urlparse
4✔
49

50
import toml
4✔
51
from pep517.envbuild import BuildEnvironment
4✔
52
from pep517.wrappers import Pep517HookCaller
4✔
53

54

55
def transform_dep_for_pip(dependency):
4✔
56
    if dependency.find("@") > 0 and (
4✔
57
            dependency.find("@") < dependency.find("://") or
58
            "://" not in dependency
59
            ):
60
        # WORKAROUND FOR UPSTREAM BUG:
61
        # https://github.com/pypa/pip/issues/6097
62
        # (Please REMOVE workaround once that is fixed & released upstream!)
63
        #
64
        # Basically, setup_requires() can contain a format pip won't install
65
        # from a requirements.txt (PEP 508 URLs).
66
        # To avoid this, translate to an #egg= reference:
67
        if dependency.endswith("#"):
4✔
68
            dependency = dependency[:-1]
4✔
69
        url = (dependency.partition("@")[2].strip().partition("#egg")[0] +
4✔
70
               "#egg=" +
71
               dependency.partition("@")[0].strip()
72
              )
73
        return url
4✔
74
    return dependency
4✔
75

76

77
def extract_metainfo_files_from_package(
4✔
78
        package,
79
        output_folder,
80
        debug=False
81
        ):
82
    """ Extracts metdata files from the given package to the given folder,
83
        which may be referenced in any way that is permitted in
84
        a requirements.txt file or install_requires=[] listing.
85

86
        Current supported metadata files that will be extracted:
87

88
        - pytoml.yml  (only if package wasn't obtained as wheel)
89
        - METADATA
90
    """
91

92
    if package is None:
4!
93
        raise ValueError("package cannot be None")
×
94

95
    if not os.path.exists(output_folder) or os.path.isfile(output_folder):
4!
96
        raise ValueError("output folder needs to be existing folder")
×
97

98
    if debug:
4✔
99
        print("extract_metainfo_files_from_package: extracting for " +
4✔
100
              "package: " + str(package))
101

102
    # A temp folder for making a package copy in case it's a local folder,
103
    # because extracting metadata might modify files
104
    # (creating sdists/wheels...)
105
    temp_folder = tempfile.mkdtemp(prefix="pythonpackage-package-copy-")
4✔
106
    try:
4✔
107
        # Package is indeed a folder! Get a temp copy to work on:
108
        if is_filesystem_path(package):
4✔
109
            shutil.copytree(
4✔
110
                parse_as_folder_reference(package),
111
                os.path.join(temp_folder, "package"),
112
                ignore=shutil.ignore_patterns(".tox")
113
            )
114
            package = os.path.join(temp_folder, "package")
4✔
115

116
        # Because PEP517 can be noisy and contextlib.redirect_* fails to
117
        # contain it, we will run the actual analysis in a separate process:
118
        try:
4✔
119
            subprocess.check_output([
4✔
120
                sys.executable,
121
                "-c",
122
                "import importlib\n"
123
                "import json\n"
124
                "import os\n"
125
                "import sys\n"
126
                "sys.path = [os.path.dirname(sys.argv[3])] + sys.path\n"
127
                "m = importlib.import_module(\n"
128
                "    os.path.basename(sys.argv[3]).partition('.')[0]\n"
129
                ")\n"
130
                "m._extract_metainfo_files_from_package_unsafe("
131
                "    sys.argv[1],"
132
                "    sys.argv[2],"
133
                ")",
134
                package, output_folder, os.path.abspath(__file__)],
135
                stderr=subprocess.STDOUT,  # make sure stderr is muted.
136
                cwd=os.path.join(os.path.dirname(__file__), "..")
137
            )
UNCOV
138
        except subprocess.CalledProcessError as e:
×
UNCOV
139
            output = e.output.decode("utf-8", "replace")
×
UNCOV
140
            if debug:
×
141
                print("Got error obtaining meta info.")
×
142
                print("Detail output:")
×
143
                print(output)
×
144
                print("End of Detail output.")
×
UNCOV
145
            raise ValueError(
×
146
                "failed to obtain meta info - "
147
                "is '{}' a valid package? "
148
                "Detailed output:\n{}".format(package, output)
149
                )
150
    finally:
151
        shutil.rmtree(temp_folder)
4✔
152

153

154
def _get_system_python_executable():
4✔
155
    """ Returns the path the system-wide python binary.
156
        (In case we're running in a virtualenv or venv)
157
    """
158
    # This function is required by get_package_as_folder() to work
159
    # inside a virtualenv, since venv creation will fail with
160
    # the virtualenv's local python binary.
161
    # (venv/virtualenv incompatibility)
162

163
    # Abort if not in virtualenv or venv:
164
    if not hasattr(sys, "real_prefix") and (
4!
165
            not hasattr(sys, "base_prefix") or
166
            os.path.normpath(sys.base_prefix) ==
167
            os.path.normpath(sys.prefix)):
168
        return sys.executable
×
169

170
    # Extract prefix we need to look in:
171
    if hasattr(sys, "real_prefix"):
4!
172
        search_prefix = sys.real_prefix  # virtualenv
×
173
    else:
174
        search_prefix = sys.base_prefix  # venv
4✔
175

176
    def python_binary_from_folder(path):
4✔
177
        def binary_is_usable(python_bin):
4✔
178
            """ Helper function to see if a given binary name refers
179
                to a usable python interpreter binary
180
            """
181

182
            # Abort if path isn't present at all or a directory:
183
            if not os.path.exists(
4✔
184
                os.path.join(path, python_bin)
185
            ) or os.path.isdir(os.path.join(path, python_bin)):
186
                return
4✔
187
            # We should check file not found anyway trying to run it,
188
            # since it might be a dead symlink:
189
            try:
4✔
190
                filenotfounderror = FileNotFoundError
4✔
191
            except NameError:  # Python 2
×
192
                filenotfounderror = OSError
×
193
            try:
4✔
194
                # Run it and see if version output works with no error:
195
                subprocess.check_output([
4✔
196
                    os.path.join(path, python_bin), "--version"
197
                ], stderr=subprocess.STDOUT)
198
                return True
4✔
199
            except (subprocess.CalledProcessError, filenotfounderror):
×
200
                return False
×
201

202
        python_name = "python" + sys.version
4✔
203
        while (not binary_is_usable(python_name) and
4✔
204
               python_name.find(".") > 0):
205
            # Try less specific binary name:
206
            python_name = python_name.rpartition(".")[0]
4✔
207
        if binary_is_usable(python_name):
4✔
208
            return os.path.join(path, python_name)
4✔
209
        return None
4✔
210

211
    # Return from sys.real_prefix if present:
212
    result = python_binary_from_folder(search_prefix)
4✔
213
    if result is not None:
4!
214
        return result
×
215

216
    # Check out all paths in $PATH:
217
    bad_candidates = []
4✔
218
    good_candidates = []
4✔
219
    ever_had_nonvenv_path = False
4✔
220
    ever_had_path_starting_with_prefix = False
4✔
221
    for p in os.environ.get("PATH", "").split(":"):
4✔
222
        # Skip if not possibly the real system python:
223
        if not os.path.normpath(p).startswith(
4✔
224
                os.path.normpath(search_prefix)
225
                ):
226
            continue
4✔
227

228
        ever_had_path_starting_with_prefix = True
4✔
229

230
        # First folders might be virtualenv/venv we want to avoid:
231
        if not ever_had_nonvenv_path:
4!
232
            sep = os.path.sep
4✔
233
            if (
4!
234
                ("system32" not in p.lower() and
235
                 "usr" not in p and
236
                 not p.startswith("/opt/python")) or
237
                {"home", ".tox"}.intersection(set(p.split(sep))) or
238
                "users" in p.lower()
239
            ):
240
                # Doesn't look like bog-standard system path.
241
                if (p.endswith(os.path.sep + "bin") or
4✔
242
                        p.endswith(os.path.sep + "bin" + os.path.sep)):
243
                    # Also ends in "bin" -> likely virtualenv/venv.
244
                    # Add as unfavorable / end of candidates:
245
                    bad_candidates.append(p)
4✔
246
                    continue
4✔
247
            ever_had_nonvenv_path = True
4✔
248

249
        good_candidates.append(p)
4✔
250

251
    # If we have a bad env with PATH not containing any reference to our
252
    # real python (travis, why would you do that to me?) then just guess
253
    # based from the search prefix location itself:
254
    if not ever_had_path_starting_with_prefix:
4!
255
        # ... and yes we're scanning all the folders for that, it's dumb
256
        # but i'm not aware of a better way: (@JonasT)
257
        for root, dirs, files in os.walk(search_prefix, topdown=True):
×
258
            for name in dirs:
×
259
                bad_candidates.append(os.path.join(root, name))
×
260

261
    # Sort candidates by length (to prefer shorter ones):
262
    def candidate_cmp(a, b):
4✔
263
        return len(a) - len(b)
×
264
    good_candidates = sorted(
4✔
265
        good_candidates, key=functools.cmp_to_key(candidate_cmp)
266
    )
267
    bad_candidates = sorted(
4✔
268
        bad_candidates, key=functools.cmp_to_key(candidate_cmp)
269
    )
270

271
    # See if we can now actually find the system python:
272
    for p in good_candidates + bad_candidates:
4!
273
        result = python_binary_from_folder(p)
4✔
274
        if result is not None:
4✔
275
            return result
4✔
276

277
    raise RuntimeError(
×
278
        "failed to locate system python in: {}"
279
        " - checked candidates were: {}, {}"
280
        .format(sys.real_prefix, good_candidates, bad_candidates)
281
    )
282

283

284
def get_package_as_folder(dependency):
4✔
285
    """ This function downloads the given package / dependency and extracts
286
        the raw contents into a folder.
287

288
        Afterwards, it returns a tuple with the type of distribution obtained,
289
        and the temporary folder it extracted to. It is the caller's
290
        responsibility to delete the returned temp folder after use.
291

292
        Examples of returned values:
293

294
        ("source", "/tmp/pythonpackage-venv-e84toiwjw")
295
        ("wheel", "/tmp/pythonpackage-venv-85u78uj")
296

297
        What the distribution type will be depends on what pip decides to
298
        download.
299
    """
300

301
    venv_parent = tempfile.mkdtemp(
×
302
        prefix="pythonpackage-venv-"
303
    )
304
    try:
×
305
        # Create a venv to install into:
306
        try:
×
307
            if int(sys.version.partition(".")[0]) < 3:
×
308
                # Python 2.x has no venv.
309
                subprocess.check_output([
×
310
                    sys.executable,  # no venv conflict possible,
311
                                     # -> no need to use system python
312
                    "-m", "virtualenv",
313
                    "--python=" + _get_system_python_executable(),
314
                    os.path.join(venv_parent, 'venv')
315
                ], cwd=venv_parent)
316
            else:
317
                # On modern Python 3, use venv.
318
                subprocess.check_output([
×
319
                    _get_system_python_executable(), "-m", "venv",
320
                    os.path.join(venv_parent, 'venv')
321
                ], cwd=venv_parent)
322
        except subprocess.CalledProcessError as e:
×
323
            output = e.output.decode('utf-8', 'replace')
×
324
            raise ValueError(
×
325
                'venv creation unexpectedly ' +
326
                'failed. error output: ' + str(output)
327
            )
328
        venv_path = os.path.join(venv_parent, "venv")
×
329

330
        # Update pip and wheel in venv for latest feature support:
331
        try:
×
332
            filenotfounderror = FileNotFoundError
×
333
        except NameError:  # Python 2.
×
334
            filenotfounderror = OSError
×
335
        try:
×
336
            subprocess.check_output([
×
337
                os.path.join(venv_path, "bin", "pip"),
338
                "install", "-U", "pip", "wheel",
339
            ])
340
        except filenotfounderror:
×
341
            raise RuntimeError(
×
342
                "venv appears to be missing pip. "
343
                "did we fail to use a proper system python??\n"
344
                "system python path detected: {}\n"
345
                "os.environ['PATH']: {}".format(
346
                    _get_system_python_executable(),
347
                    os.environ.get("PATH", "")
348
                )
349
            )
350

351
        # Create download subfolder:
352
        os.mkdir(os.path.join(venv_path, "download"))
×
353

354
        # Write a requirements.txt with our package and download:
355
        with open(os.path.join(venv_path, "requirements.txt"),
×
356
                  "w", encoding="utf-8"
357
                 ) as f:
358
            def to_unicode(s):  # Needed for Python 2.
×
359
                try:
×
360
                    return s.decode("utf-8")
×
361
                except AttributeError:
×
362
                    return s
×
363
            f.write(to_unicode(transform_dep_for_pip(dependency)))
×
364
        try:
×
365
            subprocess.check_output(
×
366
                [
367
                    os.path.join(venv_path, "bin", "pip"),
368
                    "download", "--no-deps", "-r", "../requirements.txt",
369
                    "-d", os.path.join(venv_path, "download")
370
                ],
371
                stderr=subprocess.STDOUT,
372
                cwd=os.path.join(venv_path, "download")
373
            )
374
        except subprocess.CalledProcessError as e:
×
375
            raise RuntimeError("package download failed: " + str(e.output))
×
376

377
        if len(os.listdir(os.path.join(venv_path, "download"))) == 0:
×
378
            # No download. This can happen if the dependency has a condition
379
            # which prohibits install in our environment.
380
            # (the "package ; ... conditional ... " type of condition)
381
            return (None, None)
×
382

383
        # Get the result and make sure it's an extracted directory:
384
        result_folder_or_file = os.path.join(
×
385
            venv_path, "download",
386
            os.listdir(os.path.join(venv_path, "download"))[0]
387
        )
388
        dl_type = "source"
×
389
        if not os.path.isdir(result_folder_or_file):
×
390
            # Must be an archive.
391
            if result_folder_or_file.endswith((".zip", ".whl")):
×
392
                if result_folder_or_file.endswith(".whl"):
×
393
                    dl_type = "wheel"
×
394
                with zipfile.ZipFile(result_folder_or_file) as f:
×
395
                    f.extractall(os.path.join(venv_path,
×
396
                                              "download", "extracted"
397
                                             ))
398
                    result_folder_or_file = os.path.join(
×
399
                        venv_path, "download", "extracted"
400
                    )
401
            elif result_folder_or_file.find(".tar.") > 0:
×
402
                # Probably a tarball.
403
                with tarfile.open(result_folder_or_file) as f:
×
404
                    f.extractall(os.path.join(venv_path,
×
405
                                              "download", "extracted"
406
                                             ))
407
                    result_folder_or_file = os.path.join(
×
408
                        venv_path, "download", "extracted"
409
                    )
410
            else:
411
                raise RuntimeError(
×
412
                    "unknown archive or download " +
413
                    "type: " + str(result_folder_or_file)
414
                )
415

416
        # If the result is hidden away in an additional subfolder,
417
        # descend into it:
418
        while os.path.isdir(result_folder_or_file) and \
×
419
                len(os.listdir(result_folder_or_file)) == 1 and \
420
                os.path.isdir(os.path.join(
421
                    result_folder_or_file,
422
                    os.listdir(result_folder_or_file)[0]
423
                )):
424
            result_folder_or_file = os.path.join(
×
425
                result_folder_or_file,
426
                os.listdir(result_folder_or_file)[0]
427
            )
428

429
        # Copy result to new dedicated folder so we can throw away
430
        # our entire virtualenv nonsense after returning:
431
        result_path = tempfile.mkdtemp()
×
432
        shutil.rmtree(result_path)
×
433
        shutil.copytree(result_folder_or_file, result_path)
×
434
        return (dl_type, result_path)
×
435
    finally:
436
        shutil.rmtree(venv_parent)
×
437

438

439
def _extract_metainfo_files_from_package_unsafe(
4✔
440
        package,
441
        output_path
442
        ):
443
    # This is the unwrapped function that will
444
    # 1. make lots of stdout/stderr noise
445
    # 2. possibly modify files (if the package source is a local folder)
446
    # Use extract_metainfo_files_from_package_folder instead which avoids
447
    # these issues.
448

449
    clean_up_path = False
×
450
    path_type = "source"
×
451
    path = parse_as_folder_reference(package)
×
452
    if path is None:
×
453
        # This is not a path. Download it:
454
        (path_type, path) = get_package_as_folder(package)
×
455
        if path_type is None:
×
456
            # Download failed.
457
            raise ValueError(
×
458
                "cannot get info for this package, " +
459
                "pip says it has no downloads (conditional dependency?)"
460
            )
461
        clean_up_path = True
×
462

463
    try:
×
464
        build_requires = []
×
465
        metadata_path = None
×
466

467
        if path_type != "wheel":
×
468
            # We need to process this first to get the metadata.
469

470
            # Ensure pyproject.toml is available (pep517 expects it)
471
            if not os.path.exists(os.path.join(path, "pyproject.toml")):
×
472
                with open(os.path.join(path, "pyproject.toml"), "w") as f:
×
473
                    f.write(textwrap.dedent(u"""\
×
474
                    [build-system]
475
                    requires = ["setuptools", "wheel"]
476
                    build-backend = "setuptools.build_meta"
477
                    """))
478

479
            # Copy the pyproject.toml:
480
            shutil.copyfile(
×
481
                os.path.join(path, 'pyproject.toml'),
482
                os.path.join(output_path, 'pyproject.toml')
483
            )
484

485
            # Get build backend and requirements from pyproject.toml:
486
            with open(os.path.join(path, 'pyproject.toml')) as f:
×
487
                build_sys = toml.load(f)['build-system']
×
488
                backend = build_sys["build-backend"]
×
489
                build_requires.extend(build_sys["requires"])
×
490

491
            # Get a virtualenv with build requirements and get all metadata:
492
            env = BuildEnvironment()
×
493
            metadata = None
×
494
            with env:
×
495
                hooks = Pep517HookCaller(path, backend)
×
496
                env.pip_install(
×
497
                    [transform_dep_for_pip(req) for req in build_requires]
498
                )
499
                reqs = hooks.get_requires_for_build_wheel({})
×
500
                env.pip_install([transform_dep_for_pip(req) for req in reqs])
×
501
                try:
×
502
                    metadata = hooks.prepare_metadata_for_build_wheel(path)
×
503
                except Exception:  # sadly, pep517 has no good error here
×
504
                    pass
×
505
            if metadata is not None:
×
506
                metadata_path = os.path.join(
×
507
                    path, metadata, "METADATA"
508
                )
509
        else:
510
            # This is a wheel, so metadata should be in *.dist-info folder:
511
            metadata_path = os.path.join(
×
512
                path,
513
                [f for f in os.listdir(path) if f.endswith(".dist-info")][0],
514
                "METADATA"
515
            )
516

517
        # Store type of metadata source. Can be "wheel", "source" for source
518
        # distribution, and others get_package_as_folder() may support
519
        # in the future.
520
        with open(os.path.join(output_path, "metadata_source"), "w") as f:
×
521
            try:
×
522
                f.write(path_type)
×
523
            except TypeError:  # in python 2 path_type may be str/bytes:
×
524
                f.write(path_type.decode("utf-8", "replace"))
×
525

526
        # Copy the metadata file:
527
        shutil.copyfile(metadata_path, os.path.join(output_path, "METADATA"))
×
528
    finally:
529
        if clean_up_path:
×
530
            shutil.rmtree(path)
×
531

532

533
def is_filesystem_path(dep):
4✔
534
    """ Convenience function around parse_as_folder_reference() to
535
        check if a dependency refers to a folder path or something remote.
536

537
        Returns True if local, False if remote.
538
    """
539
    return (parse_as_folder_reference(dep) is not None)
4✔
540

541

542
def parse_as_folder_reference(dep):
4✔
543
    """ See if a dependency reference refers to a folder path.
544
        If it does, return the folder path (which parses and
545
        resolves file:// urls in the process).
546
        If it doesn't, return None.
547
    """
548
    # Special case: pep508 urls
549
    if dep.find("@") > 0 and (
4✔
550
            (dep.find("@") < dep.find("/") or "/" not in dep) and
551
            (dep.find("@") < dep.find(":") or ":" not in dep)
552
            ):
553
        # This should be a 'pkgname @ https://...' style path, or
554
        # 'pkname @ /local/file/path'.
555
        return parse_as_folder_reference(dep.partition("@")[2].lstrip())
4✔
556

557
    # Check if this is either not an url, or a file URL:
558
    if dep.startswith(("/", "file://")) or (
4✔
559
            dep.find("/") > 0 and
560
            dep.find("://") < 0) or (dep in ["", "."]):
561
        if dep.startswith("file://"):
4✔
562
            dep = urlunquote(urlparse(dep).path)
4✔
563
        return dep
4✔
564
    return None
4✔
565

566

567
def _extract_info_from_package(dependency,
4✔
568
                               extract_type=None,
569
                               debug=False,
570
                               include_build_requirements=False
571
                               ):
572
    """ Internal function to extract metainfo from a package.
573
        Currently supported info types:
574

575
        - name
576
        - dependencies  (a list of dependencies)
577
    """
578
    if debug:
4✔
579
        print("_extract_info_from_package called with "
4✔
580
              "extract_type={} include_build_requirements={}".format(
581
                  extract_type, include_build_requirements,
582
              ))
583
    output_folder = tempfile.mkdtemp(prefix="pythonpackage-metafolder-")
4✔
584
    try:
4✔
585
        extract_metainfo_files_from_package(
4✔
586
            dependency, output_folder, debug=debug
587
        )
588

589
        # Extract the type of data source we used to get the metadata:
590
        with open(os.path.join(output_folder,
4✔
591
                               "metadata_source"), "r") as f:
592
            metadata_source_type = f.read().strip()
4✔
593

594
        # Extract main METADATA file:
595
        with open(os.path.join(output_folder, "METADATA"),
4✔
596
                  "r", encoding="utf-8"
597
                 ) as f:
598
            # Get metadata and cut away description (is after 2 linebreaks)
599
            metadata_entries = f.read().partition("\n\n")[0].splitlines()
4✔
600

601
        if extract_type == "name":
4✔
602
            name = None
4✔
603
            for meta_entry in metadata_entries:
4!
604
                if meta_entry.lower().startswith("name:"):
4✔
605
                    return meta_entry.partition(":")[2].strip()
4✔
606
            if name is None:
×
607
                raise ValueError("failed to obtain package name")
×
608
            return name
×
609
        elif extract_type == "dependencies":
4!
610
            # First, make sure we don't attempt to return build requirements
611
            # for wheels since they usually come without pyproject.toml
612
            # and we haven't implemented another way to get them:
613
            if include_build_requirements and \
4✔
614
                    metadata_source_type == "wheel":
615
                if debug:
4✔
616
                    print("_extract_info_from_package: was called "
4✔
617
                          "with include_build_requirements=True on "
618
                          "package obtained as wheel, raising error...")
619
                raise NotImplementedError(
620
                    "fetching build requirements for "
621
                    "wheels is not implemented"
622
                )
623

624
            # Get build requirements from pyproject.toml if requested:
625
            requirements = []
4✔
626
            if os.path.exists(os.path.join(output_folder,
4✔
627
                                           'pyproject.toml')
628
                              ) and include_build_requirements:
629
                # Read build system from pyproject.toml file: (PEP518)
630
                with open(os.path.join(output_folder, 'pyproject.toml')) as f:
4✔
631
                    build_sys = toml.load(f)['build-system']
4✔
632
                    if "requires" in build_sys:
4!
633
                        requirements += build_sys["requires"]
4✔
634
            elif include_build_requirements:
4!
635
                # For legacy packages with no pyproject.toml, we have to
636
                # add setuptools as default build system.
637
                requirements.append("setuptools")
×
638

639
            # Add requirements from metadata:
640
            requirements += [
4✔
641
                entry.rpartition("Requires-Dist:")[2].strip()
642
                for entry in metadata_entries
643
                if entry.startswith("Requires-Dist")
644
            ]
645

646
            return list(set(requirements))  # remove duplicates
4✔
647
    finally:
648
        shutil.rmtree(output_folder)
4✔
649

650

651
package_name_cache = dict()
4✔
652

653

654
def get_package_name(dependency,
4✔
655
                     use_cache=True):
656
    def timestamp():
4✔
657
        try:
4✔
658
            return time.monotonic()
4✔
659
        except AttributeError:
×
660
            return time.time()  # Python 2.
×
661
    try:
4✔
662
        value = package_name_cache[dependency]
4✔
663
        if value[0] + 600.0 > timestamp() and use_cache:
4!
664
            return value[1]
4✔
665
    except KeyError:
4✔
666
        pass
4✔
667
    result = _extract_info_from_package(dependency, extract_type="name")
4✔
668
    package_name_cache[dependency] = (timestamp(), result)
4✔
669
    return result
4✔
670

671

672
def get_package_dependencies(package,
4✔
673
                             recursive=False,
674
                             verbose=False,
675
                             include_build_requirements=False):
676
    """ Obtain the dependencies from a package. Please note this
677
        function is possibly SLOW, especially if you enable
678
        the recursive mode.
679
    """
680
    packages_processed = set()
4✔
681
    package_queue = [package]
4✔
682
    reqs = set()
4✔
683
    reqs_as_names = set()
4✔
684
    while len(package_queue) > 0:
4✔
685
        current_queue = package_queue
4✔
686
        package_queue = []
4✔
687
        for package_dep in current_queue:
4!
688
            new_reqs = set()
4✔
689
            if verbose:
4✔
690
                print("get_package_dependencies: resolving dependency "
4✔
691
                      f"to package name: {package_dep}")
692
            package = get_package_name(package_dep)
4✔
693
            if package.lower() in packages_processed:
4!
694
                continue
×
695
            if verbose:
4✔
696
                print("get_package_dependencies: "
4✔
697
                      "processing package: {}".format(package))
698
                print("get_package_dependencies: "
4✔
699
                      "Packages seen so far: {}".format(
700
                          packages_processed
701
                      ))
702
            packages_processed.add(package.lower())
4✔
703

704
            # Use our regular folder processing to examine:
705
            new_reqs = new_reqs.union(_extract_info_from_package(
4✔
706
                package_dep, extract_type="dependencies",
707
                debug=verbose,
708
                include_build_requirements=include_build_requirements,
709
            ))
710

711
            # Process new requirements:
712
            if verbose:
4✔
713
                print('get_package_dependencies: collected '
4✔
714
                      "deps of '{}': {}".format(
715
                          package_dep, str(new_reqs),
716
                      ))
717
            for new_req in new_reqs:
4✔
718
                try:
4✔
719
                    req_name = get_package_name(new_req)
4✔
UNCOV
720
                except ValueError as e:
×
UNCOV
721
                    if new_req.find(";") >= 0:
×
722
                        # Conditional dep where condition isn't met?
723
                        # --> ignore it
UNCOV
724
                        continue
×
725
                    if verbose:
×
726
                        print("get_package_dependencies: " +
×
727
                              "unexpected failure to get name " +
728
                              "of '" + str(new_req) + "': " +
729
                              str(e))
730
                    raise RuntimeError(
×
731
                        "failed to get " +
732
                        "name of dependency: " + str(e)
733
                    )
734
                if req_name.lower() in reqs_as_names:
4!
735
                    continue
×
736
                if req_name.lower() not in packages_processed:
4!
737
                    package_queue.append(new_req)
4✔
738
                reqs.add(new_req)
4✔
739
                reqs_as_names.add(req_name.lower())
4✔
740

741
            # Bail out here if we're not scanning recursively:
742
            if not recursive:
4!
743
                package_queue[:] = []  # wipe queue
4✔
744
                break
4✔
745
    if verbose:
4✔
746
        print("get_package_dependencies: returning result: {}".format(reqs))
4✔
747
    return reqs
4✔
748

749

750
def get_dep_names_of_package(
4✔
751
        package,
752
        keep_version_pins=False,
753
        recursive=False,
754
        verbose=False,
755
        include_build_requirements=False
756
        ):
757
    """ Gets the dependencies from the package in the given folder,
758
        then attempts to deduce the actual package name resulting
759
        from each dependency line, stripping away everything else.
760
    """
761

762
    # First, obtain the dependencies:
763
    dependencies = get_package_dependencies(
4✔
764
        package, recursive=recursive, verbose=verbose,
765
        include_build_requirements=include_build_requirements,
766
    )
767
    if verbose:
4✔
768
        print("get_dep_names_of_package_folder: " +
4✔
769
              "processing dependency list to names: " +
770
              str(dependencies))
771

772
    # Transform dependencies to their stripped down names:
773
    # (they can still have version pins/restrictions, conditionals, ...)
774
    dependency_names = set()
4✔
775
    for dep in dependencies:
4✔
776
        # If we are supposed to keep exact version pins, extract first:
777
        pin_to_append = ""
4✔
778
        if keep_version_pins and "(==" in dep and dep.endswith(")"):
4✔
779
            # This is a dependency of the format: 'pkg (==1.0)'
780
            pin_to_append = "==" + dep.rpartition("==")[2][:-1]
4✔
781
        elif keep_version_pins and "==" in dep and not dep.endswith(")"):
4!
782
            # This is a dependency of the format: 'pkg==1.0'
783
            pin_to_append = "==" + dep.rpartition("==")[2]
×
784
        # Now get true (and e.g. case-corrected) dependency name:
785
        dep_name = get_package_name(dep) + pin_to_append
4✔
786
        dependency_names.add(dep_name)
4✔
787
    return dependency_names
4✔
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

© 2025 Coveralls, Inc