def get_stack_resources( self, empty_allow: bool = False, header: str = None, no_progress: bool = False ) -> List[str]: """List all stack logical resources and return the selected resources. :param empty_allow: allow empty selection :type empty_allow: bool, optional :param header: information to be displayed in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected list of logical resources LogicalResourceId :rtype: List[str] """ fzf = Pyfzf() with Spinner.spin( message="Fetching stack resources ...", no_progress=no_progress ): paginator = self.client.get_paginator("list_stack_resources") for result in paginator.paginate(StackName=self.stack_name): for resource in result.get("StackResourceSummaries"): resource["Drift"] = resource.get("DriftInformation").get( "StackResourceDriftStatus" ) fzf.process_list( result.get("StackResourceSummaries"), "LogicalResourceId", "ResourceType", "Drift", ) return list( fzf.execute_fzf(multi_select=True, header=header, empty_allow=empty_allow) )
def set_arns( self, arns: Optional[Union[str, list]] = None, empty_allow: bool = False, header: Optional[str] = None, multi_select: bool = False, ) -> None: """Set cloudwatch arns for further operations. :param arns: arns to init :type arns: Union[list, str], optional :param empty_allow: allow empty fzf selection :type empty_allow: bool, optional :param header: header in fzf :type header: str, optional :param multi_select: allow multi selection in fzf :type multi_select: bool, optional """ if not arns: fzf = Pyfzf() with Spinner.spin(message="Fetching cloudwatch alarms ..."): paginator = self.client.get_paginator("describe_alarms") for result in paginator.paginate(): if result.get("CompositeAlarms"): fzf.process_list(result["CompositeAlarms"], "AlarmArn") if result.get("MetricAlarms"): fzf.process_list(result["MetricAlarms"], "AlarmArn") arns = fzf.execute_fzf( empty_allow=empty_allow, multi_select=multi_select, header=header ) if type(arns) == str: self.arns[0] = str(arns) elif type(arns) == list: self.arns = list(arns)
def get_vpc_id(self, multi_select: bool = False, header: str = None, no_progress: bool = False) -> Union[str, list]: """Get user selected vpc id through fzf. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param header: header to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected vpc id :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching VPCs ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_vpcs") for result in paginator.paginate(): response_generator = self._name_tag_generator( result.get("Vpcs", [])) fzf.process_list(response_generator, "VpcId", "IsDefault", "CidrBlock", "Name") return fzf.execute_fzf(empty_allow=True, multi_select=multi_select, header=header)
def get_security_groups( self, multi_select: bool = False, return_attr: str = "id", header: str = None, no_progress: bool = False, ) -> Union[str, list]: """Use paginator to get the user selected security groups. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param return_attr: what attribute to return (id|name) :type return_attr: str, optional :param header: header to display in fzf :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: selected security groups/ids :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching SecurityGroups ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_security_groups") for result in paginator.paginate(): response_generator = self._name_tag_generator( result.get("SecurityGroups", [])) if return_attr == "id": fzf.process_list(response_generator, "GroupId", "GroupName", "Name") elif return_attr == "name": fzf.process_list(response_generator, "GroupName", "Name") return fzf.execute_fzf(multi_select=multi_select, empty_allow=True, header=header)
def set_arns( self, arns: Optional[Union[str, List[str]]] = None, empty_allow: bool = False, header: Optional[str] = None, multi_select: bool = False, ) -> None: """Set the sns arn for other operations. :param arns: arns to init :type arns: Union[list, str], optional :param empty_allow: allow empty fzf selection :type empty_allow: bool, optional :param header: header in fzf :type header: str, optional :param multi_select: allow multi selection in fzf :type multi_select: bool, optional """ if not arns: fzf = Pyfzf() with Spinner.spin(message="Fetching sns topics ..."): paginator = self.client.get_paginator("list_topics") for result in paginator.paginate(): fzf.process_list(result.get("Topics", []), "TopicArn") arns = fzf.execute_fzf(empty_allow=empty_allow, multi_select=multi_select, header=header) if type(arns) == str: self.arns[0] = str(arns) elif type(arns) == list: self.arns = list(arns)
def set_ec2_instance(self, multi_select: bool = True, header: str = None, no_progress: bool = False) -> None: """Set ec2 instance for current operation. :param multi_select: enable multi select :type multi_select: bool, optional :param header: helper information to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin(message="Fetching EC2 instances ...", no_progress=no_progress): paginator = self.client.get_paginator("describe_instances") for result in paginator.paginate(): response_generator = self._instance_generator( result["Reservations"]) fzf.process_list( response_generator, "InstanceId", "Status", "InstanceType", "Name", "KeyName", "PublicDnsName", "PublicIpAddress", "PrivateIpAddress", ) selected_instance = fzf.execute_fzf(multi_select=multi_select, header=header, print_col=0) if multi_select: self.instance_ids[:] = [] self.instance_list[:] = [] for instance in selected_instance: curr = fzf.format_selected_to_dict(str(instance)) self.instance_list.append(curr) self.instance_ids.append(curr["InstanceId"]) else: self.instance_ids[:] = [] self.instance_list[:] = [] self.instance_list.append( fzf.format_selected_to_dict(str(selected_instance))) self.instance_ids.append(self.instance_list[0]["InstanceId"]) if len(self.instance_ids) == 0: self.instance_ids = [""] if len(self.instance_list) == 0: self.instance_list = [{}]
def set_s3_bucket(self, header: str = "", no_progress: bool = False) -> None: """List bucket through fzf and let user select a bucket. :param header: header to display in fzf header :type header: str, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin(message="Fetching s3 buckets ...", no_progress=no_progress): response = self.client.list_buckets() fzf.process_list(response["Buckets"], "Name") self.bucket_name = str(fzf.execute_fzf(header=header))
def _get_list_param_value(self, type_name: str, param_header: str) -> List[str]: """Handle operation if parameter type is a list type. This function is almost the same as _get_selected_param_value besides its handling list type rather than single vaiable type. :param type_name: name of the type of the parameter :type type_name: str :param param_header: information about the current parameter :type param_header: str :return: processed list of selection from the user :rtype: List[str] """ fzf = Pyfzf() if type_name == "List<AWS::EC2::AvailabilityZone::Name>": with Spinner.spin(message="Fetching AvailabilityZones ..."): response = self.ec2.client.describe_availability_zones() response_list = response["AvailabilityZones"] fzf.process_list(response_list, "ZoneName", empty_allow=True) elif type_name == "List<AWS::EC2::Instance::Id>": return list( self.ec2.get_instance_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::SecurityGroup::GroupName>": return list( self.ec2.get_security_groups(multi_select=True, return_attr="name", header=param_header)) elif type_name == "List<AWS::EC2::SecurityGroup::Id>": return list( self.ec2.get_security_groups(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::Subnet::Id>": return list( self.ec2.get_subnet_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::Volume::Id>": return list( self.ec2.get_volume_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::EC2::VPC::Id>": return list( self.ec2.get_vpc_id(multi_select=True, header=param_header)) elif type_name == "List<AWS::Route53::HostedZone::Id>": self.route53.set_zone_id(multi_select=True) return self.route53.zone_ids return list( fzf.execute_fzf(multi_select=True, empty_allow=True, header=param_header))
def _get_selected_param_value(self, type_name: str, param_header: str) -> str: """Use fzf to display aws specific parameters. :param type_name: name of the parameter type :type type_name: str :param param_header: information about current parameter :type param_header: str :return: return the selected value :rtype: str """ fzf = Pyfzf() if type_name == "AWS::EC2::KeyPair::KeyName": with Spinner.spin(message="Fetching KeyPair ..."): response = self.ec2.client.describe_key_pairs() response_list = response["KeyPairs"] fzf.process_list(response_list, "KeyName", empty_allow=True) elif type_name == "AWS::EC2::SecurityGroup::Id": return str(self.ec2.get_security_groups(header=param_header)) elif type_name == "AWS::EC2::AvailabilityZone::Name": with Spinner.spin(message="Fetching AvailabilityZones ..."): response = self.ec2.client.describe_availability_zones() response_list = response.get("AvailabilityZones", []) fzf.process_list(response_list, "ZoneName", empty_allow=True) elif type_name == "AWS::EC2::Instance::Id": return str(self.ec2.get_instance_id(header=param_header)) elif type_name == "AWS::EC2::SecurityGroup::GroupName": return str( self.ec2.get_security_groups(return_attr="name", header=param_header)) elif type_name == "AWS::EC2::Subnet::Id": return str(self.ec2.get_subnet_id(header=param_header)) elif type_name == "AWS::EC2::Volume::Id": return str(self.ec2.get_volume_id(header=param_header)) elif type_name == "AWS::EC2::VPC::Id": return str(self.ec2.get_vpc_id(header=param_header)) elif type_name == "AWS::Route53::HostedZone::Id": self.route53.set_zone_id() return self.route53.zone_ids[0] return str(fzf.execute_fzf(empty_allow=True, header=param_header))
def set_stack(self, no_progress=False) -> None: """Store the selected stack into the instance attribute. :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional """ fzf = Pyfzf() with Spinner.spin( message="Fetching cloudformation stacks ...", no_progress=no_progress ): paginator = self.client.get_paginator("describe_stacks") response = paginator.paginate() stack_generator = self._get_stack_generator(response) for result in response: fzf.process_list( result["Stacks"], "StackName", "StackStatus", "Description" ) self.stack_name = str(fzf.execute_fzf(empty_allow=False)) self.stack_details = search_dict_in_list( self.stack_name, stack_generator, "StackName" )
def get_instance_id(self, multi_select: bool = False, header: str = None) -> Union[str, list]: """Use paginator to get instance id and return it. :param multi_select: allow multiple value selection :type multi_select: bool, optional :param header: header to display in fzf header :type header: str, optional :return: selected instance id :rtype: Union[str, list] """ fzf = Pyfzf() with Spinner.spin(message="Fetching EC2 instances ..."): paginator = self.client.get_paginator("describe_instances") for result in paginator.paginate(): response_generator = self._instance_id_generator( result.get("Reservations", [])) fzf.process_list(response_generator, "InstanceId", "Name") return fzf.execute_fzf(multi_select=multi_select, empty_allow=True, header=header)
def set_keyids( self, keyids: Optional[Union[list, str]] = None, header: Optional[str] = None, multi_select: bool = False, empty_allow: bool = True, ) -> None: """Set the key for kms for further operations. :param keyids: keyids to set :type keyids: Union[list, str], optional :param header: header information in fzf :type header: str, optional :param multi_select: enable multi select :type multi_select: bool, optional :param empty_allow: allow empty selection :type empty_allow: bool, optional """ if not keyids: fzf = Pyfzf() with Spinner.spin(message="Fetching kms keys ..."): paginator = self.client.get_paginator("list_aliases") for result in paginator.paginate(): aliases = [ alias for alias in result.get("Aliases") if alias.get("TargetKeyId") ] fzf.process_list(aliases, "TargetKeyId", "AliasName", "AliasArn") keyids = fzf.execute_fzf(header=header, multi_select=multi_select, empty_allow=empty_allow) if type(keyids) == str: self.keyids[0] = str(keyids) elif type(keyids) == list: self.keyids = list(keyids)
def set_zone_id( self, zone_ids: Optional[Union[str, List[str]]] = None, multi_select: bool = False, ) -> None: """Set the hostedzone id for futher operations. :param zone_ids: list of zone_ids to set :type zone_ids: list, optional :param multi_select: allow multi_select :type multi_select: bool, optional """ if zone_ids is None: fzf = Pyfzf() with Spinner.spin(message="Fetching hostedzones ..."): paginator = self.client.get_paginator("list_hosted_zones") for result in paginator.paginate(): result = self._process_hosted_zone(result["HostedZones"]) fzf.process_list(result, "Id", "Name") zone_ids = fzf.execute_fzf(multi_select=multi_select, empty_allow=True) if type(zone_ids) == str: self.zone_ids[0] = str(zone_ids) elif type(zone_ids) == list: self.zone_ids = list(zone_ids)
def get_object_version( self, bucket: str = "", key: str = "", delete: bool = False, select_all: bool = False, non_current: bool = False, multi_select: bool = True, no_progress: bool = False, ) -> List[Dict[str, str]]: """List object versions through fzf. :param bucket: object's bucketname, if not set, class instance's bucket_name will be used :type bucket: str, optional :param key: object's key, if not set, class instance's path_list[0] will be used :type key: str, optional :param delete: allow to choose delete marker :type delete: bool, optional :param select_all: skip fzf and select all version and put into return list :type select_all: bool, optional :param non_current: only put non_current versions into list :type non_current: bool, optional :param multi_select: allow multi selection :type multi_select: bool, optional :param no_progress: don't display progress bar, useful for ls command :type no_progress: bool, optional :return: list of selected versions :rtype: List[Dict[str, str]] Example return value: [{'Key': s3keypath, 'VersionId': s3objectid}] """ bucket = bucket if bucket else self.bucket_name key_list: list = [] fzf = Pyfzf() if key: key_list.append(key) else: key_list.extend(self.path_list) selected_versions: list = [] for key in key_list: response_generator: Union[list, Generator[Dict[str, str], None, None]] = [] with Spinner.spin(message="Fetching object versions ...", no_progress=no_progress): paginator = self.client.get_paginator("list_object_versions") for result in paginator.paginate(Bucket=bucket, Prefix=key): response_generator = self._version_generator( result.get("Versions", []), result.get("DeleteMarkers", []), non_current, delete, ) if not select_all: fzf.process_list( response_generator, "VersionId", "Key", "IsLatest", "DeleteMarker", "LastModified", ) else: selected_versions.extend([{ "Key": key, "VersionId": version.get("VersionId") } for version in response_generator]) if not select_all: if delete and multi_select: for result in fzf.execute_fzf(multi_select=True): selected_versions.append({ "Key": key, "VersionId": result }) else: selected_versions.append({ "Key": key, "VersionId": str(fzf.execute_fzf()) }) return selected_versions
def changeset_stack( profile: Union[str, bool] = False, region: Union[str, bool] = False, replace: bool = False, local_path: Union[str, bool] = False, root: bool = False, wait: bool = False, info: bool = False, execute: bool = False, delete: bool = False, extra: bool = False, bucket: str = None, version: Union[str, bool] = False, ) -> None: """Handle changeset actions. Main function to interacte with changeset, use argument to control the actions. This function is using update_stack to handle all the dirty works as both functions are processing cloudformation arguments and having the same arguments. :param profile: use a different profile for this operation :type profile: Union[bool, str], optional :param region: use a different region for this operation :type region: Union[bool, str], optional :param replace: replace the template during update :type replace: bool, optional :param local_path: Select a template from local machine :type local_path: Union[bool, str], optional :param root: Search local file from root directory :type root: bool, optional :param wait: wait for stack to be completed before exiting the program :type wait: bool, optional :param info: display result of a changeset :type info: bool, optional :param execute: execute changeset :type execute: bool, optional :param delete: delete changeset :type delete: bool, optional :param extra: configure extra options for the stack, (tags, IAM, termination protection etc..) :type extra: bool, optional :param bucket: specify a bucket/bucketpath to skip s3 selection :type bucket: str, optional :param version: use previous version of template in s3 bucket :type version: Union[bool, str], optional :raises NoNameEntered: If no changset name is entered """ cloudformation = Cloudformation(profile, region) cloudformation.set_stack() # if not creating new changeset if info or execute or delete: fzf = Pyfzf() response: Dict[str, Any] = cloudformation.client.list_change_sets( StackName=cloudformation.stack_name) # get the changeset name fzf.process_list( response.get("Summaries", []), "ChangeSetName", "StackName", "ExecutionStatus", "Status", "Description", ) if info: selected_changeset = str(fzf.execute_fzf()) describe_changes(cloudformation, selected_changeset) # execute the change set elif execute: selected_changeset = fzf.execute_fzf() if get_confirmation("Execute changeset %s?" % selected_changeset): response = cloudformation.client.execute_change_set( ChangeSetName=selected_changeset, StackName=cloudformation.stack_name, ) cloudformation.wait("stack_update_complete", "Wating for stack to be updated ...") print("Stack updated") elif delete: selected_changeset = fzf.execute_fzf(multi_select=True) for changeset in selected_changeset: print("(dryrun) Delete changeset %s" % changeset) if get_confirmation("Confirm?"): for changeset in selected_changeset: cloudformation.client.delete_change_set( ChangeSetName=changeset, StackName=cloudformation.stack_name) else: changeset_name = input("Enter name of this changeset: ") if not changeset_name: raise NoNameEntered("No changeset name specified") changeset_description = input("Description: ") # since is almost same operation as update stack # let update_stack handle it, but return update details instead of execute cloudformation_args = update_stack( cloudformation.profile, cloudformation.region, replace, local_path, root, wait, extra, bucket, version, dryrun=True, cloudformation=cloudformation, ) cloudformation_args[ "cloudformation_action"] = cloudformation.client.create_change_set cloudformation_args["ChangeSetName"] = changeset_name if changeset_description: cloudformation_args["Description"] = changeset_description response = cloudformation.execute_with_capabilities( **cloudformation_args) response.pop("ResponseMetadata", None) print(json.dumps(response, indent=4, default=str)) print(80 * "-") print("Changeset create initiated") if wait: cloudformation.wait( "change_set_create_complete", "Wating for changset to be created ...", ChangeSetName=changeset_name, ) print("Changeset created") describe_changes(cloudformation, changeset_name)
class TestPyfzf(unittest.TestCase): def setUp(self): fileloader = FileLoader() config_path = Path(__file__).resolve().parent.joinpath( "../data/fzfaws.yml") fileloader.load_config_file(config_path=str(config_path)) self.capturedOutput = io.StringIO() sys.stdout = self.capturedOutput self.fzf = Pyfzf() def tearDown(self): sys.stdout = sys.__stdout__ @patch("fzfaws.utils.pyfzf.sys") def test_constructor(self, mocked_sys): self.assertRegex( self.fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-(linux|darwin)_(386|amd64)", ) self.assertEqual("", self.fzf.fzf_string) mocked_sys.maxsize = 4294967295 mocked_sys.platform = "linux" fzf = Pyfzf() self.assertRegex( fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-linux_386") mocked_sys.maxsize = 42949672951 mocked_sys.platform = "darwin" fzf = Pyfzf() self.assertRegex( fzf.fzf_path, r".*/fzfaws.*/libs/fzf-[0-9]\.[0-9]+\.[0-9]-darwin_amd64") mocked_sys.maxsize = 42949672951 mocked_sys.platform = "windows" mocked_sys.exit.side_effect = sys.exit self.assertRaises(SystemExit, Pyfzf) self.assertEqual( self.capturedOutput.getvalue(), "fzfaws currently is only compatible with python3.6+ on MacOS or Linux\n", ) def test_append_fzf(self): self.fzf.fzf_string = "" self.fzf.append_fzf("hello\n") self.fzf.append_fzf("world\n") self.assertEqual("hello\nworld\n", self.fzf.fzf_string) def test_construct_fzf_command(self): cmd_list = self.fzf._construct_fzf_cmd() self.assertEqual( cmd_list[1:], [ "--ansi", "--expect=ctrl-c", "--color=dark", "--color=fg:-1,bg:-1,hl:#c678dd,fg+:#ffffff,bg+:-1,hl+:#c678dd", "--color=info:#98c379,prompt:#61afef,pointer:#e06c75,marker:#e5c07b,spinner:#61afef,header:#61afef", "--height", "100%", "--layout=reverse", "--border", "--cycle", "--bind=alt-a:toggle-all,alt-j:jump,alt-0:top,alt-s:toggle-sort", ], ) @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_execute_fzf(self, mocked_output, mocked_popen): mocked_output.return_value = b"hello" result = self.fzf.execute_fzf(print_col=1) self.assertEqual(result, "hello") mocked_output.assert_called_once() mocked_output.return_value = b"" self.assertRaises(NoSelectionMade, self.fzf.execute_fzf) mocked_output.return_value = b"" result = self.fzf.execute_fzf(empty_allow=True) self.assertEqual("", result) mocked_output.return_value = b"hello" result = self.fzf.execute_fzf(multi_select=True, print_col=1) self.assertEqual(result, ["hello"]) mocked_output.return_value = b"hello\nworld" result = self.fzf.execute_fzf(multi_select=True, print_col=1, preview="hello", header="foo boo") self.assertEqual(result, ["hello", "world"]) mocked_output.return_value = b"hello world\nfoo boo" result = self.fzf.execute_fzf(multi_select=True, print_col=0) self.assertEqual(result, ["hello world", "foo boo"]) @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_check_ctrl_c(self, mocked_output, mocked_popen): mocked_output.return_value = b"ctrl-c" self.assertRaises(KeyboardInterrupt, self.fzf.execute_fzf) mocked_output.return_value = b"hello world" try: result = self.fzf.execute_fzf() self.assertEqual(result, "world") except: self.fail("ctrl-c test failed, unexpected exception raise") @patch("fzfaws.utils.Pyfzf._check_fd") @patch.object(subprocess, "Popen") @patch.object(subprocess, "check_output") def test_get_local_file(self, mocked_output, mocked_popen, mocked_check): mocked_check.return_value = False mocked_output.return_value = b"" self.assertRaises(NoSelectionMade, self.fzf.get_local_file) mocked_popen.assert_called_with("find * -type f", shell=True, stderr=ANY, stdout=ANY) mocked_output.return_value = b"hello" result = self.fzf.get_local_file() self.assertEqual("hello", result) mocked_output.return_value = b"hello" result = self.fzf.get_local_file(multi_select=True) self.assertEqual(result, ["hello"]) mocked_output.return_value = b"hello\nworld\n" result = self.fzf.get_local_file(multi_select=True) self.assertEqual(result, ["hello", "world"]) result = self.fzf.get_local_file(directory=True, search_from_root=True) mocked_popen.assert_called_with( "echo \033[33m./\033[0m; find * -type d", shell=True, stderr=ANY, stdout=ANY) result = self.fzf.get_local_file(cloudformation=True) mocked_popen.assert_called_with( 'find * -type f -name "*.json" -o -name "*.yaml" -o -name "*.yml"', shell=True, stderr=ANY, stdout=ANY, ) mocked_output.reset_mock() mocked_check.return_value = True result = self.fzf.get_local_file(cloudformation=True, header="hello") mocked_popen.assert_called_with( "fd --type f --regex '(yaml|yml|json)$'", shell=True, stderr=ANY, stdout=ANY, ) mocked_output.assert_called_once() result = self.fzf.get_local_file(directory=True) mocked_popen.assert_called_with( "echo \033[33m./\033[0m; fd --type d", shell=True, stderr=ANY, stdout=ANY, ) result = self.fzf.get_local_file() mocked_popen.assert_called_with( "fd --type f", shell=True, stderr=ANY, stdout=ANY, ) @patch("fzfaws.utils.pyfzf.subprocess") def test_check_fd(self, mocked_subprocess): mocked_subprocess.run.return_value = True result = self.fzf._check_fd() self.assertEqual(result, True) mocked_subprocess.run.side_effect = Exception( subprocess.CalledProcessError) result = self.fzf._check_fd() self.assertEqual(result, False) def test_process_list(self): self.fzf.fzf_string = "" self.assertRaises(EmptyList, self.fzf.process_list, [], "123") self.fzf.fzf_string = "" self.fzf.process_list([], "123", "asfasd", "bbbb", empty_allow=True) test_list = [{"foo": 1, "boo": 2}, {"foo": "b"}] self.fzf.process_list(test_list, "foo") self.assertEqual(self.fzf.fzf_string, "foo: 1\nfoo: b\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "boo") self.assertEqual(self.fzf.fzf_string, "boo: 2\nboo: None\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "www") self.assertEqual(self.fzf.fzf_string, "www: None\nwww: None\n") self.fzf.fzf_string = "" self.fzf.process_list(test_list, "foo", "boo") self.assertEqual(self.fzf.fzf_string, "foo: 1 | boo: 2\nfoo: b | boo: None\n") @patch.object(Pyfzf, "execute_fzf") def test_format_selected_to_dict(self, mocked_execute): mocked_execute.return_value = "foo: 1 | boo: 2 | wtf: None" result = str(self.fzf.execute_fzf(print_col=0)) result = self.fzf.format_selected_to_dict(result) self.assertEqual(result, {"boo": "2", "foo": "1", "wtf": None})