Skip to content
Open
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
166 changes: 139 additions & 27 deletions .agents/skills/edg-python-style/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Prefer matching the file you are editing over inventing a new dialect.
neighbors.
- Package code lives under `dev_tools/pylibs/<pkg>/` (importable as
`edgtest`, `edgutil`, `edgacknowledg`, …). Put `dev_tools/bin` on `PATH`
and `dev_tools/pylibs` on `PYTHONPATH` (direnv / [HACKING.md](../../../HACKING.md)).
and `dev_tools/pylibs` on `PYTHONPATH` (direnv /
[HACKING.md](../../../HACKING.md)).

## Indentation and wrapping

Expand All @@ -36,41 +37,143 @@ Prefer matching the file you are editing over inventing a new dialect.
- Wrap long signatures, calls, and `typing` imports with parentheses; align
continuations with neighboring EDG Python, not Black/ruff defaults.

### Function parameter lists
### Function calls and wrapped message text

Call-site wrapping differs from the declaration wrapping below. When a
call's arguments need multiple lines:

- Break immediately after the opening `(`; put the arguments on following
lines, indented **two spaces** from the statement's indentation.
- Once a call is split over multiple lines, put **each positional and
keyword argument on its own line**, even when several would fit together.
A single argument may span multiple lines (for example, a long message),
but do not put another argument on any of those lines.
- Put the closing `)` on its own line, aligned with the statement's start
(including `raise`, `return`, or `await`). Keep suffixes such as `from
None` or a `with` statement's `:` after that closing parenthesis.
- Do not leave the first argument beside the opening parenthesis and hang
subsequent arguments under it. Declaration-style right alignment does
not apply to calls.

After moving arguments onto new lines, **reflow their text for the new
available width**. Count indentation, quotes, and any `f` prefix against
79 columns. Merge adjacent message literals when the whole argument fits
on one line; otherwise split at word boundaries using the available width.
Do not preserve old string breaks just because they belonged to the old
hanging layout. Preserve exact spaces, interpolation, and rendered text;
implicit concatenation must not introduce a newline into the message.

For example, replace this hanging call:

If a `def` / `async def` signature does not fit on one line, wrap the
parameter list in the same spirit as EDG C++ (`code_style.rst` — Wrapping
Function Declarations):
```python
raise ConfigError(f"{host_key} must be a host name, without a scheme, "
f"port, or path")
```

1. Prefer keeping the `def` name and opening `(` on the first line when the
first parameter(s) still fit; continue parameters on following lines.
2. When that is not enough (long name, many parameters, long annotations),
break immediately after `(` and put parameters on the next lines.
3. **Right-indent** wrapped parameter lines so they end at **column 79**
(right-aligned to the margin), packing as many parameters per line as
fit. Do not use a fixed left hanging indent (PEP 8 / Black’s 4-space
continuation).
with this layout, whose message now fits on one argument line:

```python
raise ConfigError(
f"{host_key} must be a host name, without a scheme, port, or path"
)
```

When the message still needs wrapping, reflow the words inside the call:

```python
raise ConfigError(
f"{port_key} is required for a host other than localhost "
f"({TLS_PORT} for TLS)"
)
```

For a call with multiple arguments, keep each argument separate:

```python
return replace(
self,
service_bind = service_bind,
builder_service_url = builder_service_url,
publisher_service_url = publisher_service_url
)
```

Review argument separation, parentheses, and text wrapping in added or
modified calls. An unchanged syntax tree verifies behavior, not readable
layout.

### Multiline collections

Optimize for readability and small future diffs, not fewer lines. In a
multiline list, tuple, set, or dictionary literal, put **one element or
key-value pair per line**, even when several would fit. Keep trailing
commas so adding or removing an element changes only its own line. This
also applies to collections passed to calls such as `frozenset`.
Naturally short single-line collections can remain on one line.

```python
_INT_KEYS = frozenset((
'max_artifact_bytes',
'max_extracted_bytes',
'max_artifact_entries',
))
```

### Function parameter lists

For `def` / `async def` signatures, adapt the **Wrapping Function
Declarations** rules in
[the EDG C++ style guide](../../../doc/source/code_style.rst):

1. First try wrapping with continuation parameters **left-aligned just to
the right of the opening `(`**, in the first parameter's column. Keep
initial parameters on the declaration line when they fit.
2. Use that layout only if every resulting line fits within **79 columns**.
If the declaration overflows and a parameter cannot fit at that column,
break immediately after the opening `(` and move all parameters below it.
3. In this fallback layout, use a **common indentation column chosen by
right-aligning the longest continuation line to column 79**. Shorter
lines start in that same column; do not right-justify each line
independently. Include closing punctuation and any return annotation
when measuring the longest line.
4. Treat the keyword-only separator `*,` as belonging to the preceding
parameter when wrapping. Keep it on that parameter's line whenever it
fits within 79 columns, **including the first declaration line**:
write `def with_wiring(self, *,` rather than putting `*,` below `self`.
The same applies after `cls` or another positional parameter, even when
all following keyword-only parameters wrap. Do not give `*,` its own
line when the preceding line has room for it.

When reviewing a wrapped signature, check lines beginning with `*,` and
move the separator onto the preceding line if it fits.

Normal left alignment (including `ForgeStore.open`):

```python
def get_output_filename(env: Dict[str, str],
case_number: int,
command_number: int) -> str:

async def open(cls, config: DatabaseConfig, *,
password: Optional[str] = None) -> 'ForgeStore':

def with_wiring(self, *,
service_bind: Optional[str] = None) -> 'ForgeServiceConfig':
```

Fallback when left alignment would overflow:

```python
async def invoke_recorded_process(
base_command: List[str], command_args: List[str],
working_dir: Path, env: Dict[str, str],
build_constants: BuildConstants,
output_file_handle, *,
filter_command: Optional[str] = None, debug: bool,
driver_debug: bool = False,
injected_command_args: List[str] = None) -> int:

def parse_test_status_diff(
status_line: str) -> Tuple[str, ParsedTestStatusDiff]:
base_command: List[str],
command_args: List[str],
working_dir: Path,
injected_command_args: Optional[List[str]] = None) -> int:
```

Return annotations (`-> …`) stay on the last parameter line when they fit
within 79 columns; otherwise wrap similarly to neighbors.
within 79 columns; otherwise wrap similarly to neighbors. These declaration
rules do not change the call-site rules above.

## Spacing around `=`

Expand Down Expand Up @@ -127,6 +230,14 @@ Typical order:
Avoid unused imports; do not introduce heavy formatters that rewrite the
whole tree.

## Count checks

When a condition tests whether a collection's element count is zero or
nonzero, prefer explicit `len(items) == 0` or `len(items) != 0` over
collection truthiness (`if not items:` or `if items:`). For example, write
`if len(removed) != 0:` when deciding whether there are removed settings to
report. This rule concerns counts, not boolean flags or optional values.

## Structure

- Modules are flat and explicit; classes are fine for stateful helpers.
Expand All @@ -139,9 +250,10 @@ whole tree.
## What not to do

- Do not run Black/ruff/autopep8 in a way that converts the tree to 4-space
PEP 8, strips spaces around `=`, or replaces right-aligned-to-79
parameter wraps with fixed hanging indents.
- Do not copy front-end **C** style (`edg-cpfe-style`) into Python.
PEP 8, strips spaces around `=`, or replaces the two declaration-wrapping
layouts above with fixed four-space continuations.
- Do not copy unrelated front-end **C** syntax/layout conventions into
Python; use the declaration-wrapping adaptation above.
- Do not invent a second string-quoting scheme; follow `f"..."` / `'...'`.

## Related skills
Expand Down
13 changes: 4 additions & 9 deletions .github/workflows/acknowledg-forge-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@
# installed copy of
# dev_tools/services/acknowledg-forge/deploy-remote.py, the only
# thing the deploy user may run as root) to point the service at
# the pushed digest, set its AcknowlEDG API and UI URLs, and
# restart it (only if any of those changed)
# the pushed digest and restart it when that changes
#
# The image is public and holds no credentials or configuration: the publish
# job has no access to the deployment's secrets. Everything else about the
# service (its unit, tokens, API key) is configured on the host.
#
# Host, user, and SSH credentials come from secrets in the
# `acknowledg-forge` environment; the public AcknowlEDG URLs come from
# repository variables shared with acknowledg-deploy.yml (see
# dev_tools/services/acknowledg-forge/README.md).
# `acknowledg-forge` environment. Application settings, including public
# AcknowlEDG endpoints, belong in the host administrator's TOML.

name: acknowledg-forge Deploy

Expand Down Expand Up @@ -130,11 +128,8 @@ jobs:
env:
SERVICE: ${{ secrets.ACKNOWLEDG_FORGE_SERVICE || 'acknowledg-forge-github' }}
IMAGE: ${{ needs.publish.outputs.image }}
API_URL: ${{ vars.ACKNOWLEDG_API_HOST }}
UI_URL: ${{ vars.ACKNOWLEDG_UI_HOST }}
run: |
# The arguments are deliberately expanded (and quoted) here.
# shellcheck disable=SC2029
ssh acknowledg-forge "sudo -n /usr/local/libexec/acknowledg-forge-deploy \
$(printf '%q ' "${SERVICE}" "${IMAGE}" "${API_URL}" \
"${UI_URL}")"
$(printf '%q ' "${SERVICE}" "${IMAGE}")"
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults:
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/ci-toolchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: &ci-runner-image ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: &ci-runner-image ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults: &container-defaults
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand Down Expand Up @@ -267,6 +267,17 @@ jobs:

edg-prefix-env-loader .tmp/extensive-ci.edgivk

# Reviews (acknowledg-forge, edgy-review) read only the .rt.tar.gz
# files changes.elist names; edgy -A archives every test's. Pruning
# must never keep the results from being uploaded, hence
# continue-on-error.
- name: Prune .rt.tar.gz results changes.elist doesn't name
if: ${{ failure() }}
continue-on-error: true
env:
RUN_TIMESTAMP: ${{ inputs.run_timestamp }}
run: edg-prune-test-run "tests/runs/${RUN_TIMESTAMP}"

- name: Upload partial elist, elog, and .rt.tar.gz results
if: ${{ failure() }}
uses: actions/upload-artifact@v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/promote-test-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# The test legs' image, so that actions/cache picks the same compression
# (part of an entry's version) and the test legs can restore the entry.
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
strategy:
fail-fast: false
matrix:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/strict-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults:
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand All @@ -185,7 +185,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults:
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand All @@ -212,7 +212,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults:
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand All @@ -239,7 +239,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
container:
image: ghcr.io/edgcpp/ci-runner@sha256:565d7b6581912008ca91c5dbc940280c6e28a02c8cb6487d88ee5f0d7e8c51d7
image: ghcr.io/edgcpp/ci-runner@sha256:daae67adf04085b9ae54a816b35053b79086070434d2da4aafd6662f6ecb0bf7
defaults:
run:
shell: edg-container-exec "bash -eo pipefail {0}"
Expand Down
58 changes: 58 additions & 0 deletions dev_tools/bin/edg-prune-test-run
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3

# Part of the EDG Compiler Project, under the Apache License v2.0 with LLVM
# Exceptions.
# See https://edgcpp.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

'''Prune a test run down to what a review of it reads, e.g., before CI
uploads the run:

edg-prune-test-run tests/runs/<run timestamp>

Reviews of a run read only the .rt.tar files its changes.elist files name.
The replays directory is removed whole. In each configuration, a suite
with no test named there is removed whole (its directory, and its
suite-<name> logs), as is every suite of a configuration without a
changes.elist; of the other suites, the .rt.tar files not named there are
removed.
'''

import argparse
import sys

import edgtest
import edgutil

from pathlib import Path

def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description = (
"Remove the .rt.tar files of a test run that no configuration's "
'changes.elist names.'
)
)
parser.add_argument(
'run_dir',
type = Path,
help = 'the run directory (e.g., tests/runs/<run timestamp>)'
)
return parser.parse_args()

def main() -> int:
args = _parse_args()
if not args.run_dir.is_dir():
edgutil.eprint(f"error: {args.run_dir} is not a directory")
return 1

# Counts: of .rt.tar files kept and removed, and of suites removed.
kept, removed, suites = edgtest.prune_run_archives(args.run_dir)
print(
f"Kept {kept} .rt.tar files named by changes.elist files; removed "
f"{removed} others, and {suites} suites with no changes."
)
return 0

if __name__ == '__main__':
sys.exit(main())
Loading
Loading