Skip to content

Commit

Permalink
Flip variant/rule arsg for flatten/stringize/expand so we can call th…
Browse files Browse the repository at this point in the history
…em without a rule if needed
  • Loading branch information
aappleby committed Mar 17, 2024
1 parent 8eefd4e commit 86d1ee5
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 36 deletions.
53 changes: 28 additions & 25 deletions hancho.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def maybe_as_number(text):
# suffice.


def flatten(rule, variant, depth=0):
def flatten(variant, rule=None, depth=0):
"""Turns 'variant' into a flat array of non-templated strings, paths, and callbacks."""
# pylint: disable=too-many-return-statements

Expand All @@ -155,21 +155,21 @@ def flatten(rule, variant, depth=0):
return []

if isinstance(variant, Task):
return flatten(rule, variant.promise, depth + 1)
return flatten(variant.promise, rule, depth + 1)

if isinstance(variant, Path):
return [Path(stringize(rule, str(variant), depth + 1))]
return [Path(stringize(str(variant), rule, depth + 1))]

if isinstance(variant, list):
result = []
for element in variant:
result.extend(flatten(rule, element, depth + 1))
result.extend(flatten(element, rule, depth + 1))
return result

return [stringize(rule, variant, depth + 1)]
return [stringize(variant, rule, depth + 1)]


def stringize(rule, variant, depth=0):
def stringize(variant, rule=None, depth=0):
"""Turns 'variant' into a non-templated string."""
# pylint: disable=too-many-return-statements

Expand All @@ -181,30 +181,33 @@ def stringize(rule, variant, depth=0):

if isinstance(variant, str):
if template_regex.search(variant):
return expand(rule, variant, depth + 1)
return expand(variant, rule, depth + 1)
return variant

if variant is None:
return ""

if isinstance(variant, Task):
return stringize(rule, variant.promise, depth + 1)
return stringize(variant.promise, rule, depth + 1)

if isinstance(variant, Path):
return stringize(rule, str(variant), depth + 1)
return stringize(str(variant), rule, depth + 1)

if isinstance(variant, list):
variant = flatten(rule, variant, depth + 1)
variant = flatten(variant, rule, depth + 1)
variant = [str(s) for s in variant if s is not None]
variant = " ".join(variant)
return variant

return str(variant)


def expand(rule, template, depth=0):
def expand(template, rule=None, depth=0):
"""Expands all templates to produce a non-templated string."""

if rule is None:
rule = config

if depth > MAX_EXPAND_DEPTH:
raise ValueError(f"Expanding '{template}' failed to terminate")

Expand All @@ -224,7 +227,7 @@ def expand(rule, template, depth=0):
except Exception as exc: # pylint: disable=broad-except
raise ValueError(f"Template '{exp}' failed to eval") from exc

result += stringize(rule, replacement, depth + 1)
result += stringize(replacement, rule, depth + 1)
template = template[span.end() :]

result += template
Expand Down Expand Up @@ -259,10 +262,10 @@ def load(file=None, root=None):
if file is None:
raise FileNotFoundError("No .hancho filename given")

file = Path(stringize(config, file))
file = Path(stringize(file, config))

if root is not None:
file = Path(stringize(config, root)) / file
file = Path(stringize(root, config)) / file

test_path = abspath(Path(app.mod_stack[-1].__file__).parent / file)
if test_path.exists():
Expand Down Expand Up @@ -433,10 +436,10 @@ def task_init(self):
raise ValueError("Task missing files_out")

# Stringize our directories
self.work_dir = Path(stringize(self, self.work_dir))
self.in_dir = Path(stringize(self, self.in_dir))
self.deps_dir = Path(stringize(self, self.deps_dir))
self.out_dir = Path(stringize(self, self.out_dir))
self.work_dir = Path(stringize(self.work_dir, self))
self.in_dir = Path(stringize(self.in_dir, self))
self.deps_dir = Path(stringize(self.deps_dir, self))
self.out_dir = Path(stringize(self.out_dir, self))

assert self.work_dir.is_absolute() and self.work_dir.exists()
assert self.in_dir.is_absolute() and self.in_dir.exists()
Expand All @@ -446,12 +449,12 @@ def task_init(self):
assert self.out_dir.is_absolute()

# Flatten our file lists
self.files_in = flatten(self, self.files_in)
self.deps = flatten(self, self.deps)
self.files_out = flatten(self, self.files_out)
self.files_in = flatten(self.files_in, self)
self.deps = flatten(self.deps, self)
self.files_out = flatten(self.files_out, self)

for key in self.named_deps:
self.named_deps[key] = stringize(self, self.named_deps[key])
self.named_deps[key] = stringize(self.named_deps[key], self)

# Prepend directories to filenames and then normalize + absolute them.
# If they're already absolute, this does nothing.
Expand All @@ -478,11 +481,11 @@ def strip(f):

# Now that files_in/files_out/deps are flat, we can expand our description and command
# list.
self.command = flatten(self, self.command)
self.command = flatten(self.command, self)

# pylint: disable=access-member-before-definition
self.desc = stringize(self, self.desc)
self.depfile = stringize(self, self.depfile)
self.desc = stringize(self.desc, self)
self.depfile = stringize(self.depfile, self)

# Check for missing inputs
if not self.dryrun:
Expand Down
22 changes: 11 additions & 11 deletions tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,29 +228,29 @@ to expand whatever text template we like:
```py
>>> from hancho import config, Rule
>>> rule = Rule(foo = "{bar}", bar = "{baz}", baz = "Hancho")
>>> rule.expand("One Hancho: {foo}")
>>> expand("One Hancho: {foo}", rule)
'One Hancho: Hancho'
```

Basic expressions work inside templates as well:
```py
>>> rule = Rule(foo = "{bar*3}", bar = "{baz*3}", baz = "Hancho")
>>> rule.expand("Nine Hanchos: {foo}")
>>> expand("Nine Hanchos: {foo}", rule)
'Nine Hanchos: HanchoHanchoHanchoHanchoHanchoHanchoHanchoHanchoHancho'
```

Rule fields can also be functions or lambdas:
```py
>>> rule = Rule(foo = lambda x: "Hancho" * x)
>>> rule.expand("{foo(4)}")
>>> expand("{foo(4)}", rule)
'HanchoHanchoHanchoHancho'
```

but note that you do _not_ have access to any Python globals or builtins,

```py
>>> rule = Rule(foo = "{print(4)}")
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
{print(4)}
Expanding '{print(4)}' is stuck in a loop
```
Expand All @@ -259,7 +259,7 @@ Expanding '{print(4)}' is stuck in a loop

```py
>>> rule = Rule(foo = "{print(4)}", print = print)
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
4
''
```
Expand All @@ -268,14 +268,14 @@ Arbitrarily-nested arrays of strings will be flattened out and joined with
spaces:
```py
>>> rule = Rule(foo = [1,2,3,[4,5],[[[6,7]]]])
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
'1 2 3 4 5 6 7'
```

Fields that are never defined will turn into empty strings:
```py
>>> rule = Rule(foo = "{missing}")
>>> rule.expand("?{foo}?")
>>> expand("?{foo}?", rule)
'??'
```

Expand All @@ -284,7 +284,7 @@ Fields that are used globally in multiple rules can be set on
```py
>>> config.bar = "Hancho"
>>> rule = Rule(foo = "{bar}")
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
'Hancho'
```

Expand All @@ -294,21 +294,21 @@ is a better option for common fields that shouldn't be globally visible:
```py
>>> base_rule = Rule(bar = "Hancho")
>>> rule = base_rule.extend(foo = "{bar}")
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
'Hancho'
```

Text templates that cause infinite loops will fail:
```py
>>> rule = Rule(foo = "{bar}", bar = "{foo}")
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
Expanding '{foo}...' failed to terminate
```

as will templates that create infinitely-long strings:
```py
>>> rule = Rule(foo = "!{foo}!")
>>> rule.expand("{foo}")
>>> expand("{foo}", rule)
Expanding '!!!!!!!!!!!!!!!!!!!!...' failed to terminate
```

Expand Down

0 comments on commit 86d1ee5

Please sign in to comment.