Skip to content

Commit 0b1e3ee

Browse files
Add support for recursive variables (#44)
* Use `self` in pyproject * Add support for recursive variables * Remove duplicate psutil from pyproject * Tweak implementation based on issue feedback * Add type hint to vars_dict * Use `count_of_previously_resolved_vars` name * Clarify test name * Revert refactor and use tuple for variable type return * Add non recursive variable test case * Alter test case name * Add recursive variable docs * Add recursive variables to table of contents * Use PyProject to access static methods * Add better handling of invalid variables * Fix lockfile hash * Move variable import * Only resolve variables once if we are using vars * Fix minor type issue * Reword error message
1 parent e1adc6c commit 0b1e3ee

File tree

8 files changed

+404
-58
lines changed

8 files changed

+404
-58
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [Using Variables](#using-variables)
1717
- [String Formatting](#string-formatting)
1818
- [Always Use Variables](#always-use-variables)
19+
- [Recursive Variables](#recursive-variables)
1920
- [Using Taskipy Without Poetry](#using-taskipy-without-poetry)
2021
- [Installing With PIP](#installing-with-pip)
2122
- [Running Tasks](#running-tasks-1)
@@ -258,6 +259,24 @@ lint = "pylint {path}"
258259
black = "black {path}"
259260
```
260261

262+
#### Recursive Variables
263+
264+
If we want to use variables within other variables, we can utilize recursive variables. By default, variables are not recursive, but we can specify a variable to be recursive by setting the `recursive` key to `true`.
265+
266+
```toml
267+
[tool.taskipy.settings]
268+
use_vars = true
269+
270+
[tool.taskipy.variables]
271+
src_dir = "src"
272+
package_dir = { var = "{src_dir}/package", recursive = true }
273+
274+
[tool.taskipy.tasks]
275+
echo = "echo {package_dir}"
276+
```
277+
278+
In this example, we could run `task echo` and we would then see `src/package`.
279+
261280
### Using Taskipy Without Poetry
262281

263282
Taskipy was created with poetry projects in mind, but actually only requires a valid `pyproject.toml` file in your project's directory. As a result, you can use it even eithout poetry:

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ colorama = "^0.4.4"
3030
pylint = "^2.4.4"
3131
mypy = "^0.812"
3232
rope = "^0.14.0"
33-
psutil = "^5.6.7"
33+
parameterized = "^0.8.1"
3434

3535
[tool.taskipy.tasks]
3636
test = { cmd = "python -m unittest -v tests/test_*.py", help = "runs all tests" }

taskipy/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,22 @@ def __str__(self):
7979
'no tasks found. add a [tool.taskipy.tasks] '
8080
'section to your pyproject.toml'
8181
)
82+
83+
84+
class CircularVariableError(TaskipyError):
85+
exit_code = 127
86+
87+
def __str__(self):
88+
return 'cannot resolve variables, found variables that depend on each other.'
89+
90+
91+
class InvalidVariableError(TaskipyError):
92+
exit_code = 127
93+
94+
def __init__(self, variable: str, reason: str) -> None:
95+
super().__init__()
96+
self.variable = variable
97+
self.reason = reason
98+
99+
def __str__(self):
100+
return f'variable {self.variable} is invalid. reason: {self.reason}'

taskipy/pyproject.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,64 @@
44
from typing import Any, Dict, MutableMapping, Optional, Union
55

66
from taskipy.task import Task
7+
from taskipy.variable import Variable
78
from taskipy.exceptions import (
89
InvalidRunnerTypeError,
10+
InvalidVariableError,
911
MalformedPyProjectError,
1012
MissingPyProjectFileError,
11-
MissingTaskipyTasksSectionError
13+
MissingTaskipyTasksSectionError,
1214
)
1315

1416

1517
class PyProject:
1618
def __init__(self, base_dir: Path):
17-
pyproject_path = self.__find_pyproject_path(base_dir)
19+
pyproject_path = PyProject.__find_pyproject_path(base_dir)
1820
self.__items = PyProject.__load_toml_file(pyproject_path)
1921

2022
@property
2123
def tasks(self) -> Dict[str, Task]:
2224
try:
23-
return {
24-
task_name: Task(task_name, task_toml_contents) for
25-
task_name, task_toml_contents in self.__items['tool']['taskipy']['tasks'].items() }
25+
toml_tasks = self.__items['tool']['taskipy']['tasks'].items()
2626
except KeyError:
2727
raise MissingTaskipyTasksSectionError()
2828

29+
tasks = {}
30+
for name, toml_contents in toml_tasks:
31+
tasks[name] = Task(name, toml_contents)
32+
33+
return tasks
34+
2935
@property
30-
def variables(self) -> Dict[str, Task]:
36+
def variables(self) -> Dict[str, Variable]:
3137
try:
32-
return self.__items['tool']['taskipy'].get('variables', {})
38+
toml_vars = self.__items['tool']['taskipy'].get('variables', {})
3339
except KeyError:
3440
return {}
3541

42+
vars_dict: Dict[str, Variable] = {}
43+
for name, toml_contents in toml_vars.items():
44+
if isinstance(toml_contents, str):
45+
vars_dict[name] = Variable(name, toml_contents, recursive=False)
46+
elif (
47+
isinstance(toml_contents, dict)
48+
and isinstance(toml_contents.get('var'), str)
49+
):
50+
vars_dict[name] = Variable(
51+
name,
52+
toml_contents['var'],
53+
toml_contents.get('recursive', False),
54+
)
55+
else:
56+
raise InvalidVariableError(
57+
name,
58+
f'expected variable to contain a string or be a table '
59+
'with a key "var" that contains a string value, got '
60+
f'{toml_contents}.'
61+
)
62+
63+
return vars_dict
64+
3665
@property
3766
def settings(self) -> dict:
3867
try:
@@ -71,8 +100,10 @@ def candidate_dirs(base: Path):
71100
yield base
72101
for parent in base.parents:
73102
yield parent
103+
74104
for candidate_dir in candidate_dirs(base_dir):
75105
pyproject = candidate_dir / 'pyproject.toml'
76106
if pyproject.exists():
77107
return pyproject
108+
78109
raise MissingPyProjectFileError()

0 commit comments

Comments
 (0)