Skip to content

Commit 95432bd

Browse files
author
Trong Nhan Mai
committed
feat: add maven and gradle cli parsers
Signed-off-by: Trong Nhan Mai <[email protected]>
1 parent c225e28 commit 95432bd

File tree

12 files changed

+2995
-0
lines changed

12 files changed

+2995
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""This module contain the base classes cli command parsers related."""
5+
6+
import argparse
7+
from abc import abstractmethod
8+
from collections.abc import Mapping
9+
from dataclasses import dataclass
10+
from enum import Enum
11+
from typing import Any, Generic, Protocol, TypeGuard, TypeVar
12+
13+
14+
def is_list_of_strs(value: Any) -> TypeGuard[list[str]]:
15+
"""Type guard for a list of strings."""
16+
return isinstance(value, list) and all(isinstance(ele, str) for ele in value)
17+
18+
19+
def is_dict_of_str_to_str_or_none(value: Any) -> TypeGuard[dict[str, str | None]]:
20+
"""Type guard for a dictionary with keys are string and values are strings or None."""
21+
if not isinstance(value, dict):
22+
return False
23+
24+
for key, val in value.items():
25+
if not isinstance(key, str):
26+
return False
27+
28+
if not (val is None or isinstance(val, str)):
29+
return False
30+
31+
return True
32+
33+
34+
def patch_mapping(
35+
original: Mapping[str, str],
36+
patch: Mapping[str, str | None],
37+
) -> dict[str, str]:
38+
"""Patch a mapping.
39+
40+
A key with value in patch set to None will be removed from the original.
41+
42+
Parameters
43+
----------
44+
original: Mapping[str, str]
45+
The original mapping.
46+
patch: Mapping[str, str | None]
47+
The patch.
48+
49+
Returns
50+
-------
51+
dict[str, str]:
52+
The new dictionary after applying the patch.
53+
"""
54+
patch_result = dict(original)
55+
56+
for name, value in patch.items():
57+
if value is None:
58+
patch_result.pop(name, None)
59+
else:
60+
patch_result[name] = value
61+
62+
return patch_result
63+
64+
65+
P = TypeVar("P")
66+
67+
68+
@dataclass
69+
class OptionDef(Generic[P]):
70+
"""This class represent a definition of a CLI option for argparse.ArgumentParser.
71+
72+
This class also contains the information for validating a patch value.
73+
The generic type T is the patch expected type (if it's not None).
74+
"""
75+
76+
# e.g. `--long-option-name`
77+
# We always require the long name as we use it as the unique identifier in the parser.
78+
long_name: str
79+
80+
@abstractmethod
81+
def is_valid_patch_option(self, patch: Any) -> TypeGuard[P]:
82+
"""Return True if the provide patch value is compatible with the internal type of this option."""
83+
raise NotImplementedError()
84+
85+
@abstractmethod
86+
def add_itself_to_arg_parser(self, arg_parse: argparse.ArgumentParser) -> None:
87+
"""Add a new argument to argparser.ArgumentParser representing this option."""
88+
raise NotImplementedError()
89+
90+
@abstractmethod
91+
def get_patch_type_str(self) -> str:
92+
"""Return the expected type for the patch value as string."""
93+
raise NotImplementedError()
94+
95+
96+
class PatchCommandBuildTool(str, Enum):
97+
"""Build tool supported for CLICommand patching."""
98+
99+
MAVEN = "maven"
100+
GRADLE = "gradle"
101+
102+
103+
class CLIOptions(Protocol):
104+
"""Interface of the options part of a CLICommand."""
105+
106+
def to_option_cmds(self) -> list[str]:
107+
"""Return the options as a list of strings."""
108+
109+
110+
class CLICommand(Protocol):
111+
"""Interface of a CLI Command."""
112+
113+
def to_cmds(self) -> list[str]:
114+
"""Return the CLI Command as a list of strings."""
115+
116+
117+
T = TypeVar("T", bound="CLICommand")
118+
Y_contra = TypeVar("Y_contra", contravariant=True)
119+
120+
121+
class CLICommandParser(Protocol[T, Y_contra]):
122+
"""Interface of a CLI Command Parser."""
123+
124+
@property
125+
def build_tool(self) -> PatchCommandBuildTool:
126+
"""Return the ``BuildTool`` enum corresponding to this CLICommand."""
127+
128+
def parse(self, cmd_list: list[str]) -> CLICommand:
129+
"""Parse the CLI Command.
130+
131+
Parameters
132+
----------
133+
cmd_list: list[str]
134+
The CLI Command as list of strings.
135+
136+
Returns
137+
-------
138+
CLICommand
139+
The CLICommand instance.
140+
141+
Raises
142+
------
143+
CommandLineParseError
144+
If an error happens when parsing the CLI Command.
145+
"""
146+
147+
def is_build_tool(self, executable_path: str) -> bool:
148+
"""Return True if ``executable_path`` ends the accepted executable for this build tool.
149+
150+
Parameters
151+
----------
152+
executable_path: str
153+
The executable component of a CLI command.
154+
155+
Returns
156+
-------
157+
bool
158+
"""
159+
160+
def apply_patch(
161+
self,
162+
cli_command: T,
163+
options_patch: Mapping[str, Y_contra | None],
164+
) -> T:
165+
"""Return the a new CLICommand object with its option patched, while persisting the executable path."""

0 commit comments

Comments
 (0)