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

kivy / python-for-android / 26682386815

30 May 2026 11:14AM UTC coverage: 62.67% (-1.2%) from 63.887%
26682386815

Pull #3278

github

web-flow
Merge 77aee3d95 into 74b559a3c
Pull Request #3278: Handling system bars and Edge-to-Edge enforcement (android 15+)

1832 of 3194 branches covered (57.36%)

Branch coverage included in aggregate %.

5407 of 8357 relevant lines covered (64.7%)

3.88 hits per line

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

84.79
/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 pythonforandroid.pythonpackage import get_package_name
8
       print(get_package_name("pillow"))
9
       # Outputs: "Pillow" (note the spelling!)
10

11
       # Getting package dependencies:
12
       from pythonforandroid.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 pythonforandroid.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
6✔
37
import os
6✔
38
import shutil
6✔
39
import subprocess
6✔
40
import sys
6✔
41
import tarfile
6✔
42
import tempfile
6✔
43
import time
6✔
44
from urllib.parse import unquote as urlunquote
6✔
45
from urllib.parse import urlparse
6✔
46
import zipfile
6✔
47

48
import toml
6✔
49
import build.util
6✔
50

51
from pythonforandroid.util import rmdir, ensure_dir
6✔
52

53

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

75

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

85
        Current supported metadata files that will be extracted:
86

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

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

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

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

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

115
        _extract_metainfo_files_from_package_unsafe(package, output_folder)
6✔
116
    finally:
117
        rmdir(temp_folder)
6✔
118

119

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

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

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

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

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

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

173
    # Return from sys.real_prefix if present:
174
    result = python_binary_from_folder(search_prefix)
6✔
175
    if result is not None:
6!
176
        return result
×
177

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

190
        ever_had_path_starting_with_prefix = True
6✔
191

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

211
        good_candidates.append(p)
6✔
212

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

223
    # Sort candidates by length (to prefer shorter ones):
224
    def candidate_cmp(a, b):
6✔
225
        return len(a) - len(b)
1✔
226
    good_candidates = sorted(
6✔
227
        good_candidates, key=functools.cmp_to_key(candidate_cmp)
228
    )
229
    bad_candidates = sorted(
6✔
230
        bad_candidates, key=functools.cmp_to_key(candidate_cmp)
231
    )
232

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

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

245

246
def get_package_as_folder(dependency):
6✔
247
    """ This function downloads the given package / dependency and extracts
248
        the raw contents into a folder.
249

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

254
        Examples of returned values:
255

256
        ("source", "/tmp/pythonpackage-venv-e84toiwjw")
257
        ("wheel", "/tmp/pythonpackage-venv-85u78uj")
258

259
        What the distribution type will be depends on what pip decides to
260
        download.
261
    """
262

263
    venv_parent = tempfile.mkdtemp(
6✔
264
        prefix="pythonpackage-venv-"
265
    )
266
    try:
6✔
267
        # Create a venv to install into:
268
        try:
6✔
269
            subprocess.check_output([
6✔
270
                _get_system_python_executable(), "-m", "venv",
271
                os.path.join(venv_parent, 'venv')
272
            ], cwd=venv_parent)
273
        except subprocess.CalledProcessError as e:
×
274
            output = e.output.decode('utf-8', 'replace')
×
275
            raise ValueError(
×
276
                'venv creation unexpectedly ' +
277
                'failed. error output: ' + str(output)
278
            )
279
        venv_path = os.path.join(venv_parent, "venv")
6✔
280

281
        # Update pip and wheel in venv for latest feature support:
282
        try:
6✔
283
            subprocess.check_output([
6✔
284
                os.path.join(venv_path, "bin", "pip"),
285
                "install", "-U", "pip", "wheel",
286
            ])
287
        except FileNotFoundError:
×
288
            raise RuntimeError(
×
289
                "venv appears to be missing pip. "
290
                "did we fail to use a proper system python??\n"
291
                "system python path detected: {}\n"
292
                "os.environ['PATH']: {}".format(
293
                    _get_system_python_executable(),
294
                    os.environ.get("PATH", "")
295
                )
296
            )
297

298
        # Create download subfolder:
299
        ensure_dir(os.path.join(venv_path, "download"))
6✔
300

301
        # Write a requirements.txt with our package and download:
302
        with open(os.path.join(venv_path, "requirements.txt"),
6✔
303
                  "w", encoding="utf-8"
304
                 ) as f:
305
            f.write(transform_dep_for_pip(dependency))
6✔
306
        try:
6✔
307
            subprocess.check_output(
6✔
308
                [
309
                    os.path.join(venv_path, "bin", "pip"),
310
                    "download", "--no-deps", "-r", "../requirements.txt",
311
                    "-d", os.path.join(venv_path, "download")
312
                ],
313
                stderr=subprocess.STDOUT,
314
                cwd=os.path.join(venv_path, "download")
315
            )
316
        except subprocess.CalledProcessError as e:
×
317
            raise RuntimeError("package download failed: " + str(e.output))
×
318

319
        if len(os.listdir(os.path.join(venv_path, "download"))) == 0:
6!
320
            # No download. This can happen if the dependency has a condition
321
            # which prohibits install in our environment.
322
            # (the "package ; ... conditional ... " type of condition)
323
            return (None, None)
×
324

325
        # Get the result and make sure it's an extracted directory:
326
        result_folder_or_file = os.path.join(
6✔
327
            venv_path, "download",
328
            os.listdir(os.path.join(venv_path, "download"))[0]
329
        )
330
        dl_type = "source"
6✔
331
        if not os.path.isdir(result_folder_or_file):
6!
332
            # Must be an archive.
333
            if result_folder_or_file.endswith((".zip", ".whl")):
6✔
334
                if result_folder_or_file.endswith(".whl"):
6!
335
                    dl_type = "wheel"
6✔
336
                with zipfile.ZipFile(result_folder_or_file) as f:
6✔
337
                    f.extractall(os.path.join(venv_path,
6✔
338
                                              "download", "extracted"
339
                                             ))
340
                    result_folder_or_file = os.path.join(
6✔
341
                        venv_path, "download", "extracted"
342
                    )
343
            elif result_folder_or_file.find(".tar.") > 0:
6!
344
                # Probably a tarball.
345
                with tarfile.open(result_folder_or_file) as f:
6✔
346
                    f.extractall(os.path.join(venv_path,
6✔
347
                                              "download", "extracted"
348
                                             ))
349
                    result_folder_or_file = os.path.join(
6✔
350
                        venv_path, "download", "extracted"
351
                    )
352
            else:
353
                raise RuntimeError(
×
354
                    "unknown archive or download " +
355
                    "type: " + str(result_folder_or_file)
356
                )
357

358
        # If the result is hidden away in an additional subfolder,
359
        # descend into it:
360
        while os.path.isdir(result_folder_or_file) and \
6✔
361
                len(os.listdir(result_folder_or_file)) == 1 and \
362
                os.path.isdir(os.path.join(
363
                    result_folder_or_file,
364
                    os.listdir(result_folder_or_file)[0]
365
                )):
366
            result_folder_or_file = os.path.join(
6✔
367
                result_folder_or_file,
368
                os.listdir(result_folder_or_file)[0]
369
            )
370

371
        # Copy result to new dedicated folder so we can throw away
372
        # our entire virtualenv nonsense after returning:
373
        result_path = tempfile.mkdtemp()
6✔
374
        rmdir(result_path)
6✔
375
        shutil.copytree(result_folder_or_file, result_path)
6✔
376
        return (dl_type, result_path)
6✔
377
    finally:
378
        rmdir(venv_parent)
6✔
379

380

381
def _extract_metainfo_files_from_package_unsafe(
6✔
382
        package,
383
        output_path
384
        ):
385
    # This is the unwrapped function that will
386
    # 1. make lots of stdout/stderr noise
387
    # 2. possibly modify files (if the package source is a local folder)
388
    # Use extract_metainfo_files_from_package_folder instead which avoids
389
    # these issues.
390

391
    clean_up_path = False
6✔
392
    path_type = "source"
6✔
393
    path = parse_as_folder_reference(package)
6✔
394
    if path is None:
6✔
395
        # This is not a path. Download it:
396
        (path_type, path) = get_package_as_folder(package)
6✔
397
        if path_type is None:
6!
398
            # Download failed.
399
            raise ValueError(
×
400
                "cannot get info for this package, " +
401
                "pip says it has no downloads (conditional dependency?)"
402
            )
403
        clean_up_path = True
6✔
404

405
    try:
6✔
406
        metadata_path = None
6✔
407

408
        if path_type != "wheel":
6✔
409
            # Use a build helper function to fetch the metadata directly
410
            metadata = build.util.project_wheel_metadata(path)
6✔
411
            # And write it to a file
412
            metadata_path = os.path.join(output_path, "built_metadata")
6✔
413
            with open(metadata_path, 'w') as f:
6✔
414
                for key in metadata.keys():
6✔
415
                    for value in metadata.get_all(key):
6✔
416
                        f.write("{}: {}\n".format(key, value))
6✔
417
        else:
418
            # This is a wheel, so metadata should be in *.dist-info folder:
419
            metadata_path = os.path.join(
6✔
420
                path,
421
                [f for f in os.listdir(path) if f.endswith(".dist-info")][0],
422
                "METADATA"
423
            )
424

425
        # Store type of metadata source. Can be "wheel", "source" for source
426
        # distribution, and others get_package_as_folder() may support
427
        # in the future.
428
        with open(os.path.join(output_path, "metadata_source"), "w") as f:
6✔
429
            f.write(path_type)
6✔
430

431
        # Copy the metadata file:
432
        shutil.copyfile(metadata_path, os.path.join(output_path, "METADATA"))
6✔
433
    finally:
434
        if clean_up_path:
6✔
435
            rmdir(path)
6✔
436

437

438
def is_filesystem_path(dep):
6✔
439
    """ Convenience function around parse_as_folder_reference() to
440
        check if a dependency refers to a folder path or something remote.
441

442
        Returns True if local, False if remote.
443
    """
444
    return (parse_as_folder_reference(dep) is not None)
6✔
445

446

447
def parse_as_folder_reference(dep):
6✔
448
    """ See if a dependency reference refers to a folder path.
449
        If it does, return the folder path (which parses and
450
        resolves file:// urls in the process).
451
        If it doesn't, return None.
452
    """
453
    # Special case: pep508 urls
454
    if dep.find("@") > 0 and (
6✔
455
            (dep.find("@") < dep.find("/") or "/" not in dep) and
456
            (dep.find("@") < dep.find(":") or ":" not in dep)
457
            ):
458
        # This should be a 'pkgname @ https://...' style path, or
459
        # 'pkname @ /local/file/path'.
460
        return parse_as_folder_reference(dep.partition("@")[2].lstrip())
6✔
461

462
    # Check if this is either not an url, or a file URL:
463
    if dep.startswith(("/", "file://")) or (
6✔
464
            dep.find("/") > 0 and
465
            dep.find("://") < 0) or (dep in ["", "."]):
466
        if dep.startswith("file://"):
6✔
467
            dep = urlunquote(urlparse(dep).path)
6✔
468
        return dep
6✔
469
    return None
6✔
470

471

472
def _extract_info_from_package(dependency,
6✔
473
                               extract_type=None,
474
                               debug=False,
475
                               include_build_requirements=False
476
                               ):
477
    """ Internal function to extract metainfo from a package.
478
        Currently supported info types:
479

480
        - name
481
        - dependencies  (a list of dependencies)
482
    """
483
    if debug:
6✔
484
        print("_extract_info_from_package called with "
6✔
485
              "extract_type={} include_build_requirements={}".format(
486
                  extract_type, include_build_requirements,
487
              ))
488
    output_folder = tempfile.mkdtemp(prefix="pythonpackage-metafolder-")
6✔
489
    try:
6✔
490
        extract_metainfo_files_from_package(
6✔
491
            dependency, output_folder, debug=debug
492
        )
493

494
        # Extract the type of data source we used to get the metadata:
495
        with open(os.path.join(output_folder,
6✔
496
                               "metadata_source"), "r") as f:
497
            metadata_source_type = f.read().strip()
6✔
498

499
        # Extract main METADATA file:
500
        with open(os.path.join(output_folder, "METADATA"),
6✔
501
                  "r", encoding="utf-8"
502
                 ) as f:
503
            # Get metadata and cut away description (is after 2 linebreaks)
504
            metadata_entries = f.read().partition("\n\n")[0].splitlines()
6✔
505

506
        if extract_type == "name":
6✔
507
            name = None
6✔
508
            for meta_entry in metadata_entries:
6!
509
                if meta_entry.lower().startswith("name:"):
6✔
510
                    return meta_entry.partition(":")[2].strip()
6✔
511
            if name is None:
×
512
                raise ValueError("failed to obtain package name")
×
513
            return name
×
514
        elif extract_type == "dependencies":
6!
515
            # First, make sure we don't attempt to return build requirements
516
            # for wheels since they usually come without pyproject.toml
517
            # and we haven't implemented another way to get them:
518
            if include_build_requirements and \
6✔
519
                    metadata_source_type == "wheel":
520
                if debug:
6✔
521
                    print("_extract_info_from_package: was called "
6✔
522
                          "with include_build_requirements=True on "
523
                          "package obtained as wheel, raising error...")
524
                raise NotImplementedError(
525
                    "fetching build requirements for "
526
                    "wheels is not implemented"
527
                )
528

529
            # Get build requirements from pyproject.toml if requested:
530
            requirements = []
6✔
531
            pyproject_toml_path = os.path.join(output_folder, 'pyproject.toml')
6✔
532
            if os.path.exists(pyproject_toml_path) and include_build_requirements:
6!
533
                # Read build system from pyproject.toml file: (PEP518)
534
                with open(pyproject_toml_path) as f:
×
535
                    build_sys = toml.load(f)['build-system']
×
536
                    if "requires" in build_sys:
×
537
                        requirements += build_sys["requires"]
×
538
            elif include_build_requirements:
6✔
539
                # For legacy packages with no pyproject.toml, we have to
540
                # add setuptools as default build system.
541
                requirements.append("setuptools")
6✔
542

543
            # Add requirements from metadata:
544
            requirements += [
6✔
545
                entry.rpartition("Requires-Dist:")[2].strip()
546
                for entry in metadata_entries
547
                if entry.startswith("Requires-Dist")
548
            ]
549

550
            return list(set(requirements))  # remove duplicates
6✔
551
    finally:
552
        rmdir(output_folder)
6✔
553

554

555
package_name_cache = dict()
6✔
556

557

558
def get_package_name(dependency,
6✔
559
                     use_cache=True):
560
    try:
6✔
561
        value = package_name_cache[dependency]
6✔
562
        if value[0] + 600.0 > time.monotonic() and use_cache:
6!
563
            return value[1]
6✔
564
    except KeyError:
6✔
565
        pass
6✔
566
    result = _extract_info_from_package(dependency, extract_type="name")
6✔
567
    package_name_cache[dependency] = (time.monotonic(), result)
6✔
568
    return result
6✔
569

570

571
def get_package_dependencies(package,
6✔
572
                             recursive=False,
573
                             verbose=False,
574
                             include_build_requirements=False):
575
    """ Obtain the dependencies from a package. Please note this
576
        function is possibly SLOW, especially if you enable
577
        the recursive mode.
578
    """
579
    packages_processed = set()
6✔
580
    package_queue = [package]
6✔
581
    reqs = set()
6✔
582
    reqs_as_names = set()
6✔
583
    while len(package_queue) > 0:
6✔
584
        current_queue = package_queue
6✔
585
        package_queue = []
6✔
586
        for package_dep in current_queue:
6!
587
            new_reqs = set()
6✔
588
            if verbose:
6✔
589
                print("get_package_dependencies: resolving dependency "
6✔
590
                      f"to package name: {package_dep}")
591
            package = get_package_name(package_dep)
6✔
592
            if package.lower() in packages_processed:
6!
593
                continue
×
594
            if verbose:
6✔
595
                print("get_package_dependencies: "
6✔
596
                      "processing package: {}".format(package))
597
                print("get_package_dependencies: "
6✔
598
                      "Packages seen so far: {}".format(
599
                          packages_processed
600
                      ))
601
            packages_processed.add(package.lower())
6✔
602

603
            # Use our regular folder processing to examine:
604
            new_reqs = new_reqs.union(_extract_info_from_package(
6✔
605
                package_dep, extract_type="dependencies",
606
                debug=verbose,
607
                include_build_requirements=include_build_requirements,
608
            ))
609

610
            # Process new requirements:
611
            if verbose:
6✔
612
                print('get_package_dependencies: collected '
6✔
613
                      "deps of '{}': {}".format(
614
                          package_dep, str(new_reqs),
615
                      ))
616
            for new_req in new_reqs:
6✔
617
                try:
6✔
618
                    req_name = get_package_name(new_req)
6✔
619
                except ValueError as e:
×
620
                    if new_req.find(";") >= 0:
×
621
                        # Conditional dep where condition isn't met?
622
                        # --> ignore it
623
                        continue
×
624
                    if verbose:
×
625
                        print("get_package_dependencies: " +
×
626
                              "unexpected failure to get name " +
627
                              "of '" + str(new_req) + "': " +
628
                              str(e))
629
                    raise RuntimeError(
×
630
                        "failed to get " +
631
                        "name of dependency: " + str(e)
632
                    )
633
                if req_name.lower() in reqs_as_names:
6!
634
                    continue
×
635
                if req_name.lower() not in packages_processed:
6!
636
                    package_queue.append(new_req)
6✔
637
                reqs.add(new_req)
6✔
638
                reqs_as_names.add(req_name.lower())
6✔
639

640
            # Bail out here if we're not scanning recursively:
641
            if not recursive:
6!
642
                package_queue[:] = []  # wipe queue
6✔
643
                break
6✔
644
    if verbose:
6✔
645
        print("get_package_dependencies: returning result: {}".format(reqs))
6✔
646
    return reqs
6✔
647

648

649
def get_dep_names_of_package(
6✔
650
        package,
651
        keep_version_pins=False,
652
        recursive=False,
653
        verbose=False,
654
        include_build_requirements=False
655
        ):
656
    """ Gets the dependencies from the package in the given folder,
657
        then attempts to deduce the actual package name resulting
658
        from each dependency line, stripping away everything else.
659
    """
660

661
    # First, obtain the dependencies:
662
    dependencies = get_package_dependencies(
6✔
663
        package, recursive=recursive, verbose=verbose,
664
        include_build_requirements=include_build_requirements,
665
    )
666
    if verbose:
6✔
667
        print("get_dep_names_of_package_folder: " +
6✔
668
              "processing dependency list to names: " +
669
              str(dependencies))
670

671
    # Transform dependencies to their stripped down names:
672
    # (they can still have version pins/restrictions, conditionals, ...)
673
    dependency_names = set()
6✔
674
    for dep in dependencies:
6✔
675
        # If we are supposed to keep exact version pins, extract first:
676
        pin_to_append = ""
6✔
677
        if keep_version_pins and "(==" in dep and dep.endswith(")"):
6!
678
            # This is a dependency of the format: 'pkg (==1.0)'
679
            pin_to_append = "==" + dep.rpartition("==")[2][:-1]
×
680
        elif keep_version_pins and "==" in dep and not dep.endswith(")"):
6✔
681
            # This is a dependency of the format: 'pkg==1.0'
682
            pin_to_append = "==" + dep.rpartition("==")[2]
6✔
683
        # Now get true (and e.g. case-corrected) dependency name:
684
        dep_name = get_package_name(dep) + pin_to_append
6✔
685
        dependency_names.add(dep_name)
6✔
686
    return dependency_names
6✔
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