Skip to content

Commit

Permalink
Comment out failed conditionals (rather than deleting them) (#136)
Browse files Browse the repository at this point in the history
* Comment out unused lines, dont delete

* update documentation
  • Loading branch information
echo-lalia authored Sep 2, 2024
1 parent 1081ce0 commit 553ab3b
Showing 1 changed file with 147 additions and 31 deletions.
178 changes: 147 additions & 31 deletions tools/parse_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
based on a given feature.
- Can also be used to match a device name, or whether or not a module is "frozen".
- Follow this syntax: `# mh_if {feature}:` or `# mh_if not {feature}:`
- elif supported using `# mh_else_if {feature}:`
- Are closed with this syntax: `# mh_end_if`
- If the entire conditional is commented out,
automatically uncomments it (for easier development/testing)
- "else"/"elif" supported using `# mh_else_if {feature}:` or `# mh_else:`
- Are closed with this phrase: `# mh_end_if`
- If the conditional passes and is commented out, uncomments it.
- If the conditional fails and is not commented out, it comments it out.
- Can be nested, but this is discouraged
(It's hard to read because Python indentation must be maintained)
Example:
Expand All @@ -38,7 +38,11 @@
```
On CARDPUTER this becomes:
```
# mh_if touchscreen:
# print("this device has a touchscreen!")
# mh_else:
print("this device has no touchscreen!")
# mh_end_if
```
"""

Expand Down Expand Up @@ -108,6 +112,14 @@
]



# Designate unicode "noncharacter" as representation of completed mh conditional
# 1-byte noncharacters = U+FDD0..U+FDEF, choosing from these arbitrarily.
CONDITIONAL_PARSED_FLAG = chr(0xFDD1)
CONDITIONAL_PARSED_ORIGINAL_DELIMITER = chr(0xFDD2)
CONDITIONAL_PARSED_TEMP_FLAG = chr(0xFDD3)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def main():
"""
Expand Down Expand Up @@ -382,6 +394,10 @@ def parse_constants(self, device):
@staticmethod
def _is_hydra_conditional(line:str) -> bool:
"""Check if line contains a hydra conditional statement."""
# early return for marked lines
if CONDITIONAL_PARSED_FLAG in line:
return False

if "#" in line \
and "mh_if" in line \
and ":" in line:
Expand All @@ -400,6 +416,10 @@ def _is_hydra_conditional(line:str) -> bool:
@staticmethod
def _is_conditional_else(line:str) -> bool|str:
"""Check if line is an else OR elif statement."""
# early return for marked lines
if CONDITIONAL_PARSED_FLAG in line:
return False

if "#" in line and "mh_else" in line:
found_comment = False
while line:
Expand All @@ -420,6 +440,10 @@ def _is_conditional_end(line:str, includes_else=True) -> bool:
"""Check if line contains a conditional end.
(Includes else/elif by default!)
"""
# early return for marked lines
if CONDITIONAL_PARSED_FLAG in line:
return False

if "#" in line and ("mh_end_if" in line or ("mh_else" in line and includes_else)):
found_comment = False
while line:
Expand All @@ -442,9 +466,7 @@ def _uncomment_conditional(self, start_idx, end_idx):
# ignore blank lines!
if not (line == "" or line.isspace()):
# remove leading spaces to look for '#'
while line and line[0].isspace():
line = line[1:]
if not line.startswith('#'):
if not line.strip().startswith('#'):
return

# check if comment has "# " or "#"
Expand Down Expand Up @@ -478,7 +500,75 @@ def _uncomment_conditional(self, start_idx, end_idx):
# replace only a single comment in every line (to preserve actual comments)
self.lines[idx] = line.replace("# ", "", 1)



def _comment_out_conditional(self, start_idx, end_idx):
"""If a conditional is excluded and isn't already commented out, comment it out!"""
relevant_slice = self.lines[start_idx:end_idx + 1]

# check if any lines are not commented
any_uncommented = False
for line in relevant_slice:
# ignore blank lines!
if not (line == "" or line.isspace()):
# remove leading spaces to look for '#'
if not line.strip().startswith('#'):
any_uncommented = True
break

# if all lines are commented, we shouldn't comment them again.
if not any_uncommented:
return

# find the length of the minimum indentation. This will be the place we insert "#"s
insert_idx = self._minimum_indentation(relevant_slice)
# track nested conditionals
condition_depth = 0
# assume we can actually remove all the comments, now
for i, line in enumerate(relevant_slice):

# track nested conditionals
if self._is_hydra_conditional(line):
condition_depth += 1
elif self._is_conditional_end(line, includes_else=False):
condition_depth -= 1

# if not inside a nested conditional, then remove the comments
if condition_depth <= 0:
# only add comments to non-empty (and non-space) lines
if line and not line.isspace():
idx = i + start_idx
# add comment after indentation (preserve formatting)
# indentation, right_split = self._split_indentation(line)
self.lines[idx] = line[:insert_idx] + "# " + line[insert_idx:]


@staticmethod
def _minimum_indentation(lines) -> int:
"""Return the length of the shortest indentation from the given lines"""
indents = []
for line in lines:
# ignore blank or space lines
if line and not line.isspace():
indents.append(FileParser._split_indentation(line)[0])

min_line = min(indents, key=len)
return len(min_line)


@staticmethod
def _split_indentation(right_split) -> tuple:
"""
Split all space before characters, and the rest of the string.
Returns (left_split:str, right_split:str)
"""
left_split = ""
while right_split and right_split[0].isspace():
left_split += right_split[0]
right_split = right_split[1:]

return left_split, right_split


@staticmethod
def slice_str_to_char(string:str, stop_char:str) -> str:
"""Slice a given string from 0 to the given character (not inclusive)."""
Expand Down Expand Up @@ -510,7 +600,8 @@ def _handle_expand_else(self, index, feature, has_not, else_type):
new_line = self.slice_str_to_char(target_line, "#")
not_str = " not" if has_not else ""
new_line = f"{new_line}# mh_if{not_str} {feature}:\n"
self.lines[index] = new_line
# store original line alongside modified line for formatting preservation
self.lines[index] = new_line + CONDITIONAL_PARSED_ORIGINAL_DELIMITER + self.lines[index]



Expand All @@ -519,6 +610,27 @@ def _process_one_conditional(self, device, frozen=False) -> bool:
Find and process a single Hydra conditional.
Returns False if no conditional found,
Returns True if conditional is processed.
The logic used by this method (and its helper methods):
- Search for an "mh_if" statement, extract the named feature
- Compare the feature to the device features,
invert the result if the "not" keyword is also present.
- Find the matching "mh_end_if" by scanning each line.
- If this conditional passes, uncomment the lines,
or comment them out if it doesn't
- Conditional lines we've seen are marked with noncharacters
so we remember to ignore them on the next cycle.
- "mh_else" / "mh_else_if" statements are converted into normal "if" statements.
The original text is preserved by appending the original line to the modified line,
delimited with a noncharacter.
- Once there are no conditionals remaining restore the converted else/elif lines,
and remove all added noncharacters.
"""
# search for the start and end of one conditional
cond_start_idx = None
Expand Down Expand Up @@ -548,18 +660,19 @@ def _process_one_conditional(self, device, frozen=False) -> bool:
if cond_start_idx is None or cond_end_idx is None:
return False

# as in, has the "not" keyword
has_not = False
# Remove stored "original data" if present
cond_line, *og_line = cond_line.split(CONDITIONAL_PARSED_ORIGINAL_DELIMITER)
og_line = ''.join(og_line)

# get feature string
*conditional, feature = cond_line.replace(":", "", 1).split()
# as in, has the "not" keyword
if conditional[-1] == "not":
has_not = True
else:
has_not = False


# keep_section = False
# if feature == "frozen" and frozen:
# keep_section = True
# elif feature in device.features:
# keep_section = True

if (feature == "frozen" and frozen) \
or feature in device.features \
or feature == device.name:
Expand All @@ -570,34 +683,31 @@ def _process_one_conditional(self, device, frozen=False) -> bool:
if has_not:
keep_section = not keep_section

if keep_section:
# remove only if and endif
self.lines.pop(cond_start_idx)
# now lines is 1 shorter:
cond_end_idx -= 1
# mark this conditional completed
self.lines[cond_start_idx] += CONDITIONAL_PARSED_FLAG

if keep_section:
# expand else/elif statement, or just remove a normal "end if"
conditional_else = self._is_conditional_else(self.lines[cond_end_idx])
if conditional_else:
self._handle_expand_else(cond_end_idx, feature, has_not, conditional_else)
else:
self.lines.pop(cond_end_idx)
# and it's shorter again
cond_end_idx -= 1
# mark normal mh_end_if as completed
self.lines[cond_end_idx] += CONDITIONAL_PARSED_FLAG

# check if all kept lines are commented out. We can uncomment them if they are.
self._uncomment_conditional(cond_start_idx, cond_end_idx)
self._uncomment_conditional(cond_start_idx + 1, cond_end_idx - 1)

else:
# remove entire section
# comment out entire section
# but if the final line is an elif, just reformat it

conditional_else = self._is_conditional_else(self.lines[cond_end_idx])
if conditional_else:
self._handle_expand_else(cond_end_idx, feature, has_not, conditional_else)
cond_end_idx -= 1

self.lines = self.lines[:cond_start_idx] + self.lines[cond_end_idx + 1:]
else:
self.lines[cond_end_idx] += CONDITIONAL_PARSED_FLAG

self._comment_out_conditional(cond_start_idx + 1, cond_end_idx - 1)

return True

Expand All @@ -612,6 +722,12 @@ def parse_conditionals(self, device, frozen=False):
# returns true until all conditionals are gone.
while self._process_one_conditional(device, frozen):
conditionals += 1

# clean noncharacter flags
self.lines = [line.replace(CONDITIONAL_PARSED_FLAG, "") for line in self.lines]
# restore original lines
self.lines = [line.split(CONDITIONAL_PARSED_ORIGINAL_DELIMITER)[-1] for line in self.lines]

vprint(f" {bcolors.OKBLUE}Parsed {conditionals} conditionals.{bcolors.ENDC}")


Expand Down

0 comments on commit 553ab3b

Please sign in to comment.