Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, "3.10", 3.11, 3.12, 3.13]
python-version: ["3.10", 3.11, 3.12, 3.13, 3.14]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -26,4 +26,4 @@ jobs:
pip install .[test]
- name: Test with pytest
run: |
pytest
pytest -m 'not with_catalog and not with_internet'
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
# Changes

## [1.14.3]
## unreleased

- Fixed issue where `makecldf` could not be run on a dataset in a git repos with no commits.
- Drop py3.8 compat.
- Removed dependency on requests and attrs.

Note: Functionality requiring `pyglottolog` or `pyconcepticon` will only work once versions of
these packages are released which are compatible with `clldutils` 4.x.

### Backwards incompatible changes

- removed `utils.get_url` function.
- `metadata.Metadata` is no longer an `attrs`-decorated class, so inheriting classes (to implement
custom scaffold metadata) must be changed to `dataclasses`.
- Pin dependencies for packages which are about to get incompatible new major versions.
- Last version of the 1.x series.

Expand Down
4 changes: 4 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
```shell
flake8 src
```
- Make sure pylint passes with a score of 10:
```shell
pylint src
```

- Make sure the docs render:
```shell
Expand Down
20 changes: 10 additions & 10 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ classifiers =
Natural Language :: English
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: 3.14
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
License :: OSI Approved :: Apache Software License
Expand All @@ -32,19 +32,16 @@ classifiers =
packages = find:
package_dir =
= src
python_requires = >=3.8
python_requires = >=3.9
install_requires =
# Pin until 2.0.1 is released, see https://github.com/python-hyper/rfc3986/issues/107
rfc3986<2
csvw<4
clldutils<4
csvw>=4.0
clldutils>=4.0
cldfcatalog>=1.5.1
pycldf<2
pycldf>=2.0
termcolor
requests
appdirs
pytest
zenodoclient>=0.3
simplepybtex
tqdm

Expand Down Expand Up @@ -99,6 +96,9 @@ max-line-length = 100
exclude = .tox

[tool:pytest]
markers =
with_catalog: mark a test requiring a catalog (Glottolog or Concepticon).
with_internet: test requiring an internet connection.
minversion = 5
testpaths = tests
addopts = --cov
Expand All @@ -116,10 +116,10 @@ show_missing = true
skip_covered = true

[tox:tox]
envlist = py38, py39, py310, py311, py312, py313
envlist = py39, py310, py311, py312, py313, py314
isolated_build = true
skip_missing_interpreter = true

[testenv]
deps = .[test]
commands = pytest {posargs}
commands = pytest -m 'not with_catalog' {posargs}
8 changes: 7 additions & 1 deletion src/cldfbench/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
#
"""
The cldfbench package.

.. seealso::

https://aclanthology.org/anthology-files/anthology-files/pdf/lrec/2020.lrec-1.864.pdf
"""
from cldfbench.dataset import * # noqa: F401, F403
from cldfbench.cldf import * # noqa: F401, F403
from cldfbench.metadata import * # noqa: F401, F403
Expand Down
89 changes: 53 additions & 36 deletions src/cldfbench/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,59 @@
"""
import csv
import sys
import argparse
import contextlib
from typing import Optional

from clldutils.clilib import (
register_subcommands, get_parser_and_subparsers, ParserError, add_csv_field_size_limit,
add_random_seed,
)
from clldutils.loglib import Logging
from cldfcatalog import Config
import termcolor
import argparse

import cldfbench
from cldfbench.catalogs import BUILTIN_CATALOGS
from cldfbench.cli_util import IGNORE_MISSING
from cldfbench.util import colored
import cldfbench.commands


def main(args=None, catch_all=False, parsed_args=None, log=None):
def print_red(text, **kw): # pylint: disable=C0116
print(colored('red', text, **kw))


def _add_catalog(
cls: type,
cfg: Config,
args: argparse.Namespace,
stack: contextlib.ExitStack,
) -> tuple[Optional[Exception], bool]:
"""Catalogs are context managers, so they have to be added to the exit stack."""
name = cls.cli_name()
if not hasattr(args, name):
return None, False
path = getattr(args, name)
from_cfg = False
if path != IGNORE_MISSING:
if (not path) and (not args.no_config):
try:
path = cfg.get_clone(name)
from_cfg = True
except KeyError as e: # pragma: no cover
return e, False
try:
version = getattr(args, name + '_version', None)
setattr(args, name, stack.enter_context(cls(path, version)))
assert getattr(args, name).api
except ValueError as e:
return e, from_cfg
else:
setattr(args, name, None) # pragma: no cover
return None, False


def main(args=None, catch_all=False, parsed_args=None, log=None): # pylint: disable=C0116,R0911
parser, subparsers = get_parser_and_subparsers(cldfbench.__name__)

# We add a "hidden" option to turn-off config file reading in tests:
Expand All @@ -42,6 +77,10 @@ def main(args=None, catch_all=False, parsed_args=None, log=None):
parser.print_help()
return 1

def cmd_help(err):
print_red(err + '\n', attrs={'bold'})
return main([args._command, '-h']) # pylint: disable=W0212

with contextlib.ExitStack() as stack:
if not log: # pragma: no cover
stack.enter_context(Logging(args.log, level=args.log_level))
Expand All @@ -54,47 +93,25 @@ def main(args=None, catch_all=False, parsed_args=None, log=None):
for cls in BUILTIN_CATALOGS:
# Now we loop over known catalogs, see whether they are used by the command,
# and if so, "enter" the catalog.
name, from_cfg = cls.cli_name(), False
if hasattr(args, name):
# If no path was passed on the command line, we look up the config:
path = getattr(args, name)
if path != IGNORE_MISSING:
if (not path) and (not args.no_config):
try:
path = cfg.get_clone(name)
from_cfg = True
except KeyError as e: # pragma: no cover
print(termcolor.colored(str(e) + '\n', 'red'))
return main([args._command, '-h'])
try:
setattr(
args,
name,
stack.enter_context(
cls(path, getattr(args, name + '_version', None))),
)
assert getattr(args, name).api
except ValueError as e:
print(termcolor.colored(
'\nError initializing catalog {0}'.format(name), 'red'))
if from_cfg:
print(
termcolor.colored('from config {0}'.format(cfg.fname()), 'red'))
print(termcolor.colored(str(e) + '\n', 'red'))
return main([args._command, '-h'])
else:
setattr(args, name, None) # pragma: no cover
e, from_cfg = _add_catalog(cls, cfg, args, stack)
if isinstance(e, KeyError): # pragma: no cover
return cmd_help(str(e))
if isinstance(e, ValueError):
print_red(f'\nError initializing catalog {cls.cli_name()}')
if from_cfg:
print_red(f'from config {cfg.fname()}')
return cmd_help(str(e))
assert e is None

try:
return args.main(args) or 0
except KeyboardInterrupt: # pragma: no cover
return 0
except ParserError as e:
print(termcolor.colored('ERROR: {}\n'.format(e), 'red', attrs={'bold'}))
return main([args._command, '-h'])
return cmd_help(f'ERROR: {e}')
except Exception as e:
if catch_all: # pragma: no cover
print(termcolor.colored('ERROR: {}\n'.format(e), 'red', attrs={'bold'}))
print_red(f'ERROR: {e}\n', attrs={'bold'})
return 1
raise

Expand Down
24 changes: 24 additions & 0 deletions src/cldfbench/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Backwards compatibility with supported python versions.
"""
import sys
import datetime
import functools


if (sys.version_info.major, sys.version_info.minor) >= (3, 10): # pragma: no cover
def entry_points_select(eps, group):
"""
Staring with Python 3.10, `importlib.metadata.entry_points` returns `EntryPoints`."""
return eps.select(group=group)
else:
def entry_points_select(eps, group): # pragma: no cover
"""In Python 3.9, `importlib.metadata.entry_points` returns a `dict`."""
return eps.get(group, [])


if (sys.version_info.major, sys.version_info.minor) >= (3, 11): # pragma: no cover
# datetime.UTC was added in py3.11.
utcnow = functools.partial(datetime.datetime.now, datetime.UTC)
else: # pragma: no cover
utcnow = datetime.datetime.utcnow
46 changes: 24 additions & 22 deletions src/cldfbench/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,50 @@
- support to access the Python API for each catalog from the `Catalog` object,
- automatic registration of catalogs as provenance information when writing CLDF.
"""
import typing
from typing import Union, Optional
import functools

from cldfcatalog import Catalog
from clldutils.misc import lazyproperty

try: # pragma: no cover
import pyglottolog
from pyglottolog.languoids import Languoid
from pyglottolog.config import Macroarea

class CachingGlottologAPI(pyglottolog.Glottolog):
"""Wraps Glottolog to avoid expensive lookups."""
def __init__(self, p):
super().__init__(p)
self.__languoids = None

def languoids(self, **kw):
def languoids(self, *args, **kw): # pylint: disable=C0116
if not kw:
if not self.__languoids:
self.__languoids = list(super().languoids())
return self.__languoids
return super().languoids(**kw)
return super().languoids(*args, **kw)

@lazyproperty
def cached_languoids(self) -> typing.Dict[str, Languoid]:
@functools.cached_property
def cached_languoids(self) -> dict[str, Languoid]: # pylint: disable=C0116
return {lang.id: lang for lang in self.languoids()}

@lazyproperty
def languoid_details(self) -> typing.Dict[str, typing.Tuple]:
@functools.cached_property
def languoid_details(self) -> dict[str, tuple[str, list, str]]: # pylint: disable=C0116
return {lid: (l.iso, l.macroareas, l.name) for lid, l in self.cached_languoids.items()}

@lazyproperty
def glottocode_by_name(self) -> typing.Dict[str, str]:
@functools.cached_property
def glottocode_by_name(self) -> dict[str, str]: # pylint: disable=C0116
return {l[2]: lid for lid, l in self.languoid_details.items()}

@lazyproperty
def glottocode_by_iso(self) -> typing.Dict[str, str]:
@functools.cached_property
def glottocode_by_iso(self) -> dict[str, str]: # pylint: disable=C0116
return {l[0]: lid for lid, l in self.languoid_details.items() if l[0]}

@lazyproperty
def macroareas_by_glottocode(self) -> typing.Dict[str, typing.List[Macroarea]]:
@functools.cached_property
def macroareas_by_glottocode(self) -> dict[str, list[Macroarea]]: # pylint: disable=C0116
return {lid: l[1] for lid, l in self.languoid_details.items()}

def get_language(self, languoid: typing.Union[str, Languoid]) \
-> typing.Union[Languoid, None]:
def get_language(self, languoid: Union[str, Languoid]) -> Optional[Languoid]:
"""
:param languoid: A languoid specified via Glottocode or passed as `Languoid` instance.
:return: Language-level languoid associated with `languoid` or `None` if `languoid` is \
Expand All @@ -59,34 +59,36 @@ def get_language(self, languoid: typing.Union[str, Languoid]) \
if isinstance(languoid, str):
languoid = self.cached_languoids[languoid]
if languoid.level == self.languoid_levels.family:
return
return None
if languoid.level == self.languoid_levels.language:
return languoid
for _, gc, _ in reversed(languoid.lineage):
parent = self.cached_languoids[gc]
if parent.level == self.languoid_levels.language:
return parent
return None


except ImportError: # pragma: no cover
CachingGlottologAPI = pyglottolog = 'pyglottolog'
CachingGlottologAPI = pyglottolog = 'pyglottolog' # pylint: disable=invalid-name

try: # pragma: no cover
import pyconcepticon

class CachingConcepticonAPI(pyconcepticon.Concepticon):
@lazyproperty
def cached_glosses(self):
"""Wraps Concepticon to avoid expensive file reads."""
@functools.cached_property
def cached_glosses(self) -> dict[int, str]: # pylint: disable=C0116
return {int(cs.id): cs.gloss for cs in self.conceptsets.values()}

except ImportError: # pragma: no cover
CachingConcepticonAPI = pyconcepticon = 'pyconcepticon'
CachingConcepticonAPI = pyconcepticon = 'pyconcepticon' # pylint: disable=invalid-name

try: # pragma: no cover
import pyclts

class CLTSAPI(pyclts.api.CLTS):
pass
"""Cross-Linguistic Transcription Systems API."""

except ImportError: # pragma: no cover
CLTSAPI = pyclts = 'pyclts'
Expand Down
Loading