Skip to content

Instantly share code, notes, and snippets.

@pzrq
Last active December 15, 2020 07:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pzrq/e90ed3ca1f77670cd8ae to your computer and use it in GitHub Desktop.
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
# -*- 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.
# -*- 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: mis6@pitt.edu
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)
@pzrq
Copy link
Author

pzrq commented Nov 13, 2015

@pzrq
Copy link
Author

pzrq commented Nov 13, 2015

# 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!

@pzrq
Copy link
Author

pzrq commented Nov 13, 2015

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:

  1. The base view classes provided by Django always go to the right.
  2. Mixins go to the left of the base view.
  3. Mixins should inherit from Python's built-in object type.

@pzrq
Copy link
Author

pzrq commented Nov 16, 2015

MRO graphs recommended by a colleague reviewing the area python mro_graph.py
mro_of_objecttemplateview
mro_of_pycharmtemplateview

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment