Skip to content

Commit fbbb55f

Browse files
authored
tests,fix(cli): Fix loading of multiple workspace files (#838)
Fixes #837
2 parents 73ffddc + fff6f4f commit fbbb55f

File tree

4 files changed

+225
-25
lines changed

4 files changed

+225
-25
lines changed

conftest.py

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from libtmux.test import namer
1717
from tests.fixtures import utils as test_utils
18+
from tmuxp.cli.utils import get_config_dir
1819

1920
if t.TYPE_CHECKING:
2021
from libtmux.session import Session
@@ -40,6 +41,24 @@ def home_path_default(monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path)
4041
monkeypatch.setenv("HOME", str(user_path))
4142

4243

44+
@pytest.fixture
45+
def tmuxp_configdir(user_path: pathlib.Path) -> pathlib.Path:
46+
xdg_config_dir = user_path / ".config"
47+
xdg_config_dir.mkdir(exist_ok=True)
48+
49+
tmuxp_configdir = xdg_config_dir / "tmuxp"
50+
tmuxp_configdir.mkdir(exist_ok=True)
51+
return tmuxp_configdir
52+
53+
54+
@pytest.fixture
55+
def tmuxp_configdir_default(
56+
monkeypatch: pytest.MonkeyPatch, tmuxp_configdir: pathlib.Path
57+
) -> None:
58+
monkeypatch.setenv("TMUXP_CONFIGDIR", str(tmuxp_configdir))
59+
assert get_config_dir() == str(tmuxp_configdir)
60+
61+
4362
@pytest.fixture(scope="function")
4463
def monkeypatch_plugin_test_packages(monkeypatch: pytest.MonkeyPatch) -> None:
4564
paths = [

src/tmuxp/cli/load.py

+34-25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from libtmux.common import has_gte_version
1717
from libtmux.server import Server
1818
from libtmux.session import Session
19+
from tmuxp.types import StrPath
1920

2021
from .. import config, config_reader, exc, log, util
2122
from ..workspacebuilder import WorkspaceBuilder
@@ -29,13 +30,17 @@
2930
)
3031

3132
if t.TYPE_CHECKING:
32-
from typing_extensions import Literal, TypeAlias
33+
from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict
3334

3435
CLIColorsLiteral: TypeAlias = Literal[56, 88]
3536

37+
class OptionOverrides(TypedDict):
38+
detached: NotRequired[bool]
39+
new_session_name: NotRequired[t.Optional[str]]
40+
3641

3742
class CLILoadNamespace(argparse.Namespace):
38-
config_file: str
43+
config_files: t.List[str]
3944
socket_name: t.Optional[str]
4045
socket_path: t.Optional[str]
4146
tmux_config_file: t.Optional[str]
@@ -251,7 +256,7 @@ def _setup_plugins(builder: WorkspaceBuilder) -> Session:
251256

252257

253258
def load_workspace(
254-
config_file: t.Union[pathlib.Path, str],
259+
config_file: StrPath,
255260
socket_name: t.Optional[str] = None,
256261
socket_path: None = None,
257262
tmux_config_file: None = None,
@@ -266,8 +271,8 @@ def load_workspace(
266271
267272
Parameters
268273
----------
269-
config_file : str
270-
absolute path to config file
274+
config_file : list of str
275+
paths or session names to workspace files
271276
socket_name : str, optional
272277
``tmux -L <socket-name>``
273278
socket_path: str, optional
@@ -356,7 +361,7 @@ def load_workspace(
356361
Accessed April 8th, 2018.
357362
"""
358363
# get the canonical path, eliminating any symlinks
359-
if isinstance(config_file, str):
364+
if isinstance(config_file, (str, os.PathLike)):
360365
config_file = pathlib.Path(config_file)
361366

362367
tmuxp_echo(
@@ -486,8 +491,9 @@ def config_file_completion(ctx, params, incomplete):
486491

487492

488493
def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
489-
config_file = parser.add_argument(
490-
"config_file",
494+
config_files = parser.add_argument(
495+
"config_files",
496+
nargs="+",
491497
metavar="config-file",
492498
help="filepath to session or filename of session if in tmuxp config directory",
493499
)
@@ -568,7 +574,7 @@ def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentP
568574
try:
569575
import shtab
570576

571-
config_file.complete = shtab.FILE # type: ignore
577+
config_files.complete = shtab.FILE # type: ignore
572578
tmux_config_file.complete = shtab.FILE # type: ignore
573579
log_file.complete = shtab.FILE # type: ignore
574580
except ImportError:
@@ -623,27 +629,30 @@ def command_load(
623629
"append": args.append,
624630
}
625631

626-
if args.config_file is None:
632+
if args.config_files is None or len(args.config_files) == 0:
627633
tmuxp_echo("Enter at least one config")
628634
if parser is not None:
629635
parser.print_help()
630636
sys.exit()
637+
return
631638

632-
config_file = scan_config(args.config_file, config_dir=get_config_dir())
639+
last_idx = len(args.config_files) - 1
640+
original_detached_option = tmux_options.pop("detached")
641+
original_new_session_name = tmux_options.pop("new_session_name")
633642

634-
if isinstance(config_file, str):
635-
load_workspace(config_file, **tmux_options)
636-
elif isinstance(config_file, tuple):
637-
config = list(config_file)
638-
# Load each configuration but the last to the background
639-
for cfg in config[:-1]:
640-
opt = tmux_options.copy()
641-
opt.update({"detached": True, "new_session_name": None})
642-
load_workspace(cfg, **opt)
643+
for idx, config_file in enumerate(args.config_files):
644+
config_file = scan_config(config_file, config_dir=get_config_dir())
643645

644-
# todo: obey the -d in the cli args only if user specifies
645-
load_workspace(config_file[-1], **tmux_options)
646-
else:
647-
raise NotImplementedError(
648-
f"config {type(config_file)} with {config_file} not valid"
646+
detached = original_detached_option
647+
new_session_name = original_new_session_name
648+
649+
if last_idx > 0 and idx < last_idx:
650+
detached = True
651+
new_session_name = None
652+
653+
load_workspace(
654+
config_file,
655+
detached=detached,
656+
new_session_name=new_session_name,
657+
**tmux_options,
649658
)

src/tmuxp/types.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Internal :term:`type annotations <annotation>`
2+
3+
Notes
4+
-----
5+
6+
:class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_.
7+
8+
.. _typeshed's: https://github.com/python/typeshed/blob/9687d5/stdlib/_typeshed/__init__.pyi#L98
9+
""" # NOQA E501
10+
from os import PathLike
11+
from typing import Union
12+
13+
StrPath = Union[str, "PathLike[str]"]
14+
""":class:`os.PathLike` or :class:`str`"""

tests/test_cli.py

+158
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,164 @@ def test_load_symlinked_workspace(
433433
assert pane.current_path == str(realtemp)
434434

435435

436+
if t.TYPE_CHECKING:
437+
from typing_extensions import TypeAlias
438+
439+
ExpectedOutput: TypeAlias = t.Optional[t.Union[str, t.List[str]]]
440+
441+
442+
class CLILoadFixture(t.NamedTuple):
443+
test_id: str
444+
cli_args: t.List[t.Union[str, t.List[str]]]
445+
config_paths: t.List[str]
446+
session_names: t.List[str]
447+
expected_exit_code: int
448+
expected_in_out: "ExpectedOutput" = None
449+
expected_not_in_out: "ExpectedOutput" = None
450+
expected_in_err: "ExpectedOutput" = None
451+
expected_not_in_err: "ExpectedOutput" = None
452+
453+
454+
TEST_LOAD_FIXTURES = [
455+
CLILoadFixture(
456+
test_id="dir-relative-dot-samedir",
457+
cli_args=["load", "."],
458+
config_paths=["{tmp_path}/.tmuxp.yaml"],
459+
session_names=["my_config"],
460+
expected_exit_code=0,
461+
expected_in_out=None,
462+
expected_not_in_out=None,
463+
),
464+
CLILoadFixture(
465+
test_id="dir-relative-dot-slash-samedir",
466+
cli_args=["load", "./"],
467+
config_paths=["{tmp_path}/.tmuxp.yaml"],
468+
session_names=["my_config"],
469+
expected_exit_code=0,
470+
expected_in_out=None,
471+
expected_not_in_out=None,
472+
),
473+
CLILoadFixture(
474+
test_id="dir-relative-file-samedir",
475+
cli_args=["load", "./.tmuxp.yaml"],
476+
config_paths=["{tmp_path}/.tmuxp.yaml"],
477+
session_names=["my_config"],
478+
expected_exit_code=0,
479+
expected_in_out=None,
480+
expected_not_in_out=None,
481+
),
482+
CLILoadFixture(
483+
test_id="filename-relative-file-samedir",
484+
cli_args=["load", "./my_config.yaml"],
485+
config_paths=["{tmp_path}/my_config.yaml"],
486+
session_names=["my_config"],
487+
expected_exit_code=0,
488+
expected_in_out=None,
489+
expected_not_in_out=None,
490+
),
491+
CLILoadFixture(
492+
test_id="configdir-session-name",
493+
cli_args=["load", "my_config"],
494+
config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"],
495+
session_names=["my_config"],
496+
expected_exit_code=0,
497+
expected_in_out=None,
498+
expected_not_in_out=None,
499+
),
500+
CLILoadFixture(
501+
test_id="configdir-absolute",
502+
cli_args=["load", "~/.config/tmuxp/my_config.yaml"],
503+
config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"],
504+
session_names=["my_config"],
505+
expected_exit_code=0,
506+
expected_in_out=None,
507+
expected_not_in_out=None,
508+
),
509+
#
510+
# Multiple configs
511+
#
512+
CLILoadFixture(
513+
test_id="configdir-session-name-double",
514+
cli_args=["load", "my_config", "second_config"],
515+
config_paths=[
516+
"{TMUXP_CONFIGDIR}/my_config.yaml",
517+
"{TMUXP_CONFIGDIR}/second_config.yaml",
518+
],
519+
session_names=["my_config", "second_config"],
520+
expected_exit_code=0,
521+
expected_in_out=None,
522+
expected_not_in_out=None,
523+
),
524+
]
525+
526+
527+
@pytest.mark.parametrize(
528+
list(CLILoadFixture._fields),
529+
TEST_LOAD_FIXTURES,
530+
ids=[test.test_id for test in TEST_LOAD_FIXTURES],
531+
)
532+
@pytest.mark.usefixtures("tmuxp_configdir_default")
533+
def test_load(
534+
tmp_path: pathlib.Path,
535+
tmuxp_configdir: pathlib.Path,
536+
server: "Server",
537+
session: Session,
538+
capsys: pytest.CaptureFixture,
539+
monkeypatch: pytest.MonkeyPatch,
540+
test_id: str,
541+
cli_args: t.List[str],
542+
config_paths: t.List[str],
543+
session_names: t.List[str],
544+
expected_exit_code: int,
545+
expected_in_out: "ExpectedOutput",
546+
expected_not_in_out: "ExpectedOutput",
547+
expected_in_err: "ExpectedOutput",
548+
expected_not_in_err: "ExpectedOutput",
549+
) -> None:
550+
assert server.socket_name is not None
551+
552+
monkeypatch.chdir(tmp_path)
553+
for session_name, config_path in zip(session_names, config_paths):
554+
tmuxp_config = pathlib.Path(
555+
config_path.format(tmp_path=tmp_path, TMUXP_CONFIGDIR=tmuxp_configdir)
556+
)
557+
tmuxp_config.write_text(
558+
"""
559+
session_name: {session_name}
560+
windows:
561+
- window_name: test
562+
panes:
563+
-
564+
""".format(
565+
session_name=session_name
566+
),
567+
encoding="utf-8",
568+
)
569+
570+
try:
571+
cli.cli([*cli_args, "-d", "-L", server.socket_name, "-y"])
572+
except SystemExit:
573+
pass
574+
575+
result = capsys.readouterr()
576+
output = "".join(list(result.out))
577+
578+
if expected_in_out is not None:
579+
if isinstance(expected_in_out, str):
580+
expected_in_out = [expected_in_out]
581+
for needle in expected_in_out:
582+
assert needle in output
583+
584+
if expected_not_in_out is not None:
585+
if isinstance(expected_not_in_out, str):
586+
expected_not_in_out = [expected_not_in_out]
587+
for needle in expected_not_in_out:
588+
assert needle not in output
589+
590+
for session_name in session_names:
591+
assert server.has_session(session_name)
592+
593+
436594
def test_regression_00132_session_name_with_dots(
437595
tmp_path: pathlib.Path,
438596
server: "Server",

0 commit comments

Comments
 (0)