Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build entrypoint package for each scenario #13

Merged
merged 1 commit into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ packse build scenario/example.json
The `build/` directory will contain sources for all of the packages in the scenario.
The `dist/` directory will contain built distributions for all of the packages in the scenario.


When a scenario is built, it is given a unique identifier based on a hash of the scenario contents and the project
templates used to generate the packages. Each package in the scenario will include the unique identifier.

The `build` command will print the unique identifier for the scenario e.g. `example-cd797223`. A special entrypoint
package is generated for the scenario which can be used later to install the scenario.

The `PACKSE_VERSION_SEED` environment variable can be used to override the seed used to generate the unique
identifier, allowing versions to differ based on information outside of packse.

### Viewing scenarios

**Not yet implemented**

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

```
$ packse view scenarios/example.json
example-9e723676
example-cd797223
└── a-1.0.0
└── requires b>=1.0.0
└── satisfied by b-1.0.0
Expand All @@ -56,22 +58,22 @@ example-9e723676

### Publishing scenarios

Built scenarios can be published to a Python Package Index.

For example, to upload to the test PyPI:
Built scenarios can be published to a Python Package Index with the `publish` command:

```bash
twine upload -r testpypi dist/<scenario>/*
packse publish dist/example-cd797223
```

By default, packages are published to the Test PyPI server.

### Testing scenarios

Published scenarios can then be tested with your choice of package manager.

For example, with `pip`:

```bash
pip install -i https://test.pypi.org/simple/ <scenario>-<package>==1.0.0
pip install -i https://test.pypi.org/simple/ example-cd797223
```

### Writing new scenarios
Expand Down
2 changes: 1 addition & 1 deletion scenarios/requires-incompatible-versions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
{
"name": "requires-direct-incompatible-versionst",
"name": "requires-direct-incompatible-versions",
"description": "Package `a` requires two incompatible versions of package `b`",
"root": "a",
"packages": {
Expand Down
78 changes: 59 additions & 19 deletions src/packse/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
InvalidScenario,
ScenarioNotFound,
)
from packse.scenario import Package, Scenario, load_scenarios, scenario_prefix
from packse.scenario import (
Package,
PackageVersion,
Scenario,
load_scenarios,
scenario_prefix,
)
from packse.template import create_from_template

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -44,7 +50,7 @@ def build_scenario(scenario: Scenario, rm_destination: bool) -> str:
"""
Build the scenario defined at the given path.

Returns the scenario's root package name.
Returns the scenario's entrypoint package name.
"""
prefix = scenario_prefix(scenario)

Expand Down Expand Up @@ -85,7 +91,34 @@ def build_scenario(scenario: Scenario, rm_destination: bool) -> str:
dist_destination=dist_destination,
)

return f"{prefix}-{scenario.root}"
build_scenario_package(
scenario=scenario,
prefix=prefix,
name="",
package=make_entrypoint_package(scenario),
work_dir=work_dir,
build_destination=build_destination,
dist_destination=dist_destination,
)

return prefix


def make_entrypoint_package(scenario: Scenario) -> Package:
"""
Generate an entrypoint `Package` for a scenario that just requires the scenario root package.
"""
return Package(
versions={
"0.0.0": PackageVersion(
requires=[scenario.root],
# Do not build wheels for the root package
wheel=False,
# The scenario's description is used for the entrypoint package
description=scenario.description,
)
}
)


def build_scenario_package(
Expand All @@ -97,12 +130,16 @@ def build_scenario_package(
build_destination: Path,
dist_destination: Path,
):
package_name = f"{prefix}-{name}"
# Only allow the name to be empty for entrypoint packages
assert name or list(package.versions.keys()) == ["0.0.0"]

package_name = f"{prefix}-{name}" if name else prefix

# Generate a Python module name
module_name = package_name.replace("-", "_")

for version, specification in package.versions.items():
for version, package_version in package.versions.items():
logger.info("Generating project for '%s'", package_name)
package_destination = create_from_template(
build_destination,
template_name=scenario.template,
Expand All @@ -111,32 +148,35 @@ def build_scenario_package(
"package-name": package_name,
"module-name": module_name,
"version": version,
"dependencies": [f"{prefix}-{spec}" for spec in specification.requires],
"requires-python": specification.requires_python,
"dependencies": [
f"{prefix}-{spec}" for spec in package_version.requires
],
"requires-python": package_version.requires_python,
"description": package_version.description,
},
)

logger.info(
"Building %s with hatch",
package_destination.relative_to(work_dir),
)

for dist in build_package_distributions(package_destination):
for dist in build_package_distributions(package_version, package_destination):
shared_path = dist_destination / dist.name
logger.info("Linked distribution to %s", shared_path.relative_to(work_dir))
shared_path.hardlink_to(dist)


def build_package_distributions(target: Path) -> Generator[Path, None, None]:
def build_package_distributions(
package_version: PackageVersion, target: Path
) -> Generator[Path, None, None]:
"""
Build package distributions, yield each built distribution path, then delete the distribution folder.
"""

command = ["hatch", "build"]
if package_version.sdist:
command.extend(["-t", "sdist"])
if package_version.wheel:
command.extend(["-t", "wheel"])

try:
output = subprocess.check_output(
["hatch", "build"],
cwd=target,
stderr=subprocess.STDOUT,
)
output = subprocess.check_output(command, cwd=target, stderr=subprocess.STDOUT)

yield from sorted((target / "dist").iterdir())
shutil.rmtree(target / "dist")
Expand Down
11 changes: 8 additions & 3 deletions src/packse/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
class PackageVersion(msgspec.Struct):
requires_python: str | None = ">=3.7"
requires: list[str] = []
sdist: bool = True
wheel: bool = True
description: str = ""

def hash(self) -> str:
"""
Expand Down Expand Up @@ -39,7 +42,7 @@ def hash(self) -> str:
class Scenario(msgspec.Struct):
name: str
"""
The name of the scenario
The name of the scenario.
"""

packages: dict[str, Package]
Expand All @@ -49,7 +52,9 @@ class Scenario(msgspec.Struct):

root: str
"""
The root package, intended to be installed to test the scenario.
The root package of the scenario.

The scenario entrypoint package will require this package.
"""

template: str = "simple"
Expand All @@ -59,7 +64,7 @@ class Scenario(msgspec.Struct):

description: str | None = None
"""
The description of the scenario
The description of the scenario.
"""

def hash(self) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ name = "{{ package-name }}"
version = "{{ version }}"
dependencies = {{ dependencies }}
requires-python = "{{ requires-python }}"
description = "{{ description }}"
Loading