升级Django 5.0后Mezzanine报TypeError: QuerySet._clone()参数异常
Django 5.0 升级后 Mezzanine 出现 TypeError: QuerySet._clone() got an unexpected argument '_search_terms' 修复方案
问题背景
通过NetBSD软件包安装Mezzanine(NetBSD环境下最简方式),升级至Django 5.0后触发错误:TypeError: QuerySet._clone() got an unexpected argument '_search_terms'。已解决其他模块升级适配问题,经分析需修改mezzanine/core/managers.py中的SearchableQuerySet类修复该搜索参数兼容问题。
报错堆栈
Traceback (most recent call last): File "/home/nate/cleanair/manage.py", line 14, in <module> execute_from_command_line(sys.argv) File "/usr/pkg/lib/python3.11/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line utility.execute() File "/usr/pkg/lib/python3.11/site-packages/django/core/management/__init__.py", line 395, in execute django.setup() File "/usr/pkg/lib/python3.11/site-packages/django/__init__.py", line 24, in setup apps.populate(settings.INSTALLED_APPS) File "/usr/pkg/lib/python3.11/site-packages/django/apps/registry.py", line 122, in populate app_config.ready() File "/usr/pkg/lib/python3.11/site-packages/django/contrib/admin/apps.py", line 27, in ready self.module.autodiscover() File "/usr/pkg/lib/python3.11/site-packages/mezzanine/boot/__init__.py", line 114, in autodiscover django_autodiscover(*args, **kwargs) File "/usr/pkg/lib/python3.11/site-packages/django/contrib/admin/__init__.py", line 24, in autodiscover autodiscover_modules('admin', register_to=site) File "/usr/pkg/lib/python3.11/site-packages/django/utils/module_loading.py", line 47, in autodiscover_modules import_module('%s.%s' % (app_config.name, module_to_search)) File "/usr/pkg/lib/python3.11/importlib/__init__.py", line 126, in import_module return _bootstrap._gcd_import(name[level:], package, level) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<frozen importlib._bootstrap>", line 1204, in _gcd_import File "<frozen importlib._bootstrap>", line 1176, in _find_and_load File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 690, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 940, in exec_module File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed File "/usr/pkg/lib/python3.11/site-packages/mezzanine/forms/admin.py", line 43, in <module> class FieldAdminInlineForm(DynamicInlineAdminForm): File "/usr/pkg/lib/python3.11/site-packages/django/forms/models.py", line 261, in __new__ fields = fields_for_model( ^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/forms/models.py", line 183, in fields_for_model formfield = f.formfield(**kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/db/models/fields/related.py", line 991, in formfield 'queryset': self.remote_field.model._default_manager.using(using), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/mezzanine/core/managers.py", line 386, in get_queryset return super(DjangoCSM, self).get_queryset().filter(**lookup) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/db/models/query.py", line 941, in filter return self._filter_or_exclude(False, args, kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/db/models/query.py", line 956, in _filter_or_exclude clone = self._chain() ^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/django/db/models/query.py", line 1301, in _chain obj = self._clone() ^^^^^^^^^^^^^ File "/usr/pkg/lib/python3.11/site-packages/mezzanine/core/managers.py", line 180, in _clone return super(SearchableQuerySet, self)._clone(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: QuerySet._clone() got an unexpected argument '_search_terms'
相关源码(mezzanine/core/managers.py 中 SearchableQuerySet 类)
class SearchableQuerySet(QuerySet): """ QuerySet providing main search functionality for ``SearchableManager``. """ def __init__(self, *args, **kwargs): self._search_ordered = False self._search_terms = set() self._search_fields = kwargs.pop("search_fields", {}) super(SearchableQuerySet, self).__init__(*args, **kwargs) def search(self, query, search_fields=None): """ Build a queryset matching words in the given search query, treating quoted terms as exact phrases and taking into account + and - symbols as modifiers controlling which terms to require and exclude. """ # ### DETERMINE FIELDS TO SEARCH ### # Use search_fields arg if given, otherwise use search_fields # initially configured by the manager class. if search_fields: self._search_fields = search_fields_to_dict(search_fields) if not self._search_fields: return self.none() # ### BUILD LIST OF TERMS TO SEARCH FOR ### # Remove extra spaces, put modifiers inside quoted terms. terms = " ".join(query.split()).replace("+ ", "+") \ .replace('+"', '"+') \ .replace("- ", "-") \ .replace('-"', '"-') \ .split('"') # Strip punctuation other than modifiers from terms and create # terms list, first from quoted terms and then remaining words. terms = [ ("" if t[0:1] not in "+-" else t[0:1]) + t.strip(punctuation) for t in terms[1::2] + "".join(terms[::2]).split() ] # Remove stop words from terms that aren't quoted or use # modifiers, since words with these are an explicit part of # the search query. If doing so ends up with an empty term # list, then keep the stop words. terms_no_stopwords = [t for t in terms if t.lower() not in settings.STOP_WORDS] get_positive_terms = lambda terms: [t.lower().strip(punctuation) for t in terms if t[0:1] != "-"] positive_terms = get_positive_terms(terms_no_stopwords) if positive_terms: terms = terms_no_stopwords else: positive_terms = get_positive_terms(terms) # Append positive terms (those without the negative modifier) # to the internal list for sorting when results are iterated. if not positive_terms: return self.none() else: self._search_terms.update(positive_terms) # ### BUILD QUERYSET FILTER ### # Create the queryset combining each set of terms. excluded = [reduce(iand, [~Q(**{"%s__icontains" % f: t[1:]}) for f in self._search_fields.keys()]) for t in terms if t[0:1] == "-"] required = [reduce(ior, [Q(**{"%s__icontains" % f: t[1:]}) for f in self._search_fields.keys()]) for t in terms if t[0:1] == "+"] optional = [reduce(ior, [Q(**{"%s__icontains" % f: t}) for f in self._search_fields.keys()]) for t in terms if t[0:1] not in "+-"] queryset = self if excluded: queryset = queryset.filter(reduce(iand, excluded)) if required: queryset = queryset.filter(reduce(iand, required)) # Optional terms aren't relevant to the filter if there are # terms that are explicitly required. elif optional: queryset = queryset.filter(reduce(ior, optional)) return queryset.distinct() def _clone(self, *args, **kwargs): """ Ensure attributes are copied to subsequent queries. """ for attr in ("_search_terms", "_search_fields", "_search_ordered"): kwargs[attr] = getattr(self, attr) return super(SearchableQuerySet, self)._clone(*args, **kwargs) def order_by(self, *field_names): """ Mark the filter as being ordered if search has occurred. """ if not self._search_ordered: self._search_ordered = len(self._search_terms) > 0 return super(SearchableQuerySet, self).order_by(*field_names) def annotate_scores(self): """ If search has occurred and no ordering has occurred, decorate each result with the number of search terms so that it can be sorted by the number of occurrence of terms. In the case of search fields that span model relationships, we cannot accurately match occurrences without some very complicated traversal code, which we won't attempt. So in this case, namely when there are no matches for a result (count=0), and search fields contain relationships (double underscores), we assume one match for one of the fields, and use the average weight of all search fields with relationships. """ results = super(SearchableQuerySet, self).iterator() if self._search_terms and not self._search_ordered: results = list(results) for i, result in enumerate(results): count = 0 related_weights = [] for (field, weight) in self._search_fields.items(): if "__" in field: related_weights.append(weight) for term in self._search_terms: field_value = getattr(result, field, None) if field_value: count += field_value.lower().count(term) * weight if not count and related_weights: count = int(sum(related_weights) / len(related_weights)) if result.publish_date: age = (now() - result.publish_date).total_seconds() if age > 0: count = count / age**settings.SEARCH_AGE_SCALE_FACTOR results[i].result_count = count return iter(results) return results
修复方案
Django 5.0 中QuerySet._clone()方法不再接受自定义关键字参数,Mezzanine原代码尝试将_search_terms等属性通过参数传递给父类_clone(),导致报错。需修改SearchableQuerySet._clone()方法:
修改前代码
def _clone(self, *args, **kwargs): """ Ensure attributes are copied to subsequent queries. """ for attr in ("_search_terms", "_search_fields", "_search_ordered"): kwargs[attr] = getattr(self, attr) return super(SearchableQuerySet, self)._clone(*args, **kwargs)
修改后代码
def _clone(self, *args, **kwargs): """ Ensure attributes are copied to subsequent queries. """ clone = super(SearchableQuerySet, self)._clone(*args, **kwargs) for attr in ("_search_terms", "_search_fields", "_search_ordered"): setattr(clone, attr, getattr(self, attr)) return clone
原理说明
先调用父类_clone()方法创建克隆对象,再通过setattr()手动将自定义属性赋值给克隆实例,而非通过参数传递,适配Django 5.0的API变更。
内容的提问来源于stack exchange,提问作者Nathan Maier
相关产品推荐
相关产品推荐

