diff --git a/modguard/cli.py b/modguard/cli.py index 38deacf4..df1d606f 100644 --- a/modguard/cli.py +++ b/modguard/cli.py @@ -2,6 +2,7 @@ import os import sys from modguard.check import check, ErrorInfo +from modguard.init import init_project class BCOLORS: @@ -39,17 +40,37 @@ def print_invalid_exclude(path: str) -> None: ) -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( +def parse_base_arguments(args) -> argparse.Namespace: + base_parser = argparse.ArgumentParser( prog="modguard", - description="Verify module boundaries are correctly implemented.", + add_help=True, epilog="Make sure modguard is run from the root of your repo that a directory is being specified. For example: `modguard .`", ) + base_parser.add_argument( + "path", + type=str, + help="The path of the root of your project that contains all defined boundaries.", + ) + base_parser.add_argument( + "-e", + "--exclude", + required=False, + type=str, + metavar="file_or_path,...", + help="Comma separated path list to exclude. tests/,ci/,etc.", + ) + return base_parser.parse_args(args) + +def parse_init_arguments(args) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="modguard init", + description="Initialize boundaries in a repository with modguard", + ) parser.add_argument( "path", type=str, - help="The path of the root of your project that contains all defined boundaries.", + help="The path of the Python project in which boundaries should be initialized.", ) parser.add_argument( "-e", @@ -59,10 +80,11 @@ def build_parser() -> argparse.ArgumentParser: metavar="file_or_path,...", help="Comma separated path list to exclude. tests/,ci/,etc.", ) - return parser + + return parser.parse_args(args) -def main(args: argparse.Namespace): +def handle_shared_arguments(args: argparse.Namespace): path = args.path if not os.path.isdir(path): print_invalid_path(path) @@ -80,7 +102,20 @@ def main(args: argparse.Namespace): print_invalid_exclude(exclude_path) if has_error: sys.exit(1) - result: list[ErrorInfo] = check(path, exclude_paths=exclude_paths) + + return argparse.Namespace(path=path, exclude_paths=exclude_paths) + + +def modguard(args: argparse.Namespace): + shared_args = handle_shared_arguments(args) + try: + result: list[ErrorInfo] = check( + shared_args.path, exclude_paths=shared_args.exclude_paths + ) + except Exception as e: + print(str(e)) + sys.exit(1) + if result: print_errors(result) sys.exit(1) @@ -88,11 +123,25 @@ def main(args: argparse.Namespace): sys.exit(0) -def modguard() -> None: - parser = build_parser() - args = parser.parse_args() - main(args) +def modguard_init(args: argparse.Namespace): + shared_args = handle_shared_arguments(args) + + try: + init_project(shared_args.path, exclude_paths=shared_args.exclude_paths) + except Exception as e: + print(str(e)) + sys.exit(1) + + print(f"✅ {BCOLORS.OKGREEN}Modguard initialized.") + sys.exit(0) + + +def main() -> None: + if len(sys.argv) > 1 and sys.argv[1] == "init": + modguard_init(parse_init_arguments(sys.argv[2:])) + else: + modguard(parse_base_arguments(sys.argv[1:])) if __name__ == "__main__": - modguard() + main() diff --git a/modguard/init.py b/modguard/init.py index 2ca708ea..83317c81 100644 --- a/modguard/init.py +++ b/modguard/init.py @@ -1,5 +1,5 @@ import os -import errors +from . import errors from .check import check_import from .core import PublicMember from .parsing import utils @@ -21,7 +21,9 @@ def init_project(root: str, exclude_paths: list[str] = None): exclude_paths = list(map(utils.canonical, exclude_paths)) if exclude_paths else None boundary_trie = build_boundary_trie(root, exclude_paths=exclude_paths) - initial_boundary_paths = [boundary.full_path for boundary in boundary_trie] + initial_boundary_paths = [ + boundary.full_path for boundary in boundary_trie if boundary.full_path + ] for dirpath in utils.walk_pypackages(root, exclude_paths=exclude_paths): added_boundary = ensure_boundary(dirpath + "/__init__.py") @@ -58,5 +60,10 @@ def init_project(root: str, exclude_paths: list[str] = None): continue file_path, member_name = utils.module_to_file_path(import_mod_path) - mark_as_public(file_path, member_name) - violated_boundary.add_public_member(PublicMember(name=import_mod_path)) + try: + mark_as_public(file_path, member_name) + violated_boundary.add_public_member(PublicMember(name=import_mod_path)) + except errors.ModguardParseError: + print( + f"Skipping member {member_name} in {file_path}; could not mark as public" + ) diff --git a/modguard/parsing/public.py b/modguard/parsing/public.py index c05f6116..0be6c70f 100644 --- a/modguard/parsing/public.py +++ b/modguard/parsing/public.py @@ -171,7 +171,7 @@ def _public_module_prelude(should_import: bool = True) -> str: IMPORT_MODGUARD = "import modguard" -PUBLIC_DECORATOR = "@public" +PUBLIC_DECORATOR = "@modguard.public" @public @@ -204,13 +204,13 @@ def mark_as_public(file_path: str, member_name: str = ""): ) with open(file_path, "w") as file: - file_lines = file_content.splitlines() + file_lines = file_content.splitlines(keepends=True) lines_to_write = [ *file_lines[: member_finder.matched_lineno - 1], - PUBLIC_DECORATOR, + PUBLIC_DECORATOR + "\n", *file_lines[member_finder.matched_lineno - 1 :], ] if not modguard_public_is_imported: - lines_to_write = [IMPORT_MODGUARD, *lines_to_write] + lines_to_write = [IMPORT_MODGUARD + "\n", *lines_to_write] - file.write("\n".join(lines_to_write)) + file.write("".join(lines_to_write)) diff --git a/modguard/parsing/utils.py b/modguard/parsing/utils.py index a4d98839..9888147e 100644 --- a/modguard/parsing/utils.py +++ b/modguard/parsing/utils.py @@ -73,7 +73,7 @@ def module_to_file_path( last_sep_index = fs_path.rfind(os.path.sep) file_path = fs_path[:last_sep_index] + ".py" if os.path.exists(file_path): - member_name = fs_path[last_sep_index:] + member_name = fs_path[last_sep_index + 1 :] return file_path, member_name raise errors.ModguardParseError( diff --git a/pyproject.toml b/pyproject.toml index 405552cc..d3bab599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,4 +37,4 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project.scripts] -modguard = "modguard.cli:modguard" +modguard = "modguard.cli:main"