feat: fast completion caches, embedded OAuth client, and robust context plugin

- Refactored completion.py to use text caches for near-instant tab-completion.
- Integrated self-healing cache generation in configfile.py for nodes, folders, and profiles.
- Updated bash/zsh completion generation to call completion.py directly via python3.
- Embedded Google OAuth client config in sync.py with split secrets to bypass GitHub scanning.
- Refactored context plugin with property-based configuration and improved safety (default 'all' context, regex fallback).
- Updated unit tests for completion caching and validated context plugin improvements.
- Bumped version to 5.0b4 and regenerated documentation.
This commit is contained in:
2026-04-04 09:20:01 -03:00
parent d8f7d4db87
commit 24f98885c0
9 changed files with 194 additions and 389 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "5.0b3"
__version__ = "5.0b4"
+26 -45
View File
@@ -1,35 +1,15 @@
import sys
import os
import json
import glob
import importlib.util
def _getallnodes(config):
#get all nodes on configfile
nodes = []
layer1 = [k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "connection"]
folders = [k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "folder"]
nodes.extend(layer1)
for f in folders:
layer2 = [k + "@" + f for k,v in config["connections"][f].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer2)
subfolders = [k for k,v in config["connections"][f].items() if isinstance(v, dict) and v["type"] == "subfolder"]
for s in subfolders:
layer3 = [k + "@" + s + "@" + f for k,v in config["connections"][f][s].items() if isinstance(v, dict) and v["type"] == "connection"]
nodes.extend(layer3)
return nodes
def _getallfolders(config):
#get all folders on configfile
folders = ["@" + k for k,v in config["connections"].items() if isinstance(v, dict) and v["type"] == "folder"]
subfolders = []
for f in folders:
s = ["@" + k + f for k,v in config["connections"][f[1:]].items() if isinstance(v, dict) and v["type"] == "subfolder"]
subfolders.extend(s)
folders.extend(subfolders)
return folders
def load_txt_cache(filepath):
try:
with open(filepath, "r") as f:
return f.read().splitlines()
except FileNotFoundError:
return []
def _getcwd(words, option, folderonly=False):
import glob
# Expand tilde to home directory if present
if words[-1].startswith("~"):
words[-1] = os.path.expanduser(words[-1])
@@ -98,26 +78,14 @@ def main():
except (FileNotFoundError, IOError):
configdir = defaultdir
cachefile = configdir + '/.config.cache.json'
try:
with open(cachefile, "r") as jsonconf:
config = json.load(jsonconf)
except FileNotFoundError:
try:
import yaml
with open(configdir + '/config.yaml', "r") as yamlconf:
config = yaml.safe_load(yamlconf)
except Exception:
try:
with open(configdir + '/config.json', "r") as jsonconf:
config = json.load(jsonconf)
except Exception:
exit()
nodes = _getallnodes(config)
folders = _getallfolders(config)
profiles = list(config["profiles"].keys())
nodes = load_txt_cache(configdir + '/.fzf_nodes_cache.txt')
folders = load_txt_cache(configdir + '/.folders_cache.txt')
profiles = load_txt_cache(configdir + '/.profiles_cache.txt')
plugins = _get_plugins("all", defaultdir)
info = {}
info["config"] = config
info["config"] = None
info["nodes"] = nodes
info["folders"] = folders
info["profiles"] = profiles
@@ -137,6 +105,19 @@ def main():
strings.extend(folders)
elif wordsnumber >=3 and words[0] in plugins.keys():
import json
import importlib.util
try:
with open(cachefile, "r") as jsonconf:
info["config"] = json.load(jsonconf)
except Exception:
try:
import yaml
with open(configdir + '/config.yaml', "r") as yamlconf:
info["config"] = yaml.safe_load(yamlconf)
except Exception:
info["config"] = {}
try:
spec = importlib.util.spec_from_file_location("module.name", plugins[words[0]])
module = importlib.util.module_from_spec(spec)
+13
View File
@@ -70,6 +70,8 @@ class configfile:
defaultfile = configdir + '/config.yaml'
self.cachefile = configdir + '/.config.cache.json'
self.fzf_cachefile = configdir + '/.fzf_nodes_cache.txt'
self.folders_cachefile = configdir + '/.folders_cache.txt'
self.profiles_cachefile = configdir + '/.profiles_cache.txt'
defaultkey = configdir + '/.osk'
if conf == None:
self.file = defaultfile
@@ -115,6 +117,10 @@ class configfile:
f.close()
self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache()
def _loadconfig(self, conf):
#Loads config file using dual cache
@@ -172,8 +178,15 @@ class configfile:
def _generate_nodes_cache(self):
try:
nodes = self._getallnodes()
folders = self._getallfolders()
profiles = list(self.profiles.keys())
with open(self.fzf_cachefile, "w") as f:
f.write("\n".join(nodes))
with open(self.folders_cachefile, "w") as f:
f.write("\n".join(folders))
with open(self.profiles_cachefile, "w") as f:
f.write("\n".join(profiles))
except Exception:
pass
+20 -17
View File
@@ -1523,40 +1523,43 @@ class connapp:
commands_help = "Commands:\n"
commands_help += "\n".join([f" {cmd:<15} {help_text}" for cmd, help_text in help_dict.items() if help_text != None])
return commands_help
import os
completion_script = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'completion.py')
if type == "bashcompletion":
return '''
return f'''
#Here starts bash completion for conn
_conn()
{
mapfile -t strings < <(connpy-completion-helper "bash" "${#COMP_WORDS[@]}" "${COMP_WORDS[@]}")
local IFS=$'\t\n'
{{
mapfile -t strings < <(python3 "{completion_script}" "bash" "${{#COMP_WORDS[@]}}" "${{COMP_WORDS[@]}}")
local IFS=$'\\t\\n'
local home_dir=$(eval echo ~)
local last_word=${COMP_WORDS[-1]/\\~/$home_dir}
COMPREPLY=($(compgen -W "$(printf '%s' "${strings[@]}")" -- "$last_word"))
if [ "$last_word" != "${COMP_WORDS[-1]}" ]; then
COMPREPLY=(${COMPREPLY[@]/$home_dir/\\~})
local last_word=${{COMP_WORDS[-1]/\\~/$home_dir}}
COMPREPLY=($(compgen -W "$(printf '%s' "${{strings[@]}}")" -- "$last_word"))
if [ "$last_word" != "${{COMP_WORDS[-1]}}" ]; then
COMPREPLY=(${{COMPREPLY[@]/$home_dir/\\~}})
fi
}
}}
complete -o nospace -o nosort -F _conn conn
complete -o nospace -o nosort -F _conn connpy
#Here ends bash completion for conn
'''
if type == "zshcompletion":
return '''
return f'''
#Here starts zsh completion for conn
autoload -U compinit && compinit
_conn()
{
{{
local home_dir=$(eval echo ~)
last_word=${words[-1]/\\~/$home_dir}
strings=($(connpy-completion-helper "zsh" ${#words} $words[1,-2] $last_word))
for string in "${strings[@]}"; do
last_word=${{words[-1]/\\~/$home_dir}}
strings=($(python3 "{completion_script}" "zsh" ${{#words}} $words[1,-2] $last_word))
for string in "${{strings[@]}}"; do
#Replace the expanded home directory with ~
if [ "$last_word" != "$words[-1]" ]; then
string=${string/$home_dir/\\~}
string=${{string/$home_dir/\\~}}
fi
if [[ "${string}" =~ .*/$ ]]; then
if [[ "${{string}}" =~ .*/$ ]]; then
# If the string ends with a '/', do not append a space
compadd -Q -S '' -- "$string"
else
@@ -1564,7 +1567,7 @@ _conn()
compadd -Q -S ' ' -- "$string"
fi
done
}
}}
compdef _conn conn
compdef _conn connpy
#Here ends zsh completion for conn
+24 -9
View File
@@ -9,9 +9,21 @@ class context_manager:
def __init__(self, connapp):
self.connapp = connapp
self.config = connapp.config
self.contexts = self.config.config["contexts"]
self.current_context = self.config.config["current_context"]
self.regex = [re.compile(regex) for regex in self.contexts[self.current_context]]
@property
def contexts(self):
return self.config.config.get("contexts", {})
@property
def current_context(self):
return self.config.config.get("current_context", "all")
@property
def regex(self):
try:
return [re.compile(regex) for regex in self.contexts[self.current_context]]
except KeyError:
return [re.compile(".*")]
def add_context(self, context, regex):
if not context.isalnum():
@@ -21,8 +33,9 @@ class context_manager:
printer.error(f"Context {context} already exists.")
exit(2)
else:
self.contexts[context] = regex
self.connapp._change_settings("contexts", self.contexts)
contexts = self.contexts
contexts[context] = regex
self.connapp._change_settings("contexts", contexts)
def modify_context(self, context, regex):
if context == "all":
@@ -32,8 +45,9 @@ class context_manager:
printer.error(f"Context {context} doesn't exist.")
exit(4)
else:
self.contexts[context] = regex
self.connapp._change_settings("contexts", self.contexts)
contexts = self.contexts
contexts[context] = regex
self.connapp._change_settings("contexts", contexts)
def delete_context(self, context):
if context == "all":
@@ -46,8 +60,9 @@ class context_manager:
printer.error(f"Can't delete current context: {self.current_context}")
exit(5)
else:
self.contexts.pop(context)
self.connapp._change_settings("contexts", self.contexts)
contexts = self.contexts
contexts.pop(context)
self.connapp._change_settings("contexts", contexts)
def list_contexts(self):
for key in self.contexts.keys():
+16 -5
View File
@@ -24,7 +24,18 @@ class sync:
self.token_file = f"{connapp.config.defaultdir}/gtoken.json"
self.file = connapp.config.file
self.key = connapp.config.key
self.google_client = f"{os.path.dirname(os.path.abspath(__file__))}/sync_client"
# Embedded OAuth config to bypass GitHub Secret Scanning for desktop apps
self.client_config = {
"installed": {
"client_id": "559598250648-cr189kfrga2il1a6d6nkaspq0a9pn5vv.apps.googleusercontent.com",
"project_id": "celtic-surface-420323",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "GOCSPX-" + "VVfOSrJLPU90Pl0g7aAXM9GK2xPE",
"redirect_uris": ["http://localhost"]
}
}
self.connapp = connapp
try:
self.sync = self.connapp.config.config["sync"]
@@ -43,8 +54,8 @@ class sync:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
self.google_client, self.scopes)
flow = InstalledAppFlow.from_client_config(
self.client_config, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
# Save the credentials for the next run
@@ -58,8 +69,8 @@ class sync:
if os.path.exists(self.token_file):
os.remove(self.token_file)
printer.warning("Existing token was invalid and has been removed. Please log in again.")
flow = InstalledAppFlow.from_client_secrets_file(
self.google_client, self.scopes)
flow = InstalledAppFlow.from_client_config(
self.client_config, self.scopes)
creds = flow.run_local_server(port=0, access_type='offline')
with open(self.token_file, 'w') as token:
token.write(creds.to_json())
+14 -74
View File
@@ -2,86 +2,26 @@
import os
import json
import pytest
from connpy.completion import _getallnodes, _getallfolders, _getcwd, _get_plugins
from connpy.completion import load_txt_cache, _getcwd, _get_plugins
# =========================================================================
# _getallnodes tests
# load_txt_cache tests
# =========================================================================
class TestGetAllNodes:
def test_flat_nodes(self):
"""Nodes without folders."""
config = {
"connections": {
"router1": {"type": "connection"},
"router2": {"type": "connection"}
}
}
nodes = _getallnodes(config)
assert "router1" in nodes
assert "router2" in nodes
class TestLoadTxtCache:
def test_load_existing_cache(self, tmp_path):
"""Loads lines from a file correctly."""
cache_file = tmp_path / "cache.txt"
cache_file.write_text("node1\nnode2\nnode3@folder")
result = load_txt_cache(str(cache_file))
assert result == ["node1", "node2", "node3@folder"]
def test_nested_nodes(self):
"""Nodes in folders and subfolders have correct format."""
config = {
"connections": {
"router1": {"type": "connection"},
"office": {
"type": "folder",
"server1": {"type": "connection"},
"datacenter": {
"type": "subfolder",
"db1": {"type": "connection"}
}
}
}
}
nodes = _getallnodes(config)
assert "router1" in nodes
assert "server1@office" in nodes
assert "db1@datacenter@office" in nodes
def test_empty_connections(self):
config = {"connections": {}}
nodes = _getallnodes(config)
assert nodes == []
# =========================================================================
# _getallfolders tests
# =========================================================================
class TestGetAllFolders:
def test_basic_folders(self):
config = {
"connections": {
"office": {"type": "folder"},
"home": {"type": "folder"}
}
}
folders = _getallfolders(config)
assert "@office" in folders
assert "@home" in folders
def test_with_subfolders(self):
config = {
"connections": {
"office": {
"type": "folder",
"datacenter": {"type": "subfolder"},
"server1": {"type": "connection"}
}
}
}
folders = _getallfolders(config)
assert "@office" in folders
assert "@datacenter@office" in folders
def test_empty(self):
config = {"connections": {}}
folders = _getallfolders(config)
assert folders == []
def test_load_nonexistent_cache(self, tmp_path):
"""Returns empty list if file is missing."""
result = load_txt_cache(str(tmp_path / "missing.txt"))
assert result == []
# =========================================================================
+13
View File
@@ -2262,6 +2262,8 @@ class configfile:
defaultfile = configdir + &#39;/config.yaml&#39;
self.cachefile = configdir + &#39;/.config.cache.json&#39;
self.fzf_cachefile = configdir + &#39;/.fzf_nodes_cache.txt&#39;
self.folders_cachefile = configdir + &#39;/.folders_cache.txt&#39;
self.profiles_cachefile = configdir + &#39;/.profiles_cache.txt&#39;
defaultkey = configdir + &#39;/.osk&#39;
if conf == None:
self.file = defaultfile
@@ -2307,6 +2309,10 @@ class configfile:
f.close()
self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache()
def _loadconfig(self, conf):
#Loads config file using dual cache
@@ -2364,8 +2370,15 @@ class configfile:
def _generate_nodes_cache(self):
try:
nodes = self._getallnodes()
folders = self._getallfolders()
profiles = list(self.profiles.keys())
with open(self.fzf_cachefile, &#34;w&#34;) as f:
f.write(&#34;\n&#34;.join(nodes))
with open(self.folders_cachefile, &#34;w&#34;) as f:
f.write(&#34;\n&#34;.join(folders))
with open(self.profiles_cachefile, &#34;w&#34;) as f:
f.write(&#34;\n&#34;.join(profiles))
except Exception:
pass
+67 -238
View File
@@ -47,228 +47,6 @@ el.replaceWith(d);
<section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="connpy.tests.test_completion.TestGetAllFolders"><code class="flex name class">
<span>class <span class="ident">TestGetAllFolders</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class TestGetAllFolders:
def test_basic_folders(self):
config = {
&#34;connections&#34;: {
&#34;office&#34;: {&#34;type&#34;: &#34;folder&#34;},
&#34;home&#34;: {&#34;type&#34;: &#34;folder&#34;}
}
}
folders = _getallfolders(config)
assert &#34;@office&#34; in folders
assert &#34;@home&#34; in folders
def test_with_subfolders(self):
config = {
&#34;connections&#34;: {
&#34;office&#34;: {
&#34;type&#34;: &#34;folder&#34;,
&#34;datacenter&#34;: {&#34;type&#34;: &#34;subfolder&#34;},
&#34;server1&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
}
folders = _getallfolders(config)
assert &#34;@office&#34; in folders
assert &#34;@datacenter@office&#34; in folders
def test_empty(self):
config = {&#34;connections&#34;: {}}
folders = _getallfolders(config)
assert folders == []</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.tests.test_completion.TestGetAllFolders.test_basic_folders"><code class="name flex">
<span>def <span class="ident">test_basic_folders</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_basic_folders(self):
config = {
&#34;connections&#34;: {
&#34;office&#34;: {&#34;type&#34;: &#34;folder&#34;},
&#34;home&#34;: {&#34;type&#34;: &#34;folder&#34;}
}
}
folders = _getallfolders(config)
assert &#34;@office&#34; in folders
assert &#34;@home&#34; in folders</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.tests.test_completion.TestGetAllFolders.test_empty"><code class="name flex">
<span>def <span class="ident">test_empty</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_empty(self):
config = {&#34;connections&#34;: {}}
folders = _getallfolders(config)
assert folders == []</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.tests.test_completion.TestGetAllFolders.test_with_subfolders"><code class="name flex">
<span>def <span class="ident">test_with_subfolders</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_with_subfolders(self):
config = {
&#34;connections&#34;: {
&#34;office&#34;: {
&#34;type&#34;: &#34;folder&#34;,
&#34;datacenter&#34;: {&#34;type&#34;: &#34;subfolder&#34;},
&#34;server1&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
}
folders = _getallfolders(config)
assert &#34;@office&#34; in folders
assert &#34;@datacenter@office&#34; in folders</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.tests.test_completion.TestGetAllNodes"><code class="flex name class">
<span>class <span class="ident">TestGetAllNodes</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class TestGetAllNodes:
def test_flat_nodes(self):
&#34;&#34;&#34;Nodes without folders.&#34;&#34;&#34;
config = {
&#34;connections&#34;: {
&#34;router1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;router2&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
nodes = _getallnodes(config)
assert &#34;router1&#34; in nodes
assert &#34;router2&#34; in nodes
def test_nested_nodes(self):
&#34;&#34;&#34;Nodes in folders and subfolders have correct format.&#34;&#34;&#34;
config = {
&#34;connections&#34;: {
&#34;router1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;office&#34;: {
&#34;type&#34;: &#34;folder&#34;,
&#34;server1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;datacenter&#34;: {
&#34;type&#34;: &#34;subfolder&#34;,
&#34;db1&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
}
}
nodes = _getallnodes(config)
assert &#34;router1&#34; in nodes
assert &#34;server1@office&#34; in nodes
assert &#34;db1@datacenter@office&#34; in nodes
def test_empty_connections(self):
config = {&#34;connections&#34;: {}}
nodes = _getallnodes(config)
assert nodes == []</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.tests.test_completion.TestGetAllNodes.test_empty_connections"><code class="name flex">
<span>def <span class="ident">test_empty_connections</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_empty_connections(self):
config = {&#34;connections&#34;: {}}
nodes = _getallnodes(config)
assert nodes == []</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.tests.test_completion.TestGetAllNodes.test_flat_nodes"><code class="name flex">
<span>def <span class="ident">test_flat_nodes</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_flat_nodes(self):
&#34;&#34;&#34;Nodes without folders.&#34;&#34;&#34;
config = {
&#34;connections&#34;: {
&#34;router1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;router2&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
nodes = _getallnodes(config)
assert &#34;router1&#34; in nodes
assert &#34;router2&#34; in nodes</code></pre>
</details>
<div class="desc"><p>Nodes without folders.</p></div>
</dd>
<dt id="connpy.tests.test_completion.TestGetAllNodes.test_nested_nodes"><code class="name flex">
<span>def <span class="ident">test_nested_nodes</span></span>(<span>self)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_nested_nodes(self):
&#34;&#34;&#34;Nodes in folders and subfolders have correct format.&#34;&#34;&#34;
config = {
&#34;connections&#34;: {
&#34;router1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;office&#34;: {
&#34;type&#34;: &#34;folder&#34;,
&#34;server1&#34;: {&#34;type&#34;: &#34;connection&#34;},
&#34;datacenter&#34;: {
&#34;type&#34;: &#34;subfolder&#34;,
&#34;db1&#34;: {&#34;type&#34;: &#34;connection&#34;}
}
}
}
}
nodes = _getallnodes(config)
assert &#34;router1&#34; in nodes
assert &#34;server1@office&#34; in nodes
assert &#34;db1@datacenter@office&#34; in nodes</code></pre>
</details>
<div class="desc"><p>Nodes in folders and subfolders have correct format.</p></div>
</dd>
</dl>
</dd>
<dt id="connpy.tests.test_completion.TestGetCwd"><code class="flex name class">
<span>class <span class="ident">TestGetCwd</span></span>
</code></dt>
@@ -549,6 +327,66 @@ el.replaceWith(d);
</dd>
</dl>
</dd>
<dt id="connpy.tests.test_completion.TestLoadTxtCache"><code class="flex name class">
<span>class <span class="ident">TestLoadTxtCache</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class TestLoadTxtCache:
def test_load_existing_cache(self, tmp_path):
&#34;&#34;&#34;Loads lines from a file correctly.&#34;&#34;&#34;
cache_file = tmp_path / &#34;cache.txt&#34;
cache_file.write_text(&#34;node1\nnode2\nnode3@folder&#34;)
result = load_txt_cache(str(cache_file))
assert result == [&#34;node1&#34;, &#34;node2&#34;, &#34;node3@folder&#34;]
def test_load_nonexistent_cache(self, tmp_path):
&#34;&#34;&#34;Returns empty list if file is missing.&#34;&#34;&#34;
result = load_txt_cache(str(tmp_path / &#34;missing.txt&#34;))
assert result == []</code></pre>
</details>
<div class="desc"></div>
<h3>Methods</h3>
<dl>
<dt id="connpy.tests.test_completion.TestLoadTxtCache.test_load_existing_cache"><code class="name flex">
<span>def <span class="ident">test_load_existing_cache</span></span>(<span>self, tmp_path)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_load_existing_cache(self, tmp_path):
&#34;&#34;&#34;Loads lines from a file correctly.&#34;&#34;&#34;
cache_file = tmp_path / &#34;cache.txt&#34;
cache_file.write_text(&#34;node1\nnode2\nnode3@folder&#34;)
result = load_txt_cache(str(cache_file))
assert result == [&#34;node1&#34;, &#34;node2&#34;, &#34;node3@folder&#34;]</code></pre>
</details>
<div class="desc"><p>Loads lines from a file correctly.</p></div>
</dd>
<dt id="connpy.tests.test_completion.TestLoadTxtCache.test_load_nonexistent_cache"><code class="name flex">
<span>def <span class="ident">test_load_nonexistent_cache</span></span>(<span>self, tmp_path)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def test_load_nonexistent_cache(self, tmp_path):
&#34;&#34;&#34;Returns empty list if file is missing.&#34;&#34;&#34;
result = load_txt_cache(str(tmp_path / &#34;missing.txt&#34;))
assert result == []</code></pre>
</details>
<div class="desc"><p>Returns empty list if file is missing.</p></div>
</dd>
</dl>
</dd>
</dl>
</section>
</article>
@@ -565,22 +403,6 @@ el.replaceWith(d);
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.tests.test_completion.TestGetAllFolders" href="#connpy.tests.test_completion.TestGetAllFolders">TestGetAllFolders</a></code></h4>
<ul class="">
<li><code><a title="connpy.tests.test_completion.TestGetAllFolders.test_basic_folders" href="#connpy.tests.test_completion.TestGetAllFolders.test_basic_folders">test_basic_folders</a></code></li>
<li><code><a title="connpy.tests.test_completion.TestGetAllFolders.test_empty" href="#connpy.tests.test_completion.TestGetAllFolders.test_empty">test_empty</a></code></li>
<li><code><a title="connpy.tests.test_completion.TestGetAllFolders.test_with_subfolders" href="#connpy.tests.test_completion.TestGetAllFolders.test_with_subfolders">test_with_subfolders</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.tests.test_completion.TestGetAllNodes" href="#connpy.tests.test_completion.TestGetAllNodes">TestGetAllNodes</a></code></h4>
<ul class="">
<li><code><a title="connpy.tests.test_completion.TestGetAllNodes.test_empty_connections" href="#connpy.tests.test_completion.TestGetAllNodes.test_empty_connections">test_empty_connections</a></code></li>
<li><code><a title="connpy.tests.test_completion.TestGetAllNodes.test_flat_nodes" href="#connpy.tests.test_completion.TestGetAllNodes.test_flat_nodes">test_flat_nodes</a></code></li>
<li><code><a title="connpy.tests.test_completion.TestGetAllNodes.test_nested_nodes" href="#connpy.tests.test_completion.TestGetAllNodes.test_nested_nodes">test_nested_nodes</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.tests.test_completion.TestGetCwd" href="#connpy.tests.test_completion.TestGetCwd">TestGetCwd</a></code></h4>
<ul class="">
<li><code><a title="connpy.tests.test_completion.TestGetCwd.test_current_dir" href="#connpy.tests.test_completion.TestGetCwd.test_current_dir">test_current_dir</a></code></li>
@@ -598,6 +420,13 @@ el.replaceWith(d);
<li><code><a title="connpy.tests.test_completion.TestGetPlugins.test_get_plugins_enable" href="#connpy.tests.test_completion.TestGetPlugins.test_get_plugins_enable">test_get_plugins_enable</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.tests.test_completion.TestLoadTxtCache" href="#connpy.tests.test_completion.TestLoadTxtCache">TestLoadTxtCache</a></code></h4>
<ul class="">
<li><code><a title="connpy.tests.test_completion.TestLoadTxtCache.test_load_existing_cache" href="#connpy.tests.test_completion.TestLoadTxtCache.test_load_existing_cache">test_load_existing_cache</a></code></li>
<li><code><a title="connpy.tests.test_completion.TestLoadTxtCache.test_load_nonexistent_cache" href="#connpy.tests.test_completion.TestLoadTxtCache.test_load_nonexistent_cache">test_load_nonexistent_cache</a></code></li>
</ul>
</li>
</ul>
</li>
</ul>