{"id":1270,"date":"2010-05-04T20:31:43","date_gmt":"2010-05-05T01:31:43","guid":{"rendered":"http:\/\/mikeconley.ca\/blog\/?p=1270"},"modified":"2023-12-20T16:25:16","modified_gmt":"2023-12-20T21:25:16","slug":"python-metaclasses-in-review-board","status":"publish","type":"post","link":"https:\/\/mikeconley.ca\/blog\/2010\/05\/04\/python-metaclasses-in-review-board\/","title":{"rendered":"Python Metaclasses in Review Board"},"content":{"rendered":"<p>So, after <a href=\"http:\/\/mikeconley.ca\/blog\/2010\/04\/30\/code-spelunking-review-board-extensions-2\/\">diving into the Review Board extension code<\/a>, I hit a little snag.<\/p>\n<p>It turns out I never learned about Python metaclasses.\u00a0 And the extension code in Djblets \/ Review Board uses them.\u00a0 In <a href=\"http:\/\/www.mikeconley.ca\/images\/rb_ext\/Map.html\">the map I made of the Review Board extension code<\/a>, I represented my confusion with an image of some kind of quantum-divide-by-zero implosion.<\/p>\n<p>I&#8217;ll get to the <em>why<\/em> for metaclasses in a second.\u00a0 First, I&#8217;ll demonstrate <em>how<\/em> it&#8217;s currently being used in the code.<\/p>\n<h3>The Way Things Are<\/h3>\n<p>This snippit is from the Djblets library, in the extensions folder, in base.py:<\/p>\n<pre>...\r\n\r\n<a name=\"extension_hook\" href=\"#extension_hook\">class ExtensionHook(object):<\/a>\r\n    def __init__(self, extension):\r\n        self.extension = extension\r\n        self.extension.hooks.add(self)\r\n        self.__class__.add_hook(self)\r\n\r\n    def shutdown(self):\r\n        self.__class__.remove_hook(self)\r\n\r\n<a name=\"extension_hook_point\" href=\"#extension_hook_point\">class ExtensionHookPoint(type):<\/a>\r\n    def __init__(cls, name, bases, attrs):\r\n        super(ExtensionHookPoint, cls).__init__(name, bases, attrs)\r\n\r\n        if not hasattr(cls, \"hooks\"):\r\n            cls.hooks = []\r\n\r\n    def add_hook(cls, hook):\r\n        cls.hooks.append(hook)\r\n\r\n    def remove_hook(cls, hook):\r\n        cls.hooks.remove(hook)\r\n\r\n...<\/pre>\n<p>And here are those classes being used in the Review Board extension directory, where it defines its hooks:<\/p>\n<pre>...\r\n\r\n<a name=\"dashboard_hook\" href=\"#dashboard_hook\">class DashboardHook(ExtensionHook):<\/a>\r\n    __metaclass__ = ExtensionHookPoint\r\n\r\n    def get_entries(self):\r\n        raise NotImplemented\r\n\r\n<a name=\"navigation_bar_hook\" href=\"#navigation_bar_hook\">class NavigationBarHook(ExtensionHook):<\/a>\r\n    \"\"\"\r\n    A hook for adding entries to the main navigation bar.\r\n    \"\"\"\r\n    __metaclass__ = ExtensionHookPoint\r\n\r\n    def get_entry(self):\r\n        \"\"\"\r\n        Returns the entry to add to the navigation bar.\r\n\r\n        This should be a dict with the following keys:\r\n\r\n            * `label`: The label to display\r\n            * `url`:   The URL to point to.\r\n        \"\"\"\r\n        raise NotImplemented\r\n...<\/pre>\n<p>The idea here is that, if someone wanted to write an extension, they could add a hook to the navigation bar by subclassing the hooks, like so:<\/p>\n<p>(from <a href=\"http:\/\/github.com\/chipx86\/rb-extension-pack\/blob\/master\/rbreports\/rbreports\/extension.py\">the rbreports prototype extension<\/a>)<\/p>\n<pre>...\r\n\r\nclass ReportsDashboardHook(DashboardHook):\r\n    def get_entries(self):\r\n        return [{\r\n            'label': 'Reports',\r\n            'url': settings.SITE_ROOT + 'reports\/',\r\n        }]\r\n\r\nclass ReportsExtension(Extension):\r\n    is_configurable = True\r\n\r\n    def __init__(self):\r\n        Extension.__init__(self)\r\n\r\n        self.url_hook = URLHook(self, patterns('',\r\n            (r'^reports\/', include('rbreports.urls'))))\r\n\r\n        self.dashboard_hook = ReportsDashboardHook(self)\r\n...<\/pre>\n<p>So you can see the __metaclass__ thing up there in the <a href=\"#dashboard_hook\">DashboardHook<\/a>.\u00a0 <a href=\"#dashboard_hook\">DashboardHook<\/a> subclasses <a href=\"#extension_hook\">ExtensionHook<\/a>, and has <a href=\"#extension_hook_point\">ExtensionHookPoint<\/a> as a metaclass.<\/p>\n<p><em>Why?<\/em> And what is a metaclass anyways?\u00a0 Why is this useful at all?<\/p>\n<p>Well, luckily, I think I&#8217;ve figured it out.<\/p>\n<h3>Behold &#8211; Metaclasses<\/h3>\n<blockquote><p>Metaclasses are deeper magic than 99% of users should ever worry about.         If you wonder whether you need them, you don&#8217;t (the people who actually need them         know with certainty that they need them, and don&#8217;t need an explanation about why).<br \/>\n&#8212; Python Guru Tim Peters<\/p><\/blockquote>\n<p>There are <a href=\"http:\/\/onlamp.com\/pub\/a\/python\/2003\/04\/17\/metaclasses.html\">plenty<\/a> <a href=\"http:\/\/www.voidspace.org.uk\/python\/articles\/metaclasses.shtml\">of resources<\/a> <a href=\"http:\/\/stackoverflow.com\/questions\/100003\/what-is-a-metaclass-in-python\">available<\/a> <a href=\"http:\/\/www.vrplumber.com\/programming\/metaclasses.pdf\">regarding<\/a> <a href=\"http:\/\/stackoverflow.com\/questions\/392160\/what-are-your-concrete-use-cases-for-metaclasses-in-python\">Python&#8217;s<\/a> metaclasses.\u00a0 And I ended up reading a ton of them trying to figure out just what the hell metaclasses actually do.<\/p>\n<p>As it turns out, <a href=\"http:\/\/en.wikipedia.org\/wiki\/Metaclasses\">the best explanation I got was from Wikipedia<\/a>:<\/p>\n<blockquote><p>In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances.<\/p><\/blockquote>\n<p><a href=\"http:\/\/en.wikipedia.org\/wiki\/Metaclasses#Python_example\">In particular, check out their example on Cars<\/a>.\u00a0 That, for me,\u00a0 more or less set the record straight on how metaclasses are used.<\/p>\n<p>But why does Review Board use them when defining those hooks, like <a href=\"#dashboard_hook\">DashboardHook<\/a>?<\/p>\n<h3>Why?<\/h3>\n<p>Forget the metaclasses for just a second.<\/p>\n<p>Something is missing from that code that I posted up.<\/p>\n<p>I&#8217;ll pose it as a question:\u00a0 How exactly is Review Board supposed to <em>know<\/em> about ReportsDashboardHook?\u00a0 Like&#8230;if we&#8217;re rendering the Dashboard, and want to fire off calls to all of our DashboardHooks&#8230;how do we do it?\u00a0 How can we find all of the active extension subclasses for DashboardHook?<\/p>\n<p>One way you could do it, is to dive into the extensions directories, and use regular expressions to find defined classes.\u00a0 That sounds like a lot of work, brittle, and prone to error.\u00a0 What else?<\/p>\n<p>Next, you could go back to those extension files, &#8220;eval&#8221; them (<em>shudder<\/em>), and extract the globals(), looking for <a href=\"#extension_hook\">ExtensionHook<\/a> subclasses.\u00a0 That also sounds like lots of work, brittle, and prone to error.<\/p>\n<p>You could go for <a href=\"http:\/\/docs.python.org\/library\/stdtypes.html#class.__subclasses__\">class.__subclasses__<\/a>&#8230;but this gives you every subclass, and we just want the hooks that are for active extensions.\u00a0 We could filter out all of the ones that aren&#8217;t activated, but we&#8217;d have to do that for every hook, and that blows.<\/p>\n<p>Next, we could have developers add their hooks to a registry after defining them.\u00a0 So, we could have:<\/p>\n<pre>class ReportsDashboardHook(ExtensionHook):\r\n  # ...\r\n  # code goes here\r\n  # ...\r\n\r\ndashboard_hook_list.append(ReportsDashboardHook)<\/pre>\n<p>And then when an extension is shut down, we just remove it from the global hook list.\u00a0 That doesn&#8217;t look so bad, does it?\u00a0 The only problem, is now we have to trust that extension developers will know to add those hooks to the global_hook_list?\u00a0 It&#8217;s another step, another thing to forget, and another point of failure.<\/p>\n<p>And it seems silly &#8211; I mean, why go through all this effort?\u00a0 Review Board has already seen these hooks&#8230;why should it be so hard to just access the list activated hooks easily?<\/p>\n<p><strong>And there it is.<\/strong> <em>That&#8217;s<\/em> why I think we&#8217;re using metaclasses.<\/p>\n<p>If you go back to <a href=\"#extension_hook\">ExtensionHook<\/a> and <a href=\"#extension_hook_point\">ExtensionHookPoint<\/a>, you&#8217;ll notice that ExtensionHookPoint has a list as an instance variable called hooks.\u00a0 That&#8217;s the global hook list for that class of hook (<a href=\"#dashboard_hook\">DashboardHook<\/a>).\u00a0 And then <a href=\"#extension_hook\">ExtensionHook<\/a>, in its constructor, automatically adds itself to the Extension&#8217;s internal list of hooks, as well as the global hook list, with the add_hook method.\u00a0 Shutting down the hook removes that hook from the global hook list.<\/p>\n<p>So now, all we need to do to add our implemented hook to the list of hooks, is to simply subclass <a href=\"#dashboard_hook\">DashboardHook<\/a>, or <a href=\"#navigation_bar_hook\">NavigationBarHook<\/a>, or <a href=\"http:\/\/www.mikeconley.ca\/images\/rb_ext\/Map.html\">any of those Review Board hooks<\/a>.\u00a0 No need to add the hook to the global list manually &#8211; Djblets \/ Review Board will take care of it.\u00a0 So now, we can get all of the DashboardHook subclasses for activated hooks, easily and consistently, with no extra work for the extension developer.\u00a0 Same with the NavigationBarHook subclasses.<\/p>\n<p>Smooth as glass.\u00a0 Nice.<\/p>\n<p>Anyhow, that&#8217;s my understanding of it.\u00a0 If I&#8217;m totally off, I&#8217;ll let you know when I find out.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>So, after diving into the Review Board extension code, I hit a little snag. It turns out I never learned about Python metaclasses.\u00a0 And the extension code in Djblets \/ Review Board uses them.\u00a0 In the map I made of the Review Board extension code, I represented my confusion with an image of some kind [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[657,773],"tags":[668,669,213,670,667,665,1219],"class_list":["post-1270","post","type-post","status-publish","format-standard","hentry","category-extensions-review-board-code-reviews-computer-science-technology","category-gsoc-computer-science","tag-__metaclass__","tag-cls","tag-extension","tag-hooks","tag-metaclasses","tag-python","tag-review-board"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/prmTy-ku","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/1270","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/comments?post=1270"}],"version-history":[{"count":31,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/1270\/revisions"}],"predecessor-version":[{"id":1336,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/1270\/revisions\/1336"}],"wp:attachment":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/media?parent=1270"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/categories?post=1270"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/tags?post=1270"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}