{"id":1552,"date":"2022-05-01T18:36:24","date_gmt":"2022-05-01T06:36:24","guid":{"rendered":"http:\/\/p-s.co.nz\/wordpress\/?p=1552"},"modified":"2022-05-01T18:46:30","modified_gmt":"2022-05-01T06:46:30","slug":"making-external-objects-play-nicely-with-type-hinting","status":"publish","type":"post","link":"http:\/\/p-s.co.nz\/wordpress\/making-external-objects-play-nicely-with-type-hinting\/","title":{"rendered":"Making External Objects Play Nicely with Type Hinting"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">The Problem<\/h2>\n\n\n\n<p>We want our function to type hint such that only objects with a particular method e.g. <code>.attack()<\/code>, can be received. Something like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def all_attack(units: Sequence(HasAttackMethod)):<\/pre>\n\n\n\n<p>One of the objects only has a <code>.fire()<\/code> method. How do we make sure it can meet the requirement and be used in the same way as the other objects?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Extend the External Object &#8211; Make a Wrapper<\/h2>\n\n\n\n<p>Here we simply make a light wrapper around the object that doesn&#8217;t change the interface apart from adding the missing method. The new method is added to the object using <code>setattr<\/code> and everything else is delegated to the original object.<\/p>\n\n\n\n<p>Now we can supply the extended object and it will function as required. It will also meet the required type specification, meeting the Attacker Protocol by having an <code>.attack()<\/code> method.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"\nOK - we have one weird external object - how do we handle it?\n\"\"\"\nfrom functools import partial\nfrom typing import Protocol, Sequence\n\n\nclass Soldier:\n    def __init__(self, name):\n        self.name = name\n    def attack(self):\n        print(f\"Soldier {self.name} swings their sword!\")\n\nclass Archer:\n    def __init__(self, name):\n        self.name = name\n    def attack(self):\n        print(f\"Archer {self.name} fires their arrow!\")\n\nclass Catapult:\n    def __init__(self, name):\n        self.name = name\n    def fire(self):  ## not the required name\n        print(f\"Catapult {self.name} hurls their fireball!\")\n\ns1 = Soldier('Tim')\ns2 = Soldier('Sal')\na1 = Archer('Cal')\nc1 = Catapult('Mech')\n\n\nclass Extend():\n    def __init__(self, original, method_name, method):\n        self.original = original\n        ## being a method self needs to come first\n        method_with_self_arg = partial(method, self=self)\n        ## Add method e.g. .attack() \n        setattr(self, method_name, method_with_self_arg)\n    def __getattr__(self, name, *args, **kwargs):\n        \"\"\"\n        Delegate everything other than the added method\n        to the original object\n        \"\"\"\n        original_attr = self.original.__getattribute__(name)\n        if hasattr(original_attr, '__call__'):  ## handle methods\n            return original_attr(*args, **kwargs)\n        else:  ## handle ordinary attributes\n            return original_attr\n\ndef catapult_attack(self):\n    self.original.fire()\n\nc1_extended = Extend(original=c1, method_name='attack',\n    method=catapult_attack)\n\nclass Attacker(Protocol):\n    def attack(self) -&gt; None:\n        ...\n\ndef all_attack(units: Sequence[Attacker]):\n    for unit in units:\n        unit.attack()\n\nall_attack([s1, s2, a1, c1_extended])\nprint(c1_extended.name)<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Making a Polymorpher<\/h2>\n\n\n\n<p>Another approach is to make a polymorpher function. This relies upon the classes pip package  <a href=\"https:\/\/pypi.org\/project\/classes\/\">https:\/\/pypi.org\/project\/classes\/<\/a>. See also: <a href=\"https:\/\/classes.readthedocs.io\/en\/latest\/pages\/supports.html#regular-types\">https:\/\/classes.readthedocs.io\/en\/latest\/pages\/supports.html#regular-types<\/a><\/p>\n\n\n\n<p>As at the time of writing there seem to be problems making this code pass mypy which defeats the purpose. The explanation seems to be some brittleness in the (very promising) <code>classes<\/code> package but that is not fully confirmed.<\/p>\n\n\n\n<p>For example, if you were making a game and you needed a function to only accept objects with an <code>attack()<\/code> method you could do it like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>def all_attack(units: Sequence[Supports[RequiredAttackType]]):\n    for unit in units:\n        unit.attack()<\/code><\/pre>\n\n\n\n<p>There are some nasty and opaque steps required to make this work. These can be concentrated in one function to make it easy to use this duck hinting approach. In the code below the nastiest part is in <code>polymorpher()<\/code>. The function is included to show how it works and so others can potentially improve on it.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"\nPurpose: in a static-typing context,\nhow do we enable external objects\nto comply with type requirements in a function?\n\nE.g. all_attack(units: Sequence[RequiredType])\nwhich only permits sequences of the required type.\n\nThat is no problem with Soldier and Archer\nas they have attack methods.\nBut Catapult doesn't have an attack method.\nHow do we give it one without:\n* changing the source code\n* monkey patching\n\nAnd how do we type hint (enforced by mypy) so that only objects which have the attack method can be received?\n\nBasically it would be nice if we could add an attack method to\nCatapult using a decorator e.g. like\n\n@attack.instance(Catapult)\ndef _fn_catapult(catapult: Catapult):\n    print(f\"Catapult {catapult.name} hurls their fireball!\")\n\nAnd, instead of saying which types are acceptable,\nfollow a duck typing approach and say\nwhat behaviours are required e.g. must have an attack method. E.g.\n\ndef all_attack(units: Sequence[Supports[RequiredAttackType]]):\n    \u2026\n\nTo make that all work, we have made a nasty, opaque function (polymorpher) that gives us both parts needed - the decorator,\nand the behaviour type.\n\"\"\"\n\nfrom typing import Protocol, Sequence\n\n## pip installation required\nfrom classes import AssociatedType, Supports, typeclass\n\n## UNITS *********************************************************\n\nclass Soldier:\n    def __init__(self, name):\n        self.name = name\n    def attack(self):\n        print(f\"Soldier {self.name} swings their sword!\")\n\nclass Archer:\n    def __init__(self, name):\n        self.name = name\n    def attack(self):\n        print(f\"Archer {self.name} fires their arrow!\")\n\n## assume this class defined an object we can't \/ shouldn't modify\nclass Catapult:\n    def __init__(self, name):\n        self.name = name\n\ns1 = Soldier('Tim')\ns2 = Soldier('Sal')\na1 = Archer('Cal')\nc1 = Catapult('Mech')\n\n## ATTACKING *******************************************************\nOpaque, somewhat nasty function that makes it easy to make external objects polymorphic so they'll work with a function requiring \ncompliant objects\n\ndef polymorpher(*, method_name: str):\n    ## want separate types depending on method_name\n    RequiredType = type(method_name, (AssociatedType, ), {})\n    @typeclass(RequiredType)\n    def fn(instance):\n        pass\n    def self2pass(self):\n        pass\n    RequiredProtocol = type(\n        f'{method_name}_protocol',\n        (Protocol, ),\n        {'_is_runtime_protocol': True,\n             method_name: self2pass,\n        })\n    @fn.instance(protocol=RequiredProtocol)\n    def _fn_run_method(obj: RequiredProtocol):\n        return getattr(obj, method_name)()\n    return fn, RequiredType\n\n## method_name and returned fn don't have to share name\nattack, RequiredAttackType = polymorpher(method_name='attack')\n\n## now easy to make external objects polymorphic\nso they'll work with a function requiring compliant objects\n\n@attack.instance(Catapult)\ndef _fn_catapult(catapult: Catapult):\n    print(f\"Catapult {catapult.name} hurls their fireball!\")\n\ndef all_attack(units: Sequence[Supports[RequiredAttackType]]):\n    for unit in units:\n        attack(unit)  ## note - not unit.attack()\n\n## UNITs ATTACKing <strong><em>*<\/em><\/strong>***************************************************\n\nunits = [s1, s2, a1, c1]\nall_attack(units)<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Wrap Up<\/h2>\n\n\n\n<p>Type hinting and duck typing are both parts of modern Python and they need to play together well &#8211; ideally in a simpler and standardised way. The code above hopefully assists with that evolution.<\/p>\n\n\n\n<p>See also <a rel=\"noreferrer noopener\" href=\"https:\/\/www.daan.fyi\/writings\/python-protocols\" data-type=\"URL\" data-id=\"https:\/\/www.daan.fyi\/writings\/python-protocols\" target=\"_blank\">https:\/\/www.daan.fyi\/writings\/python-protocols<\/a> and <a rel=\"noreferrer noopener\" href=\"https:\/\/medium.com\/alan\/python-typing-with-mypy-progressive-type-checking-on-a-large-code-base-74e13356bd3a\" data-type=\"URL\" data-id=\"https:\/\/medium.com\/alan\/python-typing-with-mypy-progressive-type-checking-on-a-large-code-base-74e13356bd3a\" target=\"_blank\">https:\/\/medium.com\/alan\/python-typing-with-mypy-progressive-type-checking-on-a-large-code-base-74e13356bd3a<\/a><\/p>\n\n\n\n<p>And a big thanks to Ben Denham for doing most of the heavy lifting with making the polymorpher (almost ;-)) work.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Problem We want our function to type hint such that only objects with a particular method e.g. .attack(), can be received. Something like this: def all_attack(units: Sequence(HasAttackMethod)): One of the objects only has a .fire() method. How do we &hellip; <a href=\"http:\/\/p-s.co.nz\/wordpress\/making-external-objects-play-nicely-with-type-hinting\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[21,18,15,25],"class_list":["post-1552","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-code","tag-programming","tag-python","tag-python3"],"_links":{"self":[{"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/posts\/1552"}],"collection":[{"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/comments?post=1552"}],"version-history":[{"count":2,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/posts\/1552\/revisions"}],"predecessor-version":[{"id":1555,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/posts\/1552\/revisions\/1555"}],"wp:attachment":[{"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/media?parent=1552"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/categories?post=1552"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/p-s.co.nz\/wordpress\/wp-json\/wp\/v2\/tags?post=1552"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}