Skip to content

feat(cli): add create-tests command for autogenerating unit test stubs (#74) - #1796

Open
Kulraj69 wants to merge 4 commits into
apache:mainfrom
Kulraj69:feat/cli-create-tests
Open

Kulraj69 wants to merge 4 commits into
apache:mainfrom
Kulraj69:feat/cli-create-tests

Conversation

@Kulraj69

@Kulraj69 Kulraj69 commented Nov 1, 2026 •

Copy link
Copy Markdown

Implements #74: a hamilton create-tests MODULE.py command that generates pytest-style unit
test stubs for the Hamilton functions in a module. This revision works through the
review on the branch.

Changes

  • Signatures are read from the module source with ast, so generating stubs no longer
    imports the module, cannot run its top-level code, and works for a module in a regular
    package with a relative import (pkg/orders.py gets import pkg.orders and calls
    pkg.orders.total_spend(...)).
  • Generated calls keep each parameter's kind: positional-only and positional-or-keyword
    parameters are passed positionally, *args is unpacked, keyword-only parameters are
    passed by name and **kwargs is unpacked, so
    def mixed(a, /, b, c=1, *args, flag=False, **kwargs) becomes
    mixed(a, b, c, *args, flag=flag, **kwargs). Every parameter is passed, so a stub does
    not exercise an omitted default; it is a stub to edit.
  • A literal default is written into the stub. A default the stub cannot name, such as
    def bump(level=DEFAULT_LEVEL), and an annotation the generated file cannot resolve,
    such as one that needs typing, are kept in the TODO comment instead, so the generated
    file has no undefined names.
  • Appending adds the imports the new stubs need (import pandas as pd when the stubs call
    pd.*, plus the module import) above the first line of code in the file. A file that is
    not valid Python is left untouched and the command reports an error.
  • async def functions are skipped: a generated synchronous test cannot await them.
  • uses_pandas and the unused ad_hoc_utils import are gone (ruff F841/F401).

How I tested this

Commands run from the repository root with PYTHONPATH set to it, using a python 3.12.3
venv holding pandas and the repository's pinned ruff 0.16.9:

  • python -m pytest tests/cli -q -> 27 passed.
  • python -m pytest tests/cli/test_logic.py -q -> 24 passed with this branch, and
    15 failed, 9 passed with hamilton/cli/logic.py replaced by the version at the
    reviewed revision ea3931b and the same test file. At ea3931b, ruff reports the F841
    and F401 findings from the review.
  • ruff check hamilton/cli tests/cli -> All checks passed!
  • ruff format --check hamilton/cli/logic.py tests/cli/test_logic.py ->
    2 files already formatted.
  • End to end with the CLI: creating a file, appending into an existing file that has no
    imports, a package module with a relative import, and a module with a non-literal
    default; python -m pytest --collect-only then collects all four generated files.

The new tests cover top-level side effects, decorators, relative imports, postponed
annotations, every parameter kind and default, non-literal defaults, append-mode imports,
aliased imports, CRLF files and an unparsable test file. Several of them import the
generated file for real, so an undefined name at import time fails the test.

Notes

prek is not installed on this machine; the pre-commit hooks' tools were run directly with
the repository's pinned versions (ruff 0.16.9 check and format).

Only synchronous module-level functions are stubbed. The generated test file imports the
target module when the tests run, so its top-level code runs then, as it would for any
test. Package detection covers directories with an __init__.py; namespace packages are
not detected, and the generated import is relative to the package root.

Checklist

  • PR has an informative and human-readable title (this will be pulled into the release notes)
  • Changes are limited to a single goal (no scope creep)
  • Code passed the pre-commit check & code is left cleaner/nicer than when first encountered. (prek is not installed on this machine, so the hooks were not run as a hook; ruff check and ruff format --check were run directly at the pinned ruff 0.16.9 and both pass. The code is left cleaner/nicer than when first encountered.)
  • Any change in functionality is tested
  • New functions are documented (with a description, list of inputs, and expected output)
  • Placeholder code is flagged / future TODOs are captured in comments
  • Project documentation has been updated if adding/changing functionality.

@Kulraj69
Kulraj69 marked this pull request as ready for review November 4, 2026 08:02

@skrawcz skrawcz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted preliminary review — findings must be fixed or technically refuted before human review.

The example and absolute sibling-import handling address the main feedback from #824, and the basic create/append/skip tests pass. The implementation still needs significant work before it is safe as a CLI feature:

  • It executes the target module through module_from_source() merely to inspect signatures, so arbitrary top-level side effects run; package-relative imports still fail. Please use AST-based source discovery or justify the execution model and its security contract.
  • Generated calls discard Python parameter kinds/defaults. For example, def f(x=1, *args, flag=False, **kwargs) becomes f(x, args, flag, kwargs), which changes semantics. Preserve positional/starred/keyword-only/double-starred arguments.
  • Append mode can add pd.Series and module references to an existing file without adding the necessary imports, producing runtime NameErrors.
  • Current Ruff reports F841 for uses_pandas and F401 for ad_hoc_utils.

Please fix these and add tests for top-level side effects, relative imports, postponed annotations, all parameter kinds/defaults, and append-mode imports—or technically refute why those cases are outside the command’s supported contract.

… kinds

Review feedback on apache#1796:
- discover functions and signatures with ast instead of importing the module,
  so generation cannot run top-level code and package-relative imports work
- render each parameter with its own kind (positional-only, *args, keyword-only,
  **kwargs) and carry literal defaults into the stub; a default or annotation
  the generated file cannot resolve goes into the TODO comment instead
- add the imports the new stubs need when appending; leave an unparsable file
  untouched instead of appending to it
- skip async functions: a generated sync test cannot await them
- tests for side effects, relative imports, postponed annotations, every
  parameter kind, append-mode imports, aliasing, CRLF and unparsable files
The previous insertion point was after the last top-level import, which could be
below executable top-level code. Only the leading import block is considered now.
Also narrows the README wording on call semantics and package-relative imports to
what the tests cover.
@Kulraj69

Copy link
Copy Markdown
Author

@skrawcz Worked through the review in the commits on the branch. Signatures are read from the module source with
ast now, so nothing gets imported and a package-relative module gets its dotted import path;
generated calls keep each parameter's kind and carry literal defaults, with a default or
annotation the stub cannot name left in the TODO comment; appending adds the imports the new
stubs need above the first line of code, and a test file that is not valid Python is refused
rather than appended to; the F841 and F401 findings are gone.

Tests for top-level side effects, decorators, relative imports, postponed annotations, every
parameter kind, non-literal defaults, append-mode imports, CRLF files and an unparsable test
file: python -m pytest tests/cli -q gives 27 passed here, and 15 of the 24 tests in
test_logic.py fail with hamilton/cli/logic.py at ea3931b.

One point I changed rather than fixed: async def functions are now skipped, since a generated
synchronous test cannot await them. If you would rather have async stubs, say which test runner
you want them for and I will add it.

prek is not installed on this machine, so the pre-commit checklist line in the PR body is left
unticked; the ruff check and format hooks were run directly at the pinned version instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants