From e80b43306f0dfe2247257476440e1e2d9861c9f9 Mon Sep 17 00:00:00 2001 From: layday Date: Fri, 2 Sep 2022 23:01:39 +0300 Subject: [PATCH 1/5] tests: add circular check dependency test --- tests/test_projectbuilder.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py index a05b6cab..5988cbb0 100644 --- a/tests/test_projectbuilder.py +++ b/tests/test_projectbuilder.py @@ -45,6 +45,10 @@ def from_name(cls, name): return RecursiveMockDistribution() elif name == 'prerelease_dep': return PrereleaseMockDistribution() + elif name == 'circular_dep': + return CircularMockDistribution() + elif name == 'nested_circular_dep': + return NestedCircularMockDistribution() raise importlib_metadata.PackageNotFoundError @@ -96,6 +100,28 @@ def read_text(self, filename): """.strip() +class CircularMockDistribution(MockDistribution): + def read_text(self, filename): + if filename == 'METADATA': + return """ +Metadata-Version: 2.2 +Name: circular_dep +Version: 1.0.0 +Requires-Dist: nested_circular_dep +""".strip() + + +class NestedCircularMockDistribution(MockDistribution): + def read_text(self, filename): + if filename == 'METADATA': + return """ +Metadata-Version: 2.2 +Name: nested_circular_dep +Version: 1.0.0 +Requires-Dist: circular_dep +""".strip() + + @pytest.mark.parametrize( ('requirement_string', 'expected'), [ @@ -126,6 +152,7 @@ def read_text(self, filename): ('extras_dep[extra_without_associated_deps] == 1.0.0', None), ('extras_dep[extra_without_associated_deps] == 2.0.0', ('extras_dep[extra_without_associated_deps] == 2.0.0',)), ('prerelease_dep >= 1.0.0', None), + ('circular_dep', None), ], ) def test_check_dependency(monkeypatch, requirement_string, expected): From 0869ebe6cb76896b0d1bd6fcfce9b863bd6309e1 Mon Sep 17 00:00:00 2001 From: layday Date: Fri, 2 Sep 2022 23:01:39 +0300 Subject: [PATCH 2/5] build: add circular dependency protection Fixes #511. --- src/build/__init__.py | 13 ++++++++++--- tests/test_projectbuilder.py | 18 +++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/build/__init__.py b/src/build/__init__.py index ea534b3d..549f7385 100644 --- a/src/build/__init__.py +++ b/src/build/__init__.py @@ -176,6 +176,13 @@ def check_dependency( import importlib_metadata req = packaging.requirements.Requirement(req_string) + normalised_req_string = str(req) + + # ``Requirement`` doesn't implement ``__eq__`` so we cannot compare reqs for + # equality directly but the string representation is stable. + if normalised_req_string in ancestral_req_strings: + # cyclical dependency, already checked. + return if req.marker: extras = frozenset(('',)).union(parent_extras) @@ -190,15 +197,15 @@ def check_dependency( dist = importlib_metadata.distribution(req.name) # type: ignore[no-untyped-call] except importlib_metadata.PackageNotFoundError: # dependency is not installed in the environment. - yield ancestral_req_strings + (req_string,) + yield ancestral_req_strings + (normalised_req_string,) else: if req.specifier and not req.specifier.contains(dist.version, prereleases=True): # the installed version is incompatible. - yield ancestral_req_strings + (req_string,) + yield ancestral_req_strings + (normalised_req_string,) elif dist.requires: for other_req_string in dist.requires: # yields transitive dependencies that are not satisfied. - yield from check_dependency(other_req_string, ancestral_req_strings + (req_string,), req.extras) + yield from check_dependency(other_req_string, ancestral_req_strings + (normalised_req_string,), req.extras) def _find_typo(dictionary: Mapping[str, str], expected: str) -> None: diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py index 5988cbb0..61e0f8b5 100644 --- a/tests/test_projectbuilder.py +++ b/tests/test_projectbuilder.py @@ -133,24 +133,24 @@ def read_text(self, filename): ('extras_dep[extra_without_associated_deps]', None), ( 'extras_dep[extra_with_unmet_deps]', - ('extras_dep[extra_with_unmet_deps]', "unmet_dep; extra == 'extra_with_unmet_deps'"), + ('extras_dep[extra_with_unmet_deps]', 'unmet_dep; extra == "extra_with_unmet_deps"'), ), ( 'extras_dep[recursive_extra_with_unmet_deps]', ( 'extras_dep[recursive_extra_with_unmet_deps]', - "recursive_dep; extra == 'recursive_extra_with_unmet_deps'", + 'recursive_dep; extra == "recursive_extra_with_unmet_deps"', 'recursive_unmet_dep', ), ), ('extras_dep[extra_with_met_deps]', None), ('missing_dep; python_version>"10"', None), ('missing_dep; python_version<="1"', None), - ('missing_dep; python_version>="1"', ('missing_dep; python_version>="1"',)), + ('missing_dep; python_version>="1"', ('missing_dep; python_version >= "1"',)), ('extras_dep == 1.0.0', None), - ('extras_dep == 2.0.0', ('extras_dep == 2.0.0',)), + ('extras_dep == 2.0.0', ('extras_dep==2.0.0',)), ('extras_dep[extra_without_associated_deps] == 1.0.0', None), - ('extras_dep[extra_without_associated_deps] == 2.0.0', ('extras_dep[extra_without_associated_deps] == 2.0.0',)), + ('extras_dep[extra_without_associated_deps] == 2.0.0', ('extras_dep[extra_without_associated_deps]==2.0.0',)), ('prerelease_dep >= 1.0.0', None), ('circular_dep', None), ], @@ -259,12 +259,12 @@ def test_check_dependencies(mocker, package_test_flit): builder._hook.get_requires_for_build_wheel.side_effect = copy.copy(side_effects) # requires = [] - assert builder.check_dependencies('sdist') == {('flit_core >=2,<3',)} - assert builder.check_dependencies('wheel') == {('flit_core >=2,<3',)} + assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',)} + assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',)} # requires = ['something'] - assert builder.check_dependencies('sdist') == {('flit_core >=2,<3',), ('something',)} - assert builder.check_dependencies('wheel') == {('flit_core >=2,<3',), ('something',)} + assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',), ('something',)} + assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',), ('something',)} # BackendUnavailable with pytest.raises(build.BuildBackendException): From f3a87cd517738ae54880c0f81846132d7f90521e Mon Sep 17 00:00:00 2001 From: layday Date: Sat, 3 Sep 2022 04:44:49 +0300 Subject: [PATCH 3/5] tests: indent multi-line strings --- tests/test_projectbuilder.py | 92 ++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py index 61e0f8b5..8d75d1e6 100644 --- a/tests/test_projectbuilder.py +++ b/tests/test_projectbuilder.py @@ -55,71 +55,83 @@ def from_name(cls, name): class ExtraMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: extras_dep -Version: 1.0.0 -Provides-Extra: extra_without_associated_deps -Provides-Extra: extra_with_unmet_deps -Requires-Dist: unmet_dep; extra == 'extra_with_unmet_deps' -Provides-Extra: extra_with_met_deps -Requires-Dist: extras_dep; extra == 'extra_with_met_deps' -Provides-Extra: recursive_extra_with_unmet_deps -Requires-Dist: recursive_dep; extra == 'recursive_extra_with_unmet_deps' -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: extras_dep + Version: 1.0.0 + Provides-Extra: extra_without_associated_deps + Provides-Extra: extra_with_unmet_deps + Requires-Dist: unmet_dep; extra == 'extra_with_unmet_deps' + Provides-Extra: extra_with_met_deps + Requires-Dist: extras_dep; extra == 'extra_with_met_deps' + Provides-Extra: recursive_extra_with_unmet_deps + Requires-Dist: recursive_dep; extra == 'recursive_extra_with_unmet_deps' + """ + ).strip() class RequirelessMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: requireless_dep -Version: 1.0.0 -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: requireless_dep + Version: 1.0.0 + """ + ).strip() class RecursiveMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: recursive_dep -Version: 1.0.0 -Requires-Dist: recursive_unmet_dep -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: recursive_dep + Version: 1.0.0 + Requires-Dist: recursive_unmet_dep + """ + ).strip() class PrereleaseMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: prerelease_dep -Version: 1.0.1a0 -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: prerelease_dep + Version: 1.0.1a0 + """ + ).strip() class CircularMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: circular_dep -Version: 1.0.0 -Requires-Dist: nested_circular_dep -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: circular_dep + Version: 1.0.0 + Requires-Dist: nested_circular_dep + """ + ).strip() class NestedCircularMockDistribution(MockDistribution): def read_text(self, filename): if filename == 'METADATA': - return """ -Metadata-Version: 2.2 -Name: nested_circular_dep -Version: 1.0.0 -Requires-Dist: circular_dep -""".strip() + return textwrap.dedent( + """ + Metadata-Version: 2.2 + Name: nested_circular_dep + Version: 1.0.0 + Requires-Dist: circular_dep + """ + ).strip() @pytest.mark.parametrize( From 40de9e5155b1adff1c21aa9457b9726a84534e49 Mon Sep 17 00:00:00 2001 From: layday Date: Sat, 3 Sep 2022 04:54:23 +0300 Subject: [PATCH 4/5] tests: update logging line no --- tests/test_projectbuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py index 8d75d1e6..5475d7e2 100644 --- a/tests/test_projectbuilder.py +++ b/tests/test_projectbuilder.py @@ -609,7 +609,7 @@ def test_log(mocker, caplog, package_test_flit): ('INFO', 'something'), ] if sys.version_info >= (3, 8): # stacklevel - assert caplog.records[-1].lineno == 562 + assert caplog.records[-1].lineno == 601 @pytest.mark.parametrize( From fc700ae56a0402d49c2c0390d03f77d27493ff16 Mon Sep 17 00:00:00 2001 From: layday Date: Sat, 3 Sep 2022 12:16:48 +0300 Subject: [PATCH 5/5] changelog: add `check_dependency` recursion error entry --- CHANGELOG.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f918d90b..8e9f768b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,16 @@ Changelog +++++++++ +Unreleased +========== + +- Fix infinite recursion error in ``check_dependency`` with circular + dependencies (`PR #512`_, Fixes `#511`_) + +.. _PR #512: https://github.com/pypa/build/pull/512 +.. _#511: https://github.com/pypa/build/issues/511 + + 0.8.0 (2022-05-22) ==================