-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
perf: Optimize placeholder and plugin utilities #8337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Reviewer's GuideThis PR optimizes placeholder and plugin utilities by caching placeholder configuration, simplifying scanning and traversal logic, introducing bulk placeholder creation, streamlining plugin class retrieval and restriction evaluation, and updating tests to use a new override context manager for placeholder settings. File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
π Hi there! Please remember to MERGE COMMIT pull requests from Do not SQUASH commits to preserve history for the changelog. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `cms/utils/placeholder.py:62` </location>
<code_context>
+ conf = get_cms_setting("PLACEHOLDER_CONF")
+ template_in_conf = any(".htm" in key for key in conf if key)
+
+ def resolve_inheritance(key, visited=None):
+ if visited is None:
+ visited = set()
+ if key in visited:
+ raise ImproperlyConfigured(f"Circular inheritance detected in CMS_PLACEHOLDER_CONF at key '{key}'")
+ visited.add(key)
+ if "inherit" in conf[key]:
+ return resolve_inheritance(conf[key]['inherit'], visited) | {k: v for k, v in conf.items() if k != "inherit"}
+ return conf[key]
</code_context>
<issue_to_address>
The inheritance resolution logic may not correctly merge inherited configs.
Using set union (`|`) with dicts does not merge them as expected. Use `{**resolve_inheritance(...), **...}` or `dict.update()` for correct merging.
</issue_to_address>
### Comment 2
<location> `cms/plugin_base.py:752` </location>
<code_context>
+ # Override skips check if current class is valid parent of child classes
return [plugin.__name__ for plugin in installed_plugins if plugin.__name__ in child_classes]
+ if only_uncached:
+ installed_plugins = [plugin for plugin in installed_plugins if not plugin.cache_parent_classes]
+
child_classes = []
</code_context>
<issue_to_address>
Filtering for uncached plugins may not be compatible with all plugin pool usages.
Check that excluding plugins based on `cache_parent_classes` does not unintentionally omit required plugins in certain scenarios.
Suggested implementation:
```python
if only_uncached:
# Exclude plugins with cache_parent_classes only if they are not required
def is_uncached_and_not_required(plugin):
# If plugin has 'required_plugin' attribute and it's True, do not exclude
if getattr(plugin, 'required_plugin', False):
return True
return not plugin.cache_parent_classes
installed_plugins = [plugin for plugin in installed_plugins if is_uncached_and_not_required(plugin)]
```
If your plugins do not have a `required_plugin` attribute, you should define what makes a plugin "required" in your context and adjust the check accordingly. You may need to update plugin classes to include this attribute or use another mechanism to mark required plugins.
</issue_to_address>
### Comment 3
<location> `cms/models/pluginmodel.py:214` </location>
<code_context>
return _("<Empty>")
- def get_plugin_class(self):
+ @cached_property
+ def plugin_class(self):
from cms.plugin_pool import plugin_pool
</code_context>
<issue_to_address>
Using a cached property for plugin_class improves performance but may cause stale data if plugin_type changes.
If `plugin_type` is mutable, the cache may not reflect updates. Consider cache invalidation or switching to a regular property if this scenario is likely.
</issue_to_address>
### Comment 4
<location> `cms/models/pluginmodel.py:452` </location>
<code_context>
This method replaces the set of legacy methods `get_add_url`, ``get_edit_url`, `get_move_url`,
`get_delete_url`, `get_copy_url`.
"""
+ if not hasattr(CMSPlugin, '_edit_url'):
+ CMSPlugin._edit_url = admin_reverse('cms_placeholder_edit_plugin', args=(0,))
+ CMSPlugin._add_url = admin_reverse('cms_placeholder_add_plugin')
+ CMSPlugin._delete_url = admin_reverse('cms_placeholder_delete_plugin', args=(0,))
+ CMSPlugin._move_url = admin_reverse('cms_placeholder_move_plugin')
+ CMSPlugin._copy_url = admin_reverse('cms_placeholder_copy_plugins')
+
if js_compat:
</code_context>
<issue_to_address>
Storing URLs as class attributes may cause issues in multi-site or multi-threaded environments.
If URLs vary by site or request, storing them as class attributes may result in incorrect values. Please verify that this approach is safe for all use cases.
</issue_to_address>
### Comment 5
<location> `cms/plugin_pool.py:259` </location>
<code_context>
else:
template = ""
- if f"{template} {slot}" in self.global_restrictions_cache:
+ if template and f"{template} {slot}" in self.global_restrictions_cache:
return self.global_restrictions_cache[f"{template} {slot}"]
if template and template in self.global_restrictions_cache:
</code_context>
<issue_to_address>
Adding a template check before key lookup may change fallback behavior.
If `template` is falsy, the new condition prevents checking for keys like `' {slot}'` that may exist. Please verify this does not unintentionally alter fallback behavior.
</issue_to_address>
### Comment 6
<location> `cms/templatetags/cms_js_tags.py:37` </location>
<code_context>
def render_cms_structure_js(context, renderer, obj):
markup_bits = []
obj_placeholders_by_slot = rescan_placeholders_for_obj(obj)
- declared_placeholders = get_declared_placeholders_for_obj(obj)
try:
lang = context["request"].toolbar.request_language
except AttributeError:
lang = None
- for placeholder_node in declared_placeholders:
- obj_placeholder = obj_placeholders_by_slot.get(placeholder_node.slot)
-
</code_context>
<issue_to_address>
Iterating over obj_placeholders_by_slot.values() may change rendering order.
If rendering order matters, ensure the dictionary iteration matches the intended placeholder sequence.
</issue_to_address>
### Comment 7
<location> `cms/utils/placeholder.py:52` </location>
<code_context>
return {}
+def _get_placeholder_settings():
+ """Convert CMS_PLACEHOLDER_CONF into a faster to access format since it is accessed many times
+ in each request response cycle:
</code_context>
<issue_to_address>
Consider replacing the upfront global cache and settings flattening with an on-demand, memoized lookup using a single function and lru_cache.
The new `_get_placeholder_settings` bit is a big abstraction and adds quite a bit of code (plus globals) just to do an occasional lookup. You can simplify it massively by in-lining the cache on your single lookup function, removing the globals entirely, and doing an on-demand, cached, single pass resolution:
```python
from functools import lru_cache
@lru_cache(maxsize=None)
def get_placeholder_conf(
setting: str,
placeholder: Optional[str],
template: Optional[str] = None,
default=None
):
"""
Returns the placeholder config, supporting optional
template-scoped keys and single-level inherit.
"""
conf = get_cms_setting("PLACEHOLDER_CONF")
# build the list of keys to check in order
if template and any(".htm" in k for k in conf if k):
keys = [f"{template} {placeholder}", placeholder, str(template), None]
else:
keys = [placeholder, None]
# 1) try direct settings
for key in keys:
section = conf.get(key, {})
if setting in section:
return section[setting]
# 2) fallback via inheritance
for key in keys:
section = conf.get(key, {})
inherit = section.get("inherit")
if not inherit:
continue
# allow "parentKey" or "None parentKey"
if isinstance(inherit, str) and " " in inherit:
tpl_key, ph_key = inherit.split(" ", 1)
else:
tpl_key, ph_key = None, inherit
val = get_placeholder_conf(setting, ph_key, tpl_key, default)
if val is not None:
return val
return default
```
What this gives you:
1) no global cache state or clear function
2) no separate βflatten everything up frontβ pass
3) inheritance in one place, directly in your lookup
4) automatic memoization of every distinct (setting, placeholder, template) triplet
You can now delete `_get_placeholder_settings`, `_clear_placeholder_conf_cache`, and the module-level `_placeholder_settings` / `_template_in_conf`.
</issue_to_address>
Help me be more useful! Please click π or π on each comment and I'll use the feedback to improve your reviews.
@@ -43,6 +49,52 @@ def get_context(): | |||
return {} | |||
|
|||
|
|||
def _get_placeholder_settings(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider replacing the upfront global cache and settings flattening with an on-demand, memoized lookup using a single function and lru_cache.
The new _get_placeholder_settings
bit is a big abstraction and adds quite a bit of code (plus globals) just to do an occasional lookup. You can simplify it massively by in-lining the cache on your single lookup function, removing the globals entirely, and doing an on-demand, cached, single pass resolution:
from functools import lru_cache
@lru_cache(maxsize=None)
def get_placeholder_conf(
setting: str,
placeholder: Optional[str],
template: Optional[str] = None,
default=None
):
"""
Returns the placeholder config, supporting optional
template-scoped keys and single-level inherit.
"""
conf = get_cms_setting("PLACEHOLDER_CONF")
# build the list of keys to check in order
if template and any(".htm" in k for k in conf if k):
keys = [f"{template} {placeholder}", placeholder, str(template), None]
else:
keys = [placeholder, None]
# 1) try direct settings
for key in keys:
section = conf.get(key, {})
if setting in section:
return section[setting]
# 2) fallback via inheritance
for key in keys:
section = conf.get(key, {})
inherit = section.get("inherit")
if not inherit:
continue
# allow "parentKey" or "None parentKey"
if isinstance(inherit, str) and " " in inherit:
tpl_key, ph_key = inherit.split(" ", 1)
else:
tpl_key, ph_key = None, inherit
val = get_placeholder_conf(setting, ph_key, tpl_key, default)
if val is not None:
return val
return default
What this gives you:
- no global cache state or clear function
- no separate βflatten everything up frontβ pass
- inheritance in one place, directly in your lookup
- automatic memoization of every distinct (setting, placeholder, template) triplet
You can now delete _get_placeholder_settings
, _clear_placeholder_conf_cache
, and the module-level _placeholder_settings
/ _template_in_conf
.
self.assertEqual(force_str(placeholder_1.get_label()), "left column") | ||
self.assertEqual(force_str(placeholder_2.get_label()), "renamed left column") | ||
self.assertEqual(force_str(placeholder_3.get_label()), "fallback") | ||
|
||
del TEST_CONF[None] | ||
|
||
with self.settings(CMS_PLACEHOLDER_CONF=TEST_CONF): | ||
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=TEST_CONF): | ||
self.assertEqual(force_str(placeholder_1.get_label()), "left column") | ||
self.assertEqual(force_str(placeholder_2.get_label()), "renamed left column") | ||
self.assertEqual(force_str(placeholder_3.get_label()), "No_Name") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): Extract duplicate code into method (extract-duplicate-method
)
@@ -239,7 +240,7 @@ def test_excluded_plugin(self): | |||
add_page_endpoint = self.get_page_add_uri("en") | |||
|
|||
# try to add a new text plugin | |||
with self.settings(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF): | |||
with override_placeholder_conf(CMS_PLACEHOLDER_CONF=CMS_PLACEHOLDER_CONF): | |||
page_data = self.get_new_page_data() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): Extract duplicate code into method (extract-duplicate-method
)
def rescan_placeholders_for_obj(obj): | ||
from cms.models import Placeholder | ||
def rescan_placeholders_for_obj(obj: models.Model) -> dict[str, Placeholder]: | ||
from cms.models import CMSPlugin, Placeholder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (code-quality): We've found these issues:
- Use named expression to simplify assignment and conditional (
use-named-expression
) - Merge dictionary updates via the union operator (
dict-assign-update-to-union
)
π Hi there! Please remember to MERGE COMMIT pull requests from Do not SQUASH commits to preserve history for the changelog. |
π Hi there! Please remember to MERGE COMMIT pull requests from Do not SQUASH commits to preserve history for the changelog. |
Description
Related resources
Checklist
main
Summary by Sourcery
Optimize placeholder and plugin utilities by adding caching layers, precomputing configurations, reducing redundant queries, and refactoring core utility functions to improve performance and reliability
New Features:
Bug Fixes:
Enhancements:
Tests: