{ "info": { "author": "Martin H\u00e4cker", "author_email": "mhaecker@mac.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: ISC License (ISCL)", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", "Topic :: Utilities" ], "description": "# fluentpy - The fluent Python library\n\nFluentpy provides fluent interfaces to existing APIs such as the standard library, allowing you to use them in an object oriented and fluent style.\n\nFluentpy is inspired by JavaScript's `jQuery` and `underscore` / `lodash` and takes some inspiration from the collections API in Ruby and SmallTalk.\n\nPlease note: **This library is based on an agressive wrapper**, that wraps anything it comes in contact with. See the section **Caveats** below for details.\n\nSee [Fowler](https://www.martinfowler.com/bliki/FluentInterface.html), [Wikipedia](https://de.wikipedia.org/wiki/Fluent_Interface) for definitions of fluent interfaces.\n\n[](https://fluentpy.readthedocs.io/en/latest/?badge=latest)\n\n## Motivation: Why use `fluentpy`?\n\nMany of the most useful standard library methods such as `map`, `zip`, `filter` and `join` are either free functions or available on the wrong type or module. This prevents fluent method chaining.\n\nLet's consider this example:\n\n >>> list(map(str.upper, sorted(\"ba,dc\".split(\",\"), reverse=True)))\n ['DC', 'BA']\n\nTo understand this code, I have to start in the middle at `\"ba,dc\".split(\",\")`, then backtrack to `sorted(\u2026, reverse=True)`, then to `list(map(str.upper, \u2026))`. All the while making sure that the parentheses all match up.\n\nWouldn't it be nice if we could think and write code in the same order? Something like how you would write this in other languages?\n\n >>> _(\"ba,dc\").split(\",\").sorted(reverse=True).map(str.upper)._\n ['DC', 'BA']\n\n\"Why no, but python has list comprehensions for that\", you might say? Let's see:\n\n >>> [each.upper() for each in sorted(\"ba,dc\".split(\",\"), reverse=True)]\n ['DC', 'BA']\n\nThis is clearly better: To read it, I have to skip back and forth less. It still leaves room for improvement though. Also, adding filtering to list comprehensions doesn't help:\n\n >>> [each.upper() for each in sorted(\"ba,dc\".split(\",\"), reverse=True) if each.upper().startswith('D')]\n ['DC']\n\nThe backtracking problem persists. Additionally, if the filtering has to be done on the processed version (on `each.upper().startswith()`), then the operation has to be applied twice - which sucks because you write it twice and compute it twice.\n\nThe solution? Nest them!\n\n >>> [each for each in \n (inner.upper() for inner in sorted(\"ba,dc\".split(\",\"), reverse=True))\n if each.startswith('D')]\n ['DC']\n\nWhich gets us back to all the initial problems with nested statements and manually having to check closing parentheses.\n\nCompare it to this:\n\n >>> processed = []\n >>> parts = \"ba,dc\".split(\",\")\n >>> for item in sorted(parts, reverse=True):\n >>> uppercases = item.upper()\n >>> if uppercased.startswith('D')\n >>> processed.append(uppercased)\n\nWith basic Python, this is as close as it gets for code to read in execution order. 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 statements. That makes it hard to combine and abstract over it with higher order methods or generators. To write it you are forced to invent names for intermediate variables that serve no documentation purpose, but force you to remember them while reading.\n\nPlus (drumroll): parsing this still requires some backtracking and especially build up of mental state to read.\n\nOh well.\n\nSo let's return to this:\n\n >>> (\n _(\"ba,dc\")\n .split(\",\")\n .sorted(reverse=True)\n .map(str.upper)\n .filter(_.each.startswith('D')._)\n ._\n )\n ('DC',)\n\nSure you are not used to this at first, but consider the advantages. The intermediate variable names are abstracted away - the data flows through the methods completely naturally. No jumping back and forth to parse this at all. It just reads and writes exactly in the order it is computed. As a bonus, there's no parentheses stack to keep track of. And it is shorter too!\n\nSo what is the essence of all of this?\n\nPython is an object oriented language - but it doesn't really use what object orientation has taught us about how we can work with collections and higher order methods in the languages that came before it (I think of SmallTalk here, but more recently also Ruby). Why can't I make those beautiful fluent call chains that SmallTalk could do 30 years ago in Python?\n\nWell, now I can and you can too.\n\n## Features\n\n### Importing the library\n\nIt is recommended to rename the library on import:\n\n >>> import fluentpy as _\n\nor\n\n >>> import fluentpy as _f\n\nI prefer `_` for small projects and `_f` for larger projects where `gettext` is used.\n\n### Aggressive (specialized) wrapping\n\n`_` is actually the function `wrap` in the `fluentpy` module, which is a factory function that returns a subclass of Wrapper, the basic and main object of this library.\n\nThis does two things: First it ensures that every attribute access, item access or method call off of the wrapped object will also return a wrapped object. This means, once you wrap something, unless you unwrap it explicitly via `._` or `.unwrap` or `.to(a_type)` it stays wrapped - pretty much no matter what you do with it. The second thing this does is that it returns a subclass of Wrapper that has a specialized set of methods, depending on the type of what is wrapped. I envision this to expand in the future, but right now the most useful wrappers are: `IterableWrapper`, where we add all the Python collection functions (map, filter, zip, reduce, \u2026), as well as a good batch of methods from `itertools` and a few extras for good measure. CallableWrapper, where we add `.curry()` and `.compose()` and TextWrapper, where most of the regex methods are added. \n\nSome exaples:\n\n # View documentation on a symbol without having to wrap the whole line it in parantheses\n >>> _([]).append.help()\n Help on built-in function append:\n\n append(object, /) method of builtins.list instance\n Append object to the end of the list.\n\n # Introspect objects without awkward wrapping stuff in parantheses\n >>> _(_).dir()\n fluentpy.wrap(['CallableWrapper', 'EachWrapper', 'IterableWrapper', 'MappingWrapper', 'ModuleWrapper', 'SetWrapper', 'TextWrapper', 'Wrapper', \n '_', '_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7', '_8', '_9', \n \u2026\n , '_args', 'each', 'lib', 'module', 'wrap'])\n >>> _(_).IterableWrapper.dir()\n fluentpy.wrap(['_', \n \u2026, \n 'accumulate', 'all', 'any', 'call', 'combinations', 'combinations_with_replacement', 'delattr', \n 'dir', 'dropwhile', 'each', 'enumerate', 'filter', 'filterfalse', 'flatten', 'freeze', 'get', \n 'getattr', 'groupby', 'grouped', 'hasattr', 'help', 'iaccumulate', 'icombinations', '\n icombinations_with_replacement', 'icycle', 'idropwhile', 'ieach', 'ienumerate', 'ifilter', \n 'ifilterfalse', 'iflatten', 'igroupby', 'igrouped', 'imap', 'ipermutations', 'iproduct', 'ireshape', \n 'ireversed', 'isinstance', 'islice', 'isorted', 'issubclass', 'istar_map', 'istarmap', 'itee', \n 'iter', 'izip', 'join', 'len', 'map', 'max', 'min', 'permutations', 'pprint', 'previous', 'print', \n 'product', 'proxy', 'reduce', 'repr', 'reshape', 'reversed', 'self', 'setattr', 'slice', 'sorted', \n 'star_call', 'star_map', 'starmap', 'str', 'sum', 'to', 'type', 'unwrap', 'vars', 'zip'])\n\n # Did I mention that I hate wrapping everything in parantheses?\n >>> _([1,2,3]).len()\n 3\n >>> _([1,2,3]).print()\n [1,2,3]\n\n # map over iterables and easily curry functions to adapt their signatures\n >>> _(range(3)).map(_(dict).curry(id=_, delay=0)._)._\n ({'id': 0, 'delay': 0}, {'id': 1, 'delay': 0}, {'id': 2, 'delay': 0})\n >>> _(range(10)).map(_.each * 3).filter(_.each < 10)._\n (0, 3, 6, 9)\n >>> _([3,2,1]).sorted().filter(_.each<=2)._\n [1,2]\n\n # Directly work with regex methods on strings\n >>> _(\"foo, bar, baz\").split(r\",\\s*\")._\n ['foo', 'bar', 'baz']\n >>> _(\"foo, bar, baz\").findall(r'\\w{3}')._\n ['foo', 'bar', 'baz']\n\nAnd much more. [Explore the method documentation for what you can do](https://fluentpy.readthedocs.io/en/latest/fluentpy/fluentpy.html)).\n\n### Imports as expressions\n\nImport statements are (ahem) statements in Python. This is fine, but can be really annoying at times.\n\nThe `_.lib` object, which is a wrapper around the Python import machinery, allows to import anything that is accessible by import to be imported as an expression for inline use.\n\nSo instead of\n\n >>> import sys\n >>> input = sys.stdin.read()\n\nYou can do\n\n >>> lines = _.lib.sys.stdin.readlines()._\n\nAs a bonus, everything imported via lib is already pre-wrapped, so you can chain off of it immediately.\n\n### Generating lambdas from expressions\n\n`lambda` is great - it's often exactly what the doctor ordered. But it can also be annoying if you have to write it down every time you just want to get an attribute or call a method on every object in a collection. For Example:\n\n >>> _([{'fnord':'foo'}, {'fnord':'bar'}]).map(lambda each: each['fnord'])._\n ('foo', 'bar')\n\n >>> class Foo(object):\n >>> attr = 'attrvalue'\n >>> def method(self, arg): return 'method+'+arg\n >>> _([Foo(), Foo()]).map(lambda each: each.attr)._\n ('attrvalue', 'attrvalue')\n\n >>> _([Foo(), Foo()]).map(lambda each: each.method('arg'))._\n ('method+arg', 'method+arg')\n\nSure it works, but wouldn't it be nice if we could save a variable and do this a bit shorter?\n\nPython does have `attrgetter`, `itemgetter` and `methodcaller` - they are just a bit inconvenient to use:\n\n >>> from operator import itemgetter, attrgetter, methodcaller\n >>> __([{'fnord':'foo'}, {'fnord':'bar'}]).map(itemgetter('fnord'))._\n ('foo', 'bar')\n >>> _([Foo(), Foo()]).map(attrgetter('attr'))._\n ('attrvalue', 'attrvalue')\n\n >>> _([Foo(), Foo()]).map(methodcaller('method', 'arg'))._\n ('method+arg', 'method+arg')\n\n _([Foo(), Foo()]).map(methodcaller('method', 'arg')).map(str.upper)._\n ('METHOD+ARG', 'METHOD+ARG')\n\nTo ease this, `_.each` is provided. `each` exposes a bit of syntactic sugar for these (and the other operators). Basically, everything you do to `_.each` it will record and later 'play back' when you generate a callable from it by either unwrapping it, or applying an operator like `+ - * / <', which automatically call unwrap.\n\n >>> _([1,2,3]).map(_.each + 3)._\n (4, 5, 6)\n\n >>> _([1,2,3]).filter(_.each < 3)._\n (1, 2)\n\n >>> _([1,2,3]).map(- _.each)._\n (-1, -2, -3)\n\n >>> _([dict(fnord='foo'), dict(fnord='bar')]).map(_.each['fnord']._)._\n ('foo', 'bar')\n\n >>> _([Foo(), Foo()]).map(_.each.attr._)._\n ('attrvalue', 'attrvalue')\n\n >>> _([Foo(), Foo()]).map(_.each.method('arg')._)._\n ('method+arg', 'method+arg')\n\n >>> _([Foo(), Foo()]).map(_.each.method('arg').upper()._)._\n ('METHOD+ARG', 'METHOD+ARG')\n # Note that there is no second map needed to call `.upper()` here!\n\n\nThe rule is that you have to unwrap `._` the each object to generate a callable that you can then hand off to `.map()`, `.filter()` or wherever you would like to use it.\n\n### Chaining off of methods that return None\n\nA major nuisance for using fluent interfaces are methods that return None. Sadly, many methods in Python return None, if they mostly exhibit a side effect on the object. Consider for example `list.sort()`. But also all methods that don't have a `return` statement return None. While this is way better than e.g. Ruby where that will just return the value of the last expression - which means objects constantly leak internals - it is very annoying if you want to chain off of one of these method calls.\n\nFear not though, Fluentpy has you covered. :)\n\nFluent wrapped objects will have a `self` property, that allows you to continue chaining off of the previous 'self' object.\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 will get the true return value of `None`.\n\n\n### Easy Shell Filtering with Python\n\nIt could often be super easy to achieve something on the shell, with a bit of Python. But, the backtracking (while writing) as well as the tendency of Python commands to span many lines (imports, function definitions, ...), makes this often just impractical enough that you won't do it.\n\nThat's why `fluentpy` is an executable module, so that you can use it on the shell like this:\n\n $ echo 'HELLO, WORLD!' \\\n | python3 -m fluentpy \"lib.sys.stdin.readlines().map(str.lower).map(print)\"\n hello, world!\n\n\nIn this mode, the variables `lib`, `_` and `each` are injected into the namespace of of the `python` commands given as the first positional argument.\n\nConsider this shell text filter, that I used to extract data from my beloved but sadly pretty legacy del.icio.us account. The format looks like this:\n\n $ tail -n 200 delicious.html|head\n