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

kivy / python-for-android / 5125700400

pending completion
5125700400

push

github

web-flow
Use build rather than pep517 for building (#2784)

pep517 has been renamed to pyproject-hooks, and as a consequence all of
the deprecated functionality has been removed. build now provides the
functionality required, and since we are only interested in the
metadata, we can leverage a helper function for that. I've also removed
all of the subprocess machinery for calling the wrapping function, since
it appears to not be as noisy as pep517.

938 of 2268 branches covered (41.36%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 1 file covered. (100.0%)

8 existing lines in 2 files now uncovered.

4744 of 7405 relevant lines covered (64.06%)

2.54 hits per line

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

81.18
/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 time
4✔
44
import zipfile
4✔
45
from io import open  # needed for python 2
4✔
46
from urllib.parse import unquote as urlunquote
4✔
47
from urllib.parse import urlparse
4✔
48

49
import toml
4✔
50
import build.util
4✔
51

52

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

74

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

84
        Current supported metadata files that will be extracted:
85

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

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

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

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

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

114
        _extract_metainfo_files_from_package_unsafe(package, output_folder)
4✔
115
    finally:
116
        shutil.rmtree(temp_folder)
4✔
117

118

119
def _get_system_python_executable():
4✔
120
    """ Returns the path the system-wide python binary.
121
        (In case we're running in a virtualenv or venv)
122
    """
123
    # This function is required by get_package_as_folder() to work
124
    # inside a virtualenv, since venv creation will fail with
125
    # the virtualenv's local python binary.
126
    # (venv/virtualenv incompatibility)
127

128
    # Abort if not in virtualenv or venv:
129
    if not hasattr(sys, "real_prefix") and (
4!
130
            not hasattr(sys, "base_prefix") or
131
            os.path.normpath(sys.base_prefix) ==
132
            os.path.normpath(sys.prefix)):
133
        return sys.executable
×
134

135
    # Extract prefix we need to look in:
136
    if hasattr(sys, "real_prefix"):
4!
137
        search_prefix = sys.real_prefix  # virtualenv
×
138
    else:
139
        search_prefix = sys.base_prefix  # venv
4✔
140

141
    def python_binary_from_folder(path):
4✔
142
        def binary_is_usable(python_bin):
4✔
143
            """ Helper function to see if a given binary name refers
144
                to a usable python interpreter binary
145
            """
146

147
            # Abort if path isn't present at all or a directory:
148
            if not os.path.exists(
4✔
149
                os.path.join(path, python_bin)
150
            ) or os.path.isdir(os.path.join(path, python_bin)):
151
                return
4✔
152
            # We should check file not found anyway trying to run it,
153
            # since it might be a dead symlink:
154
            try:
4✔
155
                filenotfounderror = FileNotFoundError
4✔
156
            except NameError:  # Python 2
×
157
                filenotfounderror = OSError
×
158
            try:
4✔
159
                # Run it and see if version output works with no error:
160
                subprocess.check_output([
4✔
161
                    os.path.join(path, python_bin), "--version"
162
                ], stderr=subprocess.STDOUT)
163
                return True
4✔
164
            except (subprocess.CalledProcessError, filenotfounderror):
×
165
                return False
×
166

167
        python_name = "python" + sys.version
4✔
168
        while (not binary_is_usable(python_name) and
4✔
169
               python_name.find(".") > 0):
170
            # Try less specific binary name:
171
            python_name = python_name.rpartition(".")[0]
4✔
172
        if binary_is_usable(python_name):
4✔
173
            return os.path.join(path, python_name)
4✔
174
        return None
4✔
175

176
    # Return from sys.real_prefix if present:
177
    result = python_binary_from_folder(search_prefix)
4✔
178
    if result is not None:
4!
179
        return result
×
180

181
    # Check out all paths in $PATH:
182
    bad_candidates = []
4✔
183
    good_candidates = []
4✔
184
    ever_had_nonvenv_path = False
4✔
185
    ever_had_path_starting_with_prefix = False
4✔
186
    for p in os.environ.get("PATH", "").split(":"):
4✔
187
        # Skip if not possibly the real system python:
188
        if not os.path.normpath(p).startswith(
4✔
189
                os.path.normpath(search_prefix)
190
                ):
191
            continue
4✔
192

193
        ever_had_path_starting_with_prefix = True
4✔
194

195
        # First folders might be virtualenv/venv we want to avoid:
196
        if not ever_had_nonvenv_path:
4!
197
            sep = os.path.sep
4✔
198
            if (
4!
199
                ("system32" not in p.lower() and
200
                 "usr" not in p and
201
                 not p.startswith("/opt/python")) or
202
                {"home", ".tox"}.intersection(set(p.split(sep))) or
203
                "users" in p.lower()
204
            ):
205
                # Doesn't look like bog-standard system path.
206
                if (p.endswith(os.path.sep + "bin") or
4✔
207
                        p.endswith(os.path.sep + "bin" + os.path.sep)):
208
                    # Also ends in "bin" -> likely virtualenv/venv.
209
                    # Add as unfavorable / end of candidates:
210
                    bad_candidates.append(p)
4✔
211
                    continue
4✔
212
            ever_had_nonvenv_path = True
4✔
213

214
        good_candidates.append(p)
4✔
215

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

226
    # Sort candidates by length (to prefer shorter ones):
227
    def candidate_cmp(a, b):
4✔
228
        return len(a) - len(b)
×
229
    good_candidates = sorted(
4✔
230
        good_candidates, key=functools.cmp_to_key(candidate_cmp)
231
    )
232
    bad_candidates = sorted(
4✔
233
        bad_candidates, key=functools.cmp_to_key(candidate_cmp)
234
    )
235

236
    # See if we can now actually find the system python:
237
    for p in good_candidates + bad_candidates:
4!
238
        result = python_binary_from_folder(p)
4✔
239
        if result is not None:
4✔
240
            return result
4✔
241

242
    raise RuntimeError(
×
243
        "failed to locate system python in: {}"
244
        " - checked candidates were: {}, {}"
245
        .format(sys.real_prefix, good_candidates, bad_candidates)
246
    )
247

248

249
def get_package_as_folder(dependency):
4✔
250
    """ This function downloads the given package / dependency and extracts
251
        the raw contents into a folder.
252

253
        Afterwards, it returns a tuple with the type of distribution obtained,
254
        and the temporary folder it extracted to. It is the caller's
255
        responsibility to delete the returned temp folder after use.
256

257
        Examples of returned values:
258

259
        ("source", "/tmp/pythonpackage-venv-e84toiwjw")
260
        ("wheel", "/tmp/pythonpackage-venv-85u78uj")
261

262
        What the distribution type will be depends on what pip decides to
263
        download.
264
    """
265

266
    venv_parent = tempfile.mkdtemp(
4✔
267
        prefix="pythonpackage-venv-"
268
    )
269
    try:
4✔
270
        # Create a venv to install into:
271
        try:
4✔
272
            if int(sys.version.partition(".")[0]) < 3:
4!
273
                # Python 2.x has no venv.
274
                subprocess.check_output([
×
275
                    sys.executable,  # no venv conflict possible,
276
                                     # -> no need to use system python
277
                    "-m", "virtualenv",
278
                    "--python=" + _get_system_python_executable(),
279
                    os.path.join(venv_parent, 'venv')
280
                ], cwd=venv_parent)
281
            else:
282
                # On modern Python 3, use venv.
283
                subprocess.check_output([
4✔
284
                    _get_system_python_executable(), "-m", "venv",
285
                    os.path.join(venv_parent, 'venv')
286
                ], cwd=venv_parent)
287
        except subprocess.CalledProcessError as e:
×
288
            output = e.output.decode('utf-8', 'replace')
×
289
            raise ValueError(
×
290
                'venv creation unexpectedly ' +
291
                'failed. error output: ' + str(output)
292
            )
293
        venv_path = os.path.join(venv_parent, "venv")
4✔
294

295
        # Update pip and wheel in venv for latest feature support:
296
        try:
4✔
297
            filenotfounderror = FileNotFoundError
4✔
298
        except NameError:  # Python 2.
×
299
            filenotfounderror = OSError
×
300
        try:
4✔
301
            subprocess.check_output([
4✔
302
                os.path.join(venv_path, "bin", "pip"),
303
                "install", "-U", "pip", "wheel",
304
            ])
305
        except filenotfounderror:
×
306
            raise RuntimeError(
×
307
                "venv appears to be missing pip. "
308
                "did we fail to use a proper system python??\n"
309
                "system python path detected: {}\n"
310
                "os.environ['PATH']: {}".format(
311
                    _get_system_python_executable(),
312
                    os.environ.get("PATH", "")
313
                )
314
            )
315

316
        # Create download subfolder:
317
        os.mkdir(os.path.join(venv_path, "download"))
4✔
318

319
        # Write a requirements.txt with our package and download:
320
        with open(os.path.join(venv_path, "requirements.txt"),
4✔
321
                  "w", encoding="utf-8"
322
                 ) as f:
323
            def to_unicode(s):  # Needed for Python 2.
4✔
324
                try:
4✔
325
                    return s.decode("utf-8")
4✔
326
                except AttributeError:
4✔
327
                    return s
4✔
328
            f.write(to_unicode(transform_dep_for_pip(dependency)))
4✔
329
        try:
4✔
330
            subprocess.check_output(
4✔
331
                [
332
                    os.path.join(venv_path, "bin", "pip"),
333
                    "download", "--no-deps", "-r", "../requirements.txt",
334
                    "-d", os.path.join(venv_path, "download")
335
                ],
336
                stderr=subprocess.STDOUT,
337
                cwd=os.path.join(venv_path, "download")
338
            )
339
        except subprocess.CalledProcessError as e:
×
340
            raise RuntimeError("package download failed: " + str(e.output))
×
341

342
        if len(os.listdir(os.path.join(venv_path, "download"))) == 0:
4!
343
            # No download. This can happen if the dependency has a condition
344
            # which prohibits install in our environment.
345
            # (the "package ; ... conditional ... " type of condition)
346
            return (None, None)
×
347

348
        # Get the result and make sure it's an extracted directory:
349
        result_folder_or_file = os.path.join(
4✔
350
            venv_path, "download",
351
            os.listdir(os.path.join(venv_path, "download"))[0]
352
        )
353
        dl_type = "source"
4✔
354
        if not os.path.isdir(result_folder_or_file):
4!
355
            # Must be an archive.
356
            if result_folder_or_file.endswith((".zip", ".whl")):
4✔
357
                if result_folder_or_file.endswith(".whl"):
4!
358
                    dl_type = "wheel"
4✔
359
                with zipfile.ZipFile(result_folder_or_file) as f:
4✔
360
                    f.extractall(os.path.join(venv_path,
4✔
361
                                              "download", "extracted"
362
                                             ))
363
                    result_folder_or_file = os.path.join(
4✔
364
                        venv_path, "download", "extracted"
365
                    )
366
            elif result_folder_or_file.find(".tar.") > 0:
4!
367
                # Probably a tarball.
368
                with tarfile.open(result_folder_or_file) as f:
4✔
369
                    f.extractall(os.path.join(venv_path,
4✔
370
                                              "download", "extracted"
371
                                             ))
372
                    result_folder_or_file = os.path.join(
4✔
373
                        venv_path, "download", "extracted"
374
                    )
375
            else:
376
                raise RuntimeError(
×
377
                    "unknown archive or download " +
378
                    "type: " + str(result_folder_or_file)
379
                )
380

381
        # If the result is hidden away in an additional subfolder,
382
        # descend into it:
383
        while os.path.isdir(result_folder_or_file) and \
4✔
384
                len(os.listdir(result_folder_or_file)) == 1 and \
385
                os.path.isdir(os.path.join(
386
                    result_folder_or_file,
387
                    os.listdir(result_folder_or_file)[0]
388
                )):
389
            result_folder_or_file = os.path.join(
4✔
390
                result_folder_or_file,
391
                os.listdir(result_folder_or_file)[0]
392
            )
393

394
        # Copy result to new dedicated folder so we can throw away
395
        # our entire virtualenv nonsense after returning:
396
        result_path = tempfile.mkdtemp()
4✔
397
        shutil.rmtree(result_path)
4✔
398
        shutil.copytree(result_folder_or_file, result_path)
4✔
399
        return (dl_type, result_path)
4✔
400
    finally:
401
        shutil.rmtree(venv_parent)
4✔
402

403

404
def _extract_metainfo_files_from_package_unsafe(
4✔
405
        package,
406
        output_path
407
        ):
408
    # This is the unwrapped function that will
409
    # 1. make lots of stdout/stderr noise
410
    # 2. possibly modify files (if the package source is a local folder)
411
    # Use extract_metainfo_files_from_package_folder instead which avoids
412
    # these issues.
413

414
    clean_up_path = False
4✔
415
    path_type = "source"
4✔
416
    path = parse_as_folder_reference(package)
4✔
417
    if path is None:
4✔
418
        # This is not a path. Download it:
419
        (path_type, path) = get_package_as_folder(package)
4✔
420
        if path_type is None:
4!
421
            # Download failed.
422
            raise ValueError(
×
423
                "cannot get info for this package, " +
424
                "pip says it has no downloads (conditional dependency?)"
425
            )
426
        clean_up_path = True
4✔
427

428
    try:
4✔
429
        metadata_path = None
4✔
430

431
        if path_type != "wheel":
4✔
432
            # Use a build helper function to fetch the metadata directly
433
            metadata = build.util.project_wheel_metadata(path)
4✔
434
            # And write it to a file
435
            metadata_path = os.path.join(output_path, "built_metadata")
4✔
436
            with open(metadata_path, 'w') as f:
4✔
437
                for key in metadata.keys():
4✔
438
                    for value in metadata.get_all(key):
4✔
439
                        f.write("{}: {}\n".format(key, value))
4✔
440
        else:
441
            # This is a wheel, so metadata should be in *.dist-info folder:
442
            metadata_path = os.path.join(
4✔
443
                path,
444
                [f for f in os.listdir(path) if f.endswith(".dist-info")][0],
445
                "METADATA"
446
            )
447

448
        # Store type of metadata source. Can be "wheel", "source" for source
449
        # distribution, and others get_package_as_folder() may support
450
        # in the future.
451
        with open(os.path.join(output_path, "metadata_source"), "w") as f:
4✔
452
            try:
4✔
453
                f.write(path_type)
4✔
454
            except TypeError:  # in python 2 path_type may be str/bytes:
×
455
                f.write(path_type.decode("utf-8", "replace"))
×
456

457
        # Copy the metadata file:
458
        shutil.copyfile(metadata_path, os.path.join(output_path, "METADATA"))
4✔
459
    finally:
460
        if clean_up_path:
4✔
461
            shutil.rmtree(path)
4✔
462

463

464
def is_filesystem_path(dep):
4✔
465
    """ Convenience function around parse_as_folder_reference() to
466
        check if a dependency refers to a folder path or something remote.
467

468
        Returns True if local, False if remote.
469
    """
470
    return (parse_as_folder_reference(dep) is not None)
4✔
471

472

473
def parse_as_folder_reference(dep):
4✔
474
    """ See if a dependency reference refers to a folder path.
475
        If it does, return the folder path (which parses and
476
        resolves file:// urls in the process).
477
        If it doesn't, return None.
478
    """
479
    # Special case: pep508 urls
480
    if dep.find("@") > 0 and (
4✔
481
            (dep.find("@") < dep.find("/") or "/" not in dep) and
482
            (dep.find("@") < dep.find(":") or ":" not in dep)
483
            ):
484
        # This should be a 'pkgname @ https://...' style path, or
485
        # 'pkname @ /local/file/path'.
486
        return parse_as_folder_reference(dep.partition("@")[2].lstrip())
4✔
487

488
    # Check if this is either not an url, or a file URL:
489
    if dep.startswith(("/", "file://")) or (
4✔
490
            dep.find("/") > 0 and
491
            dep.find("://") < 0) or (dep in ["", "."]):
492
        if dep.startswith("file://"):
4✔
493
            dep = urlunquote(urlparse(dep).path)
4✔
494
        return dep
4✔
495
    return None
4✔
496

497

498
def _extract_info_from_package(dependency,
4✔
499
                               extract_type=None,
500
                               debug=False,
501
                               include_build_requirements=False
502
                               ):
503
    """ Internal function to extract metainfo from a package.
504
        Currently supported info types:
505

506
        - name
507
        - dependencies  (a list of dependencies)
508
    """
509
    if debug:
4✔
510
        print("_extract_info_from_package called with "
4✔
511
              "extract_type={} include_build_requirements={}".format(
512
                  extract_type, include_build_requirements,
513
              ))
514
    output_folder = tempfile.mkdtemp(prefix="pythonpackage-metafolder-")
4✔
515
    try:
4✔
516
        extract_metainfo_files_from_package(
4✔
517
            dependency, output_folder, debug=debug
518
        )
519

520
        # Extract the type of data source we used to get the metadata:
521
        with open(os.path.join(output_folder,
4✔
522
                               "metadata_source"), "r") as f:
523
            metadata_source_type = f.read().strip()
4✔
524

525
        # Extract main METADATA file:
526
        with open(os.path.join(output_folder, "METADATA"),
4✔
527
                  "r", encoding="utf-8"
528
                 ) as f:
529
            # Get metadata and cut away description (is after 2 linebreaks)
530
            metadata_entries = f.read().partition("\n\n")[0].splitlines()
4✔
531

532
        if extract_type == "name":
4✔
533
            name = None
4✔
534
            for meta_entry in metadata_entries:
4!
535
                if meta_entry.lower().startswith("name:"):
4✔
536
                    return meta_entry.partition(":")[2].strip()
4✔
537
            if name is None:
×
538
                raise ValueError("failed to obtain package name")
×
539
            return name
×
540
        elif extract_type == "dependencies":
4!
541
            # First, make sure we don't attempt to return build requirements
542
            # for wheels since they usually come without pyproject.toml
543
            # and we haven't implemented another way to get them:
544
            if include_build_requirements and \
4✔
545
                    metadata_source_type == "wheel":
546
                if debug:
4✔
547
                    print("_extract_info_from_package: was called "
4✔
548
                          "with include_build_requirements=True on "
549
                          "package obtained as wheel, raising error...")
550
                raise NotImplementedError(
551
                    "fetching build requirements for "
552
                    "wheels is not implemented"
553
                )
554

555
            # Get build requirements from pyproject.toml if requested:
556
            requirements = []
4✔
557
            if os.path.exists(os.path.join(output_folder,
4!
558
                                           'pyproject.toml')
559
                              ) and include_build_requirements:
560
                # Read build system from pyproject.toml file: (PEP518)
UNCOV
561
                with open(os.path.join(output_folder, 'pyproject.toml')) as f:
×
UNCOV
562
                    build_sys = toml.load(f)['build-system']
×
UNCOV
563
                    if "requires" in build_sys:
×
UNCOV
564
                        requirements += build_sys["requires"]
×
565
            elif include_build_requirements:
4✔
566
                # For legacy packages with no pyproject.toml, we have to
567
                # add setuptools as default build system.
568
                requirements.append("setuptools")
4✔
569

570
            # Add requirements from metadata:
571
            requirements += [
4✔
572
                entry.rpartition("Requires-Dist:")[2].strip()
573
                for entry in metadata_entries
574
                if entry.startswith("Requires-Dist")
575
            ]
576

577
            return list(set(requirements))  # remove duplicates
4✔
578
    finally:
579
        shutil.rmtree(output_folder)
4✔
580

581

582
package_name_cache = dict()
4✔
583

584

585
def get_package_name(dependency,
4✔
586
                     use_cache=True):
587
    def timestamp():
4✔
588
        try:
4✔
589
            return time.monotonic()
4✔
590
        except AttributeError:
×
591
            return time.time()  # Python 2.
×
592
    try:
4✔
593
        value = package_name_cache[dependency]
4✔
594
        if value[0] + 600.0 > timestamp() and use_cache:
4!
595
            return value[1]
4✔
596
    except KeyError:
4✔
597
        pass
4✔
598
    result = _extract_info_from_package(dependency, extract_type="name")
4✔
599
    package_name_cache[dependency] = (timestamp(), result)
4✔
600
    return result
4✔
601

602

603
def get_package_dependencies(package,
4✔
604
                             recursive=False,
605
                             verbose=False,
606
                             include_build_requirements=False):
607
    """ Obtain the dependencies from a package. Please note this
608
        function is possibly SLOW, especially if you enable
609
        the recursive mode.
610
    """
611
    packages_processed = set()
4✔
612
    package_queue = [package]
4✔
613
    reqs = set()
4✔
614
    reqs_as_names = set()
4✔
615
    while len(package_queue) > 0:
4✔
616
        current_queue = package_queue
4✔
617
        package_queue = []
4✔
618
        for package_dep in current_queue:
4!
619
            new_reqs = set()
4✔
620
            if verbose:
4✔
621
                print("get_package_dependencies: resolving dependency "
4✔
622
                      f"to package name: {package_dep}")
623
            package = get_package_name(package_dep)
4✔
624
            if package.lower() in packages_processed:
4!
625
                continue
×
626
            if verbose:
4✔
627
                print("get_package_dependencies: "
4✔
628
                      "processing package: {}".format(package))
629
                print("get_package_dependencies: "
4✔
630
                      "Packages seen so far: {}".format(
631
                          packages_processed
632
                      ))
633
            packages_processed.add(package.lower())
4✔
634

635
            # Use our regular folder processing to examine:
636
            new_reqs = new_reqs.union(_extract_info_from_package(
4✔
637
                package_dep, extract_type="dependencies",
638
                debug=verbose,
639
                include_build_requirements=include_build_requirements,
640
            ))
641

642
            # Process new requirements:
643
            if verbose:
4✔
644
                print('get_package_dependencies: collected '
4✔
645
                      "deps of '{}': {}".format(
646
                          package_dep, str(new_reqs),
647
                      ))
648
            for new_req in new_reqs:
4✔
649
                try:
4✔
650
                    req_name = get_package_name(new_req)
4✔
651
                except ValueError as e:
×
652
                    if new_req.find(";") >= 0:
×
653
                        # Conditional dep where condition isn't met?
654
                        # --> ignore it
655
                        continue
×
656
                    if verbose:
×
657
                        print("get_package_dependencies: " +
×
658
                              "unexpected failure to get name " +
659
                              "of '" + str(new_req) + "': " +
660
                              str(e))
661
                    raise RuntimeError(
×
662
                        "failed to get " +
663
                        "name of dependency: " + str(e)
664
                    )
665
                if req_name.lower() in reqs_as_names:
4!
666
                    continue
×
667
                if req_name.lower() not in packages_processed:
4!
668
                    package_queue.append(new_req)
4✔
669
                reqs.add(new_req)
4✔
670
                reqs_as_names.add(req_name.lower())
4✔
671

672
            # Bail out here if we're not scanning recursively:
673
            if not recursive:
4!
674
                package_queue[:] = []  # wipe queue
4✔
675
                break
4✔
676
    if verbose:
4✔
677
        print("get_package_dependencies: returning result: {}".format(reqs))
4✔
678
    return reqs
4✔
679

680

681
def get_dep_names_of_package(
4✔
682
        package,
683
        keep_version_pins=False,
684
        recursive=False,
685
        verbose=False,
686
        include_build_requirements=False
687
        ):
688
    """ Gets the dependencies from the package in the given folder,
689
        then attempts to deduce the actual package name resulting
690
        from each dependency line, stripping away everything else.
691
    """
692

693
    # First, obtain the dependencies:
694
    dependencies = get_package_dependencies(
4✔
695
        package, recursive=recursive, verbose=verbose,
696
        include_build_requirements=include_build_requirements,
697
    )
698
    if verbose:
4✔
699
        print("get_dep_names_of_package_folder: " +
4✔
700
              "processing dependency list to names: " +
701
              str(dependencies))
702

703
    # Transform dependencies to their stripped down names:
704
    # (they can still have version pins/restrictions, conditionals, ...)
705
    dependency_names = set()
4✔
706
    for dep in dependencies:
4✔
707
        # If we are supposed to keep exact version pins, extract first:
708
        pin_to_append = ""
4✔
709
        if keep_version_pins and "(==" in dep and dep.endswith(")"):
4✔
710
            # This is a dependency of the format: 'pkg (==1.0)'
711
            pin_to_append = "==" + dep.rpartition("==")[2][:-1]
4✔
712
        elif keep_version_pins and "==" in dep and not dep.endswith(")"):
4!
713
            # This is a dependency of the format: 'pkg==1.0'
714
            pin_to_append = "==" + dep.rpartition("==")[2]
×
715
        # Now get true (and e.g. case-corrected) dependency name:
716
        dep_name = get_package_name(dep) + pin_to_append
4✔
717
        dependency_names.add(dep_name)
4✔
718
    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