From 629da0fa34e64351d9e5bd013d34e62ec124f406 Mon Sep 17 00:00:00 2001 From: Adwaitha V Date: Fri, 3 Jul 2026 12:14:51 +0530 Subject: [PATCH 1/7] Add validation for duplicate website URLs --- ecosystem/validation/test_url.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index b7a9210072..a339a1ebb9 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -16,7 +16,6 @@ # pylint: disable=missing-function-docstring, missing-class-docstring - class TestURLs: @classmethod def get_all_urls(cls, member): @@ -59,3 +58,31 @@ def test_026(self, member): "The field `member.documentation` can be empty. " "It does not have to be a link to the `README.md`." ) + + def test_027(self, member): + """Website should not duplicate GitHub or PyPI URLs""" + website_url = getattr(member, "website") + if website_url is None: + pytest.skip("No member.website") + normalized_website = str(website_url).rstrip("/") + # Reject GitHub repository URLs as website + repository_url = getattr(member, "url") + if ( + repository_url is not None + and repository_url.hostname.endswith("github.com") + ): + assert normalized_website != str(repository_url).rstrip("/"), ( + "The field `member.website` should not duplicate " + "the GitHub repository URL." + ) + # Reject PyPI URLs as website + pypi_packages = getattr(member, "pypi", {}) + for _, package in pypi_packages.items(): + package_url = getattr(package, "url", None) + if package_url is None: + continue + if package_url.hostname.endswith("pypi.org"): + assert normalized_website != str(package_url).rstrip("/"), ( + "The field `member.website` should not duplicate " + "a PyPI project URL." + ) From 64172e5391d239c5245983438c4f8d0bfa7588d9 Mon Sep 17 00:00:00 2001 From: Adwaitha V Date: Fri, 3 Jul 2026 12:20:44 +0530 Subject: [PATCH 2/7] Add validation for duplicate website URLs --- ecosystem/validation/test_url.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index a339a1ebb9..88cfbadd2f 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -16,6 +16,7 @@ # pylint: disable=missing-function-docstring, missing-class-docstring + class TestURLs: @classmethod def get_all_urls(cls, member): From 4bd0aff9c2c94cc09ccbccdf3c2d14ee7006e3a8 Mon Sep 17 00:00:00 2001 From: Adwaitha V Date: Fri, 3 Jul 2026 14:08:16 +0530 Subject: [PATCH 3/7] Format URL validation with black --- ecosystem/validation/test_url.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index 88cfbadd2f..f06a8b4031 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -68,9 +68,8 @@ def test_027(self, member): normalized_website = str(website_url).rstrip("/") # Reject GitHub repository URLs as website repository_url = getattr(member, "url") - if ( - repository_url is not None - and repository_url.hostname.endswith("github.com") + if repository_url is not None and repository_url.hostname.endswith( + "github.com" ): assert normalized_website != str(repository_url).rstrip("/"), ( "The field `member.website` should not duplicate " From 9051555d819f28a863ec849438b02f7e4ef8dbd8 Mon Sep 17 00:00:00 2001 From: Adwaitha V Date: Mon, 6 Jul 2026 13:23:53 +0530 Subject: [PATCH 4/7] Improve duplicate website URL validation --- ecosystem/validation/test_url.py | 102 ++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index f06a8b4031..77c0e9f07c 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -12,6 +12,8 @@ """URL Validations""" +from urllib.parse import urlparse + import pytest # pylint: disable=missing-function-docstring, missing-class-docstring @@ -20,7 +22,7 @@ class TestURLs: @classmethod def get_all_urls(cls, member): - """recursevly search for URLs in the member data""" + """recursively search for URLs in the member data""" for key, value in member.to_dict().items(): if isinstance(value, str) and value.startswith("http"): yield getattr(member, key) @@ -29,19 +31,63 @@ def get_all_urls(cls, member): else: continue + @staticmethod + def normalize_url(url): + """Normalize URL for comparison.""" + + if url is None: + return None, None + + parsed = urlparse(str(url)) + + hostname = (parsed.hostname or "").lower() + + if hostname.startswith("www."): + hostname = hostname[4:] + + path = parsed.path.rstrip("/") + + return hostname, path + + @staticmethod + def is_same_or_subpath(candidate, reference): + """ + Check whether candidate duplicates or points inside reference. + """ + + candidate_host, candidate_path = TestURLs.normalize_url(candidate) + + reference_host, reference_path = TestURLs.normalize_url(reference) + + if candidate_host != reference_host: + return False + + return candidate_path == reference_path or candidate_path.startswith( + reference_path + "/" + ) + def test_http(self, member): for url in TestURLs.get_all_urls(member): assert not str(url).startswith("http:"), f"{url} is not HTTPS" def test_025(self, member): """Documentation link has redundant suffix""" + fields = ["url", "documentation", "reference_paper"] + for field in fields: url = getattr(member, field) + if url is None: continue - if url.hostname.endswith("readthedocs.io"): - suffixes = ["en/latest/", "en/latest", "en"] + + if url.hostname.lower().endswith("readthedocs.io"): + suffixes = [ + "en/latest/", + "en/latest", + "en", + ] + for suffix in suffixes: assert not url.path.endswith( suffix @@ -49,40 +95,64 @@ def test_025(self, member): def test_026(self, member): """The /README.md is not documentation""" + documentation_url = getattr(member, "documentation") + if documentation_url is None: pytest.skip("No member.documentation") - if documentation_url.hostname.endswith("github.com"): - suffixes = ["main/README.md"] + + if documentation_url.hostname.lower().endswith("github.com"): + suffixes = [ + "main/README.md", + "blob/main/README.md", + "tree/main", + ] + for suffix in suffixes: assert not documentation_url.path.endswith(suffix), ( - "The field `member.documentation` can be empty. " - "It does not have to be a link to the `README.md`." + "The field `member.documentation` can be " + "empty. It does not have to be a link to " + "the `README.md` or repository root." ) def test_027(self, member): """Website should not duplicate GitHub or PyPI URLs""" + website_url = getattr(member, "website") + if website_url is None: pytest.skip("No member.website") - normalized_website = str(website_url).rstrip("/") + # Reject GitHub repository URLs as website repository_url = getattr(member, "url") - if repository_url is not None and repository_url.hostname.endswith( + + if repository_url is not None and repository_url.hostname.lower().endswith( "github.com" ): - assert normalized_website != str(repository_url).rstrip("/"), ( - "The field `member.website` should not duplicate " - "the GitHub repository URL." + assert not TestURLs.is_same_or_subpath( + website_url, + repository_url, + ), ( + "The field `member.website` should not " + "duplicate the GitHub repository URL." ) + # Reject PyPI URLs as website pypi_packages = getattr(member, "pypi", {}) + for _, package in pypi_packages.items(): package_url = getattr(package, "url", None) + if package_url is None: continue - if package_url.hostname.endswith("pypi.org"): - assert normalized_website != str(package_url).rstrip("/"), ( - "The field `member.website` should not duplicate " - "a PyPI project URL." + + hostname = (package_url.hostname or "").lower() + + if hostname.endswith("pypi.org") or hostname.endswith("pypi.python.org"): + assert not TestURLs.is_same_or_subpath( + website_url, + package_url, + ), ( + "The field `member.website` should not " + "duplicate a PyPI project URL." ) From b43ded92be7b79f9cc3b7ba145d5d9c4ca85a626 Mon Sep 17 00:00:00 2001 From: ADWAITHA V <142923950+AdwaithaV@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:56:08 +0530 Subject: [PATCH 5/7] Update ecosystem/validation/test_url.py Co-authored-by: Luciano Bello <766693+1ucian0@users.noreply.github.com> --- ecosystem/validation/test_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index 77c0e9f07c..b838510129 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -112,7 +112,7 @@ def test_026(self, member): assert not documentation_url.path.endswith(suffix), ( "The field `member.documentation` can be " "empty. It does not have to be a link to " - "the `README.md` or repository root." + "the repository's `README.md` or root." ) def test_027(self, member): From edc752de14b0391cd2b828499cd8f36e91e5dbc2 Mon Sep 17 00:00:00 2001 From: Adwaitha V Date: Sat, 11 Jul 2026 03:17:36 +0530 Subject: [PATCH 6/7] Address review comments for website URL validation --- ecosystem/validation/test_url.py | 24 +++++------------------- resources/checks.toml | 9 +++++++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/ecosystem/validation/test_url.py b/ecosystem/validation/test_url.py index b838510129..7f36f6ad91 100644 --- a/ecosystem/validation/test_url.py +++ b/ecosystem/validation/test_url.py @@ -123,6 +123,8 @@ def test_027(self, member): if website_url is None: pytest.skip("No member.website") + hostname = website_url.hostname or "" + # Reject GitHub repository URLs as website repository_url = getattr(member, "url") @@ -137,22 +139,6 @@ def test_027(self, member): "duplicate the GitHub repository URL." ) - # Reject PyPI URLs as website - pypi_packages = getattr(member, "pypi", {}) - - for _, package in pypi_packages.items(): - package_url = getattr(package, "url", None) - - if package_url is None: - continue - - hostname = (package_url.hostname or "").lower() - - if hostname.endswith("pypi.org") or hostname.endswith("pypi.python.org"): - assert not TestURLs.is_same_or_subpath( - website_url, - package_url, - ), ( - "The field `member.website` should not " - "duplicate a PyPI project URL." - ) + assert not ( + hostname.endswith("pypi.org") or hostname.endswith("pypi.python.org") + ), ("The field `member.website` should not " "point to a PyPI project URL.") diff --git a/resources/checks.toml b/resources/checks.toml index bd1484807d..e998d05516 100644 --- a/resources/checks.toml +++ b/resources/checks.toml @@ -154,6 +154,15 @@ checker = "test_url.py::TestURLs::test_026" category = "SUBMISSION" importance = "RECOMMENDATION" +[027] +title = "Website should not duplicate GitHub or PyPI URLs" +description = "The `member.website` field should point to the project's website instead of duplicating the GitHub repository or a PyPI project page." +applies_to = "all" +affects = ["member.website"] +checker = "test_url.py::TestURLs::test_027" +category = "SUBMISSION" +importance = "RECOMMENDATION" + [G05] title = "Archived GitHub repository should have `archived` support" applies_to = "GitHub projects" From a93f249c0556a825af2a2d2522289b657f6d78e6 Mon Sep 17 00:00:00 2001 From: ADWAITHA V <142923950+AdwaithaV@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:24:29 +0530 Subject: [PATCH 7/7] Update resources/checks.toml Co-authored-by: Luciano Bello <766693+1ucian0@users.noreply.github.com> --- resources/checks.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/checks.toml b/resources/checks.toml index e998d05516..24ef2add3b 100644 --- a/resources/checks.toml +++ b/resources/checks.toml @@ -155,7 +155,7 @@ category = "SUBMISSION" importance = "RECOMMENDATION" [027] -title = "Website should not duplicate GitHub or PyPI URLs" +title = "Website should not be GitHub repository or PyPI URL" description = "The `member.website` field should point to the project's website instead of duplicating the GitHub repository or a PyPI project page." applies_to = "all" affects = ["member.website"]