Skip to content

Commit

Permalink
Delete platformdirs dependency (#1511)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akuli committed Jun 9, 2024
1 parent d17ae9d commit fb0534b
Show file tree
Hide file tree
Showing 16 changed files with 102 additions and 68 deletions.
23 changes: 1 addition & 22 deletions porcupine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,11 @@
import os
import sys

import platformdirs
from porcupine import _state

version_info = (2024, 3, 31) # this is updated with scripts/release.py
__version__ = "%d.%02d.%02d" % version_info
__author__ = "Akuli"
__copyright__ = "Copyright (c) 2017-2024 Akuli"
__license__ = "MIT"

if sys.platform in {"win32", "darwin"}:
# these platforms like path names like "Program Files" or "Application Support"
dirs = platformdirs.PlatformDirs("Porcupine", "Akuli")
else:
# By default, platformdirs places logs to a weird place ~/.local/state/porcupine/log.
# No other applications I have use ~/.local/state and it doesn't even exist on my system.
# See https://github.com/platformdirs/platformdirs/issues/106
class _PorcupinePlatformDirs(platformdirs.PlatformDirs): # type: ignore
@property
def user_log_dir(self) -> str:
return os.path.join(self.user_cache_dir, "log")

# Also let's make the directory names lowercase
dirs = _PorcupinePlatformDirs("porcupine", "akuli")

# Must be after creating dirs
from porcupine import _state

get_main_window = _state.get_main_window
get_parsed_args = _state.get_parsed_args
get_horizontal_panedwindow = _state.get_horizontal_panedwindow # TODO: document this
Expand Down
7 changes: 4 additions & 3 deletions porcupine/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ def main() -> None:

args_parsed_in_first_step, junk = parser.parse_known_args()

dirs.user_cache_path.mkdir(parents=True, exist_ok=True)
(dirs.user_config_path / "plugins").mkdir(parents=True, exist_ok=True)
dirs.user_log_path.mkdir(parents=True, exist_ok=True)
dirs.cache_dir.mkdir(parents=True, exist_ok=True)
(dirs.config_dir / "plugins").mkdir(parents=True, exist_ok=True)
dirs.log_dir.mkdir(parents=True, exist_ok=True)

_logs.setup(
all_loggers_verbose=args_parsed_in_first_step.verbose,
verbose_loggers=(args_parsed_in_first_step.verbose_logger or []),
Expand Down
4 changes: 2 additions & 2 deletions porcupine/_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@


def _remove_old_logs() -> None:
for path in dirs.user_log_path.glob("*.txt"):
for path in dirs.log_dir.glob("*.txt"):
# support '<log dir>/<first_part>_<number>.txt' and '<log dir>/<firstpart>.txt'
first_part = path.stem.split("_")[0]
try:
Expand Down Expand Up @@ -56,7 +56,7 @@ def _open_log_file() -> TextIO:
)
for filename in filenames:
try:
return (dirs.user_log_path / filename).open("x", encoding="utf-8")
return (dirs.log_dir / filename).open("x", encoding="utf-8")
except FileExistsError:
continue
assert False # makes mypy happy
Expand Down
74 changes: 74 additions & 0 deletions porcupine/dirs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
r"""This module defines folders where Porcupine stores files.
Folders on most Windows systems:
config_dir = C:\Users\<username>\AppData\Local\Akuli\Porcupine
cache_dir = C:\Users\<username>\AppData\Local\Akuli\Porcupine\Cache
log_dir = C:\Users\<username>\AppData\Local\Akuli\Porcupine\Logs
Folders on most MacOS systems:
config_dir = /Users/<username>/Library/Application Support/Porcupine
cache_dir = /Users/<username>/Library/Caches/Porcupine
log_dir = /Users/<username>/Library/Logs/Porcupine
Folders on most Linux systems:
config_dir = /home/<username>/.config/porcupine
cache_dir = /home/<username>/.cache/porcupine
log_dir = /home/<username>/.cache/porcupine/log
Libraries like `platformdirs` exist, but a custom thing makes it easier to
explain to users where Porcupine is storing its files. Using many small
dependencies is also bad from a security point of view.
How to import
-------------
Good:
from porcupine import dirs
Bad:
from porcupine.dirs import config_dir
The bad import won't work as expected when running tests.
When the tests start, the `config_dir`, `cache_dir` and `log_dir` variables
are changed to point inside a temporary directory. This separates the user's
settings from Porcupine's tests.
The bad import captures the value of the variable at the time of importing,
before the tests change it. Therefore it will always point at the user's
personal settings folder, even when running tests.
"""

import os
import sys
from pathlib import Path

if sys.platform == "win32":
# %LOCALAPPDATA% seems to be a thing on all reasonably new Windows systems.
#
# Porcupine uses local appdata for historical reasons.
# I'm not sure whether it is better or worse than the roaming appdata.
# Please create an issue if you think Porcupine should use the roaming appdata instead.
_localappdata = os.getenv("LOCALAPPDATA")
if not _localappdata:
raise RuntimeError("%LOCALAPPDATA% is not set")

config_dir = Path(_localappdata) / "Akuli" / "Porcupine"
cache_dir = config_dir / "Cache"
log_dir = config_dir / "Logs"

elif sys.platform == "darwin":
config_dir = Path("~/Library/Application Support/Porcupine").expanduser()
cache_dir = Path("~/Library/Caches/Porcupine").expanduser()
log_dir = Path("~/Library/Logs/Porcupine").expanduser()

else:
# This code is for linux, and for Tuomas running Porcupine on NetBSD.
#
# See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
# for env vars and the fallbacks to be used when they are "either not set or empty".
# In reality, nobody uses these env vars, so the fallbacks are important.
config_dir = Path(os.getenv("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")) / "porcupine"
cache_dir = Path(os.getenv("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")) / "porcupine"
log_dir = cache_dir / "log"
2 changes: 1 addition & 1 deletion porcupine/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
from porcupine import dirs

# simple hack to allow user-wide plugins
__path__.insert(0, str(dirs.user_config_path / "plugins"))
__path__.insert(0, str(dirs.config_dir / "plugins"))
2 changes: 1 addition & 1 deletion porcupine/plugins/desktop_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def install_desktop_file() -> None:
activate_path = Path(venv) / "bin" / "activate"
assert activate_path.is_file()

launcher_path = dirs.user_cache_path / DESKTOP_FILE_NAME
launcher_path = dirs.cache_dir / DESKTOP_FILE_NAME

with launcher_path.open("w") as file:
file.write("[Desktop Entry]\n")
Expand Down
4 changes: 2 additions & 2 deletions porcupine/plugins/filetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def is_list_of_strings(obj: object) -> bool:

def load_filetypes() -> None:
# user_path can't be global var because tests monkeypatch
user_path = dirs.user_config_path / "filetypes.toml"
user_path = dirs.config_dir / "filetypes.toml"
defaults_path = Path(__file__).absolute().parent.parent / "default_filetypes.toml"

with defaults_path.open("rb") as defaults_file:
Expand Down Expand Up @@ -270,7 +270,7 @@ def setup() -> None:
_add_filetype_menuitem(name, filetypes_var)

get_tab_manager().bind("<<NotebookTabChanged>>", _sync_filetypes_menu, add=True)
path = dirs.user_config_path / "filetypes.toml"
path = dirs.config_dir / "filetypes.toml"
menubar.get_menu("Filetypes").add_separator()
menubar.add_config_file_button(path, menu="Filetypes")
menubar.add_config_file_button(path) # goes to "Settings/Config Files"
Expand Down
2 changes: 1 addition & 1 deletion porcupine/plugins/keybindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

def setup() -> None:
default_path = Path(__file__).absolute().parent.parent / "default_keybindings.tcl"
user_path = dirs.user_config_path / "keybindings.tcl"
user_path = dirs.config_dir / "keybindings.tcl"
menubar.add_config_file_button(user_path)

try:
Expand Down
2 changes: 1 addition & 1 deletion porcupine/plugins/restart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# https://fileinfo.com/extension/pkl
def _get_state_file() -> Path:
return dirs.user_cache_path / "restart_state.pkl"
return dirs.cache_dir / "restart_state.pkl"


# If loading a file fails, a dialog is created and it should be themed as user wants
Expand Down
2 changes: 1 addition & 1 deletion porcupine/plugins/run/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class _HistoryItem:
def _get_path() -> Path:
# config dir is better than cache dir https://github.com/davatorium/rofi/issues/769
# Change the number after v when you make incompatible changes
return dirs.user_config_path / "run_history_v3.json"
return dirs.config_dir / "run_history_v3.json"


def _load_json_file() -> list[_HistoryItem]:
Expand Down
4 changes: 2 additions & 2 deletions porcupine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def _value_to_save(obj: object) -> object:

# Must be a function, so that it updates when tests change the dirs object
def get_json_path() -> Path:
return dirs.user_config_path / "settings.json"
return dirs.config_dir / "settings.json"


def save() -> None:
Expand Down Expand Up @@ -916,7 +916,7 @@ def _is_monospace(font_family: str) -> bool:


def _get_monospace_font_families() -> list[str]:
cache_path = dirs.user_cache_path / "font_cache.json"
cache_path = dirs.config_dir / "font_cache.json"
all_families = sorted(set(tkinter.font.families()))

# This is surprisingly slow when there are lots of fonts. Let's cache.
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ license = {file = "LICENSE"}
classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = [] # Do not attempt to import porcupine before dependencies are installed
dependencies = [
"platformdirs>=3.0.0,<4.0.0",
"Pygments==2.15.0",
"colorama>=0.2.5",
"sansio-lsp-client>=0.10.0,<0.11.0",
Expand Down
1 change: 0 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Auto-generated in GitHub Actions. See autofix.yml.
platformdirs>=3.0.0,<4.0.0
Pygments==2.15.0
colorama>=0.2.5
sansio-lsp-client>=0.10.0,<0.11.0
Expand Down
31 changes: 7 additions & 24 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# see also update(3tcl)

import logging
import operator
import os
import shutil
import subprocess
Expand All @@ -16,7 +15,6 @@
from concurrent.futures import Future
from pathlib import Path

import platformdirs
import pytest

import porcupine
Expand Down Expand Up @@ -50,38 +48,23 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(skip_pastebins)


class MonkeypatchedPlatformDirs(platformdirs.PlatformDirs):
user_cache_dir = property(operator.attrgetter("_cache"))
user_config_dir = property(operator.attrgetter("_config"))
user_log_dir = property(operator.attrgetter("_logs"))


@pytest.fixture(scope="session")
def monkeypatch_dirs():
# avoid errors from user's custom plugins
user_plugindir = plugins.__path__.pop(0)
assert user_plugindir == str(dirs.user_config_path / "plugins")

font_cache = dirs.user_cache_path / "font_cache.json"
assert user_plugindir == str(dirs.config_dir / "plugins")

# Test our custom log dir before it is monkeypatched away
assert Path("~/.local/state").expanduser() not in dirs.user_log_path.parents
font_cache = dirs.cache_dir / "font_cache.json"

with tempfile.TemporaryDirectory() as d:
# This is a hack because:
# - pytest monkeypatch fixture doesn't work (not for scope='session')
# - assigning to dirs.user_cache_dir doesn't work (platformdirs uses @property)
# - "porcupine.dirs = blahblah" doesn't work (from porcupine import dirs)
dirs.__class__ = MonkeypatchedPlatformDirs
dirs._cache = os.path.join(d, "cache")
dirs._config = os.path.join(d, "config")
dirs._logs = os.path.join(d, "logs")
assert dirs.user_cache_dir.startswith(d)
dirs.cache_dir = Path(d) / "cache"
dirs.config_dir = Path(d) / "config"
dirs.log_dir = Path(d) / "logs"

# Copy font cache to speed up tests
if font_cache.exists():
dirs.user_cache_path.mkdir()
shutil.copy(font_cache, dirs.user_cache_path)
dirs.cache_dir.mkdir()
shutil.copy(font_cache, dirs.cache_dir)

yield

Expand Down
8 changes: 4 additions & 4 deletions tests/test_filetypes_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
def custom_filetypes():
# We don't overwrite the user's file because porcupine.dirs is monkeypatched
if sys.platform == "win32":
assert "Temp" in dirs.user_config_path.parts
assert "Temp" in dirs.config_dir.parts
else:
assert Path.home() not in dirs.user_config_path.parents
assert Path.home() not in dirs.config_dir.parents

(dirs.user_config_path / "filetypes.toml").write_text(
(dirs.config_dir / "filetypes.toml").write_text(
"""
["Mako template"]
filename_patterns = ["mako-templates/*.html"]
Expand All @@ -34,7 +34,7 @@ def custom_filetypes():
filetypes.load_filetypes()

yield
(dirs.user_config_path / "filetypes.toml").unlink()
(dirs.config_dir / "filetypes.toml").unlink()
filetypes.filetypes.clear()
filetypes.load_filetypes()

Expand Down
3 changes: 1 addition & 2 deletions user-doc/new-programming-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ warning message to the terminal or command prompt (if any) and start normally.
But if you start Porcupine without a terminal or command prompt,
you will never see these messages.
In that case, you need to look at Porcupine's log files.
To find the log files, open *Porcupine debug prompt* from the *Run* menu
and type `print(dirs.user_log_path)`.
See the docstring at start of [porcupine/dirs.py](../porcupine/dirs.py) to find the log files.

To get more output on the terminal or command prompt,
add `--verbose-logger=porcupine.plugins.langserver` to the end of the command that starts Porcupine.
Expand Down

0 comments on commit fb0534b

Please sign in to comment.