Skip to content

Commit

Permalink
Add support for YAML and TOML scenario definitions (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb authored Apr 16, 2024
1 parent 0254dd4 commit 1ec9c59
Show file tree
Hide file tree
Showing 20 changed files with 958 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Collect scenarios
run: |
scenarios=(scenarios/**/*.json)
scenarios=(scenarios/**/*.(json|yaml|toml))
# Display for debug
for scenario in $scenarios
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ virtual environment.

A scenario is a JSON description of a dependency tree.

See [`scenarios/example.json`](./scenarios/example.json)
See [`scenarios/examples/`](./scenarios/examples/)

Each scenario file can contain one or more scenarios.

Expand All @@ -36,7 +36,7 @@ By default, packse will search for scenarios in the current tree. You may also p
from:

```bash
packse list scenarios/example.json
packse list scenarios/examples/example.json
```

Each scenario will be listed with its unique identifier e.g. `example-cd797223`. This is the name of the package
Expand All @@ -45,15 +45,15 @@ that can be installed to run the scenario once it is built and published.
Each `packse` command supports reading multiple scenario files. For example, with `list`:

```bash
packse list scenarios/example.json scenarios/requires-does-not-exist.json
packse list scenarios/examples/example.json scenarios/requires-does-not-exist.json
```

### Viewing scenarios

The dependency tree of a scenario can be previewed using the `view` command:

```
$ packse view scenarios/example.json
$ packse view scenarios/examples/example.json
example-89cac9f1
├── root
│ └── requires a
Expand All @@ -75,7 +75,7 @@ Note the `view` command will view all scenarios in a file by default. A single s
the `--name` option:

```
$ packse view scenarios/example.json --name example
$ packse view scenarios/examples/example.json --name example
example
This is an example scenario, in which the user depends on a single package `a` which requires `b`
Expand Down
62 changes: 61 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ chevron-blue = "^0.2.1"
setuptools = "^69.1.1"
pypiserver = { version ="^2.0.1", optional = true}
watchfiles = { version = "^0.21.0", optional = true}
pyyaml = "^6.0.1"

[tool.poetry.extras]
index = ["pypiserver"]
Expand Down
File renamed without changes.
23 changes: 23 additions & 0 deletions scenarios/examples/example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name = "example-toml"
description = "This is an example scenario written in TOML, in which the user depends on a single package `a` which requires `b`."

[expected]
satisfiable = true
explanation = "The latest valid version of `b` should be installed. `b==3.0.0` is not valid because it requires `c` which does not exist."

[expected.packages]
a = "1.0.0"
b = "2.0.0"

[root]
requires = [ "a" ]

[packages.a.versions."1.0.0"]
requires = [ "b>1.0.0" ]

[packages.b.versions]
"1.0.0" = { }
"2.0.0" = { }

[packages.b.versions."3.0.0"]
requires = [ "c" ]
26 changes: 26 additions & 0 deletions scenarios/examples/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
- name: example-yaml
description: This is an example scenario written in YAML, in which the user depends on a single
package `a` which requires `b`.
expected:
satisfiable: true
packages:
a: 1.0.0
b: 2.0.0
explanation: The latest valid version of `b` should be installed. `b==3.0.0` is
not valid because it requires `c` which does not exist.
root:
requires:
- a
packages:
a:
versions:
1.0.0:
requires:
- b>1.0.0
b:
versions:
1.0.0: {}
2.0.0: {}
3.0.0:
requires:
- c
2 changes: 1 addition & 1 deletion src/packse/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def build(
raise FileNotFound(target)

if target.is_dir():
for target in target.glob("*.json"):
for target in target.glob("*.(json|yaml|toml)"):
try:
logger.debug("Loading %s", target)
scenarios.extend(load_scenarios(target))
Expand Down
10 changes: 5 additions & 5 deletions src/packse/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,13 @@ def _call_list(args):
skip_invalid = args.skip_invalid
if not args.targets:
skip_invalid = True
targets = Path.cwd().glob("**/*.json")
targets = Path.cwd().glob("**/*.(json|yaml|toml)")
else:
targets = []
for target in args.targets:
# Expand any directories to json files within
if target.is_dir():
targets.extend(target.glob("**/*.json"))
targets.extend(target.glob("**/*.(json|yaml|toml)"))
else:
targets.append(target)

Expand All @@ -183,13 +183,13 @@ def _call_inspect(args):
skip_invalid = args.skip_invalid
if not args.targets:
skip_invalid = True
targets = Path.cwd().glob("**/*.json")
targets = Path.cwd().glob("**/*.(json|yaml|toml)")
else:
targets = []
for target in args.targets:
# Expand any directories to json files within
# Expand any directories to scenario files within
if target.is_dir():
targets.extend(target.glob("**/*.json"))
targets.extend(target.glob("**/*.(json|yaml|toml)"))
else:
targets.append(target)

Expand Down
2 changes: 1 addition & 1 deletion src/packse/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def inspect(
raise FileNotFound(target)

if target.is_dir():
for target in target.glob("*.json"):
for target in target.glob("*.(json|yaml|toml)"):
try:
logger.debug("Loading %s", target)
scenarios_by_path[target] = load_scenarios(target)
Expand Down
34 changes: 26 additions & 8 deletions src/packse/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,27 +288,45 @@ def dict(self) -> dict:
return json.loads(enc.encode(self))


def _load(target: Path, type: Type):
if target.suffix == ".json":
return msgspec.json.decode(target.read_text(), type=type, dec_hook=dec_hook)
elif target.suffix == ".toml":
return msgspec.toml.decode(target.read_text(), type=type, dec_hook=dec_hook)
elif target.suffix == ".yaml":
return msgspec.yaml.decode(target.read_text(), type=type, dec_hook=dec_hook)
else:
raise ValueError(f"Unknown file type {target.suffix!r}")


def load_scenario(target: Path) -> Scenario:
"""
Loads a scenario
"""
dec = msgspec.json.Decoder(Scenario, dec_hook=dec_hook)
return dec.decode(target.read_text())
return _load(target, type=Scenario)


def load_many_scenarios(target: Path) -> list[Scenario]:
"""
Loads a file with many scenarios
"""
dec = msgspec.json.Decoder(list[Scenario], dec_hook=dec_hook)
return dec.decode(target.read_text())
return _load(target, type=list[Scenario])


def load_scenarios(target: Path) -> list[Scenario]:
def has_many_scenarios(target: Path) -> bool:
# Guess if the file contains one or many scenario
with target.open() as buffer:
many = buffer.readline().lstrip().startswith("[")
if many:
if target.suffix == ".json":
with target.open() as buffer:
return buffer.readline().lstrip().startswith("[")
elif target.suffix == ".toml":
return False # Top-level arrays are not supported by TOML
elif target.suffix == ".yaml":
with target.open() as buffer:
return buffer.readline().lstrip().startswith("-")


def load_scenarios(target: Path) -> list[Scenario]:
if has_many_scenarios(target):
return load_many_scenarios(target)
else:
return [load_scenario(target)]
Expand Down
Loading

0 comments on commit 1ec9c59

Please sign in to comment.