def list_modules(
    search_dir: Path = _DEFAULT_SEARCH_DIR, include_parent: bool = False
) -> list[Module]:
    """Returns a list of python modules based defined by setup.py files.

    Args:
        include_parent: if true, a setup.py is expected in `search_dir`, and the corresponding
        module will be included.
        search_dir: the search directory for modules, by default the repo root.
    Returns:
          a list of `Module`s that were found, where each module `m` is initialized with `m.root`
           relative to `search_dir`, `m.raw_setup` contains the dictionary equivalent to the
           keyword args passed to the `setuptools.setup` method in setup.py
    Raises:
        ValueError: if include_parent=True but there is no setup.py in `search_dir`.
    """

    relative_folders = sorted(
        f.relative_to(search_dir)
        for f in search_dir.glob("*")
        if f.is_dir() and (f / "setup.py").is_file()
    )
    if include_parent:
        parent_setup_py = search_dir / "setup.py"
        if not parent_setup_py.exists():
            raise ValueError(f"include_parent=True, but {parent_setup_py} does not exist.")
        relative_folders.append(Path("."))

    result = [
        Module(root=folder, raw_setup=_parse_module(search_dir / folder))
        for folder in relative_folders
    ]

    return result
Returns a list of python modules based defined by setup.py files.

    Args:
        include_parent: if true, a setup.py is expected in `search_dir`, and the corresponding
        module will be included.
        search_dir: the search directory for modules, by default the repo root.
    Returns:
          a list of `Module`s that were found, where each module `m` is initialized with `m.root`
           relative to `search_dir`, `m.raw_setup` contains the dictionary equivalent to the
           keyword args passed to the `setuptools.setup` method in setup.py
    Raises:
        ValueError: if include_parent=True but there is no setup.py in `search_dir`.
    
dev_tools/modules.py