Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use configdir for main script #79

Merged
merged 8 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/python/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[python]
[root]
split = "horizontal"
children = ['terminals', 'documentation']
sizes = [50, 50]
Expand Down
2 changes: 1 addition & 1 deletion examples/rezide-documentation/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[rezide-documentation]
[root]
split = "horizontal"
children = ['autocompiler', 'plaintext', 'rendered-pdf']
sizes = [20, 40, 40]
Expand Down
2 changes: 1 addition & 1 deletion examples/rezide-ide/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[rezide-ide]
[root]
split = "horizontal"
children = ['linters', 'main', 'tests']
sizes = [20, 60, 20]
Expand Down
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ flake8-annotations = "^2.6.2"
codecov = "^2.1.11"
pytest = "^6"
pytest-mock = "^3.6.1"
pyfakefs = "^4.5.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down Expand Up @@ -69,5 +70,5 @@ show_error_codes = true

[[tool.mypy.overrides]]
# ignore these modules because there are no type stubs available
module = ["i3ipc"]
module = ["i3ipc", 'pyfakefs']
ignore_missing_imports = true
10 changes: 5 additions & 5 deletions src/rezide/rezide.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ def main(
def open(context: click.Context, layout_name: str) -> None:
"""Open the IDE of your choice"""
context.obj: Dict[str, Any] # type: ignore[misc]
config_reader = config_readers.TomlReader(
filestore.LocalFilestore(), env=context.obj["env"]
)
parser = config_parser.ConfigParser(config_reader, tree.TreeFactory())
config_directory = context.obj["config_dir"]
config_file_path = config_directory.get_layout_file_path(layout_name)
config_reader = config_readers.TomlReader(filestore.LocalFilestore())
config_dict = config_reader.read(config_file_path)
parser = config_parser.ConfigParser(config_dict, tree.TreeFactory())
window_manager = sway.Sway()
layout = layouts.LayoutManager(parser, window_manager)
application = Rezide(context.obj["env"], layout)
Expand Down Expand Up @@ -119,5 +120,4 @@ def __init__(
logging.debug(f"Env is {env}")

def run(self, layout_name: str) -> None:
self._layout.select(layout_name)
self._layout.spawn_windows()
22 changes: 16 additions & 6 deletions src/rezide/utils/config_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,27 @@ def _select_config_dir(self, env: dtos.Env, specified_dir: Optional[str]) -> Non

def list_layouts(self) -> Set[str]:
"""List all available layouts in the config directory"""
files_in_directory = self._filestore.list_directory_contents(self._dir)
filenames_without_extensions = {
os.path.splitext(filename)[0] for filename in files_in_directory
}
return filenames_without_extensions
logging.info(f"listing layouts in {self._dir}")
layouts = set()
for file_or_dir in self._filestore.list_directory_contents(self._dir):
logging.debug(f"examining {file_or_dir} to see if it has a config")
absolute_path_to_dir = os.path.join(self._dir, file_or_dir)
absolute_path_to_config_file = os.path.join(
absolute_path_to_dir, "config.toml"
)
is_dir = self._filestore.exists_as_dir(absolute_path_to_dir)
has_config_file = self._filestore.exists_as_file(
absolute_path_to_config_file
)
if is_dir and has_config_file:
layouts.add(file_or_dir)
return layouts

def get_layout_file_path(self, layout_name: str) -> str:
"""Given a name of a layout, check for a matching toml file in the config directory
and return its absolute file path if it exists
"""
file_path = os.path.join(self._dir, layout_name + ".toml")
file_path = os.path.join(self._dir, layout_name, "config.toml")
if not self._filestore.exists_as_file(file_path):
raise RuntimeError(f"Layout '{layout_name}' doesn't exist in '{self._dir}'")
return file_path
27 changes: 11 additions & 16 deletions src/rezide/utils/config_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Dict, Set
from typing import Dict

from rezide.utils import interfaces

Expand All @@ -9,31 +9,27 @@ class ConfigParser(interfaces.ConfigParserInterface):

def __init__(
self,
config_reader: interfaces.ConfigReader,
config_dict: Dict,
tree_factory: interfaces.TreeFactoryInterface,
) -> None:
self._tree_factory = tree_factory
self._layout_definitions = config_reader.read()
self._layout_definitions = config_dict

def validate(self) -> None:
seen_marks: Set[str] = set()
for definition_name, definition_body in self._layout_definitions.items():
if "command" in definition_body and "mark" in definition_body:
if definition_body["mark"] in seen_marks:
raise RuntimeError(
f"There are multiple windows with mark \"{definition_body['mark']}\""
)
if "command" in definition_body:
self._validate_window(definition_name, definition_body)
seen_marks.add(definition_body["mark"])
elif "children" in definition_body:
self._validate_section(definition_name, definition_body)
else:
logging.error(definition_body)
raise RuntimeError("This definition is not a Window or Section")
raise RuntimeError(
f"This definition is not a Window or Section: {definition_name}"
)

def _validate_window(self, definition_name: str, definition_body: Dict) -> None:
if len(definition_body) != 2:
raise RuntimeError("Window must only define command and mark")
if len(definition_body) != 1:
raise RuntimeError(f"Window must only define command: {definition_name}")

def _validate_section(self, definition_name: str, definition_body: Dict) -> None:
keys = set(definition_body.keys())
Expand Down Expand Up @@ -61,12 +57,11 @@ def _validate_section(self, definition_name: str, definition_body: Dict) -> None
+ f" {definition_name}"
)

def get_tree(self, layout_name: str) -> interfaces.TreeNodeInterface:
def get_tree(self) -> interfaces.TreeNodeInterface:
"""Parse and validate the layout, stitch together the node definitions, and
create a tree out of them.
"""
layout_top_definition = self._layout_definitions[layout_name]
layout_top_definition["mark"] = layout_name
layout_top_definition = self._layout_definitions["root"]
root_node = self._construct_subtree(layout_top_definition)
return self._tree_factory.create_tree(root_node)

Expand Down
32 changes: 6 additions & 26 deletions src/rezide/utils/config_readers.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
import logging
import os
from typing import Dict

import toml

from rezide.utils import dtos
from rezide.utils import interfaces


class TomlReader(interfaces.ConfigReader):
def __init__(self, filestore: interfaces.FileStore, env: dtos.Env) -> None:
paths_to_check = []
if env.xdg_config_home:
path = os.path.join(env.xdg_config_home, "rezide", "config.toml")
paths_to_check.append(path)
else:
path = os.path.join(env.home, ".rezide.toml")
paths_to_check.append(path)
for path in paths_to_check:
logging.debug(f"checking {path}")
if filestore.path_exists(path):
target_path = path
logging.debug(f"found config at {path}")
break
else:
raise RuntimeError(
'Could not find config file at "$XDG_CONFIG_HOME/rezide/config.toml"'
+ 'or "$HOME/.rezide.toml"'
)
toml_str = filestore.read_file(target_path)
self._dict = dict(toml.loads(toml_str))
def __init__(self, filestore: interfaces.FileStore) -> None:
self._filestore = filestore

def read(self) -> Dict:
return self._dict
def read(self, path: str) -> Dict:
"""Convert a TOML file at `path` to a python Dict"""
toml_str = self._filestore.read_file(path)
return dict(toml.loads(toml_str))
4 changes: 2 additions & 2 deletions src/rezide/utils/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def get_window_sizes(self) -> Dict:

class ConfigReader(object):
@abc.abstractmethod
def read(self) -> Dict:
def read(self, path: str) -> Dict:
pass


Expand Down Expand Up @@ -125,7 +125,7 @@ def validate(self) -> None:
pass

@abc.abstractmethod
def get_tree(self, layout_name: str) -> TreeNodeInterface:
def get_tree(self) -> TreeNodeInterface:
pass


Expand Down
15 changes: 4 additions & 11 deletions src/rezide/utils/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,12 @@ def __init__(
window_manager: interfaces.TilingWindowManager,
) -> None:
self._window_manager = window_manager
self._config_parser = config_parser
# make sure that our configuration is valid
self._config_parser.validate()
self._layout_has_been_selected = False

def select(self, layout_name: str) -> None:
selected_tree = self._config_parser.get_tree(layout_name)
self._layout_has_been_selected = True
self._selected_layout = Layout(selected_tree)
config_parser.validate()
tree = config_parser.get_tree()
self._layout = Layout(tree)

def spawn_windows(self) -> None:
if not self._layout_has_been_selected:
raise RuntimeError("No layout selected")
logging.debug(
f"{self._window_manager.num_workspace_windows} windows"
+ " are open in the current workspace"
Expand All @@ -38,7 +31,7 @@ def spawn_windows(self) -> None:
"There are multiple windows open in the current workspace."
)
self._created_windows: Set[str] = set()
for window in self._selected_layout.zachstras_traversal():
for window in self._layout.zachstras_traversal():
if window.is_parent:
self._window_manager.split_and_mark_parent(window.data, "abc")
elif window.data.mark in self._created_windows:
Expand Down
57 changes: 17 additions & 40 deletions tests/fakes.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,36 @@
import logging
import os
import pathlib
from typing import Dict, List, NamedTuple, Optional, Set, Type

from pyfakefs import fake_filesystem

from rezide.utils import dtos
from rezide.utils import interfaces
from rezide.utils import tree


class FakeFilestore(interfaces.FileStore):
def __init__(self, files: Dict[str, str]):
"""Initialize with a mapping from filenames to file contents

Add a key called "any" when you don't care about the specific file path
and just want the file to exist with those contents for all read actions
"""
self._files = files
# track the directory hierarchy.
# every directory above each given file path should be considered to exist
self._directories = set()
for file_path in files:
path = pathlib.Path(file_path)
for parent in path.parents:
self._directories.add(str(parent))
logging.debug(self._directories)
"""Initialize a fake filestore with a mapping from filenames to file contents"""
self._filesystem = fake_filesystem.FakeFilesystem()
self._os_module = fake_filesystem.FakeOsModule(self._filesystem)
self._open = fake_filesystem.FakeFileOpen(self._filesystem)
for file_path, contents in files.items():
self._filesystem.create_file(file_path, contents=contents)

def path_exists(self, path: str) -> bool:
path = path.rstrip("/")
if "any" in self._files:
return True
return path in self._files or path in self._directories
return self._os_module.path.exists(path)

def read_file(self, path: str) -> str:
path = path.rstrip("/")
if "any" in self._files:
return self._files["any"]
return self._files[path]
with self._open(path) as infile:
return infile.read()

def exists_as_dir(self, path: str) -> bool:
path = path.rstrip("/")
return path in self._directories
return self.path_exists(path) and self._os_module.path.isdir(path)

def exists_as_file(self, path: str) -> bool:
path = path.rstrip("/")
return path in self._files
return self.path_exists(path) and self._os_module.path.isfile(path)

def list_directory_contents(self, path: str) -> Set[str]:
# remove a trailing slash
path = path.rstrip("/")
files_in_directory = set()
for file_ in self._files:
directory_name, file_name = os.path.split(file_)
if directory_name == path:
files_in_directory.add(file_name)
return files_in_directory
return set(self._os_module.listdir(path))


class FakeTreeFactory(interfaces.TreeFactoryInterface):
Expand All @@ -68,7 +45,7 @@ class FakeConfig(interfaces.ConfigReader):
def __init__(self, config_dict: Dict) -> None:
self._config_dict = config_dict

def read(self) -> Dict:
def read(self, path: str) -> Dict:
return self._config_dict


Expand All @@ -84,8 +61,8 @@ def validate(self) -> None:
if self._validation_error is not None:
raise self._validation_error

def get_tree(self, layout_name: str) -> interfaces.TreeNodeInterface:
return self._tree_factory.create_tree(self._config_dict[layout_name])
def get_tree(self) -> interfaces.TreeNodeInterface:
return self._tree_factory.create_tree(self._config_dict)


class FakeRect(NamedTuple):
Expand Down
Loading