Introduction:
A few days ago, I was watching a talk about Django best practices for scalable project structures, I came across a useful suggestion that emphasizes keeping the views.py
file clear.
“Your
views.py
should only have views.”
The advice is obvious. Move everything that is not a view out of views.py
and establish a clear difference between view-related and utility functions.
This advice stuck with me because I have faced this issue before, where I was going through views.py
file and didn’t know whether a function in views.py
was intended for a view or was it just a utility function? This ambiguity might cause codebase confusion, making it difficult to determine the function’s purpose.
Enforcing the Rule:
Two conventional ways to enforce this rule within a development team are:
Way 1: Communicate and Inform
The first way is by communicating the rule clearly to everyone on the team and enforcing it through pull request reviews. However effective, this method requires continuous effort and monitoring and is prune to human made errors. That is why I went with the second option.
Way 2: Test-Driven Enforcement
I investigated putting the rule into our automated tests to speed up the enforcement process and remove active intervention and monitoring from the process.
The Test:
The test involves three key steps:
-
Get all functions from the view:
In the test file, we create a helper method to obtain all of the view functions. The method excludes utility functions from consideration by filtering out functions that begin with an underscore, those that are not callable and those that are imported.
# Define a helper method to get all the view functions in views.py def get_view_functions(self): # Get all the attributes of the views module attrs = dir(views) # Filter out the ones that start with underscore or are not callable funcs = [getattr(views, attr) for attr in attrs if not attr.startswith("_") and callable(getattr(views, attr))] # Filter out the ones that are imported from other modules funcs = [ func for func in funcs if inspect.getmodule(func) == views ] # Return the list of functions return funcs
-
Get all URL patterns for that view:
Get the urls from
url.py
file from the app that we are testing. In this case it is thebase
app.from base.urls import urlpatterns
-
Compare the functions and URLs:
We create a test method that checks if all view functions are used in the imported URL patterns:
def test_view_functions_are_used_in_urls(self): # Get all the view functions and urlpatterns view_funcs = self.get_view_functions() app_urls = urlpatterns # Loop through each function for func in view_funcs: found = False for url in app_urls: if func is url.callback: found = True break if not found: self.fail(f"{func.__name__} is not used in urls")
The test loops through each function, testing to see if it matches a URL pattern callback. The test fails if a function is not found in any URL patterns, suggesting that the function is not being used as a view.
Complete Test Suite:
The full test suite ensures that all functions in views.py
are utilized in the URL patterns, providing a safeguard against inadvertently unused functions:
class ViewTestCase(TestCase):
# Define a helper method to get all the view functions in views.py
def get_view_functions(self):
# Get all the attributes of the views module
attrs = dir(views)
# Filter out the ones that start with underscore or are not callable
funcs = [getattr(views, attr) for attr in attrs if not attr.startswith("_") and callable(getattr(views, attr))]
# Filter out the ones that are imported from other modules
funcs = [
func for func in funcs if inspect.getmodule(func) == views
] # Check if the function's module is the same as the views module
# Return the list of functions
return funcs
# Define a test method to check if all the view functions are used in urls
def test_view_functions_are_used_in_urls(self):
# Get all the view functions
view_funcs = self.get_view_functions()
view_urls = urlpatterns
# Loop through each function
for func in view_funcs:
found = False
for url in view_urls:
if func is url.callback:
found = True
break
if not found:
self.fail(f"{func.__name__} is not used in urls")
By running this test, every function declared in views.py
that does not have a corresponding usage in the URL patterns will result in a test failure, alerting the development team to the problem.
Conclusion
Maintaining a scalable and understandable codebase requires enforcing a clean, consistent and transparent structure.
The test-driven methodology given here provides an automated and dependable method for ensuring that functions written in views.py
file are actively used in the URLs, which make our code predicable and consistent.