summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md35
-rw-r--r--debian/changelog7
-rw-r--r--debian/control2
-rw-r--r--docs/api/wn.rst44
-rw-r--r--docs/cli.rst34
-rw-r--r--docs/setup.rst17
-rw-r--r--pyproject.toml2
-rw-r--r--tests/_config_test.py10
-rw-r--r--tests/_util_test.py24
-rw-r--r--tests/secondary_query_test.py29
-rw-r--r--wn/__init__.py3
-rw-r--r--wn/__main__.py45
-rw-r--r--wn/_add.py8
-rw-r--r--wn/_config.py206
-rw-r--r--wn/_core.py5
-rw-r--r--wn/_db.py10
-rw-r--r--wn/_module_functions.py6
-rw-r--r--wn/_util.py6
-rw-r--r--wn/constants.py5
-rw-r--r--wn/project.py7
20 files changed, 426 insertions, 79 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d258c3..cbc4983 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,40 @@
## [Unreleased][unreleased]
+
+## [v1.1.0]
+
+**Release date: 2026-03-21**
+
+### Added
+
+* `cache` subcommand ([#313])
+* `wn.config.list_cache_entries()` method ([#313])
+* Support for `WN_DATA_DIR` environment variable ([#314])
+
+### Changed
+
+* The schema hashing function is now resilient to ordering and SQL DB
+ operations ([#319])
+
+### Fixed
+
+* `Synset.translate()` resets the lexicon configuration of the
+ translated sysets ([#316]); `Sense.translate()` and
+ `Word.translate()` derive from `Synset.translate()` so nothing
+ special needs to be done for them.
+
+
+## [v1.0.0]
+
+**Release date: 2026-01-31**
+
Notable changes in this release:
* A new version of the database schema requires a database rebuild
* A new `wn.ili` module deals with ILI files and objects; interlingual
queries still use the `Synset.ili` member, which is now a simple `str`
* The Open English Wordnet versions 2025 and 2025+ are added to the index
+* The Open Multilingual Wordnet 2.0 is added to the index
### Index
@@ -799,6 +828,8 @@ the https://github.com/nltk/wordnet/ code which had been effectively
abandoned, but this is an entirely new codebase.
+[v1.1.0]: ../../releases/tag/v1.1.0
+[v1.0.0]: ../../releases/tag/v1.0.0
[v0.14.0]: ../../releases/tag/v0.14.0
[v0.13.0]: ../../releases/tag/v0.13.0
[v0.12.0]: ../../releases/tag/v0.12.0
@@ -945,3 +976,7 @@ abandoned, but this is an entirely new codebase.
[#301]: https://github.com/goodmami/wn/issues/301
[#302]: https://github.com/goodmami/wn/issues/302
[#303]: https://github.com/goodmami/wn/issues/303
+[#313]: https://github.com/goodmami/wn/issues/313
+[#314]: https://github.com/goodmami/wn/issues/314
+[#316]: https://github.com/goodmami/wn/issues/316
+[#319]: https://github.com/goodmami/wn/issues/319
diff --git a/debian/changelog b/debian/changelog
index 93e145d..7fe5469 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-wn (1.1.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * d/control: Update Standards-Version to 4.7.4.
+
+ -- Danial Behzadi <dani.behzi@ubuntu.com> Thu, 02 Apr 2026 02:11:50 +0330
+
python-wn (1.0.0-3) unstable; urgency=medium
* Team Upload
diff --git a/debian/control b/debian/control
index 1d04e46..6dd5f11 100644
--- a/debian/control
+++ b/debian/control
@@ -12,7 +12,7 @@ Build-Depends:
python3-httpx,
python3-setuptools,
Testsuite: autopkgtest-pkg-python
-Standards-Version: 4.7.3
+Standards-Version: 4.7.4
Homepage: https://github.com/goodmami/wn
Vcs-Git: https://salsa.debian.org/python-team/packages/python-wn.git
Vcs-Browser: https://salsa.debian.org/python-team/packages/python-wn
diff --git a/docs/api/wn.rst b/docs/api/wn.rst
index 37750c6..3d4a50f 100644
--- a/docs/api/wn.rst
+++ b/docs/api/wn.rst
@@ -492,10 +492,54 @@ directly. Configuration should occur through the single
.. automethod:: add_project_version
.. automethod:: get_project_info
.. automethod:: get_cache_path
+ .. automethod:: list_cache_entries
.. automethod:: update
.. automethod:: load_index
+Auxiliary WNConfig Types
+''''''''''''''''''''''''
+
+The following classes are argument or return types of
+:class:`~wn._config.WNConfig` objects. They are documented here for reference,
+but are not meant to be created directly.
+
+.. autoclass:: wn._config.ResourceType
+
+ Enumeration of resource types.
+
+ .. autoattribute:: WORDNET
+ .. autoattribute:: ILI
+
+
+.. autoclass:: wn._config.ProjectInfo
+ :members:
+ :undoc-members:
+
+ Dictionary of information about a project.
+
+
+.. autoclass:: wn._config.VersionInfo
+ :members:
+ :undoc-members:
+
+ Dictionary of information about a resource version.
+
+
+.. autoclass:: wn._config.ResolvedProjectInfo
+ :members:
+ :undoc-members:
+
+ Dictionary of information about a specific project resource.
+
+
+.. autoclass:: wn._config.CacheEntry
+ :members:
+ :undoc-members:
+
+ Dictionary of information about files in the download cache.
+
+
Exceptions
----------
diff --git a/docs/cli.rst b/docs/cli.rst
index 4c490a3..5ad5b7e 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -40,6 +40,38 @@ specifiers or URLs.
database.
+cache
+-----
+
+View the files in the download cache. The ``download`` command caches the (often
+compressed) files to the filesystem prior to adding to Wn's database. The files
+are renamed with a hash of the URL to avoid name clashes, but this also makes it
+hard to determine what a particular file is. This command cross-references the
+downloaded files with what is in the index. An optional project specifier
+argument can help narrow down the results.
+
+.. code-block:: console
+
+ $ python -m wn cache # many results; abbreviated here
+ af909070c29845b952d1799551bffc302e28d2c5 own-en 1.0.0 https://github.com/own-pt/openWordnet-PT/releases/download/v1.0.0/own-en.tar.gz
+ e25af66e46775b00d689619787013e6a35e5cbf7 oewn 2025 https://en-word.net/static/english-wordnet-2025.xml.gz
+ 5a26d97a0081996db4cd621638a8a9b0da09aa25 odenet 1.4 https://github.com/hdaSprachtechnologie/odenet/releases/download/v1.4/odenet-1.4.tar.xz
+ [...]
+ $ python -m wn cache "oewn:2025*" # narrowed results
+ e25af66e46775b00d689619787013e6a35e5cbf7 oewn 2025 https://en-word.net/static/english-wordnet-2025.xml.gz
+ 0f5371187dcfe7e05f2a93ab85b4e1168859a5c2 oewn 2025+ https://en-word.net/static/english-wordnet-2025-plus.xml.gz
+
+.. option:: --full-paths-only
+
+ Only print the full path of each cache file. This can be useful when one
+ wants to pipe the results to other commands. For example, on Unix-like
+ systems, the following will delete matching cache entries:
+
+ .. code-block:: console
+
+ $ python -m wn cache --full-paths-only "omw*:1.4" | xargs rm
+
+
lexicons
--------
@@ -118,7 +150,7 @@ problems and print a report.
.. code-block::
- $ python -m wn validate --select E W201 W204 deWordNet.xml
+ $ python -m wn validate --select E,W201,W204 deWordNet.xml
.. option:: --output-file FILE
diff --git a/docs/setup.rst b/docs/setup.rst
index cd5f747..5602d0d 100644
--- a/docs/setup.rst
+++ b/docs/setup.rst
@@ -53,12 +53,27 @@ change the directory Wn uses for storing data locally, modify the
import wn
wn.config.data_directory = '~/Projects/wn_data'
+You can alternatively set the ``WN_DATA_DIR`` environment variable
+prior to importing Wn. On Unix-like systems, this would look like:
+
+.. code-block:: console
+
+ $ export WN_DATA_DIR=~/path/to/wn_data
+ $ python3 ...
+
+If you are using Wn from the command line, a third option is to use
+the ``--dir`` or ``-d`` option:
+
+.. code-block:: console
+
+ $ python3 -m wn --dir ~/path/to/wn_data download ...
+
There are some things to note:
- The downloads directory and database path are always relative to the
data directory and cannot be changed directly.
- This change only affects subsequent operations, so any data in the
- previous location will not be moved nor deleted.
+ previous location will not be moved or deleted.
- This change only affects the current session. If you want a script
or application to always use the new location, it must reset the
data directory each time it is initialized.
diff --git a/pyproject.toml b/pyproject.toml
index ecbf1f9..3305546 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,7 @@ requires-python = ">=3.10"
license = {file = "LICENSE"}
keywords = ["wordnet", "interlingual", "linguistics", "language", "library"]
authors = [
- {name = "Michael Wayne Goodman", email = "goodman.m.w@gmail.com"}
+ {name = "Michael Wayne Goodman", email = "1428419+goodmami@users.noreply.github.com"}
]
classifiers = [
"Development Status :: 4 - Beta",
diff --git a/tests/_config_test.py b/tests/_config_test.py
new file mode 100644
index 0000000..33e0885
--- /dev/null
+++ b/tests/_config_test.py
@@ -0,0 +1,10 @@
+from pathlib import Path
+
+from wn._config import WNConfig
+
+
+def test_envvar_data_dir(monkeypatch, tmp_path):
+ assert WNConfig().data_directory == Path.home() / ".wn_data"
+ with monkeypatch.context() as mp:
+ mp.setenv("WN_DATA_DIR", str(tmp_path))
+ assert WNConfig().data_directory == tmp_path
diff --git a/tests/_util_test.py b/tests/_util_test.py
index 9e9290e..9155b39 100644
--- a/tests/_util_test.py
+++ b/tests/_util_test.py
@@ -1,4 +1,10 @@
-from wn._util import flatten, normalize_form, unique_list
+from wn._util import (
+ flatten,
+ format_lexicon_specifier,
+ normalize_form,
+ split_lexicon_specifier,
+ unique_list,
+)
def test_flatten():
@@ -28,3 +34,19 @@ def test_normalize_form():
assert normalize_form("so\xf1ar") == "sonar" # soñar with single ñ character
assert normalize_form("son\u0303ar") == "sonar" # soñar with combining tilde
assert normalize_form("Weiß") == "weiss"
+
+
+def test_format_lexicon_specifier():
+ assert format_lexicon_specifier("", "") == ":"
+ assert format_lexicon_specifier("foo", "") == "foo:"
+ assert format_lexicon_specifier("", "bar") == ":bar"
+ assert format_lexicon_specifier("foo", "bar") == "foo:bar"
+
+
+def test_split_lexicon_specifier():
+ assert split_lexicon_specifier("") == ("", "")
+ assert split_lexicon_specifier(":") == ("", "")
+ assert split_lexicon_specifier("foo") == ("foo", "")
+ assert split_lexicon_specifier("foo:") == ("foo", "")
+ assert split_lexicon_specifier(":bar") == ("", "bar")
+ assert split_lexicon_specifier("foo:bar") == ("foo", "bar")
diff --git a/tests/secondary_query_test.py b/tests/secondary_query_test.py
index ee7763b..5af1705 100644
--- a/tests/secondary_query_test.py
+++ b/tests/secondary_query_test.py
@@ -21,6 +21,16 @@ def test_word_translate():
assert len(wn.word("test-es-ejemplo-n").translate(lang="en")) == 1
+@pytest.mark.usefixtures("mini_db")
+def test_word_translate_issue_316():
+ # https://github.com/goodmami/wn/issues/316
+ es = wn.Wordnet("test-es")
+ es_w = es.word("test-es-información-n")
+ translations = es_w.translate(lexicon="test-en")
+ assert len(translations) == 1
+ assert next(iter(translations.values()))[0].forms() == ["information"]
+
+
@pytest.mark.usefixtures("mini_db_1_1")
def test_word_lemma_tags():
en = wn.Wordnet("test-en")
@@ -118,6 +128,14 @@ def test_sense_translate():
@pytest.mark.usefixtures("mini_db")
+def test_sense_translate_issue_316():
+ # https://github.com/goodmami/wn/issues/316
+ es = wn.Wordnet("test-es")
+ es_s = es.sense("test-es-información-n-0001-01")
+ assert es_s.translate(lexicon="test-en")[0].counts() == [3]
+
+
+@pytest.mark.usefixtures("mini_db")
def test_synset_senses():
assert len(wn.synset("test-en-0003-v").senses()) == 2
assert len(wn.synset("test-es-0003-v").senses()) == 2
@@ -179,6 +197,17 @@ def test_synset_translate():
assert len(wn.synset("test-es-0001-n").translate(lang="en")) == 1
+@pytest.mark.usefixtures("mini_db")
+def test_synset_translate_issue_316():
+ # https://github.com/goodmami/wn/issues/316
+ es = wn.Wordnet("test-es")
+ en = wn.Wordnet("test-en")
+ es_ss = es.synset("test-es-0001-n")
+ en_ss = en.synset("test-en-0001-n")
+ assert es_ss.translate(lexicon="test-en")[0].definition() == en_ss.definition()
+ assert en_ss.translate(lexicon="test-es")[0].definition() == es_ss.definition()
+
+
@pytest.mark.usefixtures("uninitialized_datadir")
def test_word_sense_order(datadir):
wn.add(datadir / "sense-member-order.xml")
diff --git a/wn/__init__.py b/wn/__init__.py
index 05d162c..5e985d2 100644
--- a/wn/__init__.py
+++ b/wn/__init__.py
@@ -37,6 +37,7 @@ __all__ = (
"word",
"words",
)
+__version__ = "1.1.0"
from wn._add import add, add_lexical_resource, remove
from wn._config import config # noqa: F401
@@ -75,5 +76,3 @@ from wn._module_functions import (
words,
)
from wn._wordnet import Wordnet
-
-__version__ = "1.0.0"
diff --git a/wn/__main__.py b/wn/__main__.py
index 8874fcf..b63ed5c 100644
--- a/wn/__main__.py
+++ b/wn/__main__.py
@@ -11,19 +11,38 @@ from wn.project import iterpackages
from wn.validate import validate
-def _download(args):
+def _download(args: argparse.Namespace) -> None:
if args.index:
wn.config.load_index(args.index)
for target in args.target:
wn.download(target, add=args.add)
-def _lexicons(args):
+def _cache(args: argparse.Namespace) -> None:
+ cache_entries = wn.config.list_cache_entries(args.ARG)
+ if args.full_paths_only:
+ for cache_entry in cache_entries:
+ print(str(cache_entry["path"]))
+ else:
+ for cache_entry in cache_entries:
+ print(
+ "\t".join(
+ [
+ str(cache_entry["path"].name),
+ cache_entry["id"] or "?",
+ cache_entry["version"] or "?",
+ cache_entry["url"] or "?",
+ ]
+ )
+ )
+
+
+def _lexicons(args: argparse.Namespace) -> None:
for lex in wn.lexicons(lang=args.lang, lexicon=args.lexicon):
print("\t".join((lex.id, lex.version, f"[{lex.language}]", lex.label)))
-def _projects(args):
+def _projects(args: argparse.Namespace) -> None:
for info in wn.projects():
key = "i"
key += "c" if info["cache"] else "-"
@@ -41,7 +60,7 @@ def _projects(args):
)
-def _validate(args):
+def _validate(args: argparse.Namespace) -> None:
all_valid = True
selectseq = [check.strip() for check in args.select.split(",")]
for package in iterpackages(args.FILE):
@@ -125,6 +144,24 @@ parser_download.add_argument(
parser_download.set_defaults(func=_download)
+parser_cache = sub_parsers.add_parser(
+ "cache",
+ description="View Wn's download cache.",
+ help="view the download cache",
+)
+parser_cache.add_argument(
+ "ARG",
+ help="project specifier or URL",
+ nargs="?",
+ default="*",
+)
+parser_cache.add_argument(
+ "--full-paths-only",
+ action="store_true",
+ help="print the full paths of cache entries without other data",
+)
+parser_cache.set_defaults(func=_cache)
+
parser_lexicons = sub_parsers.add_parser(
"lexicons",
description="Display a list of installed lexicons.",
diff --git a/wn/_add.py b/wn/_add.py
index b0c3a70..180abbb 100644
--- a/wn/_add.py
+++ b/wn/_add.py
@@ -9,9 +9,9 @@ from itertools import islice
from pathlib import Path
from typing import TypeVar, cast
-from wn import constants, lmf
from wn import ili as _ili
-from wn._config import config
+from wn import lmf
+from wn._config import ResourceType, config
from wn._db import connect
from wn._exceptions import Error
from wn._queries import (
@@ -108,9 +108,9 @@ def add(
try:
for package in iterpackages(source):
match package.type:
- case constants._WORDNET:
+ case ResourceType.WORDNET:
_add_lmf(package.resource_file(), progress, progress_handler)
- case constants._ILI:
+ case ResourceType.ILI:
_add_ili(package.resource_file(), progress)
case _:
raise Error(f"unknown package type: {package.type}")
diff --git a/wn/_config.py b/wn/_config.py
index e8eb049..d058c85 100644
--- a/wn/_config.py
+++ b/wn/_config.py
@@ -2,10 +2,13 @@
Local configuration settings.
"""
+import os
from collections.abc import Sequence
+from enum import Enum
+from fnmatch import fnmatch
from importlib.resources import as_file, files
from pathlib import Path
-from typing import Any
+from typing import Any, TypedDict
try:
# python_version >= 3.11
@@ -15,20 +18,64 @@ except ImportError:
from wn._exceptions import ConfigurationError, ProjectError
from wn._types import AnyPath
-from wn._util import format_lexicon_specifier, short_hash, split_lexicon_specifier
-from wn.constants import _WORDNET
+from wn._util import (
+ format_lexicon_specifier,
+ is_str_key_dict,
+ short_hash,
+ split_lexicon_specifier,
+)
# The index file is a project file of Wn
with as_file(files("wn") / "index.toml") as index_file:
INDEX_FILE_PATH = index_file
# The directory where downloaded and added data will be stored.
-DEFAULT_DATA_DIRECTORY = Path.home() / ".wn_data"
+DEFAULT_DATA_DIR = Path.home() / ".wn_data"
DATABASE_FILENAME = "wn.db"
+class ResourceType(str, Enum):
+ WORDNET = "wordnet"
+ ILI = "ili"
+
+
+class VersionInfo(TypedDict):
+ resource_urls: list[str]
+ license: str | None
+ error: str | None
+
+
+class ProjectInfo(TypedDict):
+ type: ResourceType
+ label: str | None
+ language: str | None
+ license: str | None
+ error: str | None
+ versions: dict[str, VersionInfo]
+
+
+class ResolvedProjectInfo(TypedDict):
+ id: str
+ version: str
+ type: ResourceType
+ label: str | None
+ language: str | None
+ license: str | None
+ resource_urls: list[str]
+ cache: Path | None
+
+
+class CacheEntry(TypedDict):
+ path: Path
+ id: str | None
+ version: str | None
+ url: str | None
+
+
class WNConfig:
+ _projects: dict[str, ProjectInfo]
+
def __init__(self):
- self._data_directory = DEFAULT_DATA_DIRECTORY
+ self._data_directory = Path(os.getenv("WN_DATA_DIR", default=DEFAULT_DATA_DIR))
self._projects = {}
self._dbpath = self._data_directory / DATABASE_FILENAME
self.allow_multithreading = False
@@ -52,7 +99,7 @@ class WNConfig:
return dir
@data_directory.setter
- def data_directory(self, path):
+ def data_directory(self, path: AnyPath) -> None:
dir = Path(path).expanduser()
if dir.exists() and not dir.is_dir():
raise ConfigurationError(f"path exists and is not a directory: {dir}")
@@ -82,14 +129,14 @@ class WNConfig:
return dir
@property
- def index(self) -> dict[str, dict]:
+ def index(self) -> dict[str, ProjectInfo]:
"""The project index."""
return self._projects
def add_project(
self,
id: str,
- type: str = _WORDNET,
+ type: ResourceType = ResourceType.WORDNET,
label: str | None = None,
language: str | None = None,
license: str | None = None,
@@ -110,15 +157,14 @@ class WNConfig:
"""
if id in self._projects:
raise ValueError(f"project already added: {id}")
- self._projects[id] = {
- "type": type,
- "label": label,
- "language": language,
- "versions": {},
- "license": license,
- }
- if error:
- self._projects[id]["error"] = error
+ self._projects[id] = ProjectInfo(
+ type=ResourceType(type),
+ label=label,
+ language=language,
+ license=license,
+ error=error,
+ versions={},
+ )
def add_project_version(
self,
@@ -142,22 +188,19 @@ class WNConfig:
is accessed
"""
- version_data: dict[str, Any]
- if url and not error:
- version_data = {"resource_urls": url.split()}
- elif error and not url:
- version_data = {"error": error}
- elif url and error:
+ if url and error:
spec = format_lexicon_specifier(id, version)
raise ConfigurationError(f"{spec} specifies both url and redirect")
- else:
- version_data = {}
- if license:
- version_data["license"] = license
+
+ version_data = VersionInfo(
+ resource_urls=url.split() if (url and not error) else [],
+ license=license,
+ error=error,
+ )
project = self._projects[id]
project["versions"][version] = version_data
- def get_project_info(self, arg: str) -> dict:
+ def get_project_info(self, arg: str) -> ResolvedProjectInfo:
"""Return information about an indexed project version.
If the project has been downloaded and cached, the ``"cache"``
@@ -177,8 +220,8 @@ class WNConfig:
id, version = split_lexicon_specifier(arg)
if id not in self._projects:
raise ProjectError(f"no such project id: {id}")
- project: dict = self._projects[id]
- if "error" in project:
+ project: ProjectInfo = self._projects[id]
+ if project["error"]:
raise ProjectError(project["error"])
versions: dict = project["versions"]
@@ -189,33 +232,69 @@ class WNConfig:
elif version not in versions:
raise ProjectError(f"no such version: {version!r} ({id})")
info = versions[version]
- if "error" in info:
+ if info["error"]:
raise ProjectError(info["error"])
urls = info.get("resource_urls", [])
- return {
- "id": id,
- "version": version,
- "type": project["type"],
- "label": project["label"],
- "language": project["language"],
- "license": info.get("license", project.get("license")),
- "resource_urls": urls,
- "cache": _get_cache_path_for_urls(self, urls),
- }
+ return ResolvedProjectInfo(
+ id=id,
+ version=version,
+ type=project["type"],
+ label=project["label"],
+ language=project["language"],
+ license=info.get("license", project.get("license")),
+ resource_urls=urls,
+ cache=_get_cache_path_for_urls(self, urls),
+ )
def get_cache_path(self, url: str) -> Path:
"""Return the path for caching *url*.
- Note that in general this is just a path operation and does
+ Note that this is just a path operation and does
not signify that the file exists in the file system.
"""
filename = short_hash(url)
return self.downloads_directory / filename
- def update(self, data: dict) -> None:
+ def list_cache_entries(self, arg: str = "*") -> list[CacheEntry]:
+ """Return a list of cached resources.
+
+ Use *arg* as a pattern to match project specifiers. It
+ defaults to `"*"` to select all cached entries.
+
+ Each entry on the list is a dictionary with the keys:
+ * `"path"` -- the path of the cached file
+ * `"id"` -- the ID of the cached resource
+ * `"version"` -- the version of the cached resource
+ * `"url"` -- the URL of the cached resource
+
+ Note that cached files are stored with a hash of their URL as
+ the filename and that it is not feasible to recover the URL
+ from the hash alone. Therefore, for lexicons downloaded with a
+ URL that does not appear in the index, the ID, version, and URL
+ and will be :python:`None` instead.
+ """
+ arg = arg.strip()
+ cache_map = _cache_map(self)
+ entries: list[CacheEntry] = []
+ for cache_path in self.downloads_directory.iterdir():
+ if cache_path in cache_map:
+ id, version, url = cache_map[cache_path]
+ specifier = format_lexicon_specifier(id, version)
+ if not (fnmatch(specifier, arg) or url == arg):
+ continue
+ entries.append(
+ CacheEntry(path=cache_path, id=id, version=version, url=url)
+ )
+ elif arg in ("*", "*:*"):
+ entries.append(
+ CacheEntry(path=cache_path, id=None, version=None, url=None)
+ )
+ return entries
+
+ def update(self, data: dict[str, Any]) -> None:
"""Update the configuration with items in *data*.
Items are only inserted or replaced, not deleted. If a project
@@ -225,9 +304,22 @@ class WNConfig:
to the indexed project.
"""
- if "data_directory" in data:
- self.data_directory = data["data_directory"]
- for id, project in data.get("index", {}).items():
+ if datadir := data.get("data_directory"):
+ if not isinstance(datadir, (str, Path)):
+ raise ConfigurationError(
+ "data_directory must be a str or Path, "
+ f"not {type(datadir).__name__}"
+ )
+ self.data_directory = datadir
+ if index := data.get("index", {}):
+ if not is_str_key_dict(index):
+ raise ConfigurationError("index must be a dict with str keys")
+ self._update_index(index)
+
+ def _update_index(self, index: dict[str, Any]) -> None:
+ for id, project in index.items():
+ if not is_str_key_dict(project):
+ raise ConfigurationError(f"invalid project: {project}")
if id in self._projects:
# validate that they are the same
_project = self._projects[id]
@@ -237,14 +329,14 @@ class WNConfig:
else:
self.add_project(
id,
- type=project.get("type", _WORDNET),
+ type=project.get("type", ResourceType.WORDNET),
label=project.get("label"),
language=project.get("language"),
license=project.get("license"),
error=project.get("error"),
)
for version, info in project.get("versions", {}).items():
- if "url" in info and "error" in project:
+ if info.get("url") and project.get("error"):
spec = format_lexicon_specifier(id, version)
raise ConfigurationError(f"{spec} url specified with default error")
self.add_project_version(
@@ -281,6 +373,8 @@ class WNConfig:
index = tomllib.load(indexfile)
except tomllib.TOMLDecodeError as exc:
raise ConfigurationError("malformed index file") from exc
+ if not is_str_key_dict(index):
+ raise ConfigurationError("invalid index file")
self.update({"index": index})
@@ -295,5 +389,21 @@ def _get_cache_path_for_urls(
return None
+def _cache_map(config: WNConfig) -> dict[Path, tuple[str, str, str]]:
+ """Return a dict of cache hashes to resource info tuples.
+
+ Each tuple contains the id, version, and URL of the indexed
+ resource. The hash is based on the URL and the tuple only contains
+ information from the index. They do not indicate whether the
+ resource has been cached.
+ """
+ return {
+ config.get_cache_path(url): (id, version, url)
+ for id, p_info in config.index.items()
+ for version, v_info in p_info["versions"].items()
+ for url in v_info["resource_urls"]
+ }
+
+
config = WNConfig()
config.load_index(INDEX_FILE_PATH)
diff --git a/wn/_core.py b/wn/_core.py
index fdf62c0..5fecaa3 100644
--- a/wn/_core.py
+++ b/wn/_core.py
@@ -946,8 +946,11 @@ class Synset(_Relatable):
if not ili:
return []
lexicons = resolve_lexicon_specifiers(lexicon=(lexicon or "*"), lang=lang)
+ lexconf = LexiconConfiguration(
+ lexicons=tuple(lexicons), expands=(), default_mode=False
+ )
return [
- Synset(*data, _lexconf=self._lexconf)
+ Synset(*data, _lexconf=lexconf)
for data in get_synsets_for_ilis((ili,), lexicons)
]
diff --git a/wn/_db.py b/wn/_db.py
index 93fe587..1ea371d 100644
--- a/wn/_db.py
+++ b/wn/_db.py
@@ -34,7 +34,7 @@ DEBUG = False
# >>> wn._db.schema_hash(conn)
#
COMPATIBLE_SCHEMA_HASHES = {
- "8348fc1a6254f514294a1dc70458e0733742935d",
+ "f439c9bd27f809f64ee42896fb0fc20c5d00fd99",
}
@@ -144,7 +144,13 @@ def list_lexicons_safe(conn: sqlite3.Connection | None = None) -> list[str]:
def schema_hash(conn: sqlite3.Connection) -> str:
- query = "SELECT sql FROM sqlite_master WHERE NOT sql ISNULL"
+ query = """
+ SELECT sql
+ FROM sqlite_schema
+ WHERE NOT sql ISNULL
+ AND name NOT LIKE 'sqlite_stat%'
+ ORDER BY sql ASC
+ """
schema = "\n\n".join(row[0] for row in conn.execute(query))
return short_hash(schema)
diff --git a/wn/_module_functions.py b/wn/_module_functions.py
index 1f6f362..f2beac3 100644
--- a/wn/_module_functions.py
+++ b/wn/_module_functions.py
@@ -1,6 +1,6 @@
from typing import Literal, overload
-from wn._config import config
+from wn._config import ResolvedProjectInfo, config
from wn._core import Form, Sense, Synset, Word
from wn._db import clear_connections, connect, list_lexicons_safe
from wn._download import download
@@ -10,7 +10,7 @@ from wn._util import format_lexicon_specifier
from wn._wordnet import Wordnet
-def projects() -> list[dict]:
+def projects() -> list[ResolvedProjectInfo]:
"""Return the list of indexed projects.
This returns the same dictionaries of information as
@@ -32,7 +32,7 @@ def projects() -> list[dict]:
config.get_project_info(format_lexicon_specifier(project_id, version))
for project_id, project_info in index.items()
for version in project_info.get("versions", [])
- if "resource_urls" in project_info["versions"][version]
+ if not project_info["versions"][version]["error"]
]
diff --git a/wn/_util.py b/wn/_util.py
index 7d89cd2..de9feda 100644
--- a/wn/_util.py
+++ b/wn/_util.py
@@ -3,7 +3,7 @@
import hashlib
from collections.abc import Hashable, Iterable
from pathlib import Path
-from typing import TypeVar
+from typing import Any, TypeGuard, TypeVar
from unicodedata import combining, normalize
from wn._types import VersionInfo
@@ -34,6 +34,10 @@ def is_xml(path: Path) -> bool:
return _inspect_file_signature(path, b"<?xml ")
+def is_str_key_dict(obj: Any) -> TypeGuard[dict[str, Any]]:
+ return isinstance(obj, dict) and all(isinstance(key, str) for key in obj)
+
+
def _inspect_file_signature(path: Path, signature: bytes) -> bool:
if path.is_file():
with path.open("rb") as f:
diff --git a/wn/constants.py b/wn/constants.py
index 32b0da8..01a3ae8 100644
--- a/wn/constants.py
+++ b/wn/constants.py
@@ -342,8 +342,3 @@ LEXICOGRAPHER_FILES = {
"verb.weather": 43,
"adj.ppl": 44,
}
-
-# resource types
-
-_WORDNET = "wordnet"
-_ILI = "ili"
diff --git a/wn/project.py b/wn/project.py
index e69262f..f5c22ad 100644
--- a/wn/project.py
+++ b/wn/project.py
@@ -11,11 +11,10 @@ from collections.abc import Iterator
from pathlib import Path
from wn import ili, lmf
-from wn._config import config
+from wn._config import ResourceType, config
from wn._exceptions import Error
from wn._types import AnyPath
from wn._util import is_gzip, is_lzma
-from wn.constants import _ILI, _WORDNET
_ADDITIONAL_FILE_SUFFIXES = ("", ".txt", ".md", ".rst")
@@ -38,9 +37,9 @@ def _package_directory_types(path: Path) -> list[tuple[Path, str]]:
def _resource_file_type(path: Path) -> str | None:
if lmf.is_lmf(path):
- return _WORDNET
+ return ResourceType.WORDNET
elif ili.is_ili_tsv(path):
- return _ILI
+ return ResourceType.ILI
return None