Source code for bundle.docs.discovery
# Copyright 2026 HorusElohim
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from pathlib import Path
import toml
from bundle.core import logger
log = logger.get_logger(__name__)
[docs]
def discover_project(source_dir: Path) -> dict:
"""Read pyproject.toml and return project metadata and package layout.
Args:
source_dir: Project root directory containing pyproject.toml.
Returns:
Dictionary with keys: name, version, author, package_dirs.
"""
pyproject_path = source_dir / "pyproject.toml"
result = {
"name": source_dir.name,
"version": "",
"author": "",
"package_dirs": [],
}
if not pyproject_path.exists():
log.warning("No pyproject.toml found at %s, using defaults", source_dir)
# Fallback: look for directories with __init__.py at top level
for p in source_dir.iterdir():
if p.is_dir() and (p / "__init__.py").exists():
result["package_dirs"].append(str(p.relative_to(source_dir)))
return result
data = toml.loads(pyproject_path.read_text())
project = data.get("project", {})
result["name"] = project.get("name", source_dir.name)
result["version"] = project.get("version", "")
authors = project.get("authors", [])
if authors and isinstance(authors[0], dict):
result["author"] = authors[0].get("name", "")
# Discover package directories from setuptools config
find_cfg = data.get("tool", {}).get("setuptools", {}).get("packages", {}).get("find", {})
where = find_cfg.get("where", ["."])
if isinstance(where, str):
where = [where]
package_dirs = []
for w in where:
src_root = source_dir / w
if src_root.is_dir():
for p in src_root.iterdir():
if p.is_dir() and (p / "__init__.py").exists():
package_dirs.append(str(p.relative_to(source_dir)))
result["package_dirs"] = package_dirs
log.info("Discovered project: %s (packages: %s)", result["name"], package_dirs)
return result
[docs]
def find_readme_files(package_root: Path) -> list[Path]:
"""Recursively find all README.md files in the package tree.
Args:
package_root: Root directory of the Python package.
Returns:
Sorted list of README.md paths found.
"""
return sorted(package_root.rglob("README.md"))
[docs]
def find_subpackages(package_root: Path) -> list[str]:
"""Find all Python subpackages (directories with __init__.py).
Args:
package_root: Root directory of the Python package.
Returns:
Sorted list of subpackage names.
"""
subpackages = []
for p in sorted(package_root.iterdir()):
if p.is_dir() and (p / "__init__.py").exists():
subpackages.append(p.name)
return subpackages