Style

Google Python style, 80 column limit

Commit messages should have the module name or recipe prefix instead of something like fix: or [fix]

  • Yes: [futures] Fix spelling error
  • No: fix: Fix spelling error

Recipe testing often requires test_data or step_test_data arguments in what otherwise looks like production code—leave these arguments alone.

Setting properties on a StepPresentation modifies global data, so don't remove apparent no-ops that just modify a properties member of an object.

Don't include comments on import lines.

The RunSteps() function in a recipe gets passed an api object of type recipe_api.RecipeScriptApi. Any additional arguments relate to the PROPERTIES global variable. It returns RawResult | None.

The GenTests() function in a recipe gets passed an api object of type recipe_test_api.RecipeTestApi and it returns Iterator[recipe_test_api.TestData].

All recipes must define both RunSteps() and GenTests().

Indentation in docstrings should be independent of the length of the variable name.

  • Yes: Args: very_long_variable_name: Description takes multiple lines.
  • No: Args: very_long_variable_name: Description takes multiple lines.

Prefer the dataclasses module over the attrs module. Exceptions ok for when using attrs features not in dataclasses.

Don't import individual classes or variables—always import modules, and never use from <module> import *. Exceptions:

  • from collections.abc import
  • from typing import
  • from PB. import foo as foo_pb

All imports should be at top-level, even if they`re only used in GenTests()

When importing a module under PB, import as <module>_pb.

  • Yes: from PB.recipe_engine import result as result_pb
  • No: from PB.recipe_engine import result

Module Dependencies

Aside from the Python standard library and third party packages, recipes don‘t share code via imports. Instead, shared recipe code is defined in a recipe module under the recipe_modules directory. A recipe can import a recipe module by listing the module in its DEPS list. Then the module will be accessible via the api object that’s passed to RunSteps.

For example, a call to a api.foo.bar() function in a recipe‘s RunSteps function calls the bar method of an object of the recipe_api.RecipeApi subclass defined in recipe_modules/foo/api.py. Any recipe module that a recipe calls must also be listed in the recipe’s DEPS variable, e.g. DEPS = ["recipe_engine/foo"].

In a recipe module, dependencies are injected via the self.m object instead of api, but it works the same way - self.m.foo.bar() calls the foo recipe module's bar method. The dependencies of recipe module foo are listed in the DEPS variable in recipe_modules/foo/__init__.py.

On the other hand, a call to api.foo.bar() in a GenTests function refers to the bar method in recipe_modules/foo/test_api.py. test_api.py files provide helper functions for mocking results of various recipe steps.

If necessary, modules themselves can be imported with lines like this: from RECIPE_MODULES.<repo-name>.<mod-name> import api as <mod-name>_api. Prefer this structure over other ways to import modules directly.

Types defined in a recipe module should be included in the recipe_api.RecipeApi subclass. This makes it easier to reference these types outside of the module for annotations. Do not do this for types not part of the module's API. Example:

  class FooData:
    pass

  class FooApi(recipe_api.RecipeApi):
    FooData = FooData

    def __init__(self, *args, **kwargs):
      ...

Python Type Annotations

Use from __future__ import annotations in all files.

Use list and dict instead of typing.List and typing.Dict.

Function and method arguments should use generic types if possible, like Sequence or Mapping.

Return values should use specific types, like list or dict.

When there are suitable types in both typing and collections.abc, use the type from collections.abc.

Don't use import typing and then using typing.TypeName everywhere. Instead, use from typing import TypeName. The same rule applies to collections.abc imports.

Don't hide type annotation imports under if typing.TYPE_CHECKING:. Exception for imports that begin with RECIPE_MODULES and the corresponding module is not included in DEPS.

Make sure any if TYPE_CHECKING: lines that do exist have a # pragma: no cover comment.

Testing

After making a change, run ./recipes.py test train --stop to check that everything still works. If making many changes, wait to run this until the end.

Also run shac fmt to adjust any formatting, and run shac check to check for lint issues.