diff --git a/circleci/workflows_lib.py b/circleci/workflows_lib.py index 407e523..9c81b81 100644 --- a/circleci/workflows_lib.py +++ b/circleci/workflows_lib.py @@ -409,6 +409,8 @@ def Main(self) -> None: ) if self.args.fetch_workflow_details: Log(f"Fetching {len(runs)} workflow run details for '{workflow}'.") + if not runs: + continue run_count += len(runs) for run_index, run in enumerate(runs, 1): run["workflow"] = workflow diff --git a/circleci/workflows_lib_test.py b/circleci/workflows_lib_test.py index de8b638..9d4a694 100644 --- a/circleci/workflows_lib_test.py +++ b/circleci/workflows_lib_test.py @@ -37,9 +37,10 @@ def test_CommandList(self): set(Command._commands.keys()).issuperset( { "combine", - "fetch_details", "fetch", + "fetch_details", "filter", + "help", "request_branches", "request_workflow", "request_workflows", @@ -47,15 +48,15 @@ def test_CommandList(self): ) ) - def test_Workflows(self): + def test_RequestBranches(self): """Lower level mocking to prove the pieces work together. Here we bypass the CircleCiApiV2 almost completey as we mock the highest level function we call there. That function `RequestBranches` will be mocked, by injection, through mocking the `CircleCiCommand._InitCircleCiClient`. - Now we can call the `branches` sub-command normally by providing an appropriate argv to - `Command.Run`. The mocked return_value for the `RequestBranches` will be our result. + Now we can call the `request_branches` sub-command normally by providing an appropriate argv + to `Command.Run`. The mocked return_value for the `RequestBranches` will be our result. """ with open(os.devnull, "w") as err: with redirect_stderr(err): diff --git a/mbo/app/flags.py b/mbo/app/flags.py index ca80354..efe9a88 100644 --- a/mbo/app/flags.py +++ b/mbo/app/flags.py @@ -244,11 +244,15 @@ class ActionDateTimeOrTimeDelta(argparse.Action): If `value` starts with either `-` or `+`, then it will be parsed as `timedelta`. Otherwise `value` will be parsed as Python [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). + The resulting value is either a `datetime` or a `str` value if `verify_only==True`. + This action has the additional config: * default: The value to use if `value` is empty (defaults to `datetime.now`). + This must be `None` or of type `datetime`, `str`. * midnight: Whether to adjust the date/time to midnight of the day. * reference: The reference datetime to use for time deltas (defaults to `datetime.now`). - * tz: Fallback timezone. + This must be `None` or of type `datetime`, `str`. + * tz: Fallback timezone (defaults to `timezone.utc`). * verify_only: If True (False is default), then the input will only be verified. The resulting value is the input and its type is `str`. The sole purpose is to verify an input but process it later, which allows one flag to provide its value as a reference @@ -268,28 +272,6 @@ class ActionDateTimeOrTimeDelta(argparse.Action): ``` """ - def _ParseDateTimeStrict( - self, - name: str, - value: Any, - midnight: bool = False, - tz: tzinfo = timezone.utc, - ) -> datetime | None: - if value is None or value == "": - return None - elif not isinstance(value, str) and not isinstance(value, datetime): - raise argparse.ArgumentError( - self, - f"{name.capitalize()} value must be None or of type `datetime` or `str`, provided is `{type(value)}`.", - ) - try: - return _ParseDateTime(value, midnight=self._midnight, tz=self._tz) - except ValueError as error: - raise argparse.ArgumentError( - self, - f"{name.capitalize()} value `{value}` cannot be parsed as `datetime`.", - ) - def __init__(self, **kwargs) -> None: self._verify_only = kwargs.pop("verify_only", False) self._midnight = kwargs.pop("midnight", False) @@ -311,7 +293,8 @@ def __init__(self, **kwargs) -> None: raise argparse.ArgumentError( self, f"Type must be `datetime`, provided type is `{self._type}`." ) - self.default: datetime = ( + # Property `_default_dt` (DateTime) is required for further parsing. + self._default_dt: datetime = ( self._ParseDateTimeStrict( name="default", value=default_v, @@ -320,6 +303,10 @@ def __init__(self, **kwargs) -> None: ) or now ) + # The actual default must be set to a string value for `verify_only`. + self.default: datetime | str = ( + str(self._default_dt) if self._verify_only else self._default_dt + ) self._reference: datetime = ( self._ParseDateTimeStrict( name="reference", @@ -330,11 +317,33 @@ def __init__(self, **kwargs) -> None: or now ) - def _parse(self, value) -> datetime: + def _ParseDateTimeStrict( + self, + name: str, + value: Any, + midnight: bool = False, + tz: tzinfo = timezone.utc, + ) -> datetime | None: + if value is None or value == "": + return None + elif not isinstance(value, str) and not isinstance(value, datetime): + raise argparse.ArgumentError( + self, + f"{name.capitalize()} value must be None or of type `datetime` or `str`, provided is `{type(value)}`.", + ) + try: + return _ParseDateTime(value, midnight=self._midnight, tz=self._tz) + except ValueError as error: + raise argparse.ArgumentError( + self, + f"{name.capitalize()} value `{value}` cannot be parsed as `datetime`.", + ) + + def _parse(self, value: str) -> datetime: try: return ParseDateTimeOrTimeDelta( value=value, - default=self.default, + default=self._default_dt, midnight=self._midnight, reference=self._reference, tz=self._tz, diff --git a/mbo/app/flags_test.py b/mbo/app/flags_test.py index d17cb5f..b878bfc 100644 --- a/mbo/app/flags_test.py +++ b/mbo/app/flags_test.py @@ -201,6 +201,7 @@ def FlagTest(self, test: FlagTestData) -> None: self.assertEqual( test.expected, args.flag, "Bad value in test: " + test.test ) + self.assertIsInstance(args.flag, type(test.expected)) except argparse.ArgumentError as error: self.assertIsNotNone(test.expected_error, error) if test.expected_error: @@ -452,6 +453,17 @@ def test_EnumListAction(self, test: FlagTestData): ), input=[], ), + FlagTestData( + test="Non present flag with default.", + expected=str(_NOW_DATETIME), + expected_error=argparse.ArgumentError, + action=ActionArgs( + "--flag", + action=mbo.app.flags.ActionDateTimeOrTimeDelta, + verify_only=True, + ), + input=[], + ), FlagTestData( test="Parse from short datetime, bad input.", expected="argument flag: Invalid date string: '20240230', day is out of range for month",