Skip to content

Commit

Permalink
run: Support multiple .zuliprc files.
Browse files Browse the repository at this point in the history
Add optional positional argument for organization name.
Allow changing alias name.
Update error messages.

Tests updated.
Docs not yet updated.
  • Loading branch information
Niloth-p committed May 21, 2024
1 parent 47c34fd commit 11d7e73
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 27 deletions.
12 changes: 7 additions & 5 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
100 changes: 78 additions & 22 deletions zulipterminal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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')}"
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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 = {
Expand All @@ -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:
Expand All @@ -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."
)

Expand All @@ -402,6 +448,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.
Expand Down Expand Up @@ -440,10 +490,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:"
Expand All @@ -452,7 +505,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 (
Expand Down Expand Up @@ -528,7 +581,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(
Expand Down Expand Up @@ -577,7 +633,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,
Expand Down

0 comments on commit 11d7e73

Please sign in to comment.