diff --git a/cbv/models.py b/cbv/models.py index dddae0d3..c1818d9b 100644 --- a/cbv/models.py +++ b/cbv/models.py @@ -257,40 +257,6 @@ def get_attributes(self) -> models.QuerySet["KlassAttribute"]: self._attributes = attrs return self._attributes - def get_prepared_attributes(self) -> models.QuerySet["KlassAttribute"]: - attributes = self.get_attributes() - # Make a dictionary of attributes based on name - attribute_names: dict[str, list[KlassAttribute]] = {} - for attr in attributes: - try: - attribute_names[attr.name] += [attr] - except KeyError: - attribute_names[attr.name] = [attr] - - ancestors = self.get_all_ancestors() - - # Find overridden attributes - for name, attrs in attribute_names.items(): - # Skip if we have only one attribute. - if len(attrs) == 1: - continue - - # 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 == self, so return -1 - return -1 - - sorted_attrs = sorted(attrs, key=_key) - - # Mark overriden KlassAttributes - for a in sorted_attrs[1:]: - a.overridden = True - return attributes - def basic_yuml_data(self, first: bool = False) -> list[str]: self._basic_yuml_data: list[str] if hasattr(self, "_basic_yuml_data"): diff --git a/cbv/templates/cbv/klass_detail.html b/cbv/templates/cbv/klass_detail.html index d290f436..9e13cbde 100644 --- a/cbv/templates/cbv/klass_detail.html +++ b/cbv/templates/cbv/klass_detail.html @@ -1,17 +1,16 @@ {% extends "base.html" %} {% load pygmy %} -{% load cbv_tags %} {% load i18n %} {% load static %} -{% block title %}{{ klass.name }}{% endblock %} +{% block title %}{{ class.name }}{% endblock %} {% block meta_description %} - {{ klass.name }} in {{ project }}. - {% if klass.docstring %} - {{ klass.docstring }} + {{ class.name }} in {{ project }}. + {% if class.docstring %} + {{ class.docstring }} {% endif %} {% endblock meta_description %} @@ -40,8 +39,8 @@ {% block page_header %} -

class {{ klass.name }}

-
from {{ klass.import_path }} import {{ klass.name }}
+

class {{ class.name }}

+
from {{ class.import_path }} import {{ class.name }}
{% with url=yuml_url %} {% if url %} @@ -50,15 +49,15 @@

class {{ klass.name }}

{% trans "Hierarchy diagram" %} {% endif %} {% endwith %} - {% if klass.docs_url %} - {% trans "Documentation" %} + {% if class.docs_url %} + {% trans "Documentation" %} {% else %} {% trans "Documentation" %} {% endif %} - {% trans "Source code" %} + {% trans "Source code" %}
- {% if klass.docstring %} -
{{ klass.docstring }}
+ {% if class.docstring %} +
{{ class.docstring }}
{% endif %} {% endblock %} @@ -70,7 +69,7 @@

class {{ klass.name }}

Ancestors (MRO)

    -
  1. {{ klass.name }}
  2. +
  3. {{ class.name }}
  4. {% for ancestor in all_ancestors %}
  5. @@ -115,10 +114,10 @@

    Attributes

    - {% if attribute.klass == klass %} - {{ attribute.klass.name }} + {% if not attribute.class_url %} + {{ class.name }} {% else %} -
    {{ attribute.klass.name }} + {{ attribute.class_name }} {% endif %} @@ -140,7 +139,6 @@

    Attributes

    Methods

    {% endif %} {% ifchanged method.name %} - {% with namesakes=klass|namesake_methods:method.name %}

    @@ -148,20 +146,20 @@

    def {{ method.name }}({{ method.kwargs }}): - {% if namesakes|length == 1 %} - {{ method.klass.name }} + {% if method.namesakes|length == 1 %} + {{ method.namesakes.0.class_name }} {% endif %} - +

    - {% for namesake in namesakes %} - {% if namesakes|length != 1 %} + {% for namesake in method.namesakes %} + {% if method.namesakes|length != 1 %}
    -

    {{ namesake.klass.name }}

    +

    {{ namesake.class_name }}

    -
    +
    {% 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 } }