Skip to content

Commit aebc853

Browse files
Do not copy in versionXX_YY imports (#93)
Changes include: * not copying in versionXX_YY imports * adding missing macros * getting correct author/ticket_number * adding missing macros now working for jules case * checking for last tag variable
1 parent 2804dc5 commit aebc853

File tree

1 file changed

+220
-37
lines changed

1 file changed

+220
-37
lines changed

lfric_macros/apply_macros.py

Lines changed: 220 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import shutil
1818
import subprocess
1919
import tempfile
20+
import networkx as nx
21+
from collections import defaultdict
2022

2123
BLACK_COMMAND = "black --line-length=80"
2224
CLASS_NAME_REGEX = r"vn\d+(_t\d+\w*)?"
@@ -262,9 +264,12 @@ def __init__(self, tag, cname, version, apps, core, jules):
262264
self.version = re.search(r".*vn(\d+\.\d+)(_.*)?", tag).group(1)
263265
else:
264266
self.version = version
265-
self.ticket_number = None
266267
self.author = None
267-
self.parsed_macros = {}
268+
self.ticket_number = None
269+
# All parsed macros per metadata section
270+
self.parsed_macros = defaultdict(list)
271+
# Parsed macro with desired after tag, per metadata section
272+
self.target_macros = {}
268273
self.meta_dirs = set()
269274
self.sections_with_macro = []
270275
self.python_imports = set()
@@ -439,29 +444,33 @@ def parse_macro(self, macro, meta_dir):
439444
- macro, an upgrade macro that matches the class name we are
440445
looking for
441446
- meta_dir, the path to the rose metadata directory
447+
Returns:
448+
- dictionary of the parsed macro, containing before tag, after tag and
449+
commands
442450
"""
443451

444452
version_file = os.path.join(meta_dir, "versions.py")
445453

446-
# The ticket number and author will always be the same across all
447-
# macros for this ticket, so only grab these once
448-
# These are not vital so don't fail if not found
449-
if self.ticket_number is None or self.author is None:
450-
ticket_details = re.search(r"Upgrade .* (#\d+) by (\S+.*)", macro)
451-
try:
452-
self.ticket_number = ticket_details.group(1)
453-
self.author = ticket_details.group(2).rstrip('".')
454-
self.author = self.author.strip("<>")
455-
except AttributeError:
456-
pass
454+
ticket_details = re.search(r"Upgrade .* (#\d+) by (\S+.*)", macro)
455+
try:
456+
ticket_number = ticket_details.group(1)
457+
author = ticket_details.group(2).rstrip('".')
458+
author = author.strip("<>")
459+
except AttributeError:
460+
ticket_number = "TTTT"
461+
author = "Unknown"
462+
pass
463+
464+
class_name = re.search(r"class (vn\d+_t\d+)", macro).group(1)
457465

458466
# Search for the before tag
459467
# Raise an exception if these are missing
460468
try:
461469
before_tag = re.search(rf"BEFORE_TAG{TAG_REGEX}", macro).group(1)
470+
after_tag = re.search(rf"AFTER_TAG{TAG_REGEX}", macro).group(1)
462471
except AttributeError as exc:
463472
raise Exception(
464-
"Couldn't find a Before tag for the requested "
473+
"Couldn't find a Before/After tag for the requested "
465474
f"macro in the file {version_file}"
466475
) from exc
467476

@@ -486,9 +495,13 @@ def parse_macro(self, macro, meta_dir):
486495
commands += line + "\n"
487496

488497
# Record macro details
489-
self.parsed_macros[meta_dir] = {
498+
return {
490499
"before_tag": before_tag,
500+
"after_tag": after_tag,
491501
"commands": commands,
502+
"ticket_number": ticket_number,
503+
"author": author,
504+
"class_name": class_name,
492505
}
493506

494507
def remove_macro(self, contents, meta_dir):
@@ -689,6 +702,9 @@ def write_python_imports(self, meta_dir):
689702
imp_str = f"from {'.'.join(m for m in mod)} {imp_str}"
690703
if alias:
691704
imp_str += f" as {alias}"
705+
# Don't add versionXX_YY import statements
706+
if re.match(r"from \.?version\d+_\d+ import \*", imp_str):
707+
continue
692708
versions_file.insert(insertion_index, imp_str)
693709

694710
with open(temppath, "w") as f:
@@ -700,7 +716,7 @@ def write_python_imports(self, meta_dir):
700716
def determine_import_order(self, app):
701717
"""
702718
Work out what order metadata is imported. This recursively works through
703-
import statements recorded in self.parsed_macros["imports"]. Produces a
719+
import statements recorded in self.target_macros["imports"]. Produces a
704720
list of the order in which macro commands should be applied (this should
705721
be the same order as the imports)
706722
Inputs:
@@ -719,7 +735,7 @@ def determine_import_order(self, app):
719735
import_list = [app_name]
720736

721737
try:
722-
imports = self.parsed_macros[app]["imports"]
738+
imports = self.target_macros[app]["imports"]
723739
except KeyError:
724740
# Jules Shared directories will produce a key error - these are
725741
# guaranteed to not import anything
@@ -744,41 +760,51 @@ def combine_macros(self, import_order):
744760
for meta_import in import_order:
745761
meta_import = self.get_full_import_path(meta_import)
746762
if (
747-
meta_import in self.parsed_macros
748-
and self.parsed_macros[meta_import]["commands"]
763+
meta_import in self.target_macros
764+
and "commands" in self.target_macros[meta_import]
749765
):
750766
# Add a comment labelling where these commands came from
751767
full_command += (
752768
" # Commands From: "
753769
f"{self.parse_application_section(meta_import)}\n"
754770
)
755-
if self.parsed_macros[meta_import]["commands"].strip("\n"):
756-
full_command += self.parsed_macros[meta_import]["commands"] + "\n"
771+
if self.target_macros[meta_import]["commands"].strip("\n"):
772+
full_command += self.target_macros[meta_import]["commands"] + "\n"
757773
else:
758774
full_command += " # Blank Upgrade Macro\n"
775+
776+
if self.target_macros[meta_import]["author"] != "Unknown":
777+
self.author = self.target_macros[meta_import]["author"]
778+
if self.target_macros[meta_import]["ticket_number"] != "TTTT":
779+
self.ticket_number = self.target_macros[meta_import][
780+
"ticket_number"
781+
]
759782
return full_command
760783

761-
def write_new_macro(self, meta_dir, full_command):
784+
def write_new_macro(self, meta_dir, full_command, macro):
762785
"""
763786
Write out the new macro with all relevant commands to the versions.py
764787
file
765788
Inputs:
766789
- meta_dir, path to the metadata directory with a versions.py file
767790
- full_command, str of the combined macro commands
791+
- macro, the parsed macro being written
768792
"""
769793

770-
parsed_macro = self.parsed_macros[meta_dir]
771794
filepath = os.path.join(meta_dir, "versions.py")
772795
temppath = os.path.join(meta_dir, ".versions.py")
773796
shutil.copy(filepath, temppath)
774797

798+
author = macro["author"] or self.author
799+
ticket_number = macro["ticket_number"] or self.ticket_number
800+
775801
with open(temppath, "a") as f:
776802
f.write(
777-
f"class {self.class_name}(MacroUpgrade):\n"
778-
f' """Upgrade macro for ticket {self.ticket_number} '
779-
f'by {self.author}."""\n\n'
780-
f' BEFORE_TAG = "{parsed_macro["before_tag"]}"\n'
781-
f' AFTER_TAG = "{self.tag}"\n\n'
803+
f'class {macro["class_name"]}(MacroUpgrade):\n'
804+
f' """Upgrade macro for ticket {ticket_number} '
805+
f'by {author}."""\n\n'
806+
f' BEFORE_TAG = "{macro["before_tag"]}"\n'
807+
f' AFTER_TAG = "{macro["after_tag"]}"\n\n'
782808
" def upgrade(self, config, meta_config=None):\n"
783809
f"{full_command}" # this variable contains required whitespace
784810
" return config, self.reports\n"
@@ -788,6 +814,140 @@ def write_new_macro(self, meta_dir, full_command):
788814

789815
os.rename(temppath, filepath)
790816

817+
def check_missing_macros(self, meta_dir, meta_imports):
818+
"""
819+
Check through macros of imported metadata sections, returning list of any that
820+
aren't in the current section (identified by the after tag)
821+
Inputs:
822+
- meta_dir, the current metadata section
823+
- meta_imports, a list of imported metadata sections
824+
Returns:
825+
- list of macros that need adding to the current section
826+
"""
827+
828+
after_tags = [m["after_tag"] for m in self.parsed_macros[meta_dir]]
829+
830+
missing_macros = []
831+
for section in meta_imports:
832+
section = self.get_full_import_path(section)
833+
section_missing = []
834+
len_section_macros = len(self.parsed_macros[section])
835+
for macro in self.parsed_macros[section]:
836+
after_tag = macro["after_tag"]
837+
# Ignore the macro being upgraded - this is expected to be missing
838+
if after_tag == self.tag:
839+
len_section_macros -= 1
840+
continue
841+
if after_tag not in after_tags:
842+
section_missing.append(after_tag)
843+
# Check that if macros are missing ALL of them are missing (this is the
844+
# case that a new metadata section has been added)
845+
# Otherwise raise an error as the macro chain is broken
846+
if section_missing and len_section_macros != len(section_missing):
847+
raise RuntimeError(
848+
f"The versions.py file for section {meta_dir} is missing macros "
849+
"from inherited metadata sections. This suggests something has "
850+
"gone wrong in the macro chain and should be investigated."
851+
)
852+
for after_tag in section_missing:
853+
if after_tag not in missing_macros:
854+
missing_macros.append(after_tag)
855+
856+
return missing_macros
857+
858+
def combine_missing_macros(self, meta_imports, missing_macros):
859+
"""
860+
Combine missing macro commands
861+
Inputs:
862+
- meta_imports, a list of imported metadata sections
863+
- missing_macros, a list of after tags missing
864+
Returns:
865+
- dictionary of parsed macros with corrected before tag. Key is after tag
866+
"""
867+
868+
new_macros = {}
869+
870+
# Loop over all metadata imports
871+
for meta_import in meta_imports:
872+
# For each missing after tag check whether it exists in this imported sect
873+
for after_tag in missing_macros:
874+
macro = None
875+
for m in self.parsed_macros[meta_import]:
876+
if m["after_tag"] == after_tag:
877+
macro = m
878+
break
879+
# if the macro exists then save it
880+
if macro:
881+
# if the macro not already saved, that's all that's required
882+
if after_tag not in new_macros:
883+
new_macros[after_tag] = macro
884+
continue
885+
# if the macro is already saved, then combine macros
886+
existing = new_macros[after_tag]
887+
existing["commands"] += macro["commands"]
888+
# if the before tags are the same, we don't need to modify the chain
889+
if existing["before_tag"] == macro["before_tag"]:
890+
continue
891+
# if the existing before tag is in the current metadata macro chain
892+
# then we want to use the new before tag
893+
for item in self.parsed_macros[meta_import]:
894+
if item["before_tag"] == existing["before_tag"]:
895+
existing["before_tag"] = macro["before_tag"]
896+
897+
return new_macros
898+
899+
def fix_missing_macros(self, meta_dir, meta_imports):
900+
"""
901+
Function to handle checking and fixing of missing upgrade macros
902+
Inputs:
903+
- meta_dir, the current metadata section
904+
- meta_imports, a list of imported metadata sections
905+
Returns:
906+
- the final after tag in the newly written macro chain if macros are
907+
missing, otherwise None
908+
"""
909+
910+
missing_macros = self.check_missing_macros(meta_dir, meta_imports)
911+
912+
if missing_macros:
913+
print(
914+
"[INFO] Writing missing macros to",
915+
self.parse_application_section(meta_dir),
916+
)
917+
macros = self.combine_missing_macros(meta_imports, missing_macros)
918+
# Record the identified missing macros for this metadata section
919+
self.parsed_macros[meta_dir] = [m for m in macros.values()]
920+
macro_strings = []
921+
for macro in macros.values():
922+
self.write_new_macro(meta_dir, macro["commands"], macro)
923+
macro_strings.append(
924+
f"BEFORE_TAG = '{macro['before_tag']}'\n"
925+
f"AFTER_TAG = '{macro['after_tag']}'"
926+
)
927+
self.parsed_macros[meta_dir].insert(0, macro)
928+
return self.find_last_macro(macro_strings, meta_dir)
929+
930+
return None
931+
932+
def order_meta_dirs(self):
933+
"""
934+
Order the self.meta_dirs list by metadata import order, such that sections
935+
higher up the import tree come first
936+
Create a networkx ordered graph, with nodes as the import tree and edges as the
937+
import statements. Then recreate list from this
938+
"""
939+
940+
import_graph = nx.DiGraph()
941+
942+
for meta_dir in self.meta_dirs:
943+
import_graph.add_node(meta_dir)
944+
for imp in self.target_macros[meta_dir]["imports"]:
945+
import_graph.add_edge(imp, meta_dir)
946+
947+
# Return an ordered list of nodes. This requires non-circular edges, but this is
948+
# guaranteed for valid rose metadata
949+
return list(nx.topological_sort(import_graph))
950+
791951
def preprocess_macros(self):
792952
"""
793953
Overarching function to pre-process added macros
@@ -817,27 +977,35 @@ def preprocess_macros(self):
817977
# info and delete the macro from the file
818978
parsed_versions = read_versions_file(meta_dir)
819979
macros = split_macros(parsed_versions)
980+
981+
# Record all macros in this metadata section
982+
for macro in macros:
983+
self.parsed_macros[meta_dir].append(self.parse_macro(macro, meta_dir))
984+
985+
# Check if target macro exists in this section
820986
found_macro = self.find_macro(meta_dir, macros)
821987
if not found_macro:
822988
# If we reach here then the new macro hasn't been added to
823989
# this versions file - in this case work out the final after
824-
# tag in the chain - if we import other commands for this
990+
# tag in the chain - if we import other commands for this
825991
# versions file, this final after tag will be the before tag of
826992
# that new macro.
827993
last_after_tag = self.find_last_macro(macros, meta_dir)
828-
self.parsed_macros[meta_dir] = {
994+
self.target_macros[meta_dir] = {
829995
"before_tag": last_after_tag,
830-
"commands": "",
996+
"after_tag": self.tag,
831997
"imports": "",
998+
"class_name": self.class_name,
999+
"author": None,
1000+
"ticket_number": None,
8321001
}
8331002
else:
834-
self.parse_macro(found_macro, meta_dir)
1003+
self.target_macros[meta_dir] = self.parse_macro(found_macro, meta_dir)
8351004
# Remove the macro from the file
8361005
self.remove_macro(parsed_versions, meta_dir)
8371006

838-
# Read through rose-meta files for import statements
839-
# of other metadata
840-
self.parsed_macros[meta_dir]["imports"] = self.read_meta_imports(meta_dir)
1007+
# Read through rose-meta files for import statements of other metadata
1008+
self.target_macros[meta_dir]["imports"] = self.read_meta_imports(meta_dir)
8411009

8421010
# Read through the versions.py file for python import statements
8431011
self.python_imports.update(
@@ -846,18 +1014,33 @@ def preprocess_macros(self):
8461014

8471015
# Now reconstruct the macro for all applications which have the newly
8481016
# added macro or import metadata with the new macro
849-
for meta_dir in self.meta_dirs:
1017+
# The macro sections need to be processed in the order of import
1018+
for meta_dir in self.order_meta_dirs():
8501019
import_order = self.determine_import_order(meta_dir)
8511020
full_command = self.combine_macros(import_order)
1021+
8521022
# If there are commands to write out, do so and record this
8531023
# application as having the macro
8541024
if full_command:
1025+
# Check if there are any macros in imported metadata versions.py files
1026+
# that aren't in the current section.
1027+
# If there are, then combine these and write them out first
1028+
last_after_tag = None
1029+
last_after_tag = self.fix_missing_macros(
1030+
meta_dir, self.target_macros[meta_dir]["imports"]
1031+
)
1032+
1033+
if last_after_tag:
1034+
self.target_macros[meta_dir]["before_tag"] = last_after_tag
1035+
8551036
print(
8561037
"[INFO] Writing macros to",
8571038
self.parse_application_section(meta_dir),
8581039
)
8591040
self.write_python_imports(meta_dir)
860-
self.write_new_macro(meta_dir, full_command)
1041+
self.write_new_macro(
1042+
meta_dir, full_command, self.target_macros[meta_dir]
1043+
)
8611044
self.sections_with_macro.append(meta_dir)
8621045

8631046
############################################################################

0 commit comments

Comments
 (0)