From 043332c967c2022d5f6a2bd661fc9158b4844c3c Mon Sep 17 00:00:00 2001 From: jensens Date: Thu, 2 May 2024 11:57:49 +0200 Subject: [PATCH] [fc] Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-04-08T17:44:12+02:00 Author: Paul Grunewald (pgrunewald) Commit: https://github.com/plone/plone.app.linkintegrity/commit/2e7043d891b12610212bd030db63005fd57abb33 Improve performance for calculating breaches Files changed: A news/100.bugfix M plone/app/linkintegrity/browser/info.py Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-04-09T23:38:21+02:00 Author: Paul Grunewald (pgrunewald) Commit: https://github.com/plone/plone.app.linkintegrity/commit/23d72f341ee00d17cc32b00bb65c666b249d569f Refactor and optimize get_breaches code Files changed: M plone/app/linkintegrity/browser/info.py Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-04-10T00:54:02+02:00 Author: Paul Grunewald (pgrunewald) Commit: https://github.com/plone/plone.app.linkintegrity/commit/6856dea0a2a182990d38162dd107abdd5bc72509 Drop de-duplication By the way the code runs now, this is not needed anymore. Files changed: M plone/app/linkintegrity/browser/info.py Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-04-10T01:10:55+02:00 Author: Paul Grunewald (pgrunewald) Commit: https://github.com/plone/plone.app.linkintegrity/commit/de71e4c919e91eccadd325d6ecec7290560c4c10 Linting Files changed: M plone/app/linkintegrity/browser/info.py Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-04-10T15:35:43+02:00 Author: Paul Grunewald (pgrunewald) Commit: https://github.com/plone/plone.app.linkintegrity/commit/cdf1fce48c4a64a87397b95e0127ed6d44cc8d93 Cleanup Files changed: M plone/app/linkintegrity/browser/info.py Repository: plone.app.linkintegrity Branch: refs/heads/master Date: 2024-05-02T11:57:49+02:00 Author: Jens W. Klein (jensens) Commit: https://github.com/plone/plone.app.linkintegrity/commit/1cf1945af7c73b06c552974df465586f6765d802 Merge pull request #101 from plone/performance_speedup Improve performance for calculating breaches Files changed: A news/100.bugfix M plone/app/linkintegrity/browser/info.py --- last_commit.txt | 97 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 1c9892d3b2..c530d8b108 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,19 +1,96 @@ -Repository: plone.app.theming +Repository: plone.app.linkintegrity Branch: refs/heads/master -Date: 2024-05-02T08:34:10+02:00 -Author: Alessandro Pisa (ale-rt) -Commit: https://github.com/plone/plone.app.theming/commit/ebaa95f526e99685a437dd48fdc45e6591eedcf1 +Date: 2024-04-08T17:44:12+02:00 +Author: Paul Grunewald (pgrunewald) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/2e7043d891b12610212bd030db63005fd57abb33 -When calling the html serializer pass an encoding (#239) +Improve performance for calculating breaches -Fixes #238 +Files changed: +A news/100.bugfix +M plone/app/linkintegrity/browser/info.py + +b'diff --git a/news/100.bugfix b/news/100.bugfix\nnew file mode 100644\nindex 0000000..8aea426\n--- /dev/null\n+++ b/news/100.bugfix\n@@ -0,0 +1,2 @@\n+Improve performance for calculating breaches.\n+[pgrunewald]\n\\ No newline at end of file\ndiff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex 40a65e6..dda6e5a 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -59,6 +59,8 @@ def get_breaches(self, items=None):\n # add the current items uid and all its children\'s uids to the\n # list of uids that are ignored\n uids_to_ignore.extend([i.UID for i in brains_to_delete])\n+ # prepare the collection of breaches for current item\n+ get_breaches_for_item = None\n for brain_to_delete in brains_to_delete:\n try:\n obj_to_delete = brain_to_delete.getObject() # noqa\n@@ -67,7 +69,9 @@ def get_breaches(self, items=None):\n "No object found for %s! Skipping", brain_to_delete\n )\n continue\n- for breach in self.get_breaches_for_item(obj):\n+ if get_breaches_for_item is None:\n+ get_breaches_for_item = self.get_breaches_for_item(obj)\n+ for breach in get_breaches_for_item:\n add_breach = False\n for source in breach["sources"]:\n # Only add the breach if one the sources is not in the\n' + +Repository: plone.app.linkintegrity + + +Branch: refs/heads/master +Date: 2024-04-09T23:38:21+02:00 +Author: Paul Grunewald (pgrunewald) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/23d72f341ee00d17cc32b00bb65c666b249d569f + +Refactor and optimize get_breaches code + +Files changed: +M plone/app/linkintegrity/browser/info.py + +b'diff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex dda6e5a..8952661 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -14,6 +14,7 @@\n from zope.i18n import translate\n \n import logging\n+import warnings\n \n \n logger = logging.getLogger(__name__)\n@@ -50,17 +51,26 @@ def get_breaches(self, items=None):\n items = [self.context]\n catalog = getToolByName(self.context, "portal_catalog")\n results = []\n- uids_to_ignore = []\n+ uids_to_ignore = set()\n uids_visited = set()\n self.breach_count = {}\n+ brains_to_delete = []\n+ path2obj = dict()\n+ path2brains = dict()\n+\n+ # build various helper structures\n for obj in items:\n obj_path = "/".join(obj.getPhysicalPath())\n- brains_to_delete = catalog(path={"query": obj_path})\n+ path2obj[obj_path] = obj\n+ path2brains[obj_path] = brains_to_delete = catalog(path={"query": obj_path})\n # add the current items uid and all its children\'s uids to the\n # list of uids that are ignored\n- uids_to_ignore.extend([i.UID for i in brains_to_delete])\n+ uids_to_ignore.update([i.UID for i in brains_to_delete])\n+\n+ # determine breaches\n+ for obj_path, obj in path2obj.items():\n+ brains_to_delete = path2brains[obj_path]\n # prepare the collection of breaches for current item\n- get_breaches_for_item = None\n for brain_to_delete in brains_to_delete:\n try:\n obj_to_delete = brain_to_delete.getObject() # noqa\n@@ -69,10 +79,9 @@ def get_breaches(self, items=None):\n "No object found for %s! Skipping", brain_to_delete\n )\n continue\n- if get_breaches_for_item is None:\n- get_breaches_for_item = self.get_breaches_for_item(obj)\n- for breach in get_breaches_for_item:\n- add_breach = False\n+ # look into potential breach\n+ breach = self.check_object(obj=obj_to_delete, excluded_paths=set(path2obj.keys()))\n+ if breach:\n for source in breach["sources"]:\n # Only add the breach if one the sources is not in the\n # list of items that are to be deleted.\n@@ -80,11 +89,10 @@ def get_breaches(self, items=None):\n source["uid"] not in uids_to_ignore\n and source["uid"] not in uids_visited\n ):\n- add_breach = True\n+ results.append(breach)\n uids_visited.add(source["uid"])\n break\n- if add_breach:\n- results.append(breach)\n+\n if IFolder.providedBy(obj):\n count = len(catalog(path={"query": obj_path}))\n count_dirs = len(catalog(path={"query": obj_path}, is_folderish=True))\n@@ -94,19 +102,6 @@ def get_breaches(self, items=None):\n if count:\n self.breach_count[obj_path] = [count, count_dirs, count_public]\n \n- # Cleanup: Some breaches where added before it was known\n- # that their source will be deleted too.\n- for result in results:\n- for source in result["sources"]:\n- if source["uid"] in uids_to_ignore:\n- # Drop sources that are also being deleted\n- result["sources"].remove(source)\n- if not result["sources"]:\n- # Remove the breach is there are no more sources\n- # This check is necessary since there can be multiple\n- # sources for a breach\n- results.remove(result)\n-\n # De-duplicate targets * sources\n uid_target = {}\n uid_sources = defaultdict(list)\n@@ -129,35 +124,24 @@ def get_breaches_for_item(self, obj=None):\n \n Breaches coming from the children of a folder are ignored by default.\n """\n- if obj is None:\n- obj = self.context\n- results = []\n- catalog = getToolByName(obj, "portal_catalog")\n- obj_path = "/".join(obj.getPhysicalPath())\n-\n- breaches = self.check_object(obj)\n- if breaches:\n- results.append(breaches)\n-\n- if IFolder.providedBy(obj):\n- brains = catalog(path={"query": obj_path})\n- for brain in brains:\n- try:\n- child = brain.getObject()\n- except (AttributeError, KeyError):\n- continue\n- if child == obj:\n- continue\n- breaches = self.check_object(obj=child, excluded_path=obj_path)\n- if breaches:\n- results.append(breaches)\n- self.breaches = results\n- return results\n-\n- def check_object(self, obj, excluded_path=None):\n+ # BBB: No direct usage is known, but keep this for backwards compatibility.\n+ # Sooner or later, we should use only one method.\n+ warnings.warn("""Using `get_breaches_for_item` is deprecated. Use `get_breaches`\n+ instead.""", DeprecationWarning)\n+ if obj is not None:\n+ obj = [obj]\n+ return self.get_breaches(obj)\n+\n+ def check_object(self, obj, excluded_path=None, excluded_paths=None):\n """Check one object for breaches.\n- Breaches originating from excluded_path are ignored.\n+ Breaches originating from excluded_paths are ignored.\n """\n+ # BBB: Support old and new parameters likewise\n+ if excluded_paths is None:\n+ excluded_paths = set()\n+ if excluded_path:\n+ excluded_paths.add(excluded_path)\n+\n breaches = {}\n direct_links = getIncomingLinks(obj, from_attribute=None)\n has_breaches = False\n@@ -166,7 +150,7 @@ def check_object(self, obj, excluded_path=None):\n if not source_path:\n # link is broken\n continue\n- if excluded_path and source_path.startswith(excluded_path):\n+ if any([source_path.startswith(excluded_path) for excluded_path in excluded_paths]):\n # source is in excluded_path\n continue\n source = direct_link.from_object\n' + +Repository: plone.app.linkintegrity + + +Branch: refs/heads/master +Date: 2024-04-10T00:54:02+02:00 +Author: Paul Grunewald (pgrunewald) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/6856dea0a2a182990d38162dd107abdd5bc72509 + +Drop de-duplication + +By the way the code runs now, this is not needed anymore. + +Files changed: +M plone/app/linkintegrity/browser/info.py + +b'diff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex 8952661..cfc6f77 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -102,22 +102,7 @@ def get_breaches(self, items=None):\n if count:\n self.breach_count[obj_path] = [count, count_dirs, count_public]\n \n- # De-duplicate targets * sources\n- uid_target = {}\n- uid_sources = defaultdict(list)\n- for result in results:\n- target_uid = result["target"]["uid"]\n- uid_target[target_uid] = result["target"]\n- sources = uid_sources[target_uid]\n- for source in result["sources"]:\n- if source not in sources:\n- sources.append(source)\n-\n- # List of breaches\n- return [\n- {"target": uid_target[uid], "sources": sources}\n- for uid, sources in uid_sources.items()\n- ]\n+ return results\n \n def get_breaches_for_item(self, obj=None):\n """Get breaches for one object and its children.\n' + +Repository: plone.app.linkintegrity + + +Branch: refs/heads/master +Date: 2024-04-10T01:10:55+02:00 +Author: Paul Grunewald (pgrunewald) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/de71e4c919e91eccadd325d6ecec7290560c4c10 + +Linting + +Files changed: +M plone/app/linkintegrity/browser/info.py + +b'diff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex cfc6f77..799d5c5 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -1,5 +1,4 @@\n from Acquisition import aq_inner\n-from collections import defaultdict\n from OFS.interfaces import IFolder\n from plone.app.linkintegrity.utils import getIncomingLinks\n from plone.app.linkintegrity.utils import linkintegrity_enabled\n@@ -80,7 +79,9 @@ def get_breaches(self, items=None):\n )\n continue\n # look into potential breach\n- breach = self.check_object(obj=obj_to_delete, excluded_paths=set(path2obj.keys()))\n+ breach = self.check_object(\n+ obj=obj_to_delete, excluded_paths=set(path2obj.keys())\n+ )\n if breach:\n for source in breach["sources"]:\n # Only add the breach if one the sources is not in the\n@@ -111,8 +112,11 @@ def get_breaches_for_item(self, obj=None):\n """\n # BBB: No direct usage is known, but keep this for backwards compatibility.\n # Sooner or later, we should use only one method.\n- warnings.warn("""Using `get_breaches_for_item` is deprecated. Use `get_breaches`\n- instead.""", DeprecationWarning)\n+ warnings.warn(\n+ """Using `get_breaches_for_item` is deprecated. Use `get_breaches`\n+ instead.""",\n+ DeprecationWarning,\n+ )\n if obj is not None:\n obj = [obj]\n return self.get_breaches(obj)\n@@ -135,7 +139,12 @@ def check_object(self, obj, excluded_path=None, excluded_paths=None):\n if not source_path:\n # link is broken\n continue\n- if any([source_path.startswith(excluded_path) for excluded_path in excluded_paths]):\n+ if any(\n+ [\n+ source_path.startswith(excluded_path)\n+ for excluded_path in excluded_paths\n+ ]\n+ ):\n # source is in excluded_path\n continue\n source = direct_link.from_object\n' + +Repository: plone.app.linkintegrity + + +Branch: refs/heads/master +Date: 2024-04-10T15:35:43+02:00 +Author: Paul Grunewald (pgrunewald) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/cdf1fce48c4a64a87397b95e0127ed6d44cc8d93 + +Cleanup + +Files changed: +M plone/app/linkintegrity/browser/info.py + +b'diff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex 799d5c5..b2be8f2 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -53,7 +53,6 @@ def get_breaches(self, items=None):\n uids_to_ignore = set()\n uids_visited = set()\n self.breach_count = {}\n- brains_to_delete = []\n path2obj = dict()\n path2brains = dict()\n \n@@ -69,7 +68,6 @@ def get_breaches(self, items=None):\n # determine breaches\n for obj_path, obj in path2obj.items():\n brains_to_delete = path2brains[obj_path]\n- # prepare the collection of breaches for current item\n for brain_to_delete in brains_to_delete:\n try:\n obj_to_delete = brain_to_delete.getObject() # noqa\n' + +Repository: plone.app.linkintegrity + + +Branch: refs/heads/master +Date: 2024-05-02T11:57:49+02:00 +Author: Jens W. Klein (jensens) +Commit: https://github.com/plone/plone.app.linkintegrity/commit/1cf1945af7c73b06c552974df465586f6765d802 + +Merge pull request #101 from plone/performance_speedup + +Improve performance for calculating breaches Files changed: -A news/238.bugfix -M src/plone/app/theming/tests/test_transform.py -M src/plone/app/theming/transform.py +A news/100.bugfix +M plone/app/linkintegrity/browser/info.py -b'diff --git a/news/238.bugfix b/news/238.bugfix\nnew file mode 100644\nindex 00000000..c90b2a4b\n--- /dev/null\n+++ b/news/238.bugfix\n@@ -0,0 +1 @@\n+Fix an issue with unicode characters happening with lxml 5 [ale-rt]\ndiff --git a/src/plone/app/theming/tests/test_transform.py b/src/plone/app/theming/tests/test_transform.py\nindex ddb57c9d..faafa34c 100644\n--- a/src/plone/app/theming/tests/test_transform.py\n+++ b/src/plone/app/theming/tests/test_transform.py\n@@ -1,11 +1,13 @@\n from App.config import getConfiguration\n from diazo.compiler import compile_theme\n+from html import unescape\n from lxml import etree\n from os import environ\n from plone.app.testing import setRoles\n from plone.app.testing import TEST_USER_ID\n from plone.app.theming.interfaces import IThemeSettings\n from plone.app.theming.testing import THEMING_FUNCTIONAL_TESTING\n+from plone.app.theming.testing import THEMING_INTEGRATION_TESTING\n from plone.app.theming.transform import ThemeTransform\n from plone.app.theming.utils import applyTheme\n from plone.app.theming.utils import getTheme\n@@ -25,7 +27,30 @@\n import unittest\n \n \n-class TestCase(unittest.TestCase):\n+class IntegrationTestCase(unittest.TestCase):\n+\n+ layer = THEMING_INTEGRATION_TESTING\n+\n+ def test_transform_parseTree_with_unicode(self):\n+ request = self.layer["request"]\n+ request.response.setHeader("Content-Type", "text/html; charset=utf-8")\n+ transform = ThemeTransform(None, request)\n+ snippet = "\\n".join(\n+ (\n+ "",\n+ "",\n+ "",\n+ "
\xc3\xa0
",\n+ "",\n+ "",\n+ )\n+ )\n+ parsed = transform.parseTree([snippet.encode()])\n+ serialized = unescape(parsed.serialize().decode())\n+ self.assertEqual(snippet, serialized)\n+\n+\n+class FunctionalTestCase(unittest.TestCase):\n layer = THEMING_FUNCTIONAL_TESTING\n \n def setUp(self):\ndiff --git a/src/plone/app/theming/transform.py b/src/plone/app/theming/transform.py\nindex eb388d5c..3f74c015 100644\n--- a/src/plone/app/theming/transform.py\n+++ b/src/plone/app/theming/transform.py\n@@ -13,6 +13,7 @@\n from zope.component import adapter\n from zope.interface import implementer\n from zope.interface import Interface\n+from ZPublisher.HTTPRequest import default_encoding\n \n import logging\n \n@@ -120,7 +121,9 @@ def parseTree(self, result):\n return None\n \n try:\n- return getHTMLSerializer(result, pretty_print=False)\n+ return getHTMLSerializer(\n+ result, pretty_print=False, encoding=default_encoding\n+ )\n except (AttributeError, TypeError, etree.ParseError):\n return None\n \n' +b'diff --git a/news/100.bugfix b/news/100.bugfix\nnew file mode 100644\nindex 0000000..8aea426\n--- /dev/null\n+++ b/news/100.bugfix\n@@ -0,0 +1,2 @@\n+Improve performance for calculating breaches.\n+[pgrunewald]\n\\ No newline at end of file\ndiff --git a/plone/app/linkintegrity/browser/info.py b/plone/app/linkintegrity/browser/info.py\nindex 40a65e6..b2be8f2 100644\n--- a/plone/app/linkintegrity/browser/info.py\n+++ b/plone/app/linkintegrity/browser/info.py\n@@ -1,5 +1,4 @@\n from Acquisition import aq_inner\n-from collections import defaultdict\n from OFS.interfaces import IFolder\n from plone.app.linkintegrity.utils import getIncomingLinks\n from plone.app.linkintegrity.utils import linkintegrity_enabled\n@@ -14,6 +13,7 @@\n from zope.i18n import translate\n \n import logging\n+import warnings\n \n \n logger = logging.getLogger(__name__)\n@@ -50,15 +50,24 @@ def get_breaches(self, items=None):\n items = [self.context]\n catalog = getToolByName(self.context, "portal_catalog")\n results = []\n- uids_to_ignore = []\n+ uids_to_ignore = set()\n uids_visited = set()\n self.breach_count = {}\n+ path2obj = dict()\n+ path2brains = dict()\n+\n+ # build various helper structures\n for obj in items:\n obj_path = "/".join(obj.getPhysicalPath())\n- brains_to_delete = catalog(path={"query": obj_path})\n+ path2obj[obj_path] = obj\n+ path2brains[obj_path] = brains_to_delete = catalog(path={"query": obj_path})\n # add the current items uid and all its children\'s uids to the\n # list of uids that are ignored\n- uids_to_ignore.extend([i.UID for i in brains_to_delete])\n+ uids_to_ignore.update([i.UID for i in brains_to_delete])\n+\n+ # determine breaches\n+ for obj_path, obj in path2obj.items():\n+ brains_to_delete = path2brains[obj_path]\n for brain_to_delete in brains_to_delete:\n try:\n obj_to_delete = brain_to_delete.getObject() # noqa\n@@ -67,8 +76,11 @@ def get_breaches(self, items=None):\n "No object found for %s! Skipping", brain_to_delete\n )\n continue\n- for breach in self.get_breaches_for_item(obj):\n- add_breach = False\n+ # look into potential breach\n+ breach = self.check_object(\n+ obj=obj_to_delete, excluded_paths=set(path2obj.keys())\n+ )\n+ if breach:\n for source in breach["sources"]:\n # Only add the breach if one the sources is not in the\n # list of items that are to be deleted.\n@@ -76,11 +88,10 @@ def get_breaches(self, items=None):\n source["uid"] not in uids_to_ignore\n and source["uid"] not in uids_visited\n ):\n- add_breach = True\n+ results.append(breach)\n uids_visited.add(source["uid"])\n break\n- if add_breach:\n- results.append(breach)\n+\n if IFolder.providedBy(obj):\n count = len(catalog(path={"query": obj_path}))\n count_dirs = len(catalog(path={"query": obj_path}, is_folderish=True))\n@@ -90,70 +101,34 @@ def get_breaches(self, items=None):\n if count:\n self.breach_count[obj_path] = [count, count_dirs, count_public]\n \n- # Cleanup: Some breaches where added before it was known\n- # that their source will be deleted too.\n- for result in results:\n- for source in result["sources"]:\n- if source["uid"] in uids_to_ignore:\n- # Drop sources that are also being deleted\n- result["sources"].remove(source)\n- if not result["sources"]:\n- # Remove the breach is there are no more sources\n- # This check is necessary since there can be multiple\n- # sources for a breach\n- results.remove(result)\n-\n- # De-duplicate targets * sources\n- uid_target = {}\n- uid_sources = defaultdict(list)\n- for result in results:\n- target_uid = result["target"]["uid"]\n- uid_target[target_uid] = result["target"]\n- sources = uid_sources[target_uid]\n- for source in result["sources"]:\n- if source not in sources:\n- sources.append(source)\n-\n- # List of breaches\n- return [\n- {"target": uid_target[uid], "sources": sources}\n- for uid, sources in uid_sources.items()\n- ]\n+ return results\n \n def get_breaches_for_item(self, obj=None):\n """Get breaches for one object and its children.\n \n Breaches coming from the children of a folder are ignored by default.\n """\n- if obj is None:\n- obj = self.context\n- results = []\n- catalog = getToolByName(obj, "portal_catalog")\n- obj_path = "/".join(obj.getPhysicalPath())\n-\n- breaches = self.check_object(obj)\n- if breaches:\n- results.append(breaches)\n-\n- if IFolder.providedBy(obj):\n- brains = catalog(path={"query": obj_path})\n- for brain in brains:\n- try:\n- child = brain.getObject()\n- except (AttributeError, KeyError):\n- continue\n- if child == obj:\n- continue\n- breaches = self.check_object(obj=child, excluded_path=obj_path)\n- if breaches:\n- results.append(breaches)\n- self.breaches = results\n- return results\n-\n- def check_object(self, obj, excluded_path=None):\n+ # BBB: No direct usage is known, but keep this for backwards compatibility.\n+ # Sooner or later, we should use only one method.\n+ warnings.warn(\n+ """Using `get_breaches_for_item` is deprecated. Use `get_breaches`\n+ instead.""",\n+ DeprecationWarning,\n+ )\n+ if obj is not None:\n+ obj = [obj]\n+ return self.get_breaches(obj)\n+\n+ def check_object(self, obj, excluded_path=None, excluded_paths=None):\n """Check one object for breaches.\n- Breaches originating from excluded_path are ignored.\n+ Breaches originating from excluded_paths are ignored.\n """\n+ # BBB: Support old and new parameters likewise\n+ if excluded_paths is None:\n+ excluded_paths = set()\n+ if excluded_path:\n+ excluded_paths.add(excluded_path)\n+\n breaches = {}\n direct_links = getIncomingLinks(obj, from_attribute=None)\n has_breaches = False\n@@ -162,7 +137,12 @@ def check_object(self, obj, excluded_path=None):\n if not source_path:\n # link is broken\n continue\n- if excluded_path and source_path.startswith(excluded_path):\n+ if any(\n+ [\n+ source_path.startswith(excluded_path)\n+ for excluded_path in excluded_paths\n+ ]\n+ ):\n # source is in excluded_path\n continue\n source = direct_link.from_object\n'