1 # Copyright 2020 The Pigweed Authors
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 # use this file except in compliance with the License. You may obtain a copy of
7 # https://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations under
14 """Generates a setup.py and __init__.py for a Python package."""
17 from collections import defaultdict
18 from pathlib import Path
20 from typing import Dict, List, Set
22 # Make sure dependencies are optional, since this script may be run when
23 # installing Python package dependencies through GN.
25 from pw_cli.log import install as setup_logging
27 from logging import basicConfig as setup_logging # type: ignore
29 _SETUP_TEMPLATE = """# Generated file. Do not modify.
35 author='Pigweed Authors',
36 author_email='pigweed-developers@googlegroups.com',
37 description='Generated protobuf files',
38 packages={packages!r},
39 package_data={package_data!r},
40 include_package_data=True,
42 install_requires=['protobuf'],
48 """Parses and returns the command line arguments."""
49 parser = argparse.ArgumentParser(description=__doc__)
50 parser.add_argument('--package',
52 help='Name of the generated Python package')
53 parser.add_argument('--setup',
56 help='Path to setup.py file')
57 parser.add_argument('--standalone',
59 help='The package is a standalone external proto')
60 parser.add_argument('sources',
63 help='Relative paths to the .py and .pyi files')
64 return parser.parse_args()
67 def main(package: str, setup: Path, standalone: bool,
68 sources: List[Path]) -> int:
69 """Generates __init__.py and py.typed files and a setup.py."""
70 assert not standalone or len(sources) == 2
72 base = setup.parent.resolve()
73 base.mkdir(exist_ok=True)
75 # Find all directories in the package, including empty ones.
76 subpackages: Set[Path] = set()
77 for source in sources:
78 subpackages.update(base / path for path in source.parents)
79 subpackages.remove(base)
81 pkg_data: Dict[str, List[str]] = defaultdict(list)
83 # Create __init__.py and py.typed files for each subdirectory.
84 for pkg in subpackages:
85 pkg.mkdir(exist_ok=True, parents=True)
86 pkg.joinpath('__init__.py').write_text('')
88 package_name = pkg.relative_to(base).as_posix().replace('/', '.')
89 pkg.joinpath('py.typed').touch()
90 pkg_data[package_name].append('py.typed')
92 # Add the Mypy stub (.pyi) for each source file.
93 for mypy_stub in (s for s in sources if s.suffix == '.pyi'):
94 pkg = base / mypy_stub.parent
95 package_name = pkg.relative_to(base).as_posix().replace('/', '.')
96 pkg_data[package_name].append(mypy_stub.name)
99 pkg.joinpath('__init__.py').write_text(
100 f'from {mypy_stub.stem}.{mypy_stub.stem} import *\n')
103 _SETUP_TEMPLATE.format(name=package,
104 packages=list(pkg_data),
105 package_data=dict(pkg_data)))
110 if __name__ == '__main__':
112 sys.exit(main(**vars(_parse_args())))