{ "info": { "author": "Patrick Lehmann", "author_email": "Paebbels@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Topic :: Utilities" ], "description": "![PyPI - License](https://img.shields.io/pypi/l/pyAttributes)\n![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/Paebbels/pyAttributes) \n![GitHub release (latest by date)](https://img.shields.io/github/v/release/Paebbels/pyAttributes)\n[![Documentation Status](https://readthedocs.org/projects/pyattributes/badge/?version=latest)](https://pyattributes.readthedocs.io/en/latest/?badge=latest) \n[![PyPI](https://img.shields.io/pypi/v/pyAttributes)](https://pypi.org/project/pyAttributes/)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyAttributes)\n![PyPI - Wheel](https://img.shields.io/pypi/wheel/pyAttributes)\n![PyPI - Status](https://img.shields.io/pypi/status/pyAttributes)\n\n# pyAttributes\n\n**.NET-like Attributes implemented as Python decorators**\n\nI have played a bit with class-based decorators and as far as I can tell it's possible to implement .NET-like attributes in Python.\n\n## So first let's develop a meaningful use case:\nMost of us know the Python `argparse` command line argument parser. This parser can handle sub-commands like `git commit -m \"message\"` where *commit* is a sub-command and `-m ` is a parameter of this sub-command parser. It's possible to assign a callback function to each sub-command parser.\n\n> **Python 3.4.2 for Windows** has a bug in handling callback functions. It's fixed in 3.5.0 (I haven't tested other 3.4.x versions).\n\nHere is a classic `argparse` example:\n\n```python\nclass MyProg():\n\tdef Run(self):\n\t\t# create a commandline argument parser\n\t\tMainParser = argparse.ArgumentParser(\n\t\t\tdescription = textwrap.dedent('''This is the User Service Tool.'''),\n\t\t\tformatter_class = argparse.RawDescriptionHelpFormatter,\n\t\t\tadd_help=False)\n\n\t\tMainParser.add_argument('-v', '--verbose', dest=\"verbose\", help='print out detailed messages', action='store_const', const=True, default=False)\n\t\tMainParser.add_argument('-d', '--debug', dest=\"debug\", help='enable debug mode', action='store_const', const=True, default=False)\n\t\tMainParser.set_defaults(func=self.HandleDefault)\n\t\tsubParsers = MainParser.add_subparsers(help='sub-command help')\n\n\t\t# UserManagement commads\n\t\t# create the sub-parser for the \"create-user\" command\n\t\tCreateUserParser = subParsers.add_parser('create-user', help='create-user help')\n\t\tCreateUserParser.add_argument(metavar='', dest=\"Users\", type=str, nargs='+', help='todo help')\n\t\tCreateUserParser.set_defaults(func=self.HandleCreateUser)\n\n\t\t# create the sub-parser for the \"remove-user\" command\n\t\tRemoveUserParser = subParsers.add_parser('remove-user', help='remove-user help')\n\t\tRemoveUserParser.add_argument(metavar='', dest=\"UserIDs\", type=str, nargs='+', help='todo help')\n\t\tRemoveUserParser.set_defaults(func=self.HandleRemoveUser)\n\n\tdef HandleDefault(self, args):\n\t\tprint(\"HandleDefault:\")\n\n\tdef HandleCreateUser(self, args):\n\t\tprint(\"HandleCreateUser: {0}\".format(str(args.Users)))\n\n\tdef HandleRemoveUser(self, args):\n\t\tprint(\"HandleRemoveUser: {0}\".format(str(args.UserIDs)))\n\nmy = MyProg()\nmy.Run()\n```\n\nA better and more descriptive solution could look like this:\n\n```python\nclass MyProg():\n\tdef __init__(self):\n\t\tself.BuildParser()\n\t\t# ...\n\tdef BuiltParser(self):\n\t\t# 1. search self for methods (potential handlers)\n\t\t# 2. search this methods for attributes\n\t\t# 3. extract Command and Argument attributes\n\t\t# 4. create the parser with that provided metadata\n\n\t# UserManagement commads\n\t@CommandAttribute('create-user', help=\"create-user help\")\n\t@ArgumentAttribute(metavar='', dest=\"Users\", type=str, nargs='+', help='todo help')\n\tdef HandleCreateUser(self, args):\n\t\tprint(\"HandleCreateUser: {0}\".format(str(args.Users)))\n\n\t@CommandAttribute('remove-user',help=\"remove-user help\")\n\t@ArgumentAttribute(metavar='', dest=\"UserIDs\", type=str, nargs='+', help='todo help')\n\tdef HandleRemoveUser(self, args):\n\t\tprint(\"HandleRemoveUser: {0}\".format(str(args.UserIDs)))\n```\n\n-------\n\nSo let's develop a solution step-by-step.\n\n## Step 1 - A common `Attribute` class\n\nSo let's develop a common `Attribute` class, which is also a class-based decorator. This decorator adds himself to a list called `__attributes__`, which is registered on the function which is to be decorated.\n\n```python\nclass Attribute():\n\tAttributesMemberName = \"__attributes__\"\n\t_debug = False\n\n\tdef __call__(self, func):\n\t\t# inherit attributes and append myself or create a new attributes list\n\t\tif (func.__dict__.__contains__(Attribute.AttributesMemberName)):\n\t\t\tfunc.__dict__[Attribute.AttributesMemberName].append(self)\n\t\telse:\n\t\t\tfunc.__setattr__(Attribute.AttributesMemberName, [self])\n\t\treturn func\n\n\tdef __str__(self):\n\t\treturn self.__name__\n\n\t@classmethod\n\tdef GetAttributes(self, method):\n\t\tif method.__dict__.__contains__(Attribute.AttributesMemberName):\n\t\t\tattributes = method.__dict__[Attribute.AttributesMemberName]\n\t\t\tif isinstance(attributes, list):\n\t\t\t\treturn [attribute for attribute in attributes if isinstance(attribute, self)]\n\t\treturn list()\n```\n\n## Step 2 - User defined attributes\n\nNow we can create custom attributes which inherit the basic decorative functionality from `Attribute`. I'll declare 3 attributes:\n\n - **DefaultAttribute** - If no sub-command parser recognizes a command, this decorated method will be the fallback handler.\n - **CommandAttribute** - Define a sub-command and register the decorated function as a callback.\n - **ArgumentAttribute** - Add parameters to the sub-command parser.\n\n```python\nclass DefaultAttribute(Attribute):\n\t__handler = None\n\n\tdef __call__(self, func):\n\t\tself.__handler = func\n\t\treturn super().__call__(func)\n\n\t@property\n\tdef Handler(self):\n\t\treturn self.__handler\n```\n\n```python\nclass CommandAttribute(Attribute):\n\t__command = \"\"\n\t__handler = None\n\t__kwargs = None\n\n\tdef __init__(self, command, **kwargs):\n\t\tsuper().__init__()\n\t\tself.__command = command\n\t\tself.__kwargs = kwargs\n\n\tdef __call__(self, func):\n\t\tself.__handler = func\n\t\treturn super().__call__(func)\n\n\t@property\n\tdef Command(self):\n\t\treturn self.__command\n\n\t@property\n\tdef Handler(self):\n\t\treturn self.__handler\n\n\t@property\n\tdef KWArgs(self):\n\t\treturn self.__kwargs\n```\n\n```python\nclass ArgumentAttribute(Attribute):\n\t__args = None\n\t__kwargs = None\n\n\tdef __init__(self, *args, **kwargs):\n\t\tsuper().__init__()\n\t\tself.__args = args\n\t\tself.__kwargs = kwargs\n\n\t@property\n\tdef Args(self):\n\t\treturn self.__args\n\n\t@property\n\tdef KWArgs(self):\n\t\treturn self.__kwargs\n```\n\n## Step 3 - Building a helper mixin class to handle attributes on methods\n\nTo ease the work with attributes I implemented a `AttributeHelperMixin` class, that can:\n\n - retrieve all methods of a class\n - check if a method has attributes and\n - return a list of attributes on a given method.\n\n```python\nclass AttributeHelperMixin():\n\tdef GetMethods(self):\n\t\treturn {funcname: func\n\t\t\t\t\t\tfor funcname, func in self.__class__.__dict__.items()\n\t\t\t\t\t\tif hasattr(func, '__dict__')\n\t\t\t\t\t }.items()\n\n\tdef HasAttribute(self, method):\n\t\tif method.__dict__.__contains__(Attribute.AttributesMemberName):\n\t\t\tattributeList = method.__dict__[Attribute.AttributesMemberName]\n\t\t\treturn (isinstance(attributeList, list) and (len(attributeList) != 0))\n\t\telse:\n\t\t\treturn False\n\n\tdef GetAttributes(self, method):\n\t\tif method.__dict__.__contains__(Attribute.AttributesMemberName):\n\t\t\tattributeList = method.__dict__[Attribute.AttributesMemberName]\n\t\t\tif isinstance(attributeList, list):\n\t\t\t\treturn attributeList\n\t\treturn list()\n```\n\n## Step 4 - Build an application class\n\nNow it's time to build an application class that inherits from `MyBase` and from `ArgParseMixin`. I'll discuss `ArgParseMixin` later. The class has a normal constructor, which calls both base-class constructors. It also adds 2 arguments for *verbose* and *debug* to the main-parser. All callback handlers are decorated with the new Attributes.\n\n```python\nclass MyBase():\n\tdef __init__(self):\n\t\tpass\n\nclass prog(MyBase, ArgParseMixin):\n\tdef __init__(self):\n\t\timport argparse\n\t\timport textwrap\n\n\t\t# call constructor of the main interitance tree\n\t\tMyBase.__init__(self)\n\n\t\t# Call the constructor of the ArgParseMixin\n\t\tArgParseMixin.__init__(self,\n\t\t\tdescription = textwrap.dedent('''\\\n\t\t\t\tThis is the Admin Service Tool.\n\t\t\t\t'''),\n\t\t\tformatter_class = argparse.RawDescriptionHelpFormatter,\n\t\t\tadd_help=False)\n\n\t\tself.MainParser.add_argument('-v', '--verbose', dest=\"verbose\", help='print out detailed messages', action='store_const', const=True, default=False)\n\t\tself.MainParser.add_argument('-d', '--debug', dest=\"debug\", help='enable debug mode', action='store_const', const=True, default=False)\n\n\tdef Run(self):\n\t\tArgParseMixin.Run(self)\n\n\t@DefaultAttribute()\n\tdef HandleDefault(self, args):\n\t\tprint(\"DefaultHandler: verbose={0} debug={1}\".format(str(args.verbose), str(args.debug)))\n\n\t@CommandAttribute(\"create-user\", help=\"my new command\")\n\t@ArgumentAttribute(metavar='', dest=\"Users\", type=str, help='todo help')\n\tdef HandleCreateUser(self, args):\n\t\tprint(\"HandleCreateUser: {0}\".format(str(args.Users)))\n\n\t@CommandAttribute(\"remove-user\", help=\"my new command\")\n\t@ArgumentAttribute(metavar='', dest=\"UserIDs\", type=str, help='todo help')\n\tdef HandleRemoveUser(self, args):\n\t\tprint(\"HandleRemoveUser: {0}\".format(str(args.UserIDs)))\n\np = prog()\np.Run()\n```\n\n## Step 5 - The `ArgParseMixin` helper class.\n\nThis class constructs the `argparse` based parser with the provided data from attributes. The parsing process is invoked by `Run()`.\n\n```python\nclass ArgParseMixin(AttributeHelperMixin):\n\t__mainParser = None\n\t__subParser = None\n\t__subParsers = {}\n\n\tdef __init__(self, **kwargs):\n\t\tsuper().__init__()\n\n\t\t# create a commandline argument parser\n\t\timport argparse\n\t\tself.__mainParser = argparse.ArgumentParser(**kwargs)\n\t\tself.__subParser = self.__mainParser.add_subparsers(help='sub-command help')\n\n\t\tfor funcname,func in self.GetMethods():\n\t\t\tdefAttributes = DefaultAttribute.GetAttributes(func)\n\t\t\tif (len(defAttributes) != 0):\n\t\t\t\tdefAttribute = defAttributes[0]\n\t\t\t\tself.__mainParser.set_defaults(func=defAttribute.Handler)\n\t\t\t\tcontinue\n\n\t\t\tcmdAttributes = CommandAttribute.GetAttributes(func)\n\t\t\tif (len(cmdAttributes) != 0):\n\t\t\t\tcmdAttribute = cmdAttributes[0]\n\t\t\t\tsubParser = self.__subParser.add_parser(cmdAttribute.Command, **(cmdAttribute.KWArgs))\n\t\t\t\tsubParser.set_defaults(func=cmdAttribute.Handler)\n\n\t\t\t\tfor argAttribute in ArgumentAttribute.GetAttributes(func):\n\t\t\t\t\tsubParser.add_argument(*(argAttribute.Args), **(argAttribute.KWArgs))\n\n\t\t\t\tself.__subParsers[cmdAttribute.Command] = subParser\n\t\t\t\tcontinue\n\n\tdef Run(self):\n\t\t# parse command line options and process splitted arguments in callback functions\n\t\targs = self.__mainParser.parse_args()\n\t\t# because func is a function (unbound to an object), it MUST be called with self as a first parameter\n\t\targs.func(self, args)\n\n\t@property\n\tdef MainParser(self):\n\t\treturn self.__mainParser\n\n\t@property\n\tdef SubParsers(self):\n\t\treturn self.__subParsers\n```\n\n## Contributors:\n\n* [Patrick Lehmann](https://github.com/Paebbels) (Maintainer)\n\n\n## License\n\nThis library is licensed under [Apache License 2.0](LICENSE.md)\n\n-------------------------\n\nSPDX-License-Identifier: Apache-2.0\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/Paebbels/pyAttributes", "keywords": "Python3 Decorators", "license": "", "maintainer": "", "maintainer_email": "", "name": "pyAttributes", "package_url": "https://pypi.org/project/pyAttributes/", "platform": "", "project_url": "https://pypi.org/project/pyAttributes/", "project_urls": { "Documentation": "https://pyAttributes.readthedocs.io/en/latest/", "Homepage": "https://github.com/Paebbels/pyAttributes", "Issue Tracker": "https://github.com/Paebbels/pyAttributes/issues", "Source Code": "https://github.com/Paebbels/pyAttributes" }, "release_url": "https://pypi.org/project/pyAttributes/0.2.6/", "requires_dist": null, "requires_python": ">=3.5", "summary": ".NET-like Attributes implemented as Python decorators.", "version": "0.2.6" }, "last_serial": 5966087, "releases": { "0.2.2": [ { "comment_text": "", "digests": { "md5": "02a5de2d16fe096e07764123487ee634", "sha256": "bbade02fec4acb55f74c824e95148b30cb3275c851755623b03329596ff8f9f6" }, "downloads": -1, "filename": "pyAttributes-0.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "02a5de2d16fe096e07764123487ee634", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11556, "upload_time": "2019-10-10T20:14:13", "url": "https://files.pythonhosted.org/packages/39/87/e851cd2163d5bbb9350c63b324704314122f2b8305bb6b78e5198e1f54da/pyAttributes-0.2.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3c22c1659f63528df99f744fb30bf56f", "sha256": "3600dc0b248df918aa7e8a96bdc9d4702711bb6ac57eb1ae0f86af707ad3d741" }, "downloads": -1, "filename": "pyAttributes-0.2.2.tar.gz", "has_sig": false, "md5_digest": "3c22c1659f63528df99f744fb30bf56f", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 7801, "upload_time": "2019-10-10T20:14:16", "url": "https://files.pythonhosted.org/packages/00/75/010125ae5fc324e053d977a1a853929f9d0695b7ee4cc94338eeb931d897/pyAttributes-0.2.2.tar.gz" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "ca37cb878e8013ad38e9dcc6247c60df", "sha256": "4c3928eeadcc6c561815ff4dd460d0e1c1712da58277703554aab3b58025ec51" }, "downloads": -1, "filename": "pyAttributes-0.2.3-py3-none-any.whl", "has_sig": false, "md5_digest": "ca37cb878e8013ad38e9dcc6247c60df", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11593, "upload_time": "2019-10-11T00:34:58", "url": "https://files.pythonhosted.org/packages/af/e6/7bbf645a5ef8b67d070043bd309612a75e2738c6d0d3b95870cbd7d20f42/pyAttributes-0.2.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "75fb31df9bbee1cdd98106cd3e6e19d8", "sha256": "7fd3cc4e28bb2d7ed8b66178b020448cca99054ca2d7c60441157fbf8909c075" }, "downloads": -1, "filename": "pyAttributes-0.2.3.tar.gz", "has_sig": false, "md5_digest": "75fb31df9bbee1cdd98106cd3e6e19d8", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 7828, "upload_time": "2019-10-11T00:34:59", "url": "https://files.pythonhosted.org/packages/28/7d/c7aa9ff2ef33731346f2cd5200560015bcb7902f0c6aaa2871a7fed25303/pyAttributes-0.2.3.tar.gz" } ], "0.2.4": [ { "comment_text": "", "digests": { "md5": "c623187c49395659727a3f436f06436d", "sha256": "cf185c3c25b9ebb0a53cabb0e512baa5cc4e8caf47366b7b189d7666b995ced2" }, "downloads": -1, "filename": "pyAttributes-0.2.4-py3-none-any.whl", "has_sig": false, "md5_digest": "c623187c49395659727a3f436f06436d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11746, "upload_time": "2019-10-12T11:38:58", "url": "https://files.pythonhosted.org/packages/1f/6f/096d49cfdec13118e3e7799d6862cbd5d7f7317fb84c231db081c4575bd1/pyAttributes-0.2.4-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "07b831293e469081b35e3ae59ed69bce", "sha256": "33193bb5c917ceeb6a49f703dc9c36f58069f94a7db78d1f380a80f1d445353e" }, "downloads": -1, "filename": "pyAttributes-0.2.4.tar.gz", "has_sig": false, "md5_digest": "07b831293e469081b35e3ae59ed69bce", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 8292, "upload_time": "2019-10-12T11:39:00", "url": "https://files.pythonhosted.org/packages/47/25/a8a558b0ae330d4f7d96b86e900d6881774a36e8c2b9d606a4c802688bea/pyAttributes-0.2.4.tar.gz" } ], "0.2.5": [ { "comment_text": "", "digests": { "md5": "32dceb409e24a180bc3fcce52240425c", "sha256": "f55207d081bad1a6430e39a7e7db39349711c6787b48be5bf4f0b5cfa135a666" }, "downloads": -1, "filename": "pyAttributes-0.2.5-py3-none-any.whl", "has_sig": false, "md5_digest": "32dceb409e24a180bc3fcce52240425c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11798, "upload_time": "2019-10-12T23:02:04", "url": "https://files.pythonhosted.org/packages/7e/78/ed15a7510e02b1c65882da74ef46c44802ce624df22c8bcc52efa8395be0/pyAttributes-0.2.5-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0c66c72c96758a451724992c5eb5e148", "sha256": "c3d538e18981c8383e682dd73d0d75996376ffa1ce564caa134623f13820823a" }, "downloads": -1, "filename": "pyAttributes-0.2.5.tar.gz", "has_sig": false, "md5_digest": "0c66c72c96758a451724992c5eb5e148", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 8330, "upload_time": "2019-10-12T23:02:06", "url": "https://files.pythonhosted.org/packages/2c/b6/e162d4c10faadbdbac5f92d3ace13ee788099e70efa98498049bc096013b/pyAttributes-0.2.5.tar.gz" } ], "0.2.6": [ { "comment_text": "", "digests": { "md5": "30776f52760e2c527ee3eca32847bfb4", "sha256": "97348cf56c4ea2f891317da2e6ca05a2d9760be64f6fc8ce7cffb5222d78a4ed" }, "downloads": -1, "filename": "pyAttributes-0.2.6-py3-none-any.whl", "has_sig": false, "md5_digest": "30776f52760e2c527ee3eca32847bfb4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11880, "upload_time": "2019-10-13T02:23:09", "url": "https://files.pythonhosted.org/packages/5f/19/e3513690cef74aca3eaf093c3fcb6eecd2355b3b1367548b10428c68a70e/pyAttributes-0.2.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9472d8e41cecb0ff9576da4183364643", "sha256": "9196107f6d04f0624605ec0363a5826710a58d0592376215975c6fb26e7dab05" }, "downloads": -1, "filename": "pyAttributes-0.2.6.tar.gz", "has_sig": false, "md5_digest": "9472d8e41cecb0ff9576da4183364643", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 8427, "upload_time": "2019-10-13T02:23:11", "url": "https://files.pythonhosted.org/packages/4b/08/35f0562cd6d9e5babc6e9d8d54abae6a5f82c099003b394f08f7c443eef1/pyAttributes-0.2.6.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "30776f52760e2c527ee3eca32847bfb4", "sha256": "97348cf56c4ea2f891317da2e6ca05a2d9760be64f6fc8ce7cffb5222d78a4ed" }, "downloads": -1, "filename": "pyAttributes-0.2.6-py3-none-any.whl", "has_sig": false, "md5_digest": "30776f52760e2c527ee3eca32847bfb4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.5", "size": 11880, "upload_time": "2019-10-13T02:23:09", "url": "https://files.pythonhosted.org/packages/5f/19/e3513690cef74aca3eaf093c3fcb6eecd2355b3b1367548b10428c68a70e/pyAttributes-0.2.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9472d8e41cecb0ff9576da4183364643", "sha256": "9196107f6d04f0624605ec0363a5826710a58d0592376215975c6fb26e7dab05" }, "downloads": -1, "filename": "pyAttributes-0.2.6.tar.gz", "has_sig": false, "md5_digest": "9472d8e41cecb0ff9576da4183364643", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.5", "size": 8427, "upload_time": "2019-10-13T02:23:11", "url": "https://files.pythonhosted.org/packages/4b/08/35f0562cd6d9e5babc6e9d8d54abae6a5f82c099003b394f08f7c443eef1/pyAttributes-0.2.6.tar.gz" } ] }