Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Aspect_Oriented_programming_in_Python.pdf

 Aspect_Oriented_programming_in_Python.pdf

Talk given at Pycon Lithuania 2019.

Aspect oriented programming is another way to modularize cross cutting concerns in an application.
Python has built in features that make aspect oriented programming possible.
This talk highlights these features and a few open source libraries developed to facilitate implementing aspect oriented programming in Python.

Avatar for Ibukun

Ibukun

May 25, 2019
Tweet

Other Decks in Programming

Transcript

  1. CONFIDENTIAL & RESTRICTED Aspect Oriented programming in Python Ibukun Oluwayomi

    @ioluwayo PyCon Lithuania 2019 All opinions presented are my own
  2. CONFIDENTIAL & RESTRICTED What is Aspect Oriented Programming • Separation

    of cross cutting concerns into independent modules known as aspects. • Separation of core logic from supporting logic. • Takes modularity one step further. • It is really just another approach to modularity.
  3. CONFIDENTIAL & RESTRICTED When to use AOP • You have

    cross cutting concerns that are not core to business logic. ◦ Logging ◦ Authorization ◦ Statistics • You keep inserting the same logic in several places. • You do not mind the extra complexity it might introduce.
  4. CONFIDENTIAL & RESTRICTED What about Object Oriented programming • OOP

    and AOP are not mutually exclusive. • AOP can be a good addition to OOP. • AOP is especially handy for adding standard code like logging, performance tracking, etc. to methods without clogging up the method code. • OOP is required for good AOP design and implementation. • Everything that AOP does could also be done without it. AOP is another way to separate unrelated concerns and reduce clutter in code.
  5. CONFIDENTIAL & RESTRICTED def update_address(request): address = models.Address.objects.get(id=request.data.get('id')) address.street =

    request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address) Core business logic
  6. CONFIDENTIAL & RESTRICTED def update_address(request): logger.info("updating address {}".format(request.data)) address =

    models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() logger.info("Finished updating address: {}".format(address)) return Response(address) Let’s add some simple logging
  7. CONFIDENTIAL & RESTRICTED def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") if

    is_authorized(user): address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning And some authorization checking
  8. CONFIDENTIAL & RESTRICTED def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user,

    request) if is_authorized(user): address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning And some usage statistics
  9. CONFIDENTIAL & RESTRICTED def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user,

    request) if is_authorized(user): address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = models.AuditLog.objects.create() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning Clients want a persistent audit trail
  10. CONFIDENTIAL & RESTRICTED def change_schedule(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user,

    request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning And they want it everywhere def cancel_booking(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning def publish_history(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning def register_occupant(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address_obj = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = AuditLog() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning You end up with this awesome tool. But what does it do?
  11. CONFIDENTIAL & RESTRICTED With AOP we can reduce the clutter.

    • It is not an alternative to modularizing and encapsulation. • It takes modularity and encapsulation one step further. • It is a different way of injecting cross cutting functionality.
  12. CONFIDENTIAL & RESTRICTED Some AOP Terminologies 1. Aspect: The modularization

    of a cross cutting concern. LoggingAspect, AuthorizationAspect. 1. Join point: A specific point in the application logic such as method execution. This is where an advice takes control. 1. Pointcut: A combination of join points that meet a specific criteria. 1. Advice: The specific logic to be executed when an aspect is injected in the business logic. 2. Weaving: Linking advices to point cuts or join points so they get intercepted at run time.
  13. CONFIDENTIAL & RESTRICTED AOP in Python There are some built

    in features in python that facilitate aspect oriented programming. 1. Decorators 2. Metaclasses 3. Context managers 4. Introspection 5. Dynamic typing 6. The fact that everything is an object. There are many ways to implement AOP. What is most important is understanding the concept.
  14. CONFIDENTIAL & RESTRICTED def update_address(request): address = models.Address.objects.get(id=request.data.get('id')) address.street =

    request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address) Let’s approach the same problem with AOP
  15. CONFIDENTIAL & RESTRICTED @logging_aspect def update_address(request): address = models.Address.objects.get(id=request.data.get('id')) address.street

    = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address) Add some Logging
  16. CONFIDENTIAL & RESTRICTED def logging_aspect(func): @wraps(func) def around(*args, **kwargs): logging.debug("Starting:

    {} {} {}".format(func.__name__, args, kwargs)) result = func(*args, **kwargs) logging.debug("Finished: {} {} {}".format(func.__name__, args, kwargs)) return result return around @logging_aspect def update_address(request): # core logic return Response(address) @logging_aspect def compute_distance(request): # core logic return Response(address) The decorator can be used anywhere this type of logging is required.
  17. CONFIDENTIAL & RESTRICTED def authorization_aspect(func): @wraps(func) def before(*args, **kwargs): if

    is_authorized(user): return func(*args, **kwargs) else: raise UnAuthorizedWarning return before @authorization_aspect @logging_aspect def update_address(request): Address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address) Add some Authorization Checking
  18. CONFIDENTIAL & RESTRICTED @statistics_aspect @authorization_aspect @logging_aspect def update_address(request): address =

    models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address) Add some usage statistics
  19. CONFIDENTIAL & RESTRICTED Client wants persistent audit trail def logging_aspect(func):

    @wraps(func) def around(*args, **kwargs): logging.debug("Starting: {} {} {}".format(func.__name__, args, kwargs)) result = func(*args, **kwargs) audit_log = models.AuditLog.objects.create() audit_log.action = func.__name__ audit_log.user = user audit_log.result = result audit_log.when = now() audit_log.save() logging.debug("Finished: {} {} {}".format(func.__name__, args, kwargs)) return result return around @logging_aspect def update_address(request): # core logic return Response(address) @logging_aspect def compute_distance(request): # core logic return Response(address)
  20. CONFIDENTIAL & RESTRICTED Concerns and core logic combined Concerns and

    core logic separated def update_address(request): logger.info("updating address {}".format(request.data)) user=request.data.get("user") update_api_usage_stat(user, request) if is_authorized(user): address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() audit_log = models.AuditLog.objects.create() audit_log.action = "Update" audit_log.user = user audit_log.instance_id = address.id audit_log.class = address.__class__.__name__ audit_log.when = now() audit_log.save() logger.info("Finished updating address: {}".format(address)) return Response(address) else: raise UnAuthorizedWarning @statistics_aspect @authorization_aspect @logging_aspect def update_address(request): address = models.Address.objects.get(id=request.data.get('id')) address.street = request.data.get('street') address.number = request.data.get('number') address.postcode = request.data.get('postcode') address.city = request.data.get('city') address.save() return Response(address)
  21. CONFIDENTIAL & RESTRICTED There are some AOP libraries in Python

    that take it even further. 1. Support regex declaration of Join points. 2. Take a more object Oriented approach to defining Aspects 3. Utilize all of the awesome features Python offers.
  22. CONFIDENTIAL & RESTRICTED springpython class AnAdvice(MethodInterceptor): def invoke(self, invocation): print

    "Target name:", invocation.method_name results = invocation.proceed() print '--------------------' return results class TheClass: def delete_method(self): print "Delete method" def not_affected(self): print "I am not intercepted" def update_method(self): print "Another method" pointcutAdvisor = RegexpMethodPointcutAdvisor(advice=[AnAdvice()], patterns=[".*delete.*", ".*date.*"]) the_instance = ProxyFactoryObject(target=TheClass(), interceptors=pointcutAdvisor) the_instance.delete_method() the_instance.update_method() the_instance.not_affected() Target name: delete_method Delete method -------------------- Target name: update_method Another delete method -------------------- I am not intercepted https://docs.spring.io/spring-python/1.2.x/sphinx/html/aop.html
  23. CONFIDENTIAL & RESTRICTED pyaspects class TheClass: def delete_method(self): print "Delete

    method" def not_affected(self): print "I am not intercepted" def update_method(self): print "Another method" def advice(wobj, data, *args, **kwargs): # wobj: the object that is wrapped # data: aspect's data where you can get information about the weaved method # args: arguments passed to the original method # kwargs: keywords passed to the original method print "the advice" pyaspects.weave(TheClass.update_method, before_func=advice, after_func=advice) the_instance = TheClass() the_instance.delete_method() print '---------------' the_instance.update_method() Delete method --------------- the advice Another method the advice https://github.com/baris/pyaspects
  24. CONFIDENTIAL & RESTRICTED aspectlib class TheClass: def special_method(self, *args, **kwargs):

    return "special method" def not_special_method(self, *args, **kwargs): return "not special method" class MyTestCase(unittest.TestCase): def test_special(self): obj = TheClass() self.assertEqual(obj.special_method('special'), 'mocked-result') self.assertNotEqual(obj.special_method('random phrase'), 'mocked-result') def test_not_special(self): obj = TheClass() self.assertEqual(obj.not_special_method("special"), 'not special method') @aspectlib.Aspect def mock_advice(self, value): if value == 'special': yield aspectlib.Return('mocked-result') else: yield aspectlib.Proceed with aspectlib.weave(TheClass.special_method, mock_advice): unittest.main() .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK https://github.com/ionelmc/python-aspectlib
  25. CONFIDENTIAL & RESTRICTED • Its benefits are noticed prior to

    run time. • It definitely adds a new layer of complexity. • When you use AOP, adhere to the principle of least surprise. In the end, you have the same application during runtime. With or Without AOP
  26. CONFIDENTIAL & RESTRICTED Resources 1. Effective Python by Bret Slatkin.

    2. springpython: https://docs.spring.io/spring-python/1.2.x/sphinx/html/aop.html 3. pyaspects: https://github.com/baris/pyaspects 4. Aspectlib: https://github.com/ionelmc/python-aspectlib