Skip to content

Reduce dependency on Klass instance in KlassDetailView #279

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions cbv/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
47 changes: 22 additions & 25 deletions cbv/templates/cbv/klass_detail.html
Original file line number Diff line number Diff line change
@@ -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 %}

Expand Down Expand Up @@ -40,8 +39,8 @@


{% block page_header %}
<h1><small>class</small>&nbsp;{{ klass.name }}</h1>
<pre>from {{ klass.import_path }} import {{ klass.name }}</pre>
<h1><small>class</small>&nbsp;{{ class.name }}</h1>
<pre>from {{ class.import_path }} import {{ class.name }}</pre>
<div class="pull-right">
{% with url=yuml_url %}
{% if url %}
Expand All @@ -50,15 +49,15 @@ <h1><small>class</small>&nbsp;{{ klass.name }}</h1>
<span class="btn btn-small btn-info disabled">{% trans "Hierarchy diagram" %}</span>
{% endif %}
{% endwith %}
{% if klass.docs_url %}
<a class="btn btn-small btn-info" href="{{ klass.docs_url }}">{% trans "Documentation" %}</a>
{% if class.docs_url %}
<a class="btn btn-small btn-info" href="{{ class.docs_url }}">{% trans "Documentation" %}</a>
{% else %}
<span class="btn btn-small btn-info disabled">{% trans "Documentation" %}</span>
{% endif %}
<a class="btn btn-small btn-info" href="{{ klass.get_source_url }}">{% trans "Source code" %}</a>
<a class="btn btn-small btn-info" href="{{ class.source_url }}">{% trans "Source code" %}</a>
</div>
{% if klass.docstring %}
<pre class="docstring">{{ klass.docstring }}</pre>
{% if class.docstring %}
<pre class="docstring">{{ class.docstring }}</pre>
{% endif %}
{% endblock %}

Expand All @@ -70,7 +69,7 @@ <h1><small>class</small>&nbsp;{{ klass.name }}</h1>
<div class="span4">
<h2>Ancestors (<abbr title="Method Resolution Order">MRO</abbr>)</h2>
<ol start='0' id="ancestors">
<li><strong>{{ klass.name }}</strong></li>
<li><strong>{{ class.name }}</strong></li>
{% for ancestor in all_ancestors %}
<li>
<a href="{{ ancestor.url }}" class="{% if ancestor.is_direct %}direct{% endif %}">
Expand Down Expand Up @@ -115,10 +114,10 @@ <h2>Attributes</h2>
</code>
</td>
<td>
{% if attribute.klass == klass %}
{{ attribute.klass.name }}
{% if not attribute.class_url %}
{{ class.name }}
{% else %}
<a href="{{ attribute.klass.get_absolute_url }}">{{ attribute.klass.name }}</a>
<a href="{{ attribute.class_url }}">{{ attribute.class_name }}</a>
{% endif %}
</td>
</tr>
Expand All @@ -140,28 +139,27 @@ <h2>Attributes</h2>
<h2>Methods</h2>
{% endif %}
{% ifchanged method.name %}
{% with namesakes=klass|namesake_methods:method.name %}
<details class="method accordion-group">
<summary class="accordion-heading btn">
<h3>
<code class="signature highlight">
<span class="k">def</span>
<span class="nf">{{ method.name }}</span>(<span class="n">{{ method.kwargs }}</span>):
</code>
{% if namesakes|length == 1 %}
<small class="pull-right">{{ method.klass.name }}</small>
{% if method.namesakes|length == 1 %}
<small class="pull-right">{{ method.namesakes.0.class_name }}</small>
{% endif %}
<a class="permalink" href="{{ klass.get_absolute_url }}#{{ method.name }}">&para;</a>
<a class="permalink" href="{{ class.url }}#{{ method.name }}">&para;</a>
</h3>
</summary>
<div id="{{ method.name }}" class="accordion-body">
{% for namesake in namesakes %}
{% if namesakes|length != 1 %}
{% for namesake in method.namesakes %}
{% if method.namesakes|length != 1 %}
<details class="namesake accordion-group">
<summary class="accordion-heading">
<h4 class="accordion-toggle">{{ namesake.klass.name }}</h4>
<h4 class="accordion-toggle">{{ namesake.class_name }}</h4>
</summary>
<div id="{{ namesake.name }}-{{ namesake.klass.name }}" class="accordion-body">
<div id="{{ method.name }}-{{ namesake.class_name }}" class="accordion-body">
<div class="accordion-inner">
{% if namesake.docstring %}<pre class="docstring">{{ namesake.docstring }}</pre>{% endif %}
{% pygmy namesake.code linenos='True' linenostart=namesake.line_number lexer='python' %}
Expand All @@ -175,7 +173,6 @@ <h4 class="accordion-toggle">{{ namesake.klass.name }}</h4>
{% endfor %}
</div>
</details>
{% endwith %}
{% endifchanged %}
{% if forloop.last %}</div>{% endif %}
{% endfor %}
Expand Down
26 changes: 0 additions & 26 deletions cbv/templatetags/cbv_tags.py

This file was deleted.

132 changes: 128 additions & 4 deletions cbv/views.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
Expand All @@ -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"],
Expand All @@ -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(
Expand All @@ -72,20 +115,101 @@ 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,
"version_switcher": version_switcher,
"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):
Expand Down
6 changes: 2 additions & 4 deletions mypy-ratchet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}