feat: move edit_note function to a separate file
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
cătălin 2022-08-28 12:12:31 +02:00
commit 75d623a124
Signed by: catalin
GPG key ID: 0178DF42F43E5FD2
10 changed files with 191 additions and 42 deletions

View file

@ -46,12 +46,13 @@ class Config(BaseSettings):
return {k: str(v) for k, v in values.items()} return {k: str(v) for k, v in values.items()}
def get_config(config_path: Path | None = DEFAULT_CONFIGURATION_PATH) -> Config: def get_config(config_path: Path = DEFAULT_CONFIGURATION_PATH) -> Config:
"""Init and retrieve a config object. If the `config_path` argument is not provided, """Init and retrieve a config object. If the `config_path` argument is not provided,
the default config initialization will be provided the default config initialization will be provided
Args: Args:
config_path (Path|None): The path to the config config_path (Path): The path to the config, defaults to
DEFAULT_CONFIGURATION_PATH
Returns: Returns:
a Config object a Config object
Raises: Raises:

29
halig/edit.py Normal file
View file

@ -0,0 +1,29 @@
from pathlib import Path
from tempfile import NamedTemporaryFile
from sh import age
from halig.config import Config
from halig.utils import wait_for_editor
def edit_note(note_path: Path, config: Config):
"""Edit an existing note. This method assumes that `note_path` exists, is
a fully-formed, absolute path an age file
Args:
note_path (Path): The path to the note to edit
config (Config): config object
"""
note_contents = age("-d", "-i", config.encryption_keys.private_key_path, note_path)
with NamedTemporaryFile(delete=False) as tmpfile:
tmpfile.write(str(note_contents).encode())
wait_for_editor(tmpfile.name)
age(
"-R",
str(config.encryption_keys.public_key_path),
tmpfile.name,
_out=str(note_path),
)
Path(tmpfile.name).unlink()

View file

@ -45,6 +45,11 @@ class NoteDoesNotExist(HaligError):
error_code = 5 error_code = 5
class CouldNotEditTempfile(HaligError):
msg = "Could not edit temporary file; original file was not replaced"
error_code = 6
def handle_errors(func): def handle_errors(func):
# TODO: parse age/sh errors # TODO: parse age/sh errors
@wraps(func) @wraps(func)

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typer import Typer from typer import Typer
from pathlib import Path from pathlib import Path
from typing import Optional
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from datetime import datetime from datetime import datetime
from sh import age from sh import age
@ -16,10 +15,10 @@ import os
import yaml import yaml
from halig.exceptions import handle_errors from halig.exceptions import handle_errors
from halig import logger from halig import logger
from halig.utils import wait_for_editor
app = Typer(pretty_exceptions_enable=False) app = Typer(pretty_exceptions_enable=False)
TODAY = f"{datetime.now():%Y-%m-%d}" TODAY = f"{datetime.now():%Y-%m-%d}"
TODAY_NOTE = f"{TODAY}.norg.age" TODAY_NOTE = f"{TODAY}.norg.age"
DEFAULT_NEORG_METADATA = f"""@document.meta DEFAULT_NEORG_METADATA = f"""@document.meta
@ -36,10 +35,6 @@ version: 1.0.0
""" """
def _wait_for_editor(path):
subprocess.call([os.environ.get("EDITOR") or "vim", path])
def _edit_note(path: Path, config: Config): def _edit_note(path: Path, config: Config):
path = path.expanduser() path = path.expanduser()
if not path.exists(): if not path.exists():
@ -50,7 +45,7 @@ def _edit_note(path: Path, config: Config):
note_contents = age("-d", "-i", config.encryption_keys.private_key_path, path) note_contents = age("-d", "-i", config.encryption_keys.private_key_path, path)
with NamedTemporaryFile(delete=False) as tmpfile: with NamedTemporaryFile(delete=False) as tmpfile:
tmpfile.write(str(note_contents).encode()) tmpfile.write(str(note_contents).encode())
_wait_for_editor(tmpfile.name) wait_for_editor(tmpfile.name)
logger.info(f"encrypting updates into {path}") logger.info(f"encrypting updates into {path}")
age("-R", str(config.encryption_keys.public_key_path), tmpfile.name, _out=str(path)) age("-R", str(config.encryption_keys.public_key_path), tmpfile.name, _out=str(path))
Path(tmpfile.name).unlink() Path(tmpfile.name).unlink()
@ -103,7 +98,7 @@ contents with the default one"""
def notebooks( def notebooks(
print_files: bool = False, print_files: bool = False,
print_hidden: bool = False, print_hidden: bool = False,
configuration_path: Optional[Path] = None, configuration_path: Path = DEFAULT_CONFIGURATION_PATH,
): ):
"""Print notebooks and their contents, tree-style""" """Print notebooks and their contents, tree-style"""
config = get_config(configuration_path) config = get_config(configuration_path)
@ -114,7 +109,7 @@ def notebooks(
@app.command() @app.command()
@handle_errors @handle_errors
def edit(path: Path, configuration_path: Optional[Path] = None): def edit(path: Path, configuration_path: Path = DEFAULT_CONFIGURATION_PATH):
"""Edit a new or existing file by providing a path. Note that if only """Edit a new or existing file by providing a path. Note that if only
a dir is provided, an attempt to create or open `<dir>/<current date>.norg.age` a dir is provided, an attempt to create or open `<dir>/<current date>.norg.age`
will be made""" will be made"""

34
halig/utils.py Normal file
View file

@ -0,0 +1,34 @@
import os
import subprocess
from pathlib import Path
from halig import logger
from halig.exceptions import CouldNotEditTempfile
def resolve_path(path: Path) -> Path:
"""Resolve a path's relative attributes, like envvars, relative notations, etc
Args:
path(Path): The path to resolve
Returns:
Path: The resolved path
"""
path = Path("~/.config/halig/halig.yml").expanduser()
return path
def wait_for_editor(path):
"""Wait for $EDITOR to be opened and then manually closed by the user
Args:
path(Path): The filepath which $EDITOR should modify
Raises:
CouldNotEditTempfile if the subprocess call errors out
"""
try:
subprocess.call([os.environ.get("EDITOR", "vim"), path])
except subprocess.CalledProcessError as e:
logger.error(e.output)
raise CouldNotEditTempfile

22
poetry.lock generated
View file

@ -2,7 +2,7 @@
name = "atomicwrites" name = "atomicwrites"
version = "1.4.1" version = "1.4.1"
description = "Atomic file writes." description = "Atomic file writes."
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
@ -10,7 +10,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "attrs" name = "attrs"
version = "22.1.0" version = "22.1.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
@ -100,7 +100,7 @@ dev = ["pytest", "coverage", "coveralls"]
name = "iniconfig" name = "iniconfig"
version = "1.1.1" version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing" description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
@ -128,7 +128,7 @@ icu = ["PyICU (>=1.0.0)"]
name = "packaging" name = "packaging"
version = "21.3" version = "21.3"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -159,7 +159,7 @@ test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytes
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -171,7 +171,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "py" name = "py"
version = "1.11.0" version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@ -205,7 +205,7 @@ plugins = ["importlib-metadata"]
name = "pyparsing" name = "pyparsing"
version = "3.0.9" version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars" description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6.8" python-versions = ">=3.6.8"
@ -216,7 +216,7 @@ diagrams = ["railroad-diagrams", "jinja2"]
name = "pytest" name = "pytest"
version = "7.1.2" version = "7.1.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -252,7 +252,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale
name = "pytest-mock" name = "pytest-mock"
version = "3.8.2" version = "3.8.2"
description = "Thin-wrapper around the mock package for easier use with pytest" description = "Thin-wrapper around the mock package for easier use with pytest"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -309,7 +309,7 @@ python-versions = "*"
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -341,7 +341,7 @@ python-versions = ">=3.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "8f7eb9a03471694fe9f4950887b2857e65800302965f5add814793b5f6a4e946" content-hash = "9723100b02ae7c884db7d0ae2607d20c1e8321a7a39d93028060fd8022305370"
[metadata.files] [metadata.files]
atomicwrites = [] atomicwrites = []

View file

@ -16,6 +16,7 @@ sh = "^1.14.3"
pydantic = "^1.9.1" pydantic = "^1.9.1"
PyYAML = "^6.0" PyYAML = "^6.0"
seedir = "^0.3.1" seedir = "^0.3.1"
pytest-mock = "^3.8.2"
[tool.poetry.group.linters.dependencies] [tool.poetry.group.linters.dependencies]

21
tests/file_fixtures.py Normal file
View file

@ -0,0 +1,21 @@
import shutil
import tempfile
from pathlib import Path
from tempfile import NamedTemporaryFile
import pytest
@pytest.fixture()
def tmpfile():
_tmpfile = NamedTemporaryFile(delete=False)
with open(_tmpfile.name, "w") as file:
yield file
Path(_tmpfile.name).unlink()
@pytest.fixture()
def tmpdir():
tmpdir = Path(tempfile.mkdtemp())
yield tmpdir
shutil.rmtree(tmpdir)

View file

@ -1,5 +1,3 @@
import shutil
import tempfile
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@ -8,21 +6,7 @@ import yaml
from halig.config import get_config, Config, EncryptionKeysConfig from halig.config import get_config, Config, EncryptionKeysConfig
from halig.exceptions import ConfigFileDoesNotExist, ConfigFileIsInvalid from halig.exceptions import ConfigFileDoesNotExist, ConfigFileIsInvalid
from tests.file_fixtures import tmpfile, tmpdir # noqa: 401
@pytest.fixture()
def tmpfile():
_tmpfile = NamedTemporaryFile(delete=False)
with open(_tmpfile.name, "w") as file:
yield file
Path(_tmpfile.name).unlink()
@pytest.fixture()
def tmpdir():
tmpdir = Path(tempfile.mkdtemp())
yield tmpdir
shutil.rmtree(tmpdir)
def test_get_config_raises_config_file_does_not_exist(): def test_get_config_raises_config_file_does_not_exist():
@ -36,19 +20,19 @@ def test_get_config_with_empty_file_raises_invalid_config_file():
get_config(Path(f.name)) get_config(Path(f.name))
def test_get_config_raises_invalid_config_file_00(tmpfile): def test_get_config_raises_invalid_config_file_00(tmpfile): # noqa: F811
tmpfile.write("foobar") tmpfile.write("foobar")
with pytest.raises(ConfigFileIsInvalid): with pytest.raises(ConfigFileIsInvalid):
get_config(Path(tmpfile.name)) get_config(Path(tmpfile.name))
def test_get_config_raises_invalid_config_file_01(tmpfile): def test_get_config_raises_invalid_config_file_01(tmpfile): # noqa: F811
yaml.dump({"foo": "bar"}, tmpfile, Dumper=yaml.SafeDumper) yaml.dump({"foo": "bar"}, tmpfile, Dumper=yaml.SafeDumper)
with pytest.raises(ConfigFileIsInvalid): with pytest.raises(ConfigFileIsInvalid):
get_config(Path(tmpfile.name)) get_config(Path(tmpfile.name))
def test_get_config(tmpdir): def test_get_config(tmpdir): # noqa: F811
notes_root_path = Path(tmpdir / "notes") notes_root_path = Path(tmpdir / "notes")
notes_root_path.mkdir(exist_ok=True) notes_root_path.mkdir(exist_ok=True)
age_binary_path = Path(tmpdir / "age") age_binary_path = Path(tmpdir / "age")

79
tests/test_edit.py Normal file
View file

@ -0,0 +1,79 @@
from pathlib import Path
from subprocess import CalledProcessError
import pytest
from halig.config import Config, EncryptionKeysConfig
from halig.edit import edit_note
from halig.exceptions import CouldNotEditTempfile
from tests.file_fixtures import tmpfile, tempfile # noqa: F401
from sh import age, ssh_keygen, yes
@pytest.fixture()
def notes_path(tmpdir): # noqa: F811
notes_path = Path(tmpdir.dirname) / "notes"
notes_path.mkdir(exist_ok=True)
yield notes_path
@pytest.fixture()
def config(notes_path): # noqa: F811
keys_path = Path(notes_path.parent / ".ssh")
keys_path.mkdir(exist_ok=True)
private_key_path = keys_path / "key"
public_key_path = keys_path / "key.pub"
ssh_keygen(yes(_piped=True), t="ed25519", f=str(private_key_path))
config = Config(
notes_root_path=notes_path,
encryption_keys=EncryptionKeysConfig(
public_key_path=public_key_path,
private_key_path=private_key_path,
),
)
note_path = notes_path / "note"
note_path.touch(exist_ok=True)
with open(note_path, "w") as f:
f.write("this is a test")
age(
"-R",
str(config.encryption_keys.public_key_path),
str(note_path),
_out=f"{note_path}.age",
)
yield config
def mock_subprocess(args: list):
with open(args[1], "w") as f:
f.write("mocked")
def test_edit_note(config: Config, mocker): # noqa: F811
mocker.patch("subprocess.call", side_effect=mock_subprocess)
edit_note(config.notes_root_path / "note.age", config)
assert (
age(
"-d",
"-i",
config.encryption_keys.private_key_path,
config.notes_root_path / "note.age",
)
== "mocked"
)
def test_edit_not_raises_could_not_edit_tempfile(config: Config, mocker): # noqa: F811
mocker.patch("subprocess.call", side_effect=CalledProcessError(
returncode=1,
cmd="foo",
output="mocked error"))
with pytest.raises(CouldNotEditTempfile):
edit_note(config.notes_root_path / "note.age", config)