Thursday, June 21, 2018
Renaming a Drupal module
Renaming a Drupal module
Sometimes you find the name you gave a module is not that great. Maybe its just not by the conventions, or very similar to a contrib or a feature. Maybe its just not reflecting precisely the purpose of the module. Or a feature. And you have to rename it. But renaming a Drupal module is not easy.
Why? First you have the manual labour. Renaming all the files, renaming the functions, maybe the comments, includes, strings, etc. Its a lot of replacement. If youre leveraging a VCS you ought to keep the history clean, meaning applying a rename action instead of a delete-create. Thats quite some action on the code.
Next you have the Drupal registry. In the system table all paths are stored. When you rename a module its gonna be disabled by default. However its not as simple as re-enabling it. You have your schema and applied update hooks - which can mess up a lot. So you have to rewrite the system record.
And thats just the beginning. Now you have the cache. Some is pretty easy flush, drush takes care of it. However some cache Ive found irrationally hard to erase. Like cached class files and such.
But you know - not all modules are overly-attached. So I was looking for tools or modules that could do at least the manual work for me. Unfortunately found nothing. So I wrote a little Python script that does it.
We need an opener that could handle 3 arguments: the original name, the new name, and the path to the current module:
import sys
if __name__ == __main__:
if len(sys.argv) < 4:
sys.exit(1)
Its always better to separate the logic so lets create a simple class that takes care of the actual job:
import os
import shutil
import re
from string import capitalize as cap
class ModuleRenamer(object):
def __init__(self, from_name, to_name, path_to_module = ./):
self.from_name, self.to_name, self.path_to_module = from_name, to_name, path_to_module
First thing first lets check those arguments:
def validate_args(self):
valid_name = ^[a-z][a-z0-9_]*$
return re.match(valid_name, self.from_name) and
re.match(valid_name, self.to_name) and
os.path.exists(self.path_to_module)
And add it to the runner:
from module_renamer import ModuleRenamer
if __name__ == __main__:
# ...
renamer = ModuleRenamer(sys.argv[1], sys.argv[2], sys.argv[3])
if not renamer.validate_args():
sys.exit(1)
Now lets go and and write the replacer part. The action involves 2 main elements: copying the whole directory tree and renaming all folders / files where necessary. Then you have to take care of the file contents. The directory tree copy is easy, first we just copy the main folder and then rename each file:
def rename(self):
new_module_dir = os.path.join(self.path_to_module, .., self.to_name)
if __debug__ and os.path.exists(new_module_dir):
shutil.rmtree(new_module_dir)
shutil.copytree(self.path_to_module, new_module_dir)
for dirpath, dirnames, filenames in os.walk(new_module_dir):
for filename in filenames:
new_filename = re.sub(self.from_name, self.to_name, filename)
file_path = os.path.join(dirpath, filename)
new_file_path = os.path.join(dirpath, new_filename)
shutil.move(file_path, new_file_path)
if new_filename.lower().endswith((.module, .info, .php, .inc, .install, .test, .theme)):
self.rename_strings(new_file_path)
The next clue can be found in the last line - where we handle the content each time the file is a valid Drupal code file:
def rename_strings(self, filename):
file = open(filename, r)
content = file.read()
content = self.replace_string(self.from_name, self.to_name, content)
content = self.replace_string(cap(self.from_name), cap(self.to_name), content)
file.close()
file = open(filename, w)
file.write(content)
file.close()
We extracted here the replacement call - in order to add a simple logging system. Its kinda important to see in console what was replaced in the code:
def replace_string(self, pattern, to, text):
matches = re.findall(^.* + pattern + .*$, text, flags = re.M)
if matches:
for match in matches:
print(33[37m[sub]33[0m + re.sub(pattern, "33[31m" + to + "33[0m", match))
text = re.sub(pattern, to, text)
return text
And were done. We have run the replacer in the runner file:
renamer.rename()
Here you are a little sample of a run on a small module:
You can track what files are renamed, what code snippets were replaced and some clue what to rename in the database. You can find and download the code on GitHub, of course.
Warning: do not use it without verifying each the whole change carefully. It does what it does but its far from being a perfect module renaming tool. Modules are containing semantical information and lower level code connections to other modules. Also the algorithm is too simple here.
So any suggestion to make it better would be very much welcomed.
---
Peter