Skip to content
This repository was archived by the owner on Jan 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/usr/local/bin/python",
"pylint.path": [
"pylint"
],
"flake8.path": [
"flake8"
],
"mypy-type-checker.path": [
"mypy"
],
"mypy-type-checker.ignorePatterns": ["tests/e2e/example/**/*.py"],
"mypy.targets": [
"grizzly_cli/",
"tests/"
Expand All @@ -53,15 +43,13 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.pylint",
"ms-python.flake8",
"ms-python.mypy-type-checker",
"editorconfig.editorconfig",
"editorconfig.editorconfig",
"eamodio.gitlens",
"ryanluker.vscode-coverage-gutters",
"matangover.mypy",
"oderwat.indent-rainbow"
"oderwat.indent-rainbow",
"charliermarsh.ruff"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ indent_size = 2

[*.feature]
indent_size = 2

[tests/unit/utils/test___init__.py]
trim_trailing_whitespace = false
9 changes: 3 additions & 6 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,14 @@ jobs:
id: pip
run: python -m pip install .[dev]

- name: pylint
id: pylint
run: python -m pylint --jobs=0 --fail-under=10 grizzly_cli/ tests/

- name: mypy
id: mypy
run: python -m mypy grizzly_cli/ tests/

- name: flake8
id: flake8
run: python -m flake8
- name: ruff
id: ruff
run: python -m ruff check grizzly_cli/ tests/

test-and-coverage:
name: "test-and-coverage / ${{ matrix.runs-on }} / python-${{ matrix.python-version }}"
Expand Down
25 changes: 14 additions & 11 deletions grizzly_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import os
from __future__ import annotations

from typing import Callable, List, Optional
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Callable, ClassVar, Optional

from behave.model import Scenario
from grizzly_cli.__version__ import __version__

from .argparse import ArgumentSubParser
from .__version__ import __version__
if TYPE_CHECKING: # pragma: no cover
from behave.model import Scenario

from grizzly_cli.argparse import ArgumentSubParser

EXECUTION_CONTEXT = os.getcwd()
EXECUTION_CONTEXT = Path.cwd().as_posix()

STATIC_CONTEXT = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static')
STATIC_CONTEXT = Path.joinpath(Path(__file__).parent.absolute(), 'static').as_posix()

MOUNT_CONTEXT = os.environ.get('GRIZZLY_MOUNT_CONTEXT', EXECUTION_CONTEXT)
MOUNT_CONTEXT = environ.get('GRIZZLY_MOUNT_CONTEXT', EXECUTION_CONTEXT)

PROJECT_NAME = os.path.basename(EXECUTION_CONTEXT)
PROJECT_NAME = Path(EXECUTION_CONTEXT).name

SCENARIOS: List[Scenario] = []
SCENARIOS: list[Scenario] = []

FEATURE_DESCRIPTION: Optional[str] = None


class register_parser:
registered: List[Callable[[ArgumentSubParser], None]] = []
registered: ClassVar[list[Callable[[ArgumentSubParser], None]]] = []
order: Optional[int]

def __init__(self, order: Optional[int] = None) -> None:
Expand Down
156 changes: 81 additions & 75 deletions grizzly_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import argparse
from __future__ import annotations

import os
import sys

from pathlib import Path
from shutil import which
from typing import Tuple, Optional, List
from traceback import format_exc
from typing import TYPE_CHECKING, Optional

from .argparse import ArgumentParser
from .utils import ask_yes_no, get_distributed_system, get_dependency_versions, setup_logging
from .init import init
from .local import local
from .distributed import distributed
from .auth import auth
from .keyvault import keyvault
from . import __version__, register_parser
from grizzly_cli import __version__, register_parser
from grizzly_cli.argparse import ArgumentParser
from grizzly_cli.auth import auth
from grizzly_cli.distributed import distributed
from grizzly_cli.init import init
from grizzly_cli.keyvault import keyvault
from grizzly_cli.local import local
from grizzly_cli.utils import ask_yes_no, get_dependency_versions, get_distributed_system, setup_logging

if TYPE_CHECKING:
import argparse


def _create_parser() -> ArgumentParser:
Expand Down Expand Up @@ -53,40 +57,65 @@ def _create_parser() -> ArgumentParser:
return parser


def _parse_show_version(args: argparse.Namespace) -> None:
version = '(development)' if __version__ == '0.0.0' else __version__

grizzly_versions: Optional[tuple[Optional[str], Optional[list[str]]]] = None

if args.version == 'all':
grizzly_versions, locust_version = get_dependency_versions(local_install=False)
else:
grizzly_versions, locust_version = None, None

print(f'grizzly-cli {version}')
if grizzly_versions is not None:
grizzly_version, grizzly_extras = grizzly_versions
if grizzly_version is not None:
print(f'└── grizzly {grizzly_version}', end='')
if grizzly_extras is not None and len(grizzly_extras) > 0:
print(f' ── extras: {", ".join(grizzly_extras)}', end='')
print()

if locust_version is not None:
print(f' └── locust {locust_version}')

raise SystemExit(0)


def _parse_run(parser: ArgumentParser, args: argparse.Namespace) -> None:
if args.command == 'dist':
if args.limit_nofile < 10001 and not args.yes:
print('!! this will cause warning messages from locust later on')
ask_yes_no('are you sure you know what you are doing?')
elif args.command == 'local' and which('behave') is None:
parser.error_no_help('"behave" not found in PATH, needed when running local mode')

if args.testdata_variable is not None:
for variable in args.testdata_variable:
try:
[name, value] = variable.split('=', 1)
os.environ[f'TESTDATA_VARIABLE_{name}'] = value
except ValueError: # noqa: PERF203
parser.error_no_help('-T/--testdata-variable needs to be in the format NAME=VALUE')

if args.csv_prefix is None:
if args.csv_interval is not None:
parser.error_no_help('--csv-interval can only be used in combination with --csv-prefix')

if args.csv_flush_interval is not None:
parser.error_no_help('--csv-flush-interval can only be used in combination with --csv-prefix')


def _parse_arguments() -> argparse.Namespace:
parser = _create_parser()
args = parser.parse_args()

if hasattr(args, 'file'):
# needed to support file names with spaces, which is escaped (sh-style)
setattr(args, 'file', ' '.join(args.file))
args.file = ' '.join(args.file)

if args.version:
if __version__ == '0.0.0':
version = '(development)'
else:
version = __version__

grizzly_versions: Optional[Tuple[Optional[str], Optional[List[str]]]] = None

if args.version == 'all':
grizzly_versions, locust_version = get_dependency_versions(False)
else:
grizzly_versions, locust_version = None, None

print(f'grizzly-cli {version}')
if grizzly_versions is not None:
grizzly_version, grizzly_extras = grizzly_versions
if grizzly_version is not None:
print(f'└── grizzly {grizzly_version}', end='')
if grizzly_extras is not None and len(grizzly_extras) > 0:
print(f' ── extras: {", ".join(grizzly_extras)}', end='')
print('')

if locust_version is not None:
print(f' └── locust {locust_version}')

raise SystemExit(0)
_parse_show_version(args)

if args.command is None:
parser.error('no command specified')
Expand All @@ -101,36 +130,15 @@ def _parse_arguments() -> argparse.Namespace:
parser.error_no_help('cannot run distributed')

if args.registry is not None and not args.registry.endswith('/'):
setattr(args, 'registry', f'{args.registry}/')
args.registry = f'{args.registry}/'
elif args.command in ['init', 'auth']:
setattr(args, 'subcommand', None)
args.subcommand = None

if args.subcommand == 'run':
if args.command == 'dist':
if args.limit_nofile < 10001 and not args.yes:
print('!! this will cause warning messages from locust later on')
ask_yes_no('are you sure you know what you are doing?')
elif args.command == 'local':
if which('behave') is None:
parser.error_no_help('"behave" not found in PATH, needed when running local mode')

if args.testdata_variable is not None:
for variable in args.testdata_variable:
try:
[name, value] = variable.split('=', 1)
os.environ[f'TESTDATA_VARIABLE_{name}'] = value
except ValueError:
parser.error_no_help('-T/--testdata-variable needs to be in the format NAME=VALUE')

if args.csv_prefix is None:
if args.csv_interval is not None:
parser.error_no_help('--csv-interval can only be used in combination with --csv-prefix')

if args.csv_flush_interval is not None:
parser.error_no_help('--csv-flush-interval can only be used in combination with --csv-prefix')
_parse_run(parser, args)
elif args.command == 'dist' and args.subcommand == 'build':
setattr(args, 'force_build', args.no_cache)
setattr(args, 'build', not args.no_cache)
args.force_build = args.no_cache
args.build = not args.no_cache

log_file = getattr(args, 'log_file', None)
setup_logging(log_file)
Expand All @@ -139,8 +147,8 @@ def _parse_arguments() -> argparse.Namespace:


def _inject_additional_arguments_from_metadata(args: argparse.Namespace) -> argparse.Namespace:
with open(args.file) as fd:
file_metadata = [line.strip().replace('# grizzly-cli ', '').split(' ') for line in fd.readlines() if line.strip().startswith('# grizzly-cli ')]
with Path(args.file).open() as fd:
file_metadata = [line.strip().replace('# grizzly-cli ', '').split(' ') for line in fd if line.strip().startswith('# grizzly-cli ')]

if len(file_metadata) < 1:
return args
Expand All @@ -149,12 +157,12 @@ def _inject_additional_arguments_from_metadata(args: argparse.Namespace) -> argp
for additional_arguments in file_metadata:
try:
if additional_arguments[0].strip().startswith('-'):
raise ValueError()
raise ValueError

index = argv.index(additional_arguments[0]) + 1
for zindex, additional_argument in enumerate(additional_arguments[1:]):
argv.insert(index + zindex, additional_argument)
except ValueError:
except ValueError: # noqa: PERF203
print('?? ignoring {}'.format(' '.join(additional_arguments)))

sys.argv = sys.argv[0:1] + argv
Expand Down Expand Up @@ -182,18 +190,16 @@ def main() -> int:
elif args.command == 'keyvault':
rc = keyvault(args)
else:
raise ValueError(f'unknown command {args.command}')

return rc
message = f'unknown command {args.command}'
raise ValueError(message)
except (KeyboardInterrupt, ValueError) as e:
print('')
print()
if isinstance(e, ValueError):
if args is not None and getattr(args, 'verbose', False):
exception = format_exc()
else:
exception = str(e)
exception = format_exc() if args is not None and getattr(args, 'verbose', False) else str(e)

print(exception)

print('\n!! aborted grizzly-cli')
return 1
else:
return rc
38 changes: 22 additions & 16 deletions grizzly_cli/argparse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import sys
from __future__ import annotations

import re
import sys
from argparse import ArgumentParser as CoreArgumentParser
from argparse import Namespace, _SubParsersAction
from typing import TYPE_CHECKING, Any, Optional, cast

from typing import Any, Optional, IO, Sequence, cast
from argparse import ArgumentParser as CoreArgumentParser, Namespace, _SubParsersAction
from grizzly_cli.argparse.bashcompletion import BashCompletionAction
from grizzly_cli.argparse.bashcompletion import hook as bashcompletion_hook
from grizzly_cli.argparse.markdown import MarkdownFormatter, MarkdownHelpAction

from .markdown import MarkdownFormatter, MarkdownHelpAction
from .bashcompletion import BashCompletionAction, hook as bashcompletion_hook
if TYPE_CHECKING: # pragma: no cover
from collections.abc import Sequence
from typing import IO

from _typeshed import SupportsWrite

ArgumentSubParser = _SubParsersAction


class ArgumentParser(CoreArgumentParser):
def __init__(self, markdown_help: bool = False, bash_completion: bool = False, *args: Any, **kwargs: Any) -> None:
def __init__(self, *args: Any, markdown_help: bool = False, bash_completion: bool = False, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

self.markdown_help = markdown_help
Expand All @@ -27,17 +35,17 @@ def __init__(self, markdown_help: bool = False, bash_completion: bool = False, *
self._optionals.title = 'optional arguments'

def error_no_help(self, message: str) -> None:
sys.stderr.write('{}: error: {}\n'.format(self.prog, message))
sys.stderr.write(f'{self.prog}: error: {message}\n')
sys.exit(2)

def print_help(self, file: Optional[IO[str]] = None) -> None:
'''Hook to make help more command line friendly, if there is markdown markers in the text.
'''
def print_help(self, file: Optional[SupportsWrite[str]] = None) -> None:
"""Make help more command line friendly, if there is markdown markers in the text."""
file = cast('Optional[IO[str]]', file)
if not self.markdown_help:
super().print_help(file)
return

if cast(type, self.formatter_class) is not MarkdownFormatter:
if self.formatter_class is not MarkdownFormatter:
original_description = self.description
original_actions = self._actions

Expand All @@ -53,14 +61,12 @@ def print_help(self, file: Optional[IO[str]] = None) -> None:

super().print_help(file)

if cast(type, self.formatter_class) is not MarkdownFormatter:
if self.formatter_class is not MarkdownFormatter:
self.description = original_description
self._actions = original_actions

def parse_args(self, args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None) -> Namespace: # type: ignore
"""
Hook to add `--bash-complete` to all parsers, if enabled for parser.
"""
def parse_args(self, args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None) -> Namespace: # type: ignore[override]
"""Add `--bash-complete` to all parsers, if enabled for parser."""
if self.bash_completion:
bashcompletion_hook(self)

Expand Down
Loading
Loading