Skip to main content

.NET-like Attributes implemented as Python decorators.

Project description

PyPI - License GitHub tag (latest SemVer incl. pre-release) GitHub release (latest SemVer incl. including pre-releases) GitHub release date Libraries.io status for latest release Requires.io
Travis PyPI PyPI - Status PyPI - Python Version Dependent repos (via libraries.io)
Codacy Badge Libraries.io SourceRank Read the Docs

pyAttributes

.NET-like Attributes implemented as Python decorators

TODO: Move the following documentation parts to ReadTheDocs and add a simplified version instead.

I 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.

So first let's develop a meaningful use case:

Most 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 <message> is a parameter of this sub-command parser. It's possible to assign a callback function to each sub-command parser.

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).

Here is a classic argparse example:

class MyProg():
	def Run(self):
		# create a commandline argument parser
		MainParser = argparse.ArgumentParser(
			description = textwrap.dedent('''This is the User Service Tool.'''),
			formatter_class = argparse.RawDescriptionHelpFormatter,
			add_help=False)

		MainParser.add_argument('-v', '--verbose', dest="verbose", help='print out detailed messages', action='store_const', const=True, default=False)
		MainParser.add_argument('-d', '--debug', dest="debug", help='enable debug mode', action='store_const', const=True, default=False)
		MainParser.set_defaults(func=self.HandleDefault)
		subParsers = MainParser.add_subparsers(help='sub-command help')

		# UserManagement commads
		# create the sub-parser for the "create-user" command
		CreateUserParser = subParsers.add_parser('create-user', help='create-user help')
		CreateUserParser.add_argument(metavar='<Username>', dest="Users", type=str, nargs='+', help='todo help')
		CreateUserParser.set_defaults(func=self.HandleCreateUser)

		# create the sub-parser for the "remove-user" command
		RemoveUserParser = subParsers.add_parser('remove-user', help='remove-user help')
		RemoveUserParser.add_argument(metavar='<UserID>', dest="UserIDs", type=str, nargs='+', help='todo help')
		RemoveUserParser.set_defaults(func=self.HandleRemoveUser)

	def HandleDefault(self, args):
		print("HandleDefault:")

	def HandleCreateUser(self, args):
		print("HandleCreateUser: {0}".format(str(args.Users)))

	def HandleRemoveUser(self, args):
		print("HandleRemoveUser: {0}".format(str(args.UserIDs)))

my = MyProg()
my.Run()

A better and more descriptive solution could look like this:

class MyProg():
	def __init__(self):
		self.BuildParser()
		# ...
	def BuiltParser(self):
		# 1. search self for methods (potential handlers)
		# 2. search this methods for attributes
		# 3. extract Command and Argument attributes
		# 4. create the parser with that provided metadata

	# UserManagement commads
	@CommandAttribute('create-user', help="create-user help")
	@ArgumentAttribute(metavar='<Username>', dest="Users", type=str, nargs='+', help='todo help')
	def HandleCreateUser(self, args):
		print("HandleCreateUser: {0}".format(str(args.Users)))

	@CommandAttribute('remove-user',help="remove-user help")
	@ArgumentAttribute(metavar='<UserID>', dest="UserIDs", type=str, nargs='+', help='todo help')
	def HandleRemoveUser(self, args):
		print("HandleRemoveUser: {0}".format(str(args.UserIDs)))

So let's develop a solution step-by-step.

Step 1 - A common Attribute class

So 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.

class Attribute():
	AttributesMemberName =  "__attributes__"
	_debug =                False

	def __call__(self, func):
		# inherit attributes and append myself or create a new attributes list
		if (func.__dict__.__contains__(Attribute.AttributesMemberName)):
			func.__dict__[Attribute.AttributesMemberName].append(self)
		else:
			func.__setattr__(Attribute.AttributesMemberName, [self])
		return func

	def __str__(self):
		return self.__name__

	@classmethod
	def GetAttributes(self, method):
		if method.__dict__.__contains__(Attribute.AttributesMemberName):
			attributes = method.__dict__[Attribute.AttributesMemberName]
			if isinstance(attributes, list):
				return [attribute for attribute in attributes if isinstance(attribute, self)]
		return list()

Step 2 - User defined attributes

Now we can create custom attributes which inherit the basic decorative functionality from Attribute. I'll declare 3 attributes:

  • DefaultAttribute - If no sub-command parser recognizes a command, this decorated method will be the fallback handler.
  • CommandAttribute - Define a sub-command and register the decorated function as a callback.
  • ArgumentAttribute - Add parameters to the sub-command parser.
class DefaultAttribute(Attribute):
	__handler = None

	def __call__(self, func):
		self.__handler = func
		return super().__call__(func)

	@property
	def Handler(self):
		return self.__handler
class CommandAttribute(Attribute):
	__command = ""
	__handler = None
	__kwargs =  None

	def __init__(self, command, **kwargs):
		super().__init__()
		self.__command =  command
		self.__kwargs =   kwargs

	def __call__(self, func):
		self.__handler = func
		return super().__call__(func)

	@property
	def Command(self):
		return self.__command

	@property
	def Handler(self):
		return self.__handler

	@property
	def KWArgs(self):
		return self.__kwargs
class ArgumentAttribute(Attribute):
	__args =   None
	__kwargs = None

	def __init__(self, *args, **kwargs):
		super().__init__()
		self.__args =   args
		self.__kwargs = kwargs

	@property
	def Args(self):
		return self.__args

	@property
	def KWArgs(self):
		return self.__kwargs

Step 3 - Building a helper mixin class to handle attributes on methods

To ease the work with attributes I implemented a AttributeHelperMixin class, that can:

  • retrieve all methods of a class
  • check if a method has attributes and
  • return a list of attributes on a given method.
class AttributeHelperMixin():
	def GetMethods(self):
		return {funcname: func
						for funcname, func in self.__class__.__dict__.items()
						if hasattr(func, '__dict__')
					 }.items()

	def HasAttribute(self, method):
		if method.__dict__.__contains__(Attribute.AttributesMemberName):
			attributeList = method.__dict__[Attribute.AttributesMemberName]
			return (isinstance(attributeList, list) and (len(attributeList) != 0))
		else:
			return False

	def GetAttributes(self, method):
		if method.__dict__.__contains__(Attribute.AttributesMemberName):
			attributeList = method.__dict__[Attribute.AttributesMemberName]
			if isinstance(attributeList, list):
				return attributeList
		return list()

Step 4 - Build an application class

Now 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.

class MyBase():
	def __init__(self):
		pass

class prog(MyBase, ArgParseMixin):
	def __init__(self):
		import argparse
		import textwrap

		# call constructor of the main interitance tree
		MyBase.__init__(self)

		# Call the constructor of the ArgParseMixin
		ArgParseMixin.__init__(self,
			description = textwrap.dedent('''\
				This is the Admin Service Tool.
				'''),
			formatter_class = argparse.RawDescriptionHelpFormatter,
			add_help=False)

		self.MainParser.add_argument('-v', '--verbose',  dest="verbose",  help='print out detailed messages',  action='store_const', const=True, default=False)
		self.MainParser.add_argument('-d', '--debug',    dest="debug",    help='enable debug mode',            action='store_const', const=True, default=False)

	def Run(self):
		ArgParseMixin.Run(self)

	@DefaultAttribute()
	def HandleDefault(self, args):
		print("DefaultHandler: verbose={0}  debug={1}".format(str(args.verbose), str(args.debug)))

	@CommandAttribute("create-user", help="my new command")
	@ArgumentAttribute(metavar='<Username>', dest="Users", type=str, help='todo help')
	def HandleCreateUser(self, args):
		print("HandleCreateUser: {0}".format(str(args.Users)))

	@CommandAttribute("remove-user", help="my new command")
	@ArgumentAttribute(metavar='<UserID>', dest="UserIDs", type=str, help='todo help')
	def HandleRemoveUser(self, args):
		print("HandleRemoveUser: {0}".format(str(args.UserIDs)))

p = prog()
p.Run()

Step 5 - The ArgParseMixin helper class.

This class constructs the argparse based parser with the provided data from attributes. The parsing process is invoked by Run().

class ArgParseMixin(AttributeHelperMixin):
	__mainParser = None
	__subParser =  None
	__subParsers = {}

	def __init__(self, **kwargs):
		super().__init__()

		# create a commandline argument parser
		import argparse
		self.__mainParser = argparse.ArgumentParser(**kwargs)
		self.__subParser =  self.__mainParser.add_subparsers(help='sub-command help')

		for funcname,func in self.GetMethods():
			defAttributes = DefaultAttribute.GetAttributes(func)
			if (len(defAttributes) != 0):
				defAttribute = defAttributes[0]
				self.__mainParser.set_defaults(func=defAttribute.Handler)
				continue

			cmdAttributes = CommandAttribute.GetAttributes(func)
			if (len(cmdAttributes) != 0):
				cmdAttribute = cmdAttributes[0]
				subParser = self.__subParser.add_parser(cmdAttribute.Command, **(cmdAttribute.KWArgs))
				subParser.set_defaults(func=cmdAttribute.Handler)

				for argAttribute in ArgumentAttribute.GetAttributes(func):
					subParser.add_argument(*(argAttribute.Args), **(argAttribute.KWArgs))

				self.__subParsers[cmdAttribute.Command] = subParser
				continue

	def Run(self):
		# parse command line options and process splitted arguments in callback functions
		args = self.__mainParser.parse_args()
		# because func is a function (unbound to an object), it MUST be called with self as a first parameter
		args.func(self, args)

	@property
	def MainParser(self):
		return self.__mainParser

	@property
	def SubParsers(self):
		return self.__subParsers

Contributors:

License

This library is licensed under Apache License 2.0


SPDX-License-Identifier: Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyAttributes-1.0.1.tar.gz (9.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyAttributes-1.0.1-py3-none-any.whl (12.4 kB view details)

Uploaded Python 3

File details

Details for the file pyAttributes-1.0.1.tar.gz.

File metadata

  • Download URL: pyAttributes-1.0.1.tar.gz
  • Upload date:
  • Size: 9.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/42.0.2 requests-toolbelt/0.9.1 tqdm/4.41.0 CPython/3.6.7

File hashes

Hashes for pyAttributes-1.0.1.tar.gz
Algorithm Hash digest
SHA256 3b00a3408d69aedfb0d8b7821a0b68ea8de1d1f2517ffb08a045827fa8fc39ea
MD5 727a7fa93cd3e07748b6490636a1f682
BLAKE2b-256 e6e20dfcb16afec8e4c661f6931ad3c3cd4137e2e967a0aa24831bd13422c604

See more details on using hashes here.

File details

Details for the file pyAttributes-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: pyAttributes-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 12.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/42.0.2 requests-toolbelt/0.9.1 tqdm/4.41.0 CPython/3.6.7

File hashes

Hashes for pyAttributes-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5b52b373cdad27706a4852908c754f61328dd82ef217d8d8caa84b47c492d6c7
MD5 79c519ea7d49f9700901884d536b08e1
BLAKE2b-256 12958cee8c6c676c19bd0c086be51e63542e2a9cf45351637167283d96553e0d

See more details on using hashes here.

Supported by

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