From 9a5dc7fe0ad066543112a52e0f9bde91610bf03d Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Tue, 21 May 2024 13:05:43 +0530 Subject: [PATCH] run: Support multiple .zuliprc files. Add optional positional argument for organization name. Allow changing alias name. Update error messages. Tests updated. Docs not yet updated. --- tests/cli/test_run.py | 12 +++-- zulipterminal/cli/run.py | 100 ++++++++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 84dc2f8ddb..504d8d0f6e 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -374,9 +374,10 @@ def unreadable_dir(tmp_path: Path) -> Generator[Tuple[Path, Path], None, None]: "path_to_use, expected_exception", [ ("unreadable", "PermissionError"), - ("goodnewhome", "FileNotFoundError"), ], - ids=["valid_path_but_cannot_be_written_to", "path_does_not_exist"], + ids=[ + "valid_path_but_cannot_be_written_to", + ], ) def test_main_cannot_write_zuliprc_given_good_credentials( monkeypatch: pytest.MonkeyPatch, @@ -406,8 +407,9 @@ def test_main_cannot_write_zuliprc_given_good_credentials( expected_line = ( "\x1b[91m" - f"{expected_exception}: .zuliprc could not be created " - f"at {Path(zuliprc_path) / '.config' / 'zulip' / '.zuliprc'}" + f"{expected_exception}: " + f"{Path(zuliprc_path) / '.config' / 'zulip' / '.zuliprc'}" + " could not be created." "\x1b[0m" ) assert lines[-1] == expected_line @@ -587,7 +589,7 @@ def test__write_zuliprc__fail_file_exists( error_message = _write_zuliprc(path, api_key=key, server_url=url, login_id=id) - assert error_message == ".zuliprc already exists at " + str(path) + assert error_message == f"{path} already exists." @pytest.mark.parametrize( diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 73b20245fa..970be521ee 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -120,6 +120,15 @@ def parse_args(argv: List[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description=description, formatter_class=formatter_class ) + parser.add_argument( + "org-name", + nargs="?", + action="store", + default="", + help="Enter a unique name for your zulip organization profile. " + "It can be an acronym or a nickname. This will serve " + "as a local alias for your organization's configuration profile.", + ) parser.add_argument( "-v", "--version", @@ -132,7 +141,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace: "-c", action="store", help="config file downloaded from your zulip " - "organization (default: ~/.config/zulip/.zuliprc)", + "organization (default: ~/.config/zulip/org-name.zuliprc)", ) parser.add_argument( "--theme", @@ -265,9 +274,9 @@ def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: return None -def fetch_zuliprc(zuliprc_path: Path) -> None: +def fetch_zuliprc(zuliprc_path: Path, org_name: str) -> Path: print( - f"{in_color('red', f'.zuliprc file was not found at {zuliprc_path}')}" + f"{in_color('red', f'{zuliprc_path} was not found.')}" f"\nPlease enter your credentials to login into your Zulip organization." f"\n" f"\nNOTE: The {in_color('blue', 'Zulip server URL')}" @@ -292,6 +301,9 @@ def fetch_zuliprc(zuliprc_path: Path) -> None: login_data = get_api_key(realm_url) preferred_realm_url, login_id, api_key = login_data + if org_name == "": + zuliprc_path = default_zuliprc_path() + zuliprc_path = _prompt_org_name_change(zuliprc_path, org_name) save_zuliprc_failure = _write_zuliprc( zuliprc_path, login_id=login_id, @@ -302,32 +314,60 @@ def fetch_zuliprc(zuliprc_path: Path) -> None: print(f"Generated API key saved at {zuliprc_path}") else: exit_with_error(save_zuliprc_failure) + return zuliprc_path + + +def _prompt_org_name_change(zuliprc_path: Path, org_name: str) -> Path: + if org_name == "": + update_org_name = styled_input( + "Do you wish to assign an alias to refer to this server? [y/N] " + ) + else: + update_org_name = styled_input( + f"You have set the alias '{zuliprc_path.stem}' for this server." + f" Do you wish to use a different alias? [y/N] " + ) + if update_org_name.lower() in ["y", "yes"]: + new_org_name = styled_input("Enter new alias: ") + zuliprc_path = default_zuliprc_path(new_org_name) + return zuliprc_path def _write_zuliprc( - to_path: Path, *, login_id: str, api_key: str, server_url: str + to_path: Path, + *, + login_id: Optional[str] = None, + api_key: Optional[str] = None, + server_url: Optional[str] = None, + file_contents: Optional[str] = None, ) -> str: """ Writes a .zuliprc file, returning a non-empty error string on failure Only creates new private files; errors if file already exists """ try: + to_path.parent.mkdir(parents=True, exist_ok=True) with open( os.open(to_path, os.O_CREAT | os.O_WRONLY | os.O_EXCL, 0o600), "w" ) as f: - f.write(f"[api]\nemail={login_id}\nkey={api_key}\nsite={server_url}") + if file_contents is not None: + f.write(file_contents) + else: + f.write(f"[api]\nemail={login_id}\nkey={api_key}\nsite={server_url}") return "" except FileExistsError: - return f".zuliprc already exists at {to_path}" + return f"{to_path} already exists." except OSError as ex: - return f"{ex.__class__.__name__}: .zuliprc could not be created at {to_path}" + return f"{ex.__class__.__name__}: {to_path} could not be created." -def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]: - zuliprc_path = Path(zuliprc_str).expanduser() +def parse_zuliprc( + zuliprc_path: Path, org_name: str +) -> Tuple[Dict[str, SettingData], Path]: + zuliprc_path = zuliprc_path.expanduser() while not path.exists(zuliprc_path): try: - fetch_zuliprc(zuliprc_path) + zuliprc_path = fetch_zuliprc(zuliprc_path, org_name) # Invalid user inputs (e.g. pressing arrow keys) may cause ValueError except (OSError, ValueError): # Remove zuliprc file if created. @@ -345,13 +385,15 @@ def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]: print( in_color( "red", - "ERROR: Please ensure your .zuliprc is NOT publicly accessible:\n" + "ERROR: Please ensure your {2} is NOT publicly accessible:\n" " {0}\n" "(it currently has permissions '{1}')\n" "This can often be achieved with a command such as:\n" " chmod og-rwx {0}\n" - "Consider regenerating the [api] part of your .zuliprc to ensure " - "your account is secure.".format(zuliprc_path, stat.filemode(mode)), + "Consider regenerating the [api] part of your {2} to ensure " + "your account is secure.".format( + zuliprc_path, stat.filemode(mode), zuliprc_path.name + ), ) ) sys.exit(1) @@ -361,9 +403,13 @@ def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]: try: res = zuliprc.read(zuliprc_path) if len(res) == 0: - exit_with_error(f"Could not access .zuliprc file at {zuliprc_path}") + exit_with_error( + f"Could not access {zuliprc_path.name} file at {zuliprc_path.parent}" + ) except configparser.MissingSectionHeaderError: - exit_with_error(f"Failed to parse .zuliprc file at {zuliprc_path}") + exit_with_error( + f"Failed to parse {zuliprc_path.name} file at {zuliprc_path.parent}" + ) # Initialize with default settings settings = { @@ -376,7 +422,7 @@ def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]: for conf in config: settings[conf] = SettingData(config[conf], ConfigSource.ZULIPRC) - return settings + return settings, zuliprc_path def list_themes() -> str: @@ -388,7 +434,7 @@ def list_themes() -> str: suffix += "[default theme]" text += f" {theme} {suffix}\n" return text + ( - "Specify theme in .zuliprc file or override " + "Specify theme in a .zuliprc file or override " "using -t/--theme options on command line." ) @@ -401,6 +447,10 @@ def xdg_config_home() -> Path: return Path.home() / ".config" +def default_zuliprc_path(org_name: Optional[str] = "") -> Path: + return xdg_config_home() / "zulip" / f"{org_name}.zuliprc" + + def main(options: Optional[List[str]] = None) -> None: """ Launch Zulip Terminal. @@ -439,10 +489,13 @@ def main(options: Optional[List[str]] = None) -> None: print(list_themes()) sys.exit(0) + org_name = getattr(args, "org-name") if args.config_file: - zuliprc_path = args.config_file + if org_name != "": + exit_with_error("Cannot use --config-file and org-name together") + zuliprc_path = Path(args.config_file) else: - zuliprc_path = xdg_config_home() / "zulip" / ".zuliprc" + zuliprc_path = default_zuliprc_path(org_name) print( "Detected:" @@ -451,7 +504,7 @@ def main(options: Optional[List[str]] = None) -> None: ) try: - zterm = parse_zuliprc(zuliprc_path) + zterm, zuliprc_path = parse_zuliprc(zuliprc_path, org_name) ### Validate footlinks settings (not from command line) if ( @@ -527,7 +580,10 @@ def main(options: Optional[List[str]] = None) -> None: helper_text = ( ["Valid values are:"] + [f" {option}" for option in valid_remaining_values] - + [f"Specify the {setting} option in .zuliprc file."] + + [ + f"Specify the {setting} option " + f"in {Path(zuliprc_path).name} file." + ] ) exit_with_error( "Invalid {} setting '{}' was specified {}.".format( @@ -576,7 +632,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: boolean_settings[setting] = zterm[setting].value == valid_boolean_values[0] Controller( - config_file=zuliprc_path, + config_file=str(zuliprc_path), maximum_footlinks=maximum_footlinks, theme_name=theme_to_use.value, theme=theme_data,