The dangers of Python's super have been documented... but, in my humble opinion, not well enough.
A major problem with Python's super() is that it is not straight forward to figure out needs to call it, even if it doesn't seem like the method's parent class should need to.
Consider this example, where mixins are used to update a dictionary with some context (similar to, but less correct than, for example, Django's TemplateView):
class FirstMixin(object): def get_context(self): return {"first": True} class BaseClass(FirstMixin): def get_context(self): ctx = super(BaseClass, self).get_context() ctx.update({"base": True}) return ctx class SecondMixin(object): def get_context(self): ctx = super(SecondMixin, self).get_context() ctx.update({"second": True}) return ctx class ConcreteClass(BaseClass, SecondMixin): pass
This looks correct... but it isn't! Because FirstMixin doesn't call super(), SecondMixin.get_context is never called:
>>> c = ConcreteClass() >>> c.get_context() {"base": True, "first": True} # Note that ``"second": True`` is missing!
Alternatively, image that FirstMixin.get_context() does call super():
class FirstMixin(object): def get_context(self): ctx = super(FirstMixin, self).get_context() ctx.update({"first": True}) return ctx
This will also be incorrect, because now the call to super() in SecondMixin will trigger an error, because the final base class - object - does not have a get_context() method:
>>> c = ConcreteClass() >>> c.get_context() ... AttributeError: 'super' object has no attribute 'get_context'
What is a poor Pythonista to do?
There are three reasonably simple rules to follow when dealing with this kind of multiple inheritance:
- Mixins should always call super().
- The base class should not call super().
- The base class (or one of its super classes) needs to be at the right of sub-classe's list of base classes.
Note that this will often mean introducing an otherwise unnecessary *Base class.
To correct the example above:
# Following rule (1), every mixin calls `super()` class FirstMixin(object): def get_context(self): ctx = super(FirstMixin, self).get_context() ctx.update({"first": True}) return ctx # Following rule (2), the base class does *not* call super. class BaseClassBase(object): def get_context(self): return {"base": True} # Notice that, to follow rule (3), an otherwise uneccessary base class has # been introduced to make sure that the "real" base class (the one without # the call to super) can be at the very right of the list of base classess. class BaseClass(FirstMixin, BaseClassBase): pass # Following rule (3), the base class comes at the right end of the list # of base classess. class ConcreteClass(SecondMixin, BaseClass): pass
This will guarantee that the mixins are always called before the base class, which doesn't call super() in get_context().
Note that this will still cause problems in the even that multiple base classess are used (ie, "true" multiple inheritance)... and there isn't much which can be done about that, at least in the general case.
It is also worth noting that that in many cases the best solution is to avoid inheritance all together, opting instead for a pattern better suited to the requirements of the specific problem at hand.
For example, in the sitaution from the example above - where many different "things" (in the above example: mixins and the base class) need to contribute to the "context" dictionary - one option which might be more appropriate is an explicit set of "context providers":
class FirstContextProvider(object): def __call__(self): return {"first": True} class BaseClass(FirstMixin): context_providers = [ FirstContextProvider(), lambda: {"base": True}, ] def get_context(self): ctx = {} for provider in self.context_providers: ctx.update(provider()) return ctx class SecondContextProvider(object): def __call__(self): return {"second": True} class ConcreteClass(BaseClass, SecondMixin): context_providers = BaseClass.context_providers + [ SecondContextProvider(), ]
(recall that __call__ method is used to make instances of a call callable)
Edit: I was corrected by @lambacck, who pointed out the "base class on the right" rule: https://twitter.com/lambacck/status/451528854507905024
Often test cases require a few stateful "things" to happen during each run, which are often configured in the setUp and tearDown methods of the test case.
For example, some common "things" in applications I've worked on are:
- Capturing email messages sent during the test case
- Capturing log messages emitted during the test case
- Mocking Redis connections
- Mocking the application's job queue
And the implementation often looks something like this:
class MyRegularTestCase(TestCase): def setup(self): self.mail = setup_mock_mail() self.logs = setup_mock_logger() self.redis = setup_mock_redis() self.jobs = setup_mock_job_queue() def teardown(self): self.mail.teardown() self.logs.teardown() self.redis.teardown() self.jobs.teardown() def test_foo(self): foo() self.mail.assert_message_sent("hello, world") self.logs.assert_logged("hello, world") self.redis.assert_list_contains("foo", "bar") self.jobs.assert_job_queued("some_job")
And that is a best case example; most of the time test code doesn't use high-level mocking objects, and setup code is copy+pasted between test classes.
This makes me wonder: why aren't composable test helpers a thing?
For example, a log capturing test helper might look something like this:
class LogCapture(object): def __init__(self, logger_name=''): self.logger = logging.getLogger(logger_name) def setup(self): self.records = [] self.logger.addHandler(self) def teardown(self): self.logger.removeHandler(self) def emit(self, record): self.records.append(record) def assert_logged(self, message): for record in self.records: if message in record.getMessage(): return raise AssertionError("No log message containing %r was emitted" %(message, ))
And this sort of composable helper could be used with a TestCase like this:
class MyBetterTestCase(ComposableTestCase): mail = MailCapture() logs = LogCapture() redis = MockRedis() jobs = MockJobQueue() def test_foo(self): foo() self.mail.assert_message_sent("hello, world") self.logs.assert_logged("hello, world") self.redis.assert_list_contains("foo", "bar") self.jobs.assert_job_queued("some_job")
I'll be building this kind of composable test helper into my application, and if it works well, I'll release a library.
strftime: table of locale-aware formatters in different locales
April 13, 2013 at 10:44 PM | Python | View CommentsI got curious about what the different locale-specific strftime and strptime formatting directives produced in different locales… So I built a little script which would genreate a table showing each of the different locale-aware formatters in all of the different locales which I've got installed!
The code used to generate this table can be found at: bitbucket.org/wolever/locale-table.
%a | %A | %b | %B | %x | %X | %p | %c | |
---|---|---|---|---|---|---|---|---|
af_ZA | Tue | Tuesday | Aug | August | 08/16/1988 | 21:30:05 | PM | Tue Aug 16 21:30:05 1988 |
am_ET | ማክሰ | ማክሰኞ | ኦገስ | ኦገስት | 16/08/1988 | 21:30:05 | PM | ማክሰ ኦገስ 16 21:30:05 1988 |
be_BY | аў | аўторак | жні | жніўня | 16.08.88 | 21:30:05 | pm | аў 16 жні 21:30:05 1988 |
bg_BG | Вт | Вторник | Авг | Август | 16.08.88 | 21:30:05 | pm | Вт 16 Авг 21:30:05 1988 |
ca_ES | dim | dimarts | ago | agost | 16/08/1988 | 21:30:05 | PM | dim 16 ago 21:30:05 1988 |
cs_CZ | út | úterý | srp | srpna | 1988/08/16 | 21:30:05 | od | út 16 srp 21:30:05 1988 |
da_DK | Tir | Tirsdag | Aug | August | 16.08.1988 | 21:30:05 | pm | Tir 16 Aug 21:30:05 1988 |
de_AT | Di | Dienstag | Aug | August | 16.08.1988 | 21:30:05 | pm | Di 16 Aug 21:30:05 1988 |
de_CH | Di | Dienstag | Aug | August | 16.08.1988 | 21:30:05 | pm | Di 16 Aug 21:30:05 1988 |
de_DE | Di | Dienstag | Aug | August | 16.08.1988 | 21:30:05 | pm | Di 16 Aug 21:30:05 1988 |
el_GR | Τρι | Τρίτη | Αυγ | Αυγούστου | 16/08/1988 | 21:30:05 | μμ | Τρι 16 Αυγ 21:30:05 1988 |
en_AU | Tue | Tuesday | Aug | August | 16/08/1988 | 21:30:05 | pm | Tue 16 Aug 21:30:05 1988 |
en_CA | Tue | Tuesday | Aug | August | 16/08/1988 | 21:30:05 | pm | Tue 16 Aug 21:30:05 1988 |
en_GB | Tue | Tuesday | Aug | August | 16/08/1988 | 21:30:05 | pm | Tue 16 Aug 21:30:05 1988 |
en_IE | Tue | Tuesday | Aug | August | 16/08/1988 | 21:30:05 | pm | Tue 16 Aug 21:30:05 1988 |
en_NZ | Tue | Tuesday | Aug | August | 16/08/1988 | 21:30:05 | pm | Tue 16 Aug 21:30:05 1988 |
en_US | Tue | Tuesday | Aug | August | 08/16/1988 | 21:30:05 | PM | Tue Aug 16 21:30:05 1988 |
es_ES | mar | martes | ago | agosto | 16/08/1988 | 21:30:05 | PM | mar 16 ago 21:30:05 1988 |
et_EE | T | teisipäev | aug | august | 16.08.1988 | 21:30:05 | T, 16. aug 1988. 21:30:05 | |
eu_ES | as. | asteartea | Abu | abuztua | 1988/08/16 | 21:30:05 | p.m. | 1988 - Abu - 16 as. 21:30:05 |
fi_FI | Ti | Tiistai | Elo | Elokuu | 16.08.1988 | 21:30:05 | pm | Ti 16 Elo 21:30:05 1988 |
fr_BE | Mar | Mardi | aoû | août | 16.08.1988 | 21:30:05 | Mar 16 aoû 21:30:05 1988 | |
fr_CA | Mar | Mardi | aoû | août | 16.08.1988 | 21:30:05 | Mar 16 aoû 21:30:05 1988 | |
fr_CH | Mar | Mardi | aoû | août | 16.08.1988 | 21:30:05 | Mar 16 aoû 21:30:05 1988 | |
fr_FR | Mar | Mardi | aoû | août | 16.08.1988 | 21:30:05 | Mar 16 aoû 21:30:05 1988 | |
he_IL | ג' | שלישי | אוג | אוגוסט | 16/08/88 | 21:30:05 | PM | 21:30:05 1988 אוג 16 ג' |
hr_HR | Ut | Utorak | Kol | Kolovoz | 16.08.1988 | 21:30:05 | pm | Ut 16 Kol 21:30:05 1988 |
hu_HU | Ked | Kedd | Aug | Augusztus | 1988/08/16 | 21:30:05 | du | Ked Aug 16 21:30:05 1988 |
hy_AM | Երք | Երեքշաբթի | Օգս | Օգոստոս | 16.08.1988 | 21:30:05 | Երեքշաբթի, 16 Օգոստոս 1988 ի. 21:30:05 | |
is_IS | þri | þriðjudagur | ágú | ágúst | 16.08.1988 | 21:30:05 | eh | þri 16 ágú 21:30:05 1988 |
it_CH | Mar | Martedì | Ago | Agosto | 16.08.1988 | 21:30:05 | pm | Mar 16 Ago 21:30:05 1988 |
it_IT | Mar | Martedì | Ago | Agosto | 16.08.1988 | 21:30:05 | pm | Mar 16 Ago 21:30:05 1988 |
ja_JP | 火 | 火曜日 | 8 | 8月 | 1988/08/16 | 21時30分05秒 | PM | 火 8/16 21:30:05 1988 |
kk_KZ | сс | сейсенбі | там | тамыз | 16.08.1988 | 21:30:05 | сейсенбі, 16 тамыз 1988 ж. 21:30:05 | |
ko_KR | 화 | 화요일 | 8 | 8월 | 1988/08/16 | 21시 30분 05초 | PM | 화 8/16 21:30:05 1988 |
lt_LT | An | Antradienis | Rgp | rugpjūčio | 1988.08.16 | 21:30:05 | An Rgp 16 21:30:05 1988 | |
nl_BE | di | dinsdag | aug | augustus | 16-08-1988 | 21:30:05 | pm | di 16 aug 21:30:05 1988 |
nl_NL | di | dinsdag | aug | augustus | 16-08-1988 | 21:30:05 | pm | di 16 aug 21:30:05 1988 |
no_NO | tir | tirsdag | aug | august | 16.08.1988 | 21:30:05 | pm | tir 16 aug 21:30:05 1988 |
pl_PL | wto | wtorek | sie | sierpnia | 1988.08.16 | 21:30:05 | wto 16 sie 21:30:05 1988 | |
pt_BR | Ter | Terça Feira | Ago | Agosto | 16/08/1988 | 21:30:05 | Ter 16 Ago 21:30:05 1988 | |
pt_PT | Ter | Terça Feira | Ago | Agosto | 16.08.1988 | 21:30:05 | Ter 16 Ago 21:30:05 1988 | |
ro_RO | Mar | Marţi | Aug | August | 16.08.1988 | 21:30:05 | pm | Mar 16 Aug 1988 21:30:05 |
ru_RU | вт | вторник | авг | августа | 16.08.1988 | 21:30:05 | вторник, 16 августа 1988 г. 21:30:05 | |
sk_SK | ut | utorok | aug | august | 16.08.1988 | 21:30:05 | ut 16 aug 21:30:05 1988 | |
sl_SI | tor | torek | avg | avgust | 16.08.1988 | 21:30:05 | pm | tor 16 avg 21:30:05 1988 |
sr_YU | уто | уторак | авг | август | 16.08.1988 | 21:30:05 | уто 16 авг 21:30:05 1988 | |
sv_SE | Tis | Tisdag | Aug | Augusti | 16.08.1988 | 21:30:05 | pm | Tis 16 Aug 21:30:05 1988 |
tr_TR | Sal | Salı | Ağu | Ağustos | 16/08/1988 | 21:30:05 | PM | Sal 16 Ağu 21:30:05 1988 |
uk_UA | вт | вівторок | сер | серпня | 16.08.1988 | 21:30:05 | вт 16 сер 21:30:05 1988 | |
zh_CN | 二 | 星期二 | 8 | 八月 | 1988/08/16 | 21时30分05秒 | 下午 | 二 8/16 21:30:05 1988 |
zh_HK | 二 | 周二 | 8 | 8月 | 1988/08/16 | 21時30分05秒 | 下午 | 二 8/16 21:30:05 1988 |
zh_TW | 二 | 周二 | 8 | 8月 | 1988/08/16 | 21時30分05秒 | 下午 | 二 8/16 21:30:05 1988 |
Consider South's orm object, http://south.readthedocs.org/en/latest/ormfreezing.html, which provides "frozen-in-time" versions of ORM models:
(Pdb++) from my_app.models import MyModel (Pdb++) MyModel <class 'my_app.models.MyModel'> (Pdb++) orm['my_app.MyModel'] <class 'my_app.models.MyModel'> (Pdb++) orm['my_app.MyModel'] == MyModel False
Notice that the magic South frozen model appears to be in the same namespace as my real model: my_app.models!
This is at best stupid, and at worst potentially dangerous.
It makes perfect sense that South needs some mechanism for providing "frozen models" (ie, which match the database schema at the time of migration, of the current schema), but they are emphatically not the same as the real models (don't have any of the real methods, etc), so it should be made very clear that they are different! There are many ways this could be done, but one very simple step would be putting them in a different namespace, which makes it clear that these models are frozen, and references the migrations they belong to:
(Pdb++) orm['my_app.MyModel'] <class 'south.frozen_models.my_app.models_0003.MyModel'>
Additionally, this is a bad code smell which could potentially lead to bugs: because the "frozen model class" is lying about where it's from, any code which relies on the module path will do surprising things. In fact, it's such a big problem that pickle explicitly checks for this case:
(Pdb++) import pickle (Pdb++) pickle.dumps(orm['my_app.MyModel']) *** PicklingError: Can't pickle <class 'my_app.models.MyModel'>: it's not the same object as my_app.models.MyModel
An anti-pattern I've often seen is using try/except incorrectly in an attempt at duck typing:
try: widget.frob() except AttibuteError: # widget can't be frobbed do_something_else()
This is fundamentally broken because it will conceal bugs in the widget.frob() method (ie, if there's a bug in widget.frob() which causes an AttibuteError to be raised).
In this situation, the only safe way to use try/except would be:
try: widget_frob = widget.frob except AttibuteError: # widget can't be frobbed do_something_else() else: widget_frob()
This ensures that bugs in widget.frob() won't be accidentally hidden.
Of course, this code is no where near as pretty as the "incorrect" version, which is why I've come to believe that using try/except for duck typing could be considered an anti-pattern, and that hasattr is generally preferable (except in situations where performance is important, as try/except will almost certainly be faster):
if hasattr(widget, "frob"): widget.frob() else: # widget can't be frobbed do_something_else()
And of course, all this goes without saying that a naked except: is always an absolutely terrible idea...