Last active
December 15, 2020 07:18
-
-
Save pzrq/e90ed3ca1f77670cd8ae to your computer and use it in GitHub Desktop.
Better PyCharm mixin handling would make it far more useful for Django Class-Based Views, Managers, etc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
# usage: python mro_exploration.py | |
# https://github.com/pzrq/ | |
Better PyCharm mixin handling would make it far more useful for | |
Django Class-Based Views, Managers, and in contexts others have identified | |
such as Werkzeug. | |
https://youtrack.jetbrains.com/issue/PY-7712 | |
PyCharm should look at the "Mixin" (MixIn, mixin, etc) class name suffix and | |
treat it as a composition-based pattern, | |
i.e. distinct from inheritance-based patterns which it does well. | |
The following example simplified from a production project demonstrates: | |
(1) "class-referential" mixins, which extend subclasses by | |
relying on references that only resolve given the complete class | |
hierarchy (i.e. knowledge of all subclasses). | |
(2) "overriding" mixins, which would typically extend including a call to | |
`super()` in the method implementations. These are a common subset of (1). | |
It does not demonstrate: | |
(3) "pure" mixins, which extend a class with no direct reference to | |
other classes in the class hierarchy. These aren't interesting here | |
because PyCharm won't have any unresolved reference warnings to raise. | |
This looks a lot like the "utility functions" case described for React: | |
https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750 | |
Aside - If I'm understanding correctly, React rediscovered the | |
decorator pattern and called it a higher order component: | |
http://stackoverflow.com/questions/3118929/implementing-the-decorator-pattern-in-python | |
ObjectContextViewMixin & PyCharmContextViewMixin are examples containing both | |
(1) - `self.request` and | |
(2) - `super().get_context_data` | |
ObjectTemplateResponseMixin & PyCharmTemplateResponseMixin contain only | |
(2) - super().render_to_response | |
""" | |
from django.views.generic import TemplateView, View | |
from django.views.generic.base import ( | |
ContextMixin, | |
RedirectView, | |
TemplateResponseMixin, | |
) | |
class ObjectContextViewMixin(object): | |
def get_context_data(self, **kwargs): | |
data = super(ObjectContextViewMixin, self).get_context_data(**kwargs) | |
data.update({ | |
# e.g. Prefetch data we always use to populate menus | |
'remember-this': self.request.COOKIE.get('remember-this'), | |
}) | |
return data | |
def get(self, request, *args, **kwargs): | |
raise ValueError( | |
'This code will consistently raise ' | |
'ValueError in non-framework subclasses') | |
class PyCharmContextViewMixin(ContextMixin, View): | |
def get_context_data(self, **kwargs): | |
data = super(PyCharmContextViewMixin, self).get_context_data(**kwargs) | |
data.update({ | |
# e.g. Prefetch data we always use to populate menus | |
'remember-this': self.request.COOKIE.get('remember-this'), | |
}) | |
return data | |
def get(self, request, *args, **kwargs): | |
raise ValueError( | |
'INCONSISTENT: This is dead code for PyCharmTemplateView ' | |
'as TemplateView.get() comes first in the .mro(), ' | |
'but this code is live for PyCharmRedirectView!') | |
class ObjectTemplateResponseMixin(object): | |
def render_to_response(self, context, **response_kwargs): | |
response = super(ObjectTemplateResponseMixin, | |
self).render_to_response(context, **response_kwargs) | |
if context.get('remember-this'): | |
# Store the remember-this in a cookie which can later on be used | |
# when the remember-this parameter is not provided in the URL | |
response.set_cookie('remember-this', | |
str(context['remember-this'].pk)) | |
return response | |
class PyCharmTemplateResponseMixin(TemplateResponseMixin): | |
def render_to_response(self, context, **response_kwargs): | |
response = super(PyCharmTemplateResponseMixin, | |
self).render_to_response(context, **response_kwargs) | |
if context.get('remember-this'): | |
# Store the remember-this in a cookie which can later on be used | |
# when the remember-this parameter is not provided in the URL | |
response.set_cookie('remember-this', | |
str(context['remember-this'].pk)) | |
return response | |
class BaseObjectViewMixin(ObjectTemplateResponseMixin, | |
ObjectContextViewMixin): | |
""" | |
Bundle several view mixins together so we can apply them consistently. | |
""" | |
pass | |
class BasePyCharmViewMixin(PyCharmTemplateResponseMixin, | |
PyCharmContextViewMixin): | |
""" | |
Bundle several view mixins together so we can apply them consistently. | |
""" | |
pass | |
class ObjectTemplateView(BaseObjectViewMixin, TemplateView): | |
pass | |
class ObjectRedirectView(BaseObjectViewMixin, RedirectView): | |
pass | |
class PyCharmTemplateView(BasePyCharmViewMixin, TemplateView): | |
pass | |
class PyCharmRedirectView(BasePyCharmViewMixin, RedirectView): | |
pass | |
width = 120 | |
print('Compare the Method Resolution Order (MRO) ') | |
print('of the following two classes - which is easier to read?') | |
print('-' * width) | |
print('# ObjectDetailView.mro()') | |
print('-' * width) | |
for a in ObjectTemplateView.mro(): | |
print(a) | |
print('') | |
print('# PyCharmDetailView.mro()') | |
print('-' * width) | |
for a in PyCharmTemplateView.mro(): | |
print(a) | |
print('') | |
print('# ObjectDetailView MRO is logically grouped into project classes then ' | |
'framework (django) classes - easier to understand.') | |
print('# PyCharmDetailView MRO intertwines framework and project classes, ' | |
'must call .mro() to be 100% sure what will happen.') | |
print('# NB: Both are valid C3 Linearizations, ' | |
'but the intertwined latter is more likely to lead to WTFs. e.g.') | |
# Play with the subclasses - how should they behave? | |
def test_get(view_class): | |
instance = view_class() | |
try: | |
instance.get(request='') # We can mock a request with '' | |
print('{}: OK'.format(view_class.__name__)) | |
except ValueError as e: | |
print('{} - {}: {}'.format(view_class.__name__, | |
e.__class__.__name__, | |
str(e))) | |
except AttributeError as e: | |
print('{} - {}: {}'.format(view_class.__name__, | |
e.__class__.__name__, | |
str(e))) | |
# Usage - PyCharmTemplateView which raises no warnings is harder to understand | |
# because of inconsistent subclass behaviour, | |
# i.e. PyCharmContextViewMixin.get() is like Schrödinger's cat - | |
# dead code or live code depending on which Django View subclass it is mixed in | |
test_get(ObjectTemplateView) | |
test_get(ObjectRedirectView) | |
test_get(PyCharmTemplateView) | |
test_get(PyCharmRedirectView) | |
# Hopefully I don't need to explain to most why this inconsistency | |
# is usually a bad idea, but just in case: | |
# | |
# import this | |
# The Zen of Python, by Tim Peters | |
# ... | |
# Simple is better than complex. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# <mro_graph.py> | |
""" | |
Usage: python mro_graph.py | |
Draw inheritance hierarchies via Dot (http://www.graphviz.org/) | |
Author: M. Simionato | |
E-mail: [email protected] | |
Date: August 2003 | |
License: Python-like | |
Requires: Python 2.3, dot, standard Unix tools | |
Original source: http://www.phyast.pitt.edu/~micheles/python/drawMRO.html | |
PyCharm auto-format, and some code cleanup performed. | |
Changed default to PNG. | |
Renamed to snake_case mro_graph.py which is more common for Python modules. | |
""" | |
import itertools | |
import os | |
from mro_exploration import ObjectTemplateView, PyCharmTemplateView | |
PSVIEWER = 'gv' # you may change these with | |
PNGVIEWER = 'open' # your preferred viewers | |
PSFONT = 'Times' # you may change these too | |
PNGFONT = 'Courier' # on my system PNGFONT=Times does not work | |
def if_(cond, e1, e2=''): | |
"""Ternary operator would be""" | |
if cond: | |
return e1 | |
else: | |
return e2 | |
def MRO(cls): | |
"""Returns the MRO of cls as a text""" | |
out = ["MRO of %s:" % cls.__name__] | |
for counter, c in enumerate(cls.__mro__): | |
name = c.__name__ | |
bases = ','.join([b.__name__ for b in c.__bases__]) | |
s = " %s - %s(%s)" % (counter, name, bases) | |
if type(c) is not type: | |
s += "[%s]" % type(c).__name__ | |
out.append(s) | |
return '\n'.join(out) | |
class MROgraph(object): | |
def __init__(self, *classes, **options): | |
"""Generates the MRO graph of a set of given classes.""" | |
if not classes: | |
raise TypeError("Missing class argument!") | |
filename = options.get('filename', | |
"MRO_of_%s.png" % classes[0].__name__) | |
self.labels = options.get('labels', 2) | |
caption = options.get('caption', False) | |
setup = options.get('setup', '') | |
name, dotformat = os.path.splitext(filename) | |
format = dotformat[1:] | |
fontopt = "fontname=" + if_(format == 'ps', PSFONT, PNGFONT) | |
nodeopt = ' node [%s];\n' % fontopt | |
edgeopt = ' edge [%s];\n' % fontopt | |
viewer = if_(format == 'ps', PSVIEWER, PNGVIEWER) | |
self.textrepr = '\n'.join([MRO(cls) for cls in classes]) | |
caption = if_(caption, | |
'caption [shape=box,label="%s\n",fontsize=9];' | |
% self.textrepr).replace('\n', '\\l') | |
setupcode = nodeopt + edgeopt + caption + '\n' + setup + '\n' | |
codeiter = itertools.chain(*[self.genMROcode(cls) for cls in classes]) | |
self.dotcode = 'digraph %s{\n%s%s}' % ( | |
name, setupcode, '\n'.join(codeiter)) | |
os.system("echo '%s' | dot -T%s > %s; %s %s&" % | |
(self.dotcode, format, filename, viewer, filename)) | |
def genMROcode(self, cls): | |
"""Generates the dot code for the MRO of a given class""" | |
for mroindex, c in enumerate(cls.__mro__): | |
name = c.__name__ | |
manyparents = len(c.__bases__) > 1 | |
if c.__bases__: | |
yield ''.join([ | |
' edge [style=solid]; %s -> %s %s;\n' % ( | |
b.__name__, name, | |
if_(manyparents and self.labels == 2, | |
'[label="%s"]' % (i + 1))) | |
for i, b in enumerate(c.__bases__)]) | |
if manyparents: | |
yield " {rank=same; %s}\n" % ''.join([ | |
'"%s"; ' % b.__name__ | |
for b in c.__bases__]) | |
number = if_(self.labels, "%s-" % mroindex) | |
label = 'label="%s"' % (number + name) | |
option = if_(issubclass(cls, type), # if cls is a metaclass | |
'[%s]' % label, | |
'[shape=box,%s]' % label) | |
yield (' %s %s;\n' % (name, option)) | |
if type(c) is not type: # c has a custom metaclass | |
metaname = type(c).__name__ | |
yield ' edge [style=dashed]; %s -> %s;' % (metaname, name) | |
def __repr__(self): | |
"""Returns the Dot representation of the graph""" | |
return self.dotcode | |
def __str__(self): | |
"""Returns a text representation of the MRO""" | |
return self.textrepr | |
def test_hierarchy(**options): | |
class M(type): | |
pass # metaclass | |
class F(object): | |
pass | |
class E(object): | |
pass | |
class D(object): | |
pass | |
class G(object): | |
__metaclass__ = M | |
class C(F, D, G): | |
pass | |
class B(E, D): | |
pass | |
class A(B, C): | |
pass | |
return MROgraph(A, M, **options) | |
if __name__ == "__main__": | |
# test_hierarchy() # generates a PNG diagram of A and M hierarchies | |
MROgraph(ObjectTemplateView) | |
MROgraph(PyCharmTemplateView) |
# Example run under Python 3.5.0, Django 1.8.6
#
# Note: Extra newlines and # for readability on GH
#
$ python mro_exploration.py
Compare the Method Resolution Order (MRO)
of the following two classes - which is easier to read?
------------------------------------------------------------------------------------------------------------------------
# ObjectDetailView.mro()
------------------------------------------------------------------------------------------------------------------------
<class '__main__.ObjectTemplateView'>
<class '__main__.BaseObjectViewMixin'>
<class '__main__.ObjectTemplateResponseMixin'>
<class '__main__.ObjectContextViewMixin'>
<class 'django.views.generic.base.TemplateView'>
<class 'django.views.generic.base.TemplateResponseMixin'>
<class 'django.views.generic.base.ContextMixin'>
<class 'django.views.generic.base.View'>
<class 'object'>
# PyCharmDetailView.mro()
------------------------------------------------------------------------------------------------------------------------
<class '__main__.PyCharmTemplateView'>
<class '__main__.BasePyCharmViewMixin'>
<class '__main__.PyCharmTemplateResponseMixin'>
<class 'django.views.generic.base.TemplateView'>
<class 'django.views.generic.base.TemplateResponseMixin'>
<class '__main__.PyCharmContextViewMixin'>
<class 'django.views.generic.base.ContextMixin'>
<class 'django.views.generic.base.View'>
<class 'object'>
# ObjectDetailView MRO is logically grouped into
# project classes then framework (django) classes
# - easier to understand.
# PyCharmDetailView MRO intertwines framework and project classes,
# must call .mro() to be 100% sure what will happen.
# NB: Both are valid C3 Linearizations,
# but the intertwined latter is more likely to lead to WTFs. e.g.
ObjectTemplateView - ValueError: This code will consistently raise ValueError in non-framework subclasses
ObjectRedirectView - ValueError: This code will consistently raise ValueError in non-framework subclasses
PyCharmTemplateView - AttributeError: 'PyCharmTemplateView' object has no attribute 'request'
PyCharmRedirectView - ValueError: INCONSISTENT: This is dead code for PyCharmTemplateView as
TemplateView.get() comes first in the .mro(),
but this code is live for PyCharmRedirectView!
I apologise to anyone who got this far if the PyCharmZZZ
class hierarchy has a way to define the MRO such that __main__
all come before django
that is easier to find than just following the Two Scoops of Django / Kenneth Love recommended pattern of:
- The base view classes provided by Django always go to the right.
- Mixins go to the left of the base view.
- Mixins should inherit from Python's built-in object type.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Additional context:
https://www.python.org/download/releases/2.3/mro/