Custom Context Manager

Let's learn how to create Context Managers for our custom Transaction.

Custom Context Manager

Let's learn how to create Context Managers for our custom Transaction.

Summary

What's Context Manager?

A context manager is a concept that enables us to perform predefined actions both before and after our main action. We use them using with keyword.

You've probably used context managers before for opening files like that👇.

with open("example.txt", "r") as file:
    content = file.read()

This open context-manager ensures that file is closed after the code block. This is crucial to not corrupt files when you work in the 'write' mode with any files.

We can create similar context-manager, that will Start and Commit our Transaction. And on top of that we will add try/except statements and optionally print error message.

How to Create Context Manager?

There are 2 ways to create a context manager.

1️⃣ We can create a generator function and apply a special decorator for that function, so python recognizes it as a context manager.

2️⃣ Or we can create a Class and use its __enter__ and __exit__ methods.

💡 I prefer using functions if context manager is simple enough, and use Class if I need a lot of functionality.

Let's look how to create them.

Create Context Manager with Function

To create context manager with a function we need to import contextlib module and use context-manager decorator. This will indicate python that our generator function will be used as a context-manager.

💡Generator functions use yield instead of return.

If we apply context-manager decorator, then we can define code Before and After yield statement. And our code in with statement will be placed instead of yield.

👇It might sound confusing but have a look at this template for context-manager:

import contextlib

@contextlib.contextmanager
def ef_Transaction():

    print('Before')
    #👆 CODE BEFORE

    yield

    #👇 CODE AFTER
    print('After')

👆 That's the base template you can use.

I only made print statements 'Before'/'After', but you can put any code instead to be executed before or after the yield part.

yield part is going to replaced with any code we write under 'with ef_Transaction:'

👇 Here is an example:

#🎯 Use Context-Manager
with ef_Transaction:
    #👇 This will take place of yield
    x = 1
    y = 2
    print(x+y)

So using this context-manager will give you results like the following code.

print('Before')    #BEFORE

x = 1              #yield
y = 2              #yield
print(x+y)         #yield

print('After')     #AFTER

We have our print statements Before and After, and our code will be placed instead of yield statement.

Transaction Context-Manager

So in our case we would want to Create a Transaction and then Start and Commit it before and after. We also need to provide arguments for creating our transaction such as doc and title.

👇Here is simple context-manager that will do that.

import contextlib

@contextlib.contextmanager
def ef_Transaction(doc, title):
    t = Transaction(doc, title)
    t.Start()
        
    yield
        
    t.Commit()
    
with ef_Transaction(doc,title):
    # DO SOMETHING

👀 Let's also add try/except statements and an argument to optionally print an error message.

import contextlib

@contextlib.contextmanager
def ef_Transaction(doc, title, debug=False):
    t = Transaction(doc, title)
    t.Start()
    try:
        yield
        t.Commit()
    except:
        t.RollBack()
        if debug:
            print(traceback.format_exc())

Now this context-manager will take care of Starting and Committing a Transaction. We also have integrated try/except statement with an option to print error messages if any.

Now if we want to use it to create a random Wall, we can use the following Snippet.

# IMPORTS
from Autodesk.Revit.DB import *

#VARIABLES
doc   = __revit__.ActiveUIDocument.Document
level = doc.ActiveView.GenLevel

with ef_Transaction(doc, 'Create a Wall'):
    # Create a wall
    start    = XYZ(0, 0, 0)
    end      = XYZ(10, 10, 0)
    geomLine = Line.CreateBound(start, end)
    new_wall = Wall.Create(doc, geomLine, level.Id, False)

By using this, we don't need to use Start, Commit and Try/Except anymore. It's already included in our context-manager.

💡And you can add as much logic as you want to make it even more powerful!

Create Try/Except Context-Manager

Let's also create another context-manager to make try/except statements more compact. I saw some codes where people used more than 10+ of them, and except was everywhere just pass. So there were a lot of useless liens of code.

💡 Let's make them more readable and reusable!

We can just remove Transaction part from previous code, and we will have our try/except context-manager.

import contextlib, traceback

@contextlib.contextmanager
def try_except(debug=False):
    try:
        yield
    except:
        if debug:
            print(traceback.format_exc())

So when you use it, it will look like this in comparison:

#✅ CONTEXT MANAGER
with try_except(debug=True):
    #1️⃣ DO THIS 
  
with try_except(debug=True):
    #2️⃣ DO THIS
#❌ TRY EXCEPT
try:
    #1️⃣ DO THIS
except:
    print(traceback.format_exc())
    
try:
    #2️⃣ DO THIS
except:
    print(traceback.format_exc())

💡 These are only 2 statements. Imagine the difference if we would have 10+ of them.

Create Context Manager with Class

So you've learnt how to create context-manager by using generator function with a decorator. Now let's recreate the same context-manager for Transaction but using Class this time.

We would create a class as usual, and provide arguments in __init__ method.

And then we can use the following methods for making it as a context-manager.
__enter__(self)
__exit__(self, exc_type, exc_value, traceback)

💡Don't forget to use required arguments for __exit__ method!

class ef_Transaction2:
    def __init__(self, doc, title, debug=False):
        self.doc   = doc
        self.title = title
        self.debug = debug

    def __enter__(self):
        self.t = Transaction(self.doc, self.title)
        self.t.Start()

        return self.t

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            self.t.Commit()
        except:
            self.t.RollBack()
            if self.debug:
                print(traceback.format_exc())

👆 So now this context-manager made with a Class is exact copy of the one we made with a generator function. Give it a try.

🎯 And it's exactly the same to use it.

# IMPORTS
from Autodesk.Revit.DB import *

#VARIABLES
doc   = __revit__.ActiveUIDocument.Document
level = doc.ActiveView.GenLevel

with ef_Transaction2(doc, 'Create a Wall'):
    # Create a wall
    start    = XYZ(0, 0, 0)
    end      = XYZ(10, 10, 0)
    geomLine = Line.CreateBound(start, end)
    new_wall = Wall.Create(doc, geomLine, level.Id, False)
Reuse your Custom Context-Manager

👀 Are you a great programmer who reuses code?

If yes, then don't forget to place one of the context-manager to your lib, so you can start using it in your future tools!

Just place the code and all necessary imports in lib/Snippets/_context_managers.py and enjoy a better way to handle Transactions.

Conclusion

Overall, it's not a complicated concept, but it might be tricky to explain it in a written format. So make sure you watch the video to better understand how it works!

It might be a little confusing if you see it for the first time. But, I think once you create and use your own custom context-manager something will click and you will suddenly understand it!

Final Code
# -*- coding: utf-8 -*-
__title__ = "05.03 - Context Manager"
__doc__ = """Date    = 07.01.2023
_____________________________________________________________________
Description:
Code from lesson 05.03 - Context Manager
Example on how to create context managers in python to handle:
- Transaction
- Try/Except
_____________________________________________________________________
Author: Erik Frits"""
# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ IMPORTS
#==================================================
from Autodesk.Revit.DB import *

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ VARIABLES
#==================================================
uidoc = __revit__.ActiveUIDocument
doc   = __revit__.ActiveUIDocument.Document
level = doc.ActiveView.GenLevel

import contextlib, traceback

# ╔╦╗╦═╗╔═╗╔╗╔╔═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
#  ║ ╠╦╝╠═╣║║║╚═╗╠═╣║   ║ ║║ ║║║║╚═╗
#  ╩ ╩╚═╩ ╩╝╚╝╚═╝╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
#==================================================

@contextlib.contextmanager
def ef_Transaction(doc, title, debug=False):
    """Context Manager that will initiate Transaction .Start() and .Commit() methods.
    In case of an error - it will .RollBack() transaction and display error message if debug = True.
    :param doc:   Document where Transaction should be created
    :param title: Name of Transaction
    :param debug: True - Display error messages \ False - Suppres error messages."""

    t = Transaction(doc, title)
    t.Start()
    try:
        yield
        t.Commit()
    except:
        t.RollBack()
        if debug:
            print(traceback.format_exc())

#
# with ef_Transaction(doc, 'Create a Wall'):
#     # Create a wall
#     start = XYZ(0, 0, 0)
#     end = XYZ(10, 10, 0)
#     geomLine = Line.CreateBound(start, end)
#     new_wall = Wall.Create(doc, geomLine, level.Id, False)


# ╔╦╗╦═╗╦ ╦  ╔═╗═╗ ╦╔═╗╔═╗╔═╗╔╦╗
#  ║ ╠╦╝╚╦╝  ║╣ ╔╩╦╝║  ║╣ ╠═╝ ║
#  ╩ ╩╚═ ╩   ╚═╝╩ ╚═╚═╝╚═╝╩   ╩
#==================================================

@contextlib.contextmanager
def try_except(debug=False):
    """"""
    try:
        yield
    except:
        if debug:
            print(traceback.format_exc())

#✅ ContextManager Try/Except Example
with try_except():
    pt = XYZ(0,0,0)

with try_except():
    pt2 = XYZ(0,0,0)

with try_except():
    pt3 = XYZ(0,0,0)

#❌ Regular Try/Except Example
try:
    pt = XYZ(0,0,0)
except:
    print(traceback.format_exc())

try:
    pt = XYZ(0,0,0)
except:
    print(traceback.format_exc())

try:
    pt = XYZ(0,0,0)
except:
    print(traceback.format_exc())



# ╔═╗╔═╗╔╗╔╔╦╗╔═╗═╗ ╦╔╦╗╔╦╗╔═╗╔╗╔╔═╗╔═╗╔═╗╦═╗  ╔═╗╦  ╔═╗╔═╗╔═╗
# ║  ║ ║║║║ ║ ║╣ ╔╩╦╝ ║ ║║║╠═╣║║║╠═╣║ ╦║╣ ╠╦╝  ║  ║  ╠═╣╚═╗╚═╗
# ╚═╝╚═╝╝╚╝ ╩ ╚═╝╩ ╚═ ╩ ╩ ╩╩ ╩╝╚╝╩ ╩╚═╝╚═╝╩╚═  ╚═╝╩═╝╩ ╩╚═╝╚═╝ CONTEXT MANAGER - CLASS
#==================================================

class ef_Transaction2:
    def __init__(self, doc, title, debug=False):
        self.doc   = doc
        self.title = title
        self.debug = debug

    def __enter__(self):
        self.t = Transaction(self.doc, self.title)
        self.t.Start()

        return self.t

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            self.t.Commit()
        except:
            self.t.RollBack()
            if self.debug:
                print(traceback.format_exc())

#✅ ContextManager (class ef_Transaction2)
# with ef_Transaction2(doc, 'Create a Wall'):
#     # Create a wall
#     start = XYZ(0, 0, 0)
#     end = XYZ(10, 10, 0)
#     geomLine = Line.CreateBound(start, end)
#     new_wall = Wall.Create(doc, geomLine, level.Id, False)

#❌ ContextManager (Transaction)
# with Transaction(doc, 'Create a Wall') as t:
#     t.Start()
#     # Create a wall
#     start = XYZ(0, 0, 0)
#     end = XYZ(10, 10, 0)
#     geomLine = Line.CreateBound(start, end)
#     new_wall = Wall.Create(doc, geomLine, level.Id, False)
#     t.Commit()


from Snippets._context_managers import try_except, ef_Transaction
# with try_except():
#     ...

# with ef_Transaction(doc, 'title', debug = True):
#     ...
lib/Snippets/_context_managers.py
# -*- coding: utf-8 -*-
# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ IMPORTS
#==================================================
import contextlib, traceback
from Autodesk.Revit.DB import Transaction

# ╔╦╗╦═╗╔═╗╔╗╔╔═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
#  ║ ╠╦╝╠═╣║║║╚═╗╠═╣║   ║ ║║ ║║║║╚═╗
#  ╩ ╩╚═╩ ╩╝╚╝╚═╝╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ TRANSACTIONS
#==================================================
@contextlib.contextmanager
def ef_Transaction(doc, title, debug=False):
    """Context Manager that will initiate Transaction .Start() and .Commit() methods.
    In case of an error - it will .RollBack() transaction and display error message if debug = True.
    :param doc:   Document where Transaction should be created
    :param title: Name of Transaction
    :param debug: True - Display error messages \ False - Suppres error messages.
    Example:
    with ef_Transaction(doc, 'title', debug = True):
        #Changes Here"""

    t = Transaction(doc, title)
    t.Start()
    try:
        yield
        t.Commit()
    except:
        t.RollBack()
        if debug:
            print(traceback.format_exc())

# ╔╦╗╦═╗╦ ╦  ╔═╗═╗ ╦╔═╗╔═╗╔═╗╔╦╗
#  ║ ╠╦╝╚╦╝  ║╣ ╔╩╦╝║  ║╣ ╠═╝ ║
#  ╩ ╩╚═ ╩   ╚═╝╩ ╚═╚═╝╚═╝╩   ╩ TRY EXCEPT
#==================================================
@contextlib.contextmanager
def try_except(debug=False):
    """Context Manager for using Try/Except statements.
    If case of an error it will display error message if debug = True.
    :param debug: True - Display error messages \ False - Suppres error messages.
    Example:
    with try_except(debug = True):
        #Code for Try statement here."""
    try:
        yield
    except:
        if debug:
            print(traceback.format_exc())

HomeWork

👀 There were many examples, so give some of them a try!


💡 I would also recommend to make changes and see what happens. Try to add print statements in different places, and see what you get!


✅ Don't forget to recreate it in your lib to reuse it.

Questions:

Should I use Function or Class for creating Context-Manager?

Should I use Function or Class for creating Context-Manager?

Can I stick to regular way of using Transaction?

Can I stick to regular way of using Transaction?

Can Context-Manager be used for anything?

Can Context-Manager be used for anything?

Discuss the lesson:

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.

© 2023-2024 EF Learn Revit API

© 2023-2024 EF Learn Revit API

© 2023-2024 EF Learn Revit API