Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
810df84
Cancel the event loop monitoring task in `Reboot.stop()`
benh Sep 1, 2026
c694926
Add `reboot.bdd`: pytest-bdd support for testing Reboot applications
benh Sep 1, 2026
35fa7fa
Test `reboot.bdd` against a pydantic API
benh Sep 2, 2026
3014cc8
Let `reboot.bdd` steps save response properties
benh Sep 2, 2026
ba7cf57
Support message-valued properties in `reboot.bdd`
benh Sep 2, 2026
228237e
Type `reboot.bdd` clauses as `Assignment` and `Equals`
benh Sep 3, 2026
101c9f6
Let `reboot.bdd` assertions say `containing` and `of length`
benh Sep 3, 2026
857bd59
Let `reboot.bdd` scenarios say who they call as
benh Sep 3, 2026
154a139
Let `reboot.bdd` assertions wait with `eventually has`
benh Sep 3, 2026
b67fdcc
Spell `reboot.bdd` saves as `name` and recalls as ${name}
benh Sep 3, 2026
03b01d1
Let `reboot.bdd` scenarios spawn and await tasks
benh Sep 3, 2026
1a39c36
Say 'the authenticated user is' in `reboot.bdd`, not 'I am'
benh Sep 3, 2026
78f81d0
Require `reboot.bdd` scenarios to say who calls
benh Sep 3, 2026
3adcf7b
Let `reboot.bdd` scenarios choose their application by name
benh Sep 3, 2026
542bc1d
Ship `reboot.bdd` in the wheel as the `reboot[pytest-bdd]` extra
benh Sep 3, 2026
065b44d
Register `reboot.bdd`'s steps as a pytest plugin
benh Sep 3, 2026
77d8c24
Write the chat-room example's tests in Gherkin
benh Sep 3, 2026
cd5929c
Write the hello-constructors example's tests in Gherkin
benh Sep 3, 2026
9c79005
Write the bank example's tests in Gherkin
benh Sep 3, 2026
a3cede4
Write the bank-pydantic example's tests in Gherkin
benh Sep 3, 2026
9baa7eb
Write the chick-potle example's tests in Gherkin
benh Sep 3, 2026
3bb58eb
Write the agent-wiki example's tests in Gherkin
benh Sep 3, 2026
ba68894
Say custom `reboot.bdd` steps are plain Reboot code
benh Sep 3, 2026
9130920
Put `reboot.bdd` predicate arguments in backticks
benh Sep 4, 2026
6046ed0
Write the hello-tasks example's tests in Gherkin
benh Sep 4, 2026
70a780c
Write the swag-store example's tests in Gherkin
benh Sep 4, 2026
6a7935a
Show the application's behaviors on the dashboard
benh Sep 4, 2026
089a633
Spell `reboot.bdd` variables as <name>, the way Gherkin does
benh Sep 5, 2026
4f5bfa5
Group bank-pydantic's overdraft outline under a Rule
benh Sep 5, 2026
aa621f4
Write bank-pydantic's features as capabilities with rules
benh Sep 5, 2026
b675fbe
Nest a pydantic API's declared errors in each method's errors message
benh Sep 5, 2026
bca33a7
Declare `OverdraftError` on bank-pydantic's `transfer`
benh Sep 5, 2026
97a6f36
Name who makes each call in `reboot.bdd` scenarios
benh Sep 6, 2026
44fffbd
Add browser steps to `reboot.bdd`, driven with Playwright
benh Sep 5, 2026
e1886be
Require every `reboot.bdd` call to say who calls
benh Sep 6, 2026
63dbc58
Name each account on the Development picker by its identity alone
benh Sep 6, 2026
b852017
Sign in and out of the web app in `reboot.bdd` scenarios
benh Sep 6, 2026
f751bea
Record every browser scenario beside its feature file
benh Sep 6, 2026
bbf2e15
Set each user of a scenario in a hue of their own on the Behaviors page
benh Sep 6, 2026
6cd8059
Save the signed-in user's id as part of signing in
benh Sep 6, 2026
882484a
Set off the caller of a step with a comma
benh Sep 6, 2026
8ed2ba2
Start a call with who calls, in the present tense
benh Sep 6, 2026
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
6 changes: 6 additions & 0 deletions bazel/pip_package_rule/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ exports_files(

# A trampoline for the `pip_package` Bazel rule to invoke Python's `build` tool.
# Should only be used via the `pip_package` Bazel rule.
py_binary(
name = "strip_requirements",
srcs = ["strip_requirements.py"],
visibility = ["//visibility:public"],
)

py_binary(
name = "build_trampoline",
srcs = ["build_trampoline.py"],
Expand Down
25 changes: 14 additions & 11 deletions bazel/pip_package_rule/build_trampoline.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def find_package_name(line: str) -> str:
parser.add_argument(
"--requirements-txt",
type=str,
action="append",
help="the path to the requirements.txt file",
required=True,
)
Expand Down Expand Up @@ -114,17 +115,19 @@ def find_package_name(line: str) -> str:
normalize_package_name(dep)
for dep in args.verify_dependency_in_requirements
]
with open(args.requirements_txt) as requirements_txt:
for line in requirements_txt:
if line.startswith("#"):
continue
dependency = normalize_package_name(find_package_name(line))
try:
# This dependency is no longer missing!
missing_dependencies.remove(dependency)
except ValueError:
# Turns out we don't need this dependency. That's fine.
pass
for requirements_txt_path in args.requirements_txt:
with open(requirements_txt_path) as requirements_txt:
for line in requirements_txt:
if line.startswith("#"):
continue
dependency = normalize_package_name(find_package_name(line))
try:
# This dependency is no longer missing!
missing_dependencies.remove(dependency)
except ValueError:
# Turns out we don't need this dependency. That's
# fine.
pass
if len(missing_dependencies) > 0:
raise MissingDependenciesError(
f"Expected dependencies {missing_dependencies} to be in the "
Expand Down
108 changes: 99 additions & 9 deletions bazel/pip_package_rule/pip_package.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -560,16 +560,56 @@ def _pip_package_impl(ctx):
staged_requirements_txt = ctx.actions.declare_file(
"%s/requirements.txt" % STAGING_DIRECTORY_NAME,
)
ctx.actions.run_shell(
mnemonic = "StageRequirements",
command = "cp %s %s" % (
extras_requirements_txts = {}
for extra_target, extra_name in ctx.attr.extras.items():
extra_files = extra_target[DefaultInfo].files.to_list()
if len(extra_files) != 1:
fail("Expected exactly one requirements file for extra '%s'" %
extra_name)
extras_requirements_txts[extra_name] = extra_files[0]
if extras_requirements_txts:
strip_arguments = [
"--requirements-txt",
input_requirements_txt.path,
"--output",
staged_requirements_txt.path,
),
inputs = [input_requirements_txt],
outputs = [staged_requirements_txt],
)
]
for extra_file in extras_requirements_txts.values():
strip_arguments.append(
"--extra-requirements-txt=%s" % extra_file.path,
)
ctx.actions.run(
mnemonic = "StripExtrasRequirements",
executable = ctx.executable._strip_requirements_tool,
arguments = strip_arguments,
inputs = [input_requirements_txt] +
extras_requirements_txts.values(),
outputs = [staged_requirements_txt],
)
else:
ctx.actions.run_shell(
mnemonic = "StageRequirements",
command = "cp %s %s" % (
input_requirements_txt.path,
staged_requirements_txt.path,
),
inputs = [input_requirements_txt],
outputs = [staged_requirements_txt],
)
metadata_files.append(staged_requirements_txt)
staged_extras_requirements_txts = []
for extra_name, extra_file in extras_requirements_txts.items():
staged_extra = ctx.actions.declare_file(
"%s/requirements-%s.txt" % (STAGING_DIRECTORY_NAME, extra_name),
)
ctx.actions.run_shell(
mnemonic = "StageExtraRequirements",
command = "cp %s %s" % (extra_file.path, staged_extra.path),
inputs = [extra_file],
outputs = [staged_extra],
)
metadata_files.append(staged_extra)
staged_extras_requirements_txts.append(staged_extra)

### pyproject.toml
# The `pyproject.toml` file is the main configuration file that will control
Expand Down Expand Up @@ -623,14 +663,39 @@ def _pip_package_impl(ctx):
version = ctx.attr.version
license = ctx.attr.license
pyproject_toml_file = ctx.actions.declare_file("%s/pyproject.toml" % STAGING_DIRECTORY_NAME)
entry_points = ""
for group, entries in ctx.attr.entry_points.items():
entry_points += "[project.entry-points.%s]\n" % group
for entry in entries:
name, separator, module = entry.partition("=")
if separator == "":
fail("Expected an entry_points entry of the form " +
"'name = module', but got '%s'" % entry)
entry_points += '%s = "%s"\n' % (name.strip(), module.strip())

dynamic_fields = '["dependencies"]'
optional_dependencies = ""
if extras_requirements_txts:
dynamic_fields = '["dependencies", "optional-dependencies"]'
optional_dependencies = (
"[tool.setuptools.dynamic.optional-dependencies]\n"
)
for extra_name in extras_requirements_txts.keys():
optional_dependencies += (
'%s = { file = ["requirements-%s.txt"] }\n' %
(extra_name, extra_name)
)
ctx.actions.expand_template(
template = ctx.file._toml_template,
output = pyproject_toml_file,
substitutions = {
"{CLASSIFIERS}": str(classifiers),
"{DESCRIPTION}": ctx.attr.description,
"{DYNAMIC_FIELDS}": dynamic_fields,
"{ENTRY_POINTS}": entry_points,
"{LICENSE}": license,
"{NAME}": ctx.attr.distribution_name,
"{OPTIONAL_DEPENDENCIES}": optional_dependencies,
"{PACKAGE_DATA}": package_data,
"{SCRIPTS}": scripts,
"{VERSION}": version,
Expand Down Expand Up @@ -711,8 +776,11 @@ def _pip_package_impl(ctx):
output_directory,
"--verify-python-version",
ctx.attr._python_version,
"--requirements-txt",
staged_requirements_txt.path,
] + [
"--requirements-txt=%s" % requirements.path
for requirements in (
[staged_requirements_txt] + staged_extras_requirements_txts
)
] + [
"--verify-dependency-in-requirements=%s" % dependency
for dependency in pypi_dependencies.keys()
Expand Down Expand Up @@ -768,6 +836,23 @@ _pip_package = rule(
doc = "The name of the distribution to generate.",
mandatory = True,
),
"entry_points": attr.string_list_dict(
default = {},
doc = "The entry points this pip package advertises: " +
"each key is an entry-point group, e.g. " +
"'pytest11', and each value that group's " +
"'name = module' entries.",
),
"extras": attr.label_keyed_string_dict(
allow_files = True,
default = {},
doc = "Optional-dependency extras: each key is a " +
"requirements file and each value the extra's name; " +
"the file's packages ship as that extra instead of " +
"as dependencies, and every one of them must also " +
"be in `requirements_txt`, which stays the " +
"complete list.",
),
"license": attr.string(
doc = "The license to use for the pip package.",
mandatory = True,
Expand Down Expand Up @@ -823,6 +908,11 @@ _pip_package = rule(
default = Label("//bazel/pip_package_rule:setup.py.template"),
allow_single_file = True,
),
"_strip_requirements_tool": attr.label(
default = Label("//bazel/pip_package_rule:strip_requirements"),
executable = True,
cfg = "exec",
),
"_toml_template": attr.label(
# A pointer to the template for the `pyproject.toml` file.
default = Label("//bazel/pip_package_rule:pyproject.toml.template"),
Expand Down
6 changes: 5 additions & 1 deletion bazel/pip_package_rule/pyproject.toml.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "{NAME}"
version = "{VERSION}"
# TODO: See https://github.com/reboot-dev/mono/issues/4200.
requires-python = ">=3.10,<3.13"
dynamic = ["dependencies"]
dynamic = {DYNAMIC_FIELDS}
readme = "README.md"
description = "{DESCRIPTION}"
classifiers = {CLASSIFIERS}
Expand All @@ -12,13 +12,17 @@ license = "{LICENSE}"
[tool.setuptools.dynamic]
dependencies = { file = "requirements.txt" }

{OPTIONAL_DEPENDENCIES}

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project.scripts]
{SCRIPTS}

{ENTRY_POINTS}

[tool.setuptools]
# The default settings understand our `src/` layout and will auto-discover the
# Python packages we've placed there. We don't need to specify them here.
Expand Down
85 changes: 85 additions & 0 deletions bazel/pip_package_rule/strip_requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Writes a requirements file with the packages of the given extras
requirements files removed, so that those packages ship as a wheel's
optional-dependency extras instead of its dependencies."""

import argparse
import re


def normalize_package_name(name: str) -> str:
"""
Normalizes the package name per
https://packaging.python.org/en/latest/specifications/name-normalization/#normalization
"""
return re.sub(r"[-_.]+", "-", name).lower()


def find_package_name(line: str) -> str:
"""
Given a line like:
```
my-cool_pAcKaG3.n4me==1.2.3 # some comment.
```
Returns "my-cool.pAcKaG3.n4me".
"""
package_name = re.match("^([\\w._-]+)", line, flags=re.IGNORECASE)
if package_name is None:
raise ValueError(f"Could not find package name in line: '{line}'")

return package_name.group(1)


def requirement_packages(filename: str) -> set[str]:
"""The normalized package names the requirements file pins."""
packages = set()
with open(filename) as requirements:
for line in requirements:
line = line.strip()
if not line or line.startswith("#"):
continue
packages.add(normalize_package_name(find_package_name(line)))
return packages


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--requirements-txt", type=str, required=True)
parser.add_argument("--output", type=str, required=True)
parser.add_argument(
"--extra-requirements-txt",
type=str,
action="append",
default=[],
help="requirements file whose packages become an extra, so "
"they are removed from the output; every one of its packages "
"must be present in --requirements-txt",
)
args = parser.parse_args()

extras_packages = set()
for filename in args.extra_requirements_txt:
extras_packages.update(requirement_packages(filename))

missing = extras_packages - requirement_packages(args.requirements_txt)
if missing:
raise ValueError(
f"Expected the extras packages {sorted(missing)} to also be "
f"in '{args.requirements_txt}', which stays the complete "
"list, but they were not"
)

with open(args.requirements_txt) as requirements:
with open(args.output, "w") as output:
for line in requirements:
stripped = line.strip()
if stripped and not stripped.startswith("#"):
package = normalize_package_name(
find_package_name(stripped)
)
if package in extras_packages:
continue
output.write(line)


if __name__ == "__main__":
main()
6 changes: 0 additions & 6 deletions documentation/docs/learn_more/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,6 @@ Here's an example of how the `OverdraftError` can be caught in a type-safe way:

<Tabs groupId="language">
<TabItem value="python" label="Python" default>
<!-- MARKDOWN-AUTO-DOCS:START
(CODE:src=../../../reboot/examples/monorepo/bank/backend/tests/account_servicer_test.py&lines=51-61) -->
<!-- The below code snippet is automatically added from ../../../reboot/examples/monorepo/bank/backend/tests/account_servicer_test.py -->

```py
try:
await account.withdraw(context, amount=65)
Expand All @@ -232,8 +228,6 @@ except Account.WithdrawAborted as aborted:
)
raise
```

<!-- MARKDOWN-AUTO-DOCS:END -->
</TabItem>
<TabItem value="typescript" label="TypeScript">
<!-- MARKDOWN-AUTO-DOCS:START
Expand Down
6 changes: 0 additions & 6 deletions documentation/docs/learn_more/tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,12 @@ If all you have is a `TaskId`, you can use `retrieve()` to get a

<Tabs groupId="language">
<TabItem value="python" label="Python">
<!-- MARKDOWN-AUTO-DOCS:START
(CODE:src=../../../reboot/examples/monorepo/bank/backend/tests/account_servicer_test.py&lines=89-92) -->
<!-- The below code snippet is automatically added from ../../../reboot/examples/monorepo/bank/backend/tests/account_servicer_test.py -->

```py
response = await Account.WelcomeEmailTask.retrieve(
context,
task_id=welcome_email_task_id,
)
```

<!-- MARKDOWN-AUTO-DOCS:END -->
</TabItem>
<TabItem value="typescript" label="TypeScript">
```ts
Expand Down
6 changes: 0 additions & 6 deletions documentation/docs/learn_more/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ To write a test, you can use the `reboot.aio.tests.Reboot` class. This allows
you to start your servicer, create a context, and call the method you want to
test.

<!-- MARKDOWN-AUTO-DOCS:START
(CODE:src=../../../reboot/examples/chat-room/backend/tests/chat_room_servicer_test.py&lines=10-40) -->
<!-- The below code snippet is automatically added from ../../../reboot/examples/chat-room/backend/tests/chat_room_servicer_test.py -->

```py
async def asyncSetUp(self) -> None:
self.rbt = Reboot()
Expand Down Expand Up @@ -48,8 +44,6 @@ async def test_chat_room(self) -> None:
)
```

<!-- MARKDOWN-AUTO-DOCS:END -->

#### Setting Secrets

Some servicers may use [secrets](/learn_more/secrets) for connecting
Expand Down
Loading
Loading