Skip to main content

Python wrapper for stdlib (and your) objects to give them a fluent interface.

Project description

b'The Fluent python library\n=========================\n\nFluent helps you write more object-oriented and concise python code.\n\nIt is inspired by jQuery and underscore / lodash from the javascript\nworld. It also takes some inspiration from Ruby / SmallTalk -- in\nparticluar, collections and how to work with them.\n\nPlease Note: **This library is an experiment.** It is based on a wrapper\nthat aggressively wraps anything it comes in contact with and tries to\nstay invisible. We\'ll address this in section `Caveats <#caveats>`__\nbelow.\n\nIntroduction: Why use fluent?\n-----------------------------\n\nThe Python standard library includes many useful, time-saving\nconvenience methods such as ``map``, ``zip``, ``filter`` and ``join``.\nThe problem that motivated me to write fluent is that these convenience\nmethods are often available as free functions or on (arguably) the wrong\nobject.\n\nFor example, ``map``, ``zip``, and ``filter`` all operate on iterable\nobjects but they are implemented as free functions. This not only goes\nagainst my sense of how object oriented code should behave, but more\nimportantly, writing python code using these free functions requires\nthat the reader must often mentally skip back and forth in a line of\ncode to understand what it does, making the code more difficult to\nunderstand.\n\nLet\'s use the following simple example to analyse this problem:\n\n::\n\n >>> map(print, map(str.upper, sys.stdin.read().split(\'\\n\')))\n\nHow many backtrackings did you have to do? I read this code as follows:\n\nI start in the middle at ``sys.stdin.read().split(\'\\n\')``, then I\nbacktrack to ``map(str.upper, \xe2\x80\xa6)``, then to ``map(print, \xe2\x80\xa6)``. I also\nhave to make sure that the parenthesis all match up.\n\nI find code like this hard to write and hard to understand, as it\ndoesn\'t follow the way I think about this statement. I don\'t like to\nhave to write or read statements from the inside out and wrap them using\nmy editor as I write them. As demonstrated above, it\'s also hard to read\n- requiring quite a bit of backtracking.\n\nOne alternative to the above approach is to use list comprehension /\ngenerator statements like this:\n\n::\n\n >>> [print(line.upper()) for line in sys.stdin.read().split(\'\\n\')]\n\nThis is clearly better: I only have to skip back and forth once instead\nof twice.\n\nThis approach still leaves room for improvement though because I have to\nfind where the statement starts and to then backtrack to the beginning\nto see what is happening. Adding filtering to list comprehensions\ndoesn\'t help:\n\n::\n\n >>> [print(line.upper()) for line in sys.stdin.read().split(\'\\n\') if line.upper().startswith(\'FNORD\')]\n\nThe backtracking problem persists. Additionally, if the filtering has to\nbe done on the processed version (here artificially on\n``line.upper().startswith()``) then the operation has to be applied\ntwice - which sucks because you have to write it twice, but also because\nit is computed twice.\n\nThe solution? Nest them!\n\n::\n\n >>> [print(line) for line in \\\n >>> (line.upper() for line in sys.stdin.read().split(\'\\n\')) \\\n >>> if line.startswith(\'FNORD\')]\n\nWhich gets us back to all the initial problems with nested statements\nand manually having to check for the right ammount of closing parens.\n\nCompare it to this:\n\n::\n\n >>> for line in sys.stdin.read().split(\'\\n\'):\n >>> uppercased = line.upper()\n >>> if uppercased.startswith(\'FNORD\'):\n >>> print(uppercased)\n\nAlmost all my complaints are gone. It reads and writes almost completely\nin order it is computed.\n\nEasy to read, easy to write. So that is usually what I end up doing.\n\nBut it has a huge drawback: It\'s not an expression - it\'s a bunch of\nstatements.\n\nWhy is that bad? Because it means, that it\'s not easily combinable and\nabstractable with higher order methods or generators. Because I have to\ninvent variable names for things that are completely clear from context\nand that just serve as grease to express the flow of data through the\nprogram.\n\nDon\'t get me wrong, this is the most important function of variables in\nprograms. But in this case, it just makes the code longer and makes it\nharder to see how data flows through the expressions.\n\nPlus (drumroll): parsing this still requires some backtracking and\nbuildup of mental state to read.\n\nOh well.\n\nLets see this in action:\n\n::\n\n >>> cross_product_of_dependency_labels = \\\n >>> set(map(frozenset, itertools.product(*map(attrgetter(\'_labels\'), dependencies))))\n\nThat certainly is hard to read (and write). Pulling out explaining\nvariables, makes it better. Like so:\n\n::\n\n >>> labels = map(attrgetter(\'_labels\'), dependencies)\n >>> cross_product_of_dependency_labels = set(map(frozenset, itertools.product(*labels)))\n\nBetter, but still hard to read. Sure, those explaining variables are\nnice and sometimes essential to understand the code. - but it does take\nup space in lines, and space in my head while parsing this code. The\nquestion would be - is this really easier to read than something like\nthis?\n\n::\n\n >>> cross_product_of_dependency_labels = (\n ... _(dependencies)\n ... .map(_.each._labels)\n ... .star_call(itertools.product)\n ... .map(frozenset)\n ... .call(set)\n ... ._\n ... )\n\nSure you are not used to this at first, but consider the advantages. The\nintermediate variable names are abstracted away - the data flows through\nthe methods completely naturally. No jumping back and forth to parse\nthis at all. It just reads and writes exactly in the order it is\ncomputed.\n\nTo me this means, that what I think that I want to accomplish, I can\nwrite down directly in order. And I don\'t have to keep track of extra\nclosing parantheses at the end of the expression.\n\nSo what is the essence of all of this?\n\nPython is an object oriented language - but it doesn\'t really use what\nobject orientation has tought us about how we can work with collections\nand higher order methods in the languages that came before it (I think\nof SmallTalk here, but more recently also Ruby). Why can\'t I make those\nbeautiful fluent call chains that SmallTalk could do 20 years ago in\nPython today?\n\nWell, now I can and you can too.\n\nFeatures\n--------\n\nTo enable this style of coding this library has some features that might\nnot be so obvious at first.\n\nImporting the library\n~~~~~~~~~~~~~~~~~~~~~\n\nIt is recomended to import and use the library by renaming it to\nsomething locally unique.:\n\n::\n\n >>> import fluentpy as _f\n\nor\n\n::\n\n >>> import fluentpy as _\n\nI prefer ``_`` for small projects and ``_f`` for larger projects where\ngettext is used.\n\nIf you want you can also import the library in the classic way:\n\n::\n\n >>> from fluentpy import _, lib, each\n\nBut it is not required to import all these symbols, as they are all also\navailable as attributes on ``_``. Also, the library exposes itself as an\nexecutable module, i.e. the module ``fluentpy`` itself is the central\nwrapper function and can be used directly by renaming it to what you\nneed locally.\n\nAggressive (specialized) wrapping\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n``_`` is actually the function ``wrap`` in the fluent module, which is a\nfactory function that returns a subclass of Wrapper, the basic and main\nobject of this library.\n\nThis does two things: First it ensures that every attribute access, item\naccess or method call off of the wrapped object will also return a\nwrapped object. This means that once you wrap something, unless you\nunwrap it explicitly via ``.unwrap`` or ``._`` it stays wrapped - pretty\nmuch no matter what you do with it. The second thing this does is that\nit returns a subclass of Wrapper that has a specialized set of methods,\ndepending on the type of what is wrapped. I envision this to expand in\nthe future, but right now the most usefull wrappers are: Iterable, where\nwe add all the python collection functions (map, filter, zip, reduce, \xe2\x80\xa6)\nas well as a good batch of methods from itertools and a few extras for\ngood measure. Callable, where we add ``.curry()`` and ``.compose()`` and\nText, where most of the regex methods are added. `Explore the method\ndocumentation for what you can do <>`__).\n\nTODO add link!\n\nEasy Shell Filtering with Python\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIt could often be super easy, to achieve somethign on the shell, with a\nbit of python. But, the backtracking (while writing) as well as the\ntendency of python commands to span many lines, makes this often just\nimpractical enough that you won\'t do it.\n\nThat\'s why fluent is an executable module, so that you can use it on the\nshell like this:\n\n::\n\n $ python3 -m fluentpy "lib.sys.stdin.readlines().map(str.lower).map(print)"\n\nIn this mode, the variables \'lib\', \'\\_\' and \'each\' are injected into the\nnamespace of of the python commands given as the first positional\nargument.\n\nImports as expressions\n~~~~~~~~~~~~~~~~~~~~~~\n\nImport statements are (ahem) statements in python. This is fine, but can\nbe really annoying at times.\n\nConsider this shell text filter written in python:\n\n::\n\n $ curl -sL \'https://www.iblocklist.com/lists.php\' | egrep -A1 \'star_[345]\' \\\n > | python3 -c "import sys, re; from xml.sax.saxutils import unescape; \\\n > print(\'\\n\'.join(map(unescape, re.findall(r\'value=\\\'(.*)\\\'\', sys.stdin.read()))))" \n\nSure it has all the backtracking problems I talked about already. Using\nfluent this could be much shorter.\n\n::\n\n $ curl -sL \'https://www.iblocklist.com/lists.php\' \\\n > | egrep -A1 \'star_[345]\' \\\n > | python3 -c "import fluentpy as _; import sys, re; from xml.sax.saxutils import unescape; \\\n > _(sys.stdin.read()).findall(r\'value=\\\'(.*)\\\'\').map(unescape).map(print)"\n\nThis still leaves the problem that it has to start with this fluff\n\n::\n\n import fluentpy as _; import sys, re; from xml.sax.saxutils import unescape;\n\nThis doesn\'t really do anything to make it easier to read and write and\nis almost half the characters it took to achieve the wanted effect.\nWouldn\'t it be nice if you could have some kind of object (lets call it\n``lib`` for lack of a better word), where you could just access the\nwhole python library via attribute access and let it\'s machinery handle\nimporting behind the scenes?\n\nLike this:\n\n::\n\n $ curl -sL \'https://www.iblocklist.com/lists.php\' | egrep -A1 \'star_[345]\' \\\n > | python3 -m fluentpy "lib.sys.stdin.read().findall(r\'value=\\\'(.*)\\\'\') \\\n > .map(lib.xml.sax.saxutils.unescape).map(print)"\n\nHow\'s that for reading and writing if all the imports are inlined? Oh,\nand of course everything imported via ``lib`` comes already pre-wrapped,\nso your code becomes even shorter.\n\nMore formally:The ``lib`` object, which is a wrapper around the python\nimport machinery, allows to import anything that is accessible by import\nto be imported as an expression for inline use.\n\nSo instead of\n\n::\n\n >>> import sys\n >>> input = sys.stdin.read()\n\nYou can do\n\n::\n\n >>> input = _.lib.sys.stdin.read()\n\nAs a bonus, everything imported via lib is already pre-wrapped, so you\ncan chain off of it immediately.\n\nGenerating lambda\'s from expressions\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n``lambda`` is great - it\'s often exactly what the doctor ordered. But it\ncan also be annyoing if you have to write it down everytime you just\nwant to get an attribute or call a method on every object in a\ncollection.\n\n::\n\n >>> _([dict(fnord=\'foo\'), dict(fnord=\'bar\')]).map(lambda each: each[\'fnord\']) == [\'foo\', \'bar]\n >>> class Foo(object):\n >>> attr = \'attrvalue\'\n >>> def method(self, arg): return \'method+\'+arg\n >>> _([Foo(), Foo()]).map(lambda each: each.attr) == [\'attrvalue\', \'attrvalue\']\n >>> _([Foo(), Foo()]).map(lambda each: each.method(\'arg\')) == [\'method+arg\', \'method+arg\']\n\nSure it works, but wouldn\'t it be nice if we could save a variable and\ndo this a bit shorter?\n\nPython does have attrgetter, itemgetter and methodcaller - they are just\na bit inconvenient to use:\n\n::\n\n >>> from operator import itemgetter, attrgetter, methodcaller\n >>> _([dict(fnord=\'foo\'), dict(fnord=\'bar\')]).map(itemgetter(\'fnord\')) == [\'foo\', \'bar]\n >>> class Foo(object):\n >>> attr = \'attrvalue\'\n >>> def method(self, arg): return \'method+\'+arg\n >>> _([Foo(), Foo()]).map(attrgetter(attr)) == [\'attrvalue\', \'attrvalue\']\n >>> _([Foo(), Foo()]).map(methodcaller(method, \'arg\')) == [\'method+arg\', \'method+arg\']\n\nTo ease this the object ``_.each`` is provided, that just exposes a bit\nof syntactic shugar for these (and the other operators). Basically,\neverything you do to ``_.each`` it will do to each object in the\ncollection:\n\n::\n\n >>> _([1,2,3]).map(_.each + 3) == [4,5,6]\n >>> _([1,2,3]).filter(_.each < 3) == [1,2]\n >>> _([1,2,3]).map(- _.each) == [-1,-2,-3]\n >>> _([dict(fnord=\'foo\'), dict(fnord=\'bar\')]).map(_.each[\'fnord\']) == [\'foo\', \'bar]\n >>> class Foo(object):\n >>> attr = \'attrvalue\'\n >>> def method(self, arg): return \'method+\'+arg\n >>> _([Foo(), Foo()]).map(_.each.attr) == [\'attrvalue\', \'attrvalue\']\n >>> _([Foo(), Foo()]).map(_.each.call.method(\'arg\')) == [\'method+arg\', \'method+arg\']\n\nI know ``_.each.call.*()`` is crude - but I haven\'t found a good syntax\nto get rid of the .call yet. Feedback welcome.\n\nChaining off of methods that return None\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nA major nuissance for using fluent interfaces are methods that return\nNone. Sadly, many methods in python return None, if they mostly exhibit\na side effect on the object. Consider for example ``list.sort()``.\n\nThis is a feature of python, where methods that don\'t have a return\nstatement return None.\n\nWhile this is way better than e.g. Ruby where that will just return the\nvalue of the last expression - which means objects constantly leak\ninternals - it is very annoying if you want to chain off of one of these\nmethod calls.\n\nFear not though, fluent has you covered. :)\n\nFluent wrapped objects will have a ``self`` property, that allows you to\ncontinue chaining off of the previous self.\n\n::\n\n >>> _([3,2,1]).sort().self.reverse().self.call(print)\n\nEven though both sort() and reverse() return None\n\nOf course, if you unwrap at any point with ``.unwrap`` or ``._`` you\nwill get the true return value of ``None``.\n\nCaveats\n-------\n\nIf you do not end each fluent statement with a ``.unwrap`` or ``._``\noperation to get a normal python object back, the wrapper will spread in\nyour runtime image like a virus, \'infecting\' more and more objects\ncausing strange side effects. So remember: Always religiously unwrap\nyour objects at the end of a fluent statement, when using fluent in\nbigger projects.\n\n::\n\n >>> _("foo").uppercase().match(\'(foo)\').group(0)._\n\nThat being said, ``str()`` and ``repr()`` output is clearly marked, so\nthis is easy to debug. Also, not having to unwrap is perfect for short\nscripts and especially \'one-off\' shell commands. Use fluents power\nwisely!\n\nFamous Last Words\n-----------------\n\nThis library tries to do a little of what libraries like underscore or\nlodash or jQuery do for Javascript. Just provide the missing glue to\nmake the standard library nicer and easier to use - especially for short\noneliners or short script. Have fun!\n\nI envision this to be very usefull in quick python scripts and shell one\nliners and filters, where python was previously just that little bit too\nhard to use, that \'overflowed the barrel\' and prevented you from doing\nso.\n'


Project details


Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page