Skip to content

Commit ed88fb5

Browse files
authored
Merge pull request #341 from antonmosich/discover
Add discover addressbook type This fixes #203.
2 parents 7c5ce44 + 93fcf47 commit ed88fb5

File tree

7 files changed

+71
-4
lines changed

7 files changed

+71
-4
lines changed

doc/source/examples/khard.conf.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
path = ~/.contacts/family/
99
[[friends]]
1010
path = ~/.contacts/friends/
11+
[[work]]
12+
path = ~/.work/**/client-*-contacts/
13+
type = discover
1114

1215
[general]
1316
debug = no

doc/source/man.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ The following man pages are available for khard:
88

99
khard(1) <man/khard>
1010
khard-subcommands(1) <man/khard-subcommands>
11-
khard.conf(1) <man/khard.conf>
11+
khard.conf(5) <man/khard.conf>

doc/source/man/khard.conf.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ addressbooks
4141
This section contains several subsections, but at least one. Each subsection
4242
can have an arbitrary name which will be the name of an addressbook known to
4343
khard. Each of these subsections **must** have a *path* key with the path to
44-
the folder containing the vCard files for that addressbook. The *path* value
45-
supports environment variables and tilde prefixes. :program:`khard` expects
44+
the folder containing the vCard files for that addressbook. Optionally, you
45+
can set the *type* value to either ``discover`` or ``vdir``, the default. The
46+
*path* value supports environment variables and tilde prefixes. When using
47+
the ``discover`` type, it also supports globbing. :program:`khard` expects
4648
the vCard files to hold only one VCARD record each and end in a :file:`.vcf`
4749
extension.
4850

khard/config.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
import shlex
1010
from typing import Iterable, Optional, Union
11+
from glob import iglob
1112

1213
import configobj
1314
try:
@@ -94,7 +95,9 @@ def __init__(self, config_file: Optional[ConfigFile] = None) -> None:
9495
self.abooks: AddressBookCollection
9596
locale.setlocale(locale.LC_ALL, '')
9697
config = self._load_config_file(config_file)
97-
self.config = self._validate(config)
98+
config = self._validate(config)
99+
config["addressbooks"] = self._unfold_discover_books(config["addressbooks"])
100+
self.config = config
98101
self._set_attributes()
99102

100103
@classmethod
@@ -137,6 +140,53 @@ def _validate(config: configobj.ConfigObj) -> configobj.ConfigObj:
137140
raise ConfigError
138141
return config
139142

143+
@classmethod
144+
def _unfold_discover_books(cls, addressbooks: configobj.Section) -> configobj.Section:
145+
"""Expand globs in path of addressbooks of type "discover"
146+
147+
This expands all addressbooks of type "discover" into (potentially)
148+
multiple addressbooks of type "vdir". The names are automatically generated
149+
based on the directory name.
150+
151+
:param config: the configuration to be changed
152+
:returns: the changed configuration with no "discover" addressbooks
153+
"""
154+
for section_name, book in addressbooks.copy().items():
155+
if book["type"] != "discover":
156+
continue
157+
hits = iglob(os.path.expanduser(book["path"]), recursive=True)
158+
dirs = cls._find_leaf_dirs(hits)
159+
for bookpath in dirs:
160+
bookname = os.path.basename(bookpath)
161+
# Make sure our name is unique
162+
counter = 0
163+
while bookname in addressbooks:
164+
counter += 1
165+
if bookname + f"-{counter}" in addressbooks:
166+
continue
167+
bookname += f"-{counter}"
168+
break
169+
addressbooks[bookname] = {
170+
"type": "vdir",
171+
"path": bookpath,
172+
}
173+
addressbooks.pop(section_name)
174+
return addressbooks
175+
176+
@staticmethod
177+
def _find_leaf_dirs(hits: Iterable[str]) -> set[str]:
178+
"""Find leaf directories in a tree of hits when using glob.iglob
179+
180+
The hits are neither guaranteed to be unique nor leaf directories, both
181+
of which are enforced by this function.
182+
183+
:param hits: the hits of a glob as returned by glob.iglob
184+
:returns: a set of path strings
185+
"""
186+
dirs = {os.path.normpath(hit) for hit in hits if os.path.isdir(hit)}
187+
parents = {os.path.normpath(os.path.join(dir, os.pardir)) for dir in dirs}
188+
return dirs - parents
189+
140190
def _set_attributes(self) -> None:
141191
"""Set the attributes from the internal config instance on self."""
142192
general = self.config["general"]

khard/data/config.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ skip_unparsable = boolean(default=False)
2525
[addressbooks]
2626
[[__many__]]
2727
path = string
28+
type = option('vdir', 'discover', default='vdir')

test/fixture/discover.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[addressbooks]
2+
[[discovered]]
3+
path = test/fixture/*.abook
4+
type = discover

test/test_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ def test_load_empty_file_fails(self):
5050
with self.assertRaises(ConfigError):
5151
config.Config(name)
5252

53+
def test_discover_books(self):
54+
filename = "test/fixture/discover.conf"
55+
cfg = config.Config(filename)
56+
cfg.init_address_books()
57+
expected = {'broken.abook', 'nick.abook', 'test.abook', 'minimal.abook'}
58+
self.assertEqual(set(cfg.abooks._abooks.keys()), expected)
59+
5360
@mock.patch.dict("os.environ", EDITOR="editor", MERGE_EDITOR="meditor")
5461
def test_load_minimal_file_by_name(self):
5562
cfg = config.Config("test/fixture/minimal.conf")

0 commit comments

Comments
 (0)