Skip to content

Commit 08301ef

Browse files
committed
pr checks
1 parent 4dfffe8 commit 08301ef

File tree

11 files changed

+103
-63
lines changed

11 files changed

+103
-63
lines changed

.github/workflows/checks.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
name: static-analysis
3+
on: push
4+
jobs:
5+
static-analysis:
6+
name: Static Analysis
7+
runs-on: ubuntu-latest
8+
container: python:3.12
9+
steps:
10+
- uses: actions/checkout@v4
11+
- name: install prerequisites
12+
run: |
13+
pip install .[check] .[test] black pylint
14+
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 --output cc-test-reporter
15+
chmod +x cc-test-reporter
16+
- name: run black
17+
run: |
18+
black --check --diff .
19+
- name: run pylint
20+
run: |
21+
pylint --fail-under 10 src
22+
pylint --fail-under 10 --disable=protected-access tests
23+
- name: run tests
24+
env:
25+
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
26+
run:
27+
./cc-test-reporter before-build
28+
pytest . --cov-report lcov
29+
./cc-test-reporter after-build --exit-code $?

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,13 @@ log_cli = "true"
4040
testpaths = ["tests"]
4141
pythonpath = "src"
4242
addopts = "-n 4 -Walways --cov=cli_wrapper --cov-branch --cov-report html:coverage"
43-
asyncio_default_fixture_loop_scope = "function"
43+
asyncio_default_fixture_loop_scope = "function"
44+
45+
[tool.pylint."messages control"]
46+
disable = [
47+
"missing-module-docstring",
48+
"missing-class-docstring",
49+
"missing-function-docstring",
50+
"logging-fstring-interpolation",
51+
"fixme"
52+
]

src/cli_wrapper/cli_wrapper.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def validate_pod_name(name):
4646
to `command --arg=val`. If you want to use spaces instead, set this to ' '
4747
"""
4848

49-
import asyncio
49+
import asyncio.subprocess
5050
import logging
5151
import os
5252
import subprocess
@@ -87,7 +87,7 @@ def from_dict(cls, arg_dict):
8787
transformer=arg_dict.get("transformer", None),
8888
)
8989

90-
def _to_dict(self):
90+
def to_dict(self):
9191
"""
9292
Convert the Argument to a dictionary
9393
:return: the dictionary representation of the Argument
@@ -96,7 +96,7 @@ def _to_dict(self):
9696
return {
9797
"literal_name": self.literal_name,
9898
"default": self.default,
99-
"validator": self.validator._to_dict() if self.validator is not None else None,
99+
"validator": self.validator.to_dict() if self.validator is not None else None,
100100
}
101101

102102
def is_valid(self, value):
@@ -139,7 +139,7 @@ def arg_converter(value: dict):
139139

140140

141141
@define
142-
class Command(object):
142+
class Command: # pylint: disable=too-many-instance-attributes
143143
"""
144144
Command represents a command to be run with the cli_wrapper
145145
"""
@@ -154,7 +154,7 @@ class Command(object):
154154
arg_separator: str = field(repr=False, default="=")
155155

156156
@classmethod
157-
def _from_dict(cls, command_dict, **kwargs):
157+
def from_dict(cls, command_dict, **kwargs):
158158
"""
159159
Create a Command from a dictionary
160160
:param command_dict: the dictionary to be converted
@@ -172,7 +172,7 @@ def _from_dict(cls, command_dict, **kwargs):
172172
**kwargs,
173173
)
174174

175-
def _to_dict(self):
175+
def to_dict(self):
176176
"""
177177
Convert the Command to a dictionary.
178178
Excludes prefixes/separators, because they are set in the CLIWrapper
@@ -182,8 +182,8 @@ def _to_dict(self):
182182
return {
183183
"cli_command": self.cli_command,
184184
"default_flags": self.default_flags,
185-
"args": {k: v._to_dict() for k, v in self.args.items()},
186-
"parse": self.parse._to_dict() if self.parse is not None else None,
185+
"args": {k: v.to_dict() for k, v in self.args.items()},
186+
"parse": self.parse.to_dict() if self.parse is not None else None,
187187
}
188188

189189
def validate_args(self, *args, **kwargs):
@@ -231,12 +231,13 @@ def build_args(self, *args, **kwargs):
231231

232232

233233
@define
234-
class CLIWrapper:
234+
class CLIWrapper: # pylint: disable=too-many-instance-attributes
235235
path: str
236236
env: dict[str, str] = None
237237
commands: dict[str, Command] = {}
238238

239239
trusting: bool = True
240+
raise_exc: bool = False
240241
async_: bool = False
241242
default_transformer: str = "snake2kebab"
242243
short_prefix: str = "-"
@@ -263,17 +264,19 @@ def _get_command(self, command: str):
263264
return c
264265
return self.commands[command]
265266

266-
def _update_command(
267+
def _update_command( # pylint: disable=too-many-arguments
267268
self,
268269
command: str,
269-
cli_command: str = None,
270+
*,
271+
cli_command: str | list[str] = None,
270272
args: dict[str | int, any] = None,
271273
default_flags: dict = None,
272274
parse=None,
273275
):
274276
"""
275277
update the command to be run with the cli_wrapper
276-
:param command: the subcommand for the cli tool
278+
:param command: the command name for the wrapper
279+
:param cli_command: the command to be run, if different from the command name
277280
:param default_flags: default flags to be used with the command
278281
:param parse: function to parse the output of the command
279282
:return:
@@ -303,7 +306,7 @@ def _run(self, command: str, *args, **kwargs):
303306
env = os.environ.copy().update(self.env if self.env is not None else {})
304307
logger.debug(f"Running command: {' '.join(command_args)}")
305308
# run the command
306-
result = subprocess.run(command_args, capture_output=True, text=True, env=env)
309+
result = subprocess.run(command_args, capture_output=True, text=True, env=env, check=self.raise_exc)
307310
if result.returncode != 0:
308311
raise RuntimeError(f"Command {command} failed with error: {result.stderr}")
309312
return command_obj.parse(result.stdout)
@@ -314,7 +317,7 @@ async def _run_async(self, command: str, *args, **kwargs):
314317
command_args = [self.path] + list(command_obj.build_args(*args, **kwargs))
315318
env = os.environ.copy().update(self.env if self.env is not None else {})
316319
logger.error(f"Running command: {', '.join(command_args)}")
317-
proc = await asyncio.subprocess.create_subprocess_exec(
320+
proc = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member
318321
*command_args,
319322
stdout=asyncio.subprocess.PIPE,
320323
stderr=asyncio.subprocess.PIPE,
@@ -351,22 +354,22 @@ def from_dict(cls, cliwrapper_dict):
351354
else:
352355
if "cli_command" not in config:
353356
config["cli_command"] = command
354-
commands[command] = Command._from_dict(config)
357+
commands[command] = Command.from_dict(config)
355358

356359
return CLIWrapper(
357360
commands=commands,
358361
**cliwrapper_dict,
359362
)
360363

361-
def _to_dict(self):
364+
def to_dict(self):
362365
"""
363366
Convert the CLIWrapper to a dictionary
364367
:return:
365368
"""
366369
return {
367370
"path": self.path,
368371
"env": self.env,
369-
"commands": {k: v._to_dict() for k, v in self.commands.items()},
372+
"commands": {k: v.to_dict() for k, v in self.commands.items()},
370373
"trusting": self.trusting,
371374
"async_": self.async_,
372375
}

src/cli_wrapper/parsers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from .util.callable_chain import params_from_kwargs, CallableChain
3+
from .util.callable_chain import CallableChain
44
from .util.callable_registry import CallableRegistry
55

66
logger = logging.getLogger(__name__)
@@ -71,13 +71,15 @@ def dotted_dictify(src, *args, **kwargs):
7171

7272

7373
class Parser(CallableChain):
74-
74+
"""
75+
Parser class that allows for the chaining of multiple parsers.
76+
"""
7577
def __init__(self, config):
7678
super().__init__(config, parsers)
7779

78-
def __call__(self, input):
80+
def __call__(self, src):
7981
# For now, parser expects to be called with one input.
80-
result = input
82+
result = src
8183
for parser in self.chain:
8284
logger.debug(result)
8385
result = parser(result)

src/cli_wrapper/util/callable_chain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, config, source):
2424
name, args, kwargs = params_from_kwargs(config)
2525
self.chain = [source.get(name, args, kwargs)]
2626

27-
def _to_dict(self):
27+
def to_dict(self):
2828
return self.config
2929

3030
@abstractmethod

src/cli_wrapper/util/callable_registry.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,22 @@ def get(self, name: str | Callable, args=None, kwargs=None) -> Callable:
4040
raise KeyError(f"{self.callable_name} '{name}' not found.")
4141
return lambda *fargs: callable_(*fargs, *args, **kwargs)
4242

43-
def register(self, name: str, callable: callable, group="core"):
43+
def register(self, name: str, callable_: callable, group="core"):
4444
"""
4545
Registers a new parser function with the specified name.
4646
4747
:param name: The name to associate with the parser.
48-
:param callable: The callable function to register.
48+
:param callable_: The callable function to register.
4949
"""
5050
ngroup, name = self._parse_name(name)
5151
if ngroup is not None:
5252
if group != "core":
5353
# approximately, raise an exception if a group is specified in the name and the group arg
54-
raise KeyError(f"'{callable}' already specifies a group.")
54+
raise KeyError(f"'{callable_}' already specifies a group.")
5555
group = ngroup
5656
if name in self._all[group]:
5757
raise KeyError(f"{self.callable_name} '{name}' already registered.")
58-
self._all[group][name] = callable
58+
self._all[group][name] = callable_
5959

6060
def register_group(self, name: str, parsers: dict = None):
6161
"""
@@ -87,6 +87,6 @@ def _parse_name(self, name: str) -> tuple[str, str]:
8787
return None, name
8888
try:
8989
group, name = name.split(".")
90-
except ValueError:
91-
raise KeyError(f"{self.callable_name} name '{name}' is not valid.")
90+
except ValueError as err:
91+
raise KeyError(f"{self.callable_name} name '{name}' is not valid.") from err
9292
return group, name

src/cli_wrapper/validators.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class Validator(CallableChain):
3131

3232
def __init__(self, config):
3333
if callable(config):
34-
id = str(uuid4())
35-
validators.register(id, config)
36-
config = id
34+
id_ = str(uuid4())
35+
validators.register(id_, config)
36+
config = id_
3737
self.config = config
3838
super().__init__(config, validators)
3939

@@ -46,7 +46,7 @@ def __call__(self, value):
4646
result = result and validator_result
4747
return result
4848

49-
def _to_dict(self):
49+
def to_dict(self):
5050
"""
5151
Converts the validator configuration to a dictionary.
5252
"""

tests/test_cli_wrapper.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import json
21
import logging
32
from json import loads
43

54
import pytest
65

76
from cli_wrapper.cli_wrapper import CLIWrapper, Argument, Command
8-
from cli_wrapper.transformers import transformers
97
from cli_wrapper.validators import validators
108

119
logger = logging.getLogger(__name__)
@@ -98,7 +96,7 @@ def validator(name):
9896
validators._all.pop("test_command_group")
9997

10098
def test_command_from_dict(self):
101-
command = Command._from_dict(
99+
command = Command.from_dict(
102100
{
103101
"cli_command": "get",
104102
"default_flags": {"namespace": "default"},
@@ -120,7 +118,7 @@ def test_command_from_dict(self):
120118
"--namespace=kube-system",
121119
]
122120

123-
command = Command._from_dict({"cli_command": "get", "args": {}})
121+
command = Command.from_dict({"cli_command": "get", "args": {}})
124122

125123

126124
class TestCLIWrapper:

tests/test_parsers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def custom_extract(src):
2222
parser = Parser(["json", custom_extract])
2323
assert parser(testdata) == "baz"
2424

25-
testdata = "foo:\n" " bar: baz\n"
25+
testdata = "foo:\n bar: baz\n"
2626
parser = Parser(["yaml", "dotted_dict"])
2727
assert parser(testdata).foo.bar == "baz"
2828

tests/test_serialization.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ def test_wrapper_to_dict(self):
1414
},
1515
)
1616

17-
config = kubectl._to_dict()
17+
config = kubectl.to_dict()
1818

1919
kubectl2 = CLIWrapper.from_dict(config)
2020

21-
assert kubectl2._to_dict() == config
21+
assert kubectl2.to_dict() == config
2222

2323
def test_argument_to_dict(self):
2424
arg = Argument(
@@ -27,8 +27,8 @@ def test_argument_to_dict(self):
2727
validator=["is_alnum", "starts_alpha"],
2828
)
2929

30-
config = arg._to_dict()
30+
config = arg.to_dict()
3131

3232
arg2 = Argument.from_dict(config)
3333

34-
assert arg2._to_dict() == config
34+
assert arg2.to_dict() == config

0 commit comments

Comments
 (0)