Skip to content

Commit 5b7daf6

Browse files
authored
fix: Use the fallback PKG-INFO file if the one generated manually is incomplete (#523)
* Handle UNKNOWN and version 0.0.0 source packages * Add missing requirements file * Added missing requirements file for other tests that validate output * Fix no binary downloads on build deps * Update docstring to correctly assert the behaviour of the default check * Make lowercase package name compare
1 parent bb9e31e commit 5b7daf6

File tree

5 files changed

+240
-7
lines changed

5 files changed

+240
-7
lines changed

aws_lambda_builders/workflows/python_pip/packager.py

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import subprocess
88
import sys
99
from email.parser import FeedParser
10+
from typing import Tuple
1011

1112
from aws_lambda_builders.architecture import ARM64, X86_64
1213
from aws_lambda_builders.utils import extract_tarfile
@@ -603,12 +604,30 @@ def _get_pkg_info_filepath(self, package_dir):
603604
# This might be a pep 517 package in which case this PKG-INFO file
604605
# should be available right in the top level directory of the sdist
605606
# in the case where the egg_info command fails.
606-
LOG.debug("Using fallback location for PKG-INFO file in package directory: %s", package_dir)
607-
pkg_info_path = self._osutils.joinpath(package_dir, "PKG-INFO")
607+
pkg_info_path = self._get_fallback_pkg_info_filepath(package_dir)
608608
if not self._osutils.file_exists(pkg_info_path):
609609
raise UnsupportedPackageError(self._osutils.basename(package_dir))
610610
return pkg_info_path
611611

612+
def _get_fallback_pkg_info_filepath(self, package_dir: str) -> str:
613+
"""
614+
Gets the string path of the fallback PKG-INFO that is generated by pip
615+
616+
Parameters
617+
----------
618+
package_dir: str
619+
The path of the current working directory that has the package
620+
621+
Returns
622+
-------
623+
str
624+
Path to a potential PKG-INFO
625+
"""
626+
LOG.debug("Using fallback location for PKG-INFO file in package directory: %s", package_dir)
627+
pkg_info_path = self._osutils.joinpath(package_dir, "PKG-INFO")
628+
629+
return pkg_info_path
630+
612631
def _unpack_sdist_into_dir(self, sdist_path, unpack_dir):
613632
if sdist_path.endswith(".zip"):
614633
self._osutils.extract_zipfile(sdist_path, unpack_dir)
@@ -620,13 +639,79 @@ def _unpack_sdist_into_dir(self, sdist_path, unpack_dir):
620639
contents = self._osutils.get_directory_contents(unpack_dir)
621640
return self._osutils.joinpath(unpack_dir, contents[0])
622641

623-
def get_package_name_and_version(self, sdist_path):
642+
def _get_name_version(self, pkg_info_filepath: str) -> Tuple[str, str]:
643+
"""
644+
Extracts the name and the version from the PKG-INFO metadata file
645+
646+
Parameters
647+
----------
648+
pkg_info_filepath: str
649+
The path to the PKG-INFO file to get data from
650+
651+
Returns
652+
-------
653+
Tuple[str, str]
654+
A tuple containing the name and version
655+
"""
656+
metadata = self._parse_pkg_info_file(pkg_info_filepath)
657+
return (metadata["Name"], metadata["Version"])
658+
659+
def _is_default_setuptools_values(self, name: str, version: str) -> bool:
660+
"""
661+
Checks if the name or the version are the default values that are assigned by setuptools
662+
663+
Parameters
664+
----------
665+
name: str
666+
The name of the package
667+
version: str
668+
The version of the package as a string
669+
670+
Returns
671+
-------
672+
bool
673+
True if either name or version are the default values
674+
"""
675+
# default values logic:
676+
# https://github.com/pypa/setuptools/blob/6083e18f4afc40316c0112134c205c336afbcdfd/setuptools/_distutils/dist.py#L1185-L1189
677+
return name.lower() == "unknown" or version == "0.0.0"
678+
679+
def get_package_name_and_version(self, sdist_path: str) -> Tuple[str, str]:
680+
"""
681+
Gets the package's name and version from the metadata file
682+
683+
Parameters
684+
----------
685+
sdist_path: str
686+
The string path of the downloaded source distribution artifact
687+
688+
Returns
689+
-------
690+
Tuple[str, str]
691+
A tuple of the name and version of the package
692+
"""
624693
with self._osutils.tempdir() as tempdir:
625694
package_dir = self._unpack_sdist_into_dir(sdist_path, tempdir)
695+
696+
# get the name and version from the result setup.py
626697
pkg_info_filepath = self._get_pkg_info_filepath(package_dir)
627-
metadata = self._parse_pkg_info_file(pkg_info_filepath)
628-
name = metadata["Name"]
629-
version = metadata["Version"]
698+
name, version = self._get_name_version(pkg_info_filepath)
699+
700+
# return values if it is not the default values
701+
if not self._is_default_setuptools_values(name, version):
702+
return name, version
703+
704+
# see if we can get the fallback PKG_INFO file from the sdist
705+
fallback_pkg_info_fp = self._get_fallback_pkg_info_filepath(package_dir)
706+
707+
if self._osutils.file_exists(fallback_pkg_info_fp):
708+
# use the fallback values instead of the ones we generated
709+
fallback_name, fallback_version = self._get_name_version(fallback_pkg_info_fp)
710+
711+
if not self._is_default_setuptools_values(fallback_name, fallback_version):
712+
name = fallback_name
713+
version = fallback_version
714+
630715
return name, version
631716

632717

tests/functional/workflows/python_pip/test_packager.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,23 @@ def test_pkg_info_fallback_fails_raises_error(self, osutils, sdist_reader):
997997
with pytest.raises(UnsupportedPackageError):
998998
sdist_reader.get_package_name_and_version(filepath)
999999

1000+
def test_pkg_info_uses_fallback(self, osutils, sdist_reader):
1001+
# similar to test_cant_get_egg_info_filename
1002+
# but checks for UNKNOWN and/or 0.0.0 before
1003+
# using fallback
1004+
fallback_name = "mypkg"
1005+
fallback_version = "1.0.0"
1006+
1007+
setup_py = self._SETUP_PY % ("", "UNKNOWN", "0.0.0")
1008+
fallback_pkg_info = "Name: %s\nVersion: %s\n" % (fallback_name, fallback_version)
1009+
1010+
with osutils.tempdir() as tempdir:
1011+
filepath = self._write_fake_sdist(setup_py, tempdir, "tar.gz", fallback_pkg_info)
1012+
name, version = sdist_reader.get_package_name_and_version(filepath)
1013+
1014+
assert name == fallback_name
1015+
assert version == fallback_version
1016+
10001017

10011018
class TestPackage(object):
10021019
def test_same_pkg_sdist_and_wheel_collide(self, osutils, sdist_builder):

tests/integration/workflows/python_pip/test_python_pip.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ def setUp(self):
3939
self.manifest_path_valid = os.path.join(self.TEST_DATA_FOLDER, "requirements-numpy.txt")
4040
self.manifest_path_invalid = os.path.join(self.TEST_DATA_FOLDER, "requirements-invalid.txt")
4141
self.manifest_path_sdist = os.path.join(self.TEST_DATA_FOLDER, "requirements-wrapt.txt")
42+
self.manifest_path_inflate = os.path.join(self.TEST_DATA_FOLDER, "requirements-inflate.txt")
4243

4344
self.test_data_files = {
4445
"__init__.py",
4546
"main.py",
4647
"requirements-invalid.txt",
4748
"requirements-numpy.txt",
4849
"requirements-wrapt.txt",
50+
"requirements-inflate.txt",
4951
"local-dependencies",
5052
}
5153

@@ -234,6 +236,20 @@ def test_must_resolve_local_dependency(self):
234236
for f in expected_files:
235237
self.assertIn(f, output_files)
236238

239+
def test_must_resolve_unknown_package_name(self):
240+
self.builder.build(
241+
self.source_dir,
242+
self.artifacts_dir,
243+
self.scratch_dir,
244+
self.manifest_path_inflate,
245+
runtime=self.runtime,
246+
experimental_flags=self.experimental_flags,
247+
)
248+
expected_files = self.test_data_files.union(["inflate64", "inflate64.libs", "inflate64-0.1.4.dist-info"])
249+
output_files = set(os.listdir(self.artifacts_dir))
250+
for f in expected_files:
251+
self.assertIn(f, output_files)
252+
237253
def test_must_fail_to_resolve_dependencies(self):
238254
with self.assertRaises(WorkflowFailedError) as ctx:
239255
self.builder.build(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
inflate64==0.1.4 --no-binary=:inflate64:

tests/unit/workflows/python_pip/test_packager.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import sys
22
from collections import namedtuple
33
from unittest import TestCase, mock
4+
from unittest.mock import patch
45

56
import pytest
7+
from parameterized import parameterized
68

79
from aws_lambda_builders.architecture import ARM64, X86_64
810
from aws_lambda_builders.workflows.python_pip.utils import OSUtils
911
from aws_lambda_builders.workflows.python_pip.compat import pip_no_compile_c_env_vars
1012
from aws_lambda_builders.workflows.python_pip.compat import pip_no_compile_c_shim
11-
from aws_lambda_builders.workflows.python_pip.packager import DependencyBuilder
13+
from aws_lambda_builders.workflows.python_pip.packager import DependencyBuilder, SDistMetadataFetcher
1214
from aws_lambda_builders.workflows.python_pip.packager import PythonPipDependencyBuilder
1315
from aws_lambda_builders.workflows.python_pip.packager import Package
1416
from aws_lambda_builders.workflows.python_pip.packager import PipRunner
@@ -343,3 +345,115 @@ def test_check_pip_runner_string_pip(self):
343345

344346
pip_runner_string = fake_osutils.popens[0][0][0][2].split(";")[-1:][0]
345347
self.assertIn("main", pip_runner_string)
348+
349+
350+
class TestSDistMetadataFetcher(TestCase):
351+
@parameterized.expand(
352+
[
353+
(False,),
354+
(True,),
355+
]
356+
)
357+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._unpack_sdist_into_dir")
358+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_pkg_info_filepath")
359+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_name_version")
360+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_fallback_pkg_info_filepath")
361+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._is_default_setuptools_values")
362+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.file_exists")
363+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.tempdir")
364+
def test_get_package_name_version_fails_fallback(
365+
self,
366+
fallback_file_exists,
367+
tempdir_mock,
368+
file_exists_mock,
369+
is_default_values_mock,
370+
get_fallback_mock,
371+
get_name_ver_mock,
372+
get_pkg_mock,
373+
unpack_mock,
374+
):
375+
"""
376+
Tests if both our generated PKG-INFO and PKG-INFO are missing/invalid
377+
"""
378+
file_exists_mock.return_value = fallback_file_exists
379+
is_default_values_mock.return_value = True
380+
381+
original_name = "UNKNOWN"
382+
original_version = "5.5.0"
383+
384+
get_name_ver_mock.side_effect = [(original_name, original_version), ("UNKNOWN", "0.0.0")]
385+
386+
sdist = SDistMetadataFetcher(OSUtils)
387+
name, version = sdist.get_package_name_and_version(mock.Mock())
388+
389+
self.assertEqual(name, original_name)
390+
self.assertEqual(version, original_version)
391+
392+
@parameterized.expand(
393+
[
394+
(("UNKNOWN", "1.2.3"),),
395+
(("unknown", "1.2.3"),),
396+
(("foobar", "0.0.0"),),
397+
]
398+
)
399+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._unpack_sdist_into_dir")
400+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_pkg_info_filepath")
401+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_name_version")
402+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_fallback_pkg_info_filepath")
403+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.file_exists")
404+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.tempdir")
405+
def test_get_package_name_version_fallback(
406+
self,
407+
name_version,
408+
tempdir_mock,
409+
file_exists_mock,
410+
get_fallback_mock,
411+
get_name_ver_mock,
412+
get_pkg_mock,
413+
unpack_mock,
414+
):
415+
"""
416+
Tests if we have UNKNOWN and if we use the fall back values
417+
"""
418+
fallback_name = "fallback"
419+
fallback_version = "1.0.0"
420+
421+
get_name_ver_mock.side_effect = [name_version, (fallback_name, fallback_version)]
422+
423+
sdist = SDistMetadataFetcher(OSUtils)
424+
name, version = sdist.get_package_name_and_version(mock.Mock())
425+
426+
self.assertEqual(name, fallback_name)
427+
self.assertEqual(version, fallback_version)
428+
429+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._unpack_sdist_into_dir")
430+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_pkg_info_filepath")
431+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_name_version")
432+
@patch("aws_lambda_builders.workflows.python_pip.packager.SDistMetadataFetcher._get_fallback_pkg_info_filepath")
433+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.file_exists")
434+
@patch("aws_lambda_builders.workflows.python_pip.utils.OSUtils.tempdir")
435+
def test_get_package_name_version(
436+
self,
437+
tempdir_mock,
438+
file_exists_mock,
439+
get_fallback_mock,
440+
get_name_ver_mock,
441+
get_pkg_mock,
442+
unpack_mock,
443+
):
444+
"""
445+
Tests return original results
446+
"""
447+
not_default_name = "real"
448+
not_default_version = "1.2.3"
449+
450+
fallback_name = "fallback"
451+
fallback_version = "1.0.0"
452+
453+
get_name_ver_mock.side_effect = [(not_default_name, not_default_version), (fallback_name, fallback_version)]
454+
455+
sdist = SDistMetadataFetcher(OSUtils)
456+
name, version = sdist.get_package_name_and_version(mock.Mock())
457+
458+
self.assertEqual(name, not_default_name)
459+
self.assertEqual(version, not_default_version)

0 commit comments

Comments
 (0)