+
{% if namesake.docstring %}
{{ namesake.docstring }}
{% endif %}
{% pygmy namesake.code linenos='True' linenostart=namesake.line_number lexer='python' %}
@@ -175,7 +173,6 @@
{{ namesake.klass.name }}
{% endfor %}
- {% endwith %}
{% endifchanged %}
{% if forloop.last %}
{% endif %}
{% endfor %}
diff --git a/cbv/templatetags/cbv_tags.py b/cbv/templatetags/cbv_tags.py
deleted file mode 100644
index a727d38d..00000000
--- a/cbv/templatetags/cbv_tags.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from django import template
-
-
-register = template.Library()
-
-
-@register.filter
-def namesake_methods(parent_klass, name):
- namesakes = [m for m in parent_klass.get_methods() if m.name == name]
- assert namesakes
- # Get the methods in order of the klasses
- try:
- result = [next(m for m in namesakes if m.klass == parent_klass)]
- namesakes.pop(namesakes.index(result[0]))
- except StopIteration:
- result = []
- for klass in parent_klass.get_all_ancestors():
- # Move the namesakes from the methods to the results
- try:
- method = next(m for m in namesakes if m.klass == klass)
- namesakes.pop(namesakes.index(method))
- result.append(method)
- except StopIteration:
- pass
- assert not namesakes
- return result
diff --git a/cbv/views.py b/cbv/views.py
index 3497c057..19b46b64 100644
--- a/cbv/views.py
+++ b/cbv/views.py
@@ -1,11 +1,14 @@
+from collections import defaultdict
+from collections.abc import Sequence
from typing import Any
import attrs
from django import http
+from django.db.models import QuerySet
from django.urls import reverse
from django.views.generic import RedirectView, TemplateView, View
-from cbv.models import Klass, Module, ProjectVersion
+from cbv.models import Klass, KlassAttribute, Module, ProjectVersion
from cbv.queries import NavBuilder
@@ -21,6 +24,16 @@ def get_redirect_url(self, *, url_name: str, **kwargs):
class KlassDetailView(TemplateView):
template_name = "cbv/klass_detail.html"
+ @attrs.frozen
+ class Class:
+ db_id: int
+ docs_url: str
+ docstring: str | None
+ import_path: str
+ name: str
+ source_url: str
+ url: str
+
@attrs.frozen
class Ancestor:
name: str
@@ -32,6 +45,27 @@ class Child:
name: str
url: str
+ @attrs.frozen
+ class Method:
+ @attrs.frozen
+ class MethodInstance:
+ docstring: str
+ code: str
+ line_number: int
+ class_name: str
+
+ name: str
+ kwargs: str
+ namesakes: Sequence[MethodInstance]
+
+ @attrs.frozen
+ class Attribute:
+ name: str
+ value: str
+ overridden: bool
+ class_url: str | None
+ class_name: str
+
def get_context_data(self, **kwargs):
qs = Klass.objects.filter(
name__iexact=self.kwargs["klass"],
@@ -56,6 +90,15 @@ def get_context_data(self, **kwargs):
nav = nav_builder.get_nav_data(
klass.module.project_version, klass.module, klass
)
+ class_data = self.Class(
+ db_id=klass.id,
+ name=klass.name,
+ docstring=klass.docstring,
+ docs_url=klass.docs_url,
+ import_path=klass.import_path,
+ source_url=klass.get_source_url(),
+ url=klass.get_absolute_url(),
+ )
direct_ancestors = list(klass.get_ancestors())
ancestors = [
self.Ancestor(
@@ -72,13 +115,43 @@ def get_context_data(self, **kwargs):
)
for child in klass.get_all_children()
]
+ methods = [
+ self.Method(
+ name=method.name,
+ kwargs=method.kwargs,
+ namesakes=[
+ self.Method.MethodInstance(
+ docstring=method_instance.docstring,
+ code=method_instance.code,
+ line_number=method_instance.line_number,
+ class_name=method_instance.klass.name,
+ )
+ for method_instance in self._namesake_methods(klass, method.name)
+ ],
+ )
+ for method in klass.get_methods()
+ ]
+ attributes = [
+ self.Attribute(
+ name=attribute.name,
+ value=attribute.value,
+ overridden=hasattr(attribute, "overridden"),
+ class_url=(
+ attribute.klass.get_absolute_url()
+ if attribute.klass_id != klass.id
+ else None
+ ),
+ class_name=attribute.klass.name,
+ )
+ for attribute in self._get_prepared_attributes(klass)
+ ]
return {
"all_ancestors": ancestors,
"all_children": children,
- "attributes": klass.get_prepared_attributes(),
+ "attributes": attributes,
"canonical_url": self.request.build_absolute_uri(canonical_url_path),
- "klass": klass,
- "methods": list(klass.get_methods()),
+ "class": class_data,
+ "methods": methods,
"nav": nav,
"project": f"Django {klass.module.project_version.version_number}",
"push_state_url": push_state_url,
@@ -86,6 +159,57 @@ def get_context_data(self, **kwargs):
"yuml_url": klass.basic_yuml_url(),
}
+ def _namesake_methods(self, parent_klass, name):
+ namesakes = [m for m in parent_klass.get_methods() if m.name == name]
+ assert namesakes
+ # Get the methods in order of the klasses
+ try:
+ result = [next(m for m in namesakes if m.klass == parent_klass)]
+ namesakes.pop(namesakes.index(result[0]))
+ except StopIteration:
+ result = []
+ for klass in parent_klass.get_all_ancestors():
+ # Move the namesakes from the methods to the results
+ try:
+ method = next(m for m in namesakes if m.klass == klass)
+ namesakes.pop(namesakes.index(method))
+ result.append(method)
+ except StopIteration:
+ pass
+ assert not namesakes
+ return result
+
+ def _get_prepared_attributes(self, klass: Klass) -> QuerySet["KlassAttribute"]:
+ attributes = klass.get_attributes()
+ # Make a dictionary of attributes based on name
+ attribute_names: dict[str, list[KlassAttribute]] = defaultdict(list)
+ for attr in attributes:
+ attribute_names[attr.name].append(attr)
+
+ ancestors = klass.get_all_ancestors()
+
+ # Sort the attributes by ancestors.
+ def _key(a: KlassAttribute) -> int:
+ try:
+ # If ancestor, return the index (>= 0)
+ return ancestors.index(a.klass)
+ except ValueError: # Raised by .index if item is not in list.
+ # else a.klass == klass, so return -1
+ return -1
+
+ # Find overridden attributes
+ for klass_attributes in attribute_names.values():
+ # Skip if we have only one attribute.
+ if len(klass_attributes) == 1:
+ continue
+
+ sorted_attrs = sorted(klass_attributes, key=_key)
+
+ # Mark overriden KlassAttributes
+ for a in sorted_attrs[1:]:
+ a.overridden = True
+ return attributes
+
class LatestKlassRedirectView(RedirectView):
def get_redirect_url(self, **kwargs):
diff --git a/mypy-ratchet.json b/mypy-ratchet.json
index 55ea6bcd..16ae4776 100644
--- a/mypy-ratchet.json
+++ b/mypy-ratchet.json
@@ -25,15 +25,13 @@
"\"Callable[[Module], tuple[str, str, str]]\" has no attribute \"dependencies\" [attr-defined]": 1,
"Missing return statement [return]": 1
},
- "cbv/templatetags/cbv_tags.py": {
- "Function is missing a type annotation [no-untyped-def]": 1
- },
"cbv/views.py": {
+ "Call to untyped function \"_namesake_methods\" in typed context [no-untyped-call]": 1,
"Call to untyped function \"get_fuzzy_object\" in typed context [no-untyped-call]": 1,
"Call to untyped function \"get_object\" in typed context [no-untyped-call]": 1,
"Call to untyped function \"get_precise_object\" in typed context [no-untyped-call]": 1,
"Function is missing a return type annotation [no-untyped-def]": 1,
- "Function is missing a type annotation [no-untyped-def]": 9,
+ "Function is missing a type annotation [no-untyped-def]": 10,
"Function is missing a type annotation for one or more arguments [no-untyped-def]": 1
}
}