pycldf 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {pycldf-2.1.0 → pycldf-2.2.0}/CHANGELOG.md +16 -0
  2. {pycldf-2.1.0 → pycldf-2.2.0}/PKG-INFO +4 -4
  3. {pycldf-2.1.0 → pycldf-2.2.0}/setup.cfg +5 -5
  4. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/__init__.py +1 -1
  5. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/dataset.py +8 -8
  6. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/ext/discovery.py +6 -11
  7. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/ext/markdown.py +11 -13
  8. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/fileutil.py +2 -2
  9. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/media.py +5 -5
  10. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/module.py +3 -3
  11. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/orm.py +19 -10
  12. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/schemautil.py +4 -6
  13. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/sliceutil.py +2 -2
  14. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/sources.py +6 -6
  15. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/terms.py +2 -2
  16. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/trees.py +5 -0
  17. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/urlutil.py +4 -4
  18. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/util.py +22 -12
  19. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/validators.py +14 -0
  20. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/PKG-INFO +4 -4
  21. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/SOURCES.txt +2 -1
  22. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/requires.txt +1 -3
  23. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_dataset.py +48 -1
  24. pycldf-2.2.0/tests/test_igt_validation.py +42 -0
  25. pycldf-2.2.0/tests/test_orm_primary_key_relations.py +45 -0
  26. pycldf-2.1.0/src/pycldf/_compat.py +0 -15
  27. {pycldf-2.1.0 → pycldf-2.2.0}/CONTRIBUTING.md +0 -0
  28. {pycldf-2.1.0 → pycldf-2.2.0}/LICENSE +0 -0
  29. {pycldf-2.1.0 → pycldf-2.2.0}/MANIFEST.in +0 -0
  30. {pycldf-2.1.0 → pycldf-2.2.0}/README.md +0 -0
  31. {pycldf-2.1.0 → pycldf-2.2.0}/RELEASING.md +0 -0
  32. {pycldf-2.1.0 → pycldf-2.2.0}/pyproject.toml +0 -0
  33. {pycldf-2.1.0 → pycldf-2.2.0}/setup.py +0 -0
  34. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/__main__.py +0 -0
  35. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/cli_util.py +0 -0
  36. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/__init__.py +0 -0
  37. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/catmedia.py +0 -0
  38. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/check.py +0 -0
  39. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/createdb.py +0 -0
  40. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/downloadmedia.py +0 -0
  41. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/dumpdb.py +0 -0
  42. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/markdown.py +0 -0
  43. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/splitmedia.py +0 -0
  44. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/stats.py +0 -0
  45. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/commands/validate.py +0 -0
  46. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/BorrowingTable-metadata.json +0 -0
  47. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/CodeTable-metadata.json +0 -0
  48. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/CognateTable-metadata.json +0 -0
  49. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/CognatesetTable-metadata.json +0 -0
  50. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/ContributionTable-metadata.json +0 -0
  51. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/EntryTable-metadata.json +0 -0
  52. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/ExampleTable-metadata.json +0 -0
  53. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/FormTable-metadata.json +0 -0
  54. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/FunctionalEquivalentTable-metadata.json +0 -0
  55. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/FunctionalEquivalentsetTable-metadata.json +0 -0
  56. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/LanguageTable-metadata.json +0 -0
  57. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/MediaTable-metadata.json +0 -0
  58. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/ParameterNetwork-metadata.json +0 -0
  59. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/ParameterTable-metadata.json +0 -0
  60. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/SenseTable-metadata.json +0 -0
  61. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/TreeTable-metadata.json +0 -0
  62. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/components/ValueTable-metadata.json +0 -0
  63. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/constraints.py +0 -0
  64. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/db.py +0 -0
  65. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/ext/__init__.py +0 -0
  66. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/ext/sql.py +0 -0
  67. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/markdown.py +0 -0
  68. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/Dictionary-metadata.json +0 -0
  69. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/Generic-metadata.json +0 -0
  70. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/ParallelText-metadata.json +0 -0
  71. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/StructureDataset-metadata.json +0 -0
  72. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/TextCorpus-metadata.json +0 -0
  73. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/modules/Wordlist-metadata.json +0 -0
  74. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/stats.py +0 -0
  75. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf/terms.rdf +0 -0
  76. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/dependency_links.txt +0 -0
  77. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/entry_points.txt +0 -0
  78. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/not-zip-safe +0 -0
  79. {pycldf-2.1.0 → pycldf-2.2.0}/src/pycldf.egg-info/top_level.txt +0 -0
  80. {pycldf-2.1.0 → pycldf-2.2.0}/test.md +0 -0
  81. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_cli.py +0 -0
  82. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_cli_util.py +0 -0
  83. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_db.py +0 -0
  84. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_ext_discovery.py +0 -0
  85. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_ext_markdown.py +0 -0
  86. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_ext_sql.py +0 -0
  87. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_media.py +0 -0
  88. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_misc.py +0 -0
  89. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_orm.py +0 -0
  90. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_paralleltext.py +0 -0
  91. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_sources.py +0 -0
  92. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_terms.py +0 -0
  93. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_trees.py +0 -0
  94. {pycldf-2.1.0 → pycldf-2.2.0}/tests/test_util.py +0 -0
@@ -3,6 +3,22 @@
3
3
  The `pycldf` package adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
4
4
 
5
5
 
6
+ ## [2.2.0] - 2026-11-11
7
+
8
+ - Fixed bug where missing words in glossed examples would lead to downstream
9
+ issues (thanks @antonsoo).
10
+ - Fixed bug where explicit foreign key declaration was not honored when
11
+ resolving list-valued foreign keys (thanks @antonsoo).
12
+ - Fixed documentation issue with confusing error message.
13
+ - Enhanced support for copying datasets (thanks @antonsoo).
14
+ - Dropped py3.9 support.
15
+
16
+
17
+ ## [2.1.1] - 2026-07-17
18
+
19
+ Fixed incomplete requirements.
20
+
21
+
6
22
  ## [2.1.0] - 2026-07-17
7
23
 
8
24
  - Added support for reading media file data from locally downloaded zip archives. This helps with the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycldf
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A python library to read and write CLDF datasets
5
5
  Home-page: https://github.com/cldf/pycldf
6
6
  Author: Robert Forkel
@@ -15,19 +15,19 @@ Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Natural Language :: English
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
24
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
25
  Classifier: Programming Language :: Python :: Implementation :: PyPy
26
26
  Classifier: License :: OSI Approved :: Apache Software License
27
- Requires-Python: >=3.9
27
+ Requires-Python: >=3.10
28
28
  Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
- Requires-Dist: python-frontmatter<1.2; python_version < "3.10"
30
+ Requires-Dist: python-frontmatter
31
31
  Requires-Dist: csvw>=4.0
32
32
  Requires-Dist: clldutils>=4.0
33
33
  Requires-Dist: uritemplate>=3.0
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pycldf
3
- version = 2.1.0
3
+ version = 2.2.0
4
4
  author = Robert Forkel
5
5
  author_email = robert_forkel@eva.mpg.de
6
6
  description = A python library to read and write CLDF datasets
@@ -20,12 +20,12 @@ classifiers =
20
20
  Natural Language :: English
21
21
  Operating System :: OS Independent
22
22
  Programming Language :: Python :: 3
23
- Programming Language :: Python :: 3.9
24
23
  Programming Language :: Python :: 3.10
25
24
  Programming Language :: Python :: 3.11
26
25
  Programming Language :: Python :: 3.12
27
26
  Programming Language :: Python :: 3.13
28
27
  Programming Language :: Python :: 3.14
28
+ Programming Language :: Python :: 3.15
29
29
  Programming Language :: Python :: Implementation :: CPython
30
30
  Programming Language :: Python :: Implementation :: PyPy
31
31
  License :: OSI Approved :: Apache Software License
@@ -35,9 +35,9 @@ zip_safe = False
35
35
  packages = find:
36
36
  package_dir =
37
37
  = src
38
- python_requires = >=3.9
38
+ python_requires = >=3.10
39
39
  install_requires =
40
- python-frontmatter<1.2;python_version<"3.10"
40
+ python-frontmatter
41
41
  csvw>=4.0
42
42
  clldutils>=4.0
43
43
  uritemplate>=3.0
@@ -116,7 +116,7 @@ show_missing = true
116
116
  skip_covered = true
117
117
 
118
118
  [tox:tox]
119
- envlist = py39, py310, py311, py312, py313, py314
119
+ envlist = py310, py311, py312, py313, py314, py315
120
120
  isolated_build = true
121
121
  skip_missing_interpreter = true
122
122
 
@@ -9,4 +9,4 @@ from pycldf.sources import *
9
9
  from pycldf.terms import *
10
10
 
11
11
  # flake8: noqa
12
- __version__ = "2.1.0"
12
+ __version__ = "2.2.0"
@@ -3,7 +3,7 @@ An implementation of a CLDF dataset object.
3
3
  """
4
4
  import re
5
5
  import types
6
- from typing import Union, Optional, Type, Any
6
+ from typing import Optional, Type, Any
7
7
  import logging
8
8
  import pathlib
9
9
  import functools
@@ -39,8 +39,8 @@ __all__ = [
39
39
  'TextCorpus', 'iter_datasets', 'sniff', 'SchemaError']
40
40
 
41
41
  ORM_CLASSES = {cls.component_name(): cls for cls in orm.Object.__subclasses__()}
42
- TableSpecType = Union[str, Link, Table]
43
- SchemaObjectType = Union[TableSpecType, tuple[TableSpecType, ColType]]
42
+ TableSpecType = str | Link | Table
43
+ SchemaObjectType = TableSpecType | tuple[TableSpecType, ColType]
44
44
  ODict = collections.OrderedDict
45
45
  RowType = ODict[str, Any]
46
46
 
@@ -285,7 +285,7 @@ class Dataset: # pylint: disable=too-many-public-methods
285
285
  pass
286
286
  return None
287
287
 
288
- def __getitem__(self, item: SchemaObjectType) -> Union[csvw.Table, csvw.Column]:
288
+ def __getitem__(self, item: SchemaObjectType) -> csvw.Table | csvw.Column:
289
289
  """
290
290
  Access to tables and columns.
291
291
 
@@ -365,7 +365,7 @@ class Dataset: # pylint: disable=too-many-public-methods
365
365
  """
366
366
  return bool(self.get(item))
367
367
 
368
- def get(self, item: SchemaObjectType, default=None) -> Union[csvw.Table, csvw.Column, None]:
368
+ def get(self, item: SchemaObjectType, default=None) -> csvw.Table | csvw.Column | None:
369
369
  """
370
370
  Acts like `dict.get`.
371
371
 
@@ -485,7 +485,7 @@ class Dataset: # pylint: disable=too-many-public-methods
485
485
  # Now remove the table:
486
486
  self.tablegroup.tables = [t for t in self.tablegroup.tables if t.url != table.url]
487
487
 
488
- def add_component(self, component: Union[str, dict], *cols: ColSpecType, **kw) -> csvw.Table:
488
+ def add_component(self, component: str | dict, *cols: ColSpecType, **kw) -> csvw.Table:
489
489
  """
490
490
  Add a CLDF component to a dataset.
491
491
 
@@ -632,7 +632,7 @@ class Dataset: # pylint: disable=too-many-public-methods
632
632
  return add_auto_constraints(self, component)
633
633
 
634
634
  # Add data -------------------------------------------------------------------------------------
635
- def add_sources(self, *sources: Union[str, Source], **kw) -> None:
635
+ def add_sources(self, *sources: str | Source, **kw) -> None:
636
636
  """
637
637
  Add sources to the dataset.
638
638
 
@@ -676,7 +676,7 @@ class Dataset: # pylint: disable=too-many-public-methods
676
676
  return row
677
677
  raise ValueError(id_) # pragma: no cover
678
678
 
679
- def get_row_url(self, table: TableType, row: Union[RowType, str]) -> Optional[str]:
679
+ def get_row_url(self, table: TableType, row: RowType | str) -> Optional[str]:
680
680
  """
681
681
  Get a URL associated with a row. Tables can specify associated row URLs by
682
682
 
@@ -16,7 +16,7 @@ Additional resolvers:
16
16
  resolver for DOI URLs pointing to the Zenodo archive.
17
17
  """
18
18
  import re
19
- from typing import Optional, Union
19
+ from typing import Optional
20
20
  import pathlib
21
21
  import zipfile
22
22
  import warnings
@@ -29,7 +29,6 @@ from csvw.utils import is_url
29
29
 
30
30
  from pycldf import Dataset, iter_datasets, sniff
31
31
  from pycldf.urlutil import url_without_fragment
32
- from pycldf._compat import entry_points_select
33
32
 
34
33
  __all__ = ['get_dataset', 'DatasetResolver']
35
34
  EP = 'pycldf_dataset_resolver'
@@ -47,11 +46,7 @@ class DatasetResolver: # pylint: disable=R0903
47
46
  """
48
47
  priority = 5
49
48
 
50
- def __call__(
51
- self,
52
- loc: str,
53
- download_dir: pathlib.Path,
54
- ) -> Union[None, Dataset, pathlib.Path]:
49
+ def __call__(self, loc: str, download_dir: pathlib.Path) -> None | Dataset | pathlib.Path:
55
50
  """
56
51
  :param loc: URL pointing to a place where datasets are archived.
57
52
  :param download_dir: A directory to which resolvers can download data.
@@ -145,7 +140,7 @@ class DatasetLocator(str):
145
140
  def get_resolvers() -> list[type]:
146
141
  """Register resolvers defined via entry points."""
147
142
  if not _resolvers:
148
- for ep in set(entry_points_select(entry_points(), EP)):
143
+ for ep in set(entry_points().select(group=EP)):
149
144
  try:
150
145
  _resolvers.append(ep.load()())
151
146
  except ImportError: # pragma: no cover
@@ -156,9 +151,9 @@ def get_resolvers() -> list[type]:
156
151
 
157
152
  def _get_dataset(
158
153
  locator: DatasetLocator,
159
- location: Union[None, Dataset, pathlib.Path],
154
+ location: None | Dataset | pathlib.Path,
160
155
  get_all: bool = False,
161
- ) -> Optional[Union[Dataset, list[Dataset]]]:
156
+ ) -> Optional[Dataset | list[Dataset]]:
162
157
  """Determine whether locator matches location and if so, resolve to a Dataset instance."""
163
158
  if isinstance(location, Dataset):
164
159
  if locator.match(location):
@@ -180,7 +175,7 @@ def _get_dataset(
180
175
  def get_dataset(locator: str,
181
176
  download_dir: pathlib.Path,
182
177
  base: Optional[pathlib.Path] = None,
183
- get_all: bool = False) -> Union[Dataset, list[Dataset]]:
178
+ get_all: bool = False) -> Dataset | list[Dataset]:
184
179
  """
185
180
  :param locator: Dataset locator as specified in "Dataset discovery".
186
181
  :param download_dir: Directory to which to download remote data if necessary.
@@ -4,7 +4,7 @@ This module provides tools to build a CLDF Markdown renderer.
4
4
  For an example, see :class:`FilenameToComponent`.
5
5
  """
6
6
  import re
7
- from typing import Optional, Union, Any
7
+ from typing import Optional, Any
8
8
  import pathlib
9
9
  import warnings
10
10
  import collections
@@ -65,7 +65,7 @@ class DatasetMapping(Mapping):
65
65
  if not isinstance(self.m[k], Dataset):
66
66
  self.m[k] = get_dataset(self.m[k], download_dir, doc_path)
67
67
 
68
- def __getitem__(self, prefix: Union[str, None]) -> Dataset:
68
+ def __getitem__(self, prefix: str | None) -> Dataset:
69
69
  """
70
70
  Get a `Dataset` mapped to a prefix.
71
71
  """
@@ -130,9 +130,7 @@ class CLDFMarkdownLink(MarkdownLink):
130
130
  return None # pragma: no cover
131
131
 
132
132
  def component(
133
- self,
134
- cldf: Optional[Union[Dataset, dict[str, Dataset], DatasetMapping]] = None,
135
- ) -> Union[str, None]:
133
+ self, cldf: None | Dataset | dict[str, Dataset] | DatasetMapping = None) -> str | None:
136
134
  """
137
135
  :param cldf: `pycldf.Dataset` instance to which the link refers.
138
136
  :return: Name of the CLDF component the link pertains to or `None`.
@@ -173,7 +171,7 @@ class CLDFMarkdownLink(MarkdownLink):
173
171
  """
174
172
  return self.objid == '__all__'
175
173
 
176
- def get_row(self, cldf: Union[Dataset, DatasetMapping]) -> dict:
174
+ def get_row(self, cldf: Dataset | DatasetMapping) -> dict:
177
175
  """
178
176
  Resolve the reference in a CLDF Markdown link to a row (`dict`) in the CLDF `Dataset`.
179
177
  """
@@ -181,7 +179,7 @@ class CLDFMarkdownLink(MarkdownLink):
181
179
  ds = DatasetMapping(cldf)[self.prefix]
182
180
  return ds.get_row(self.component(cldf=ds), self.objid)
183
181
 
184
- def get_object(self, cldf: Union[Dataset, DatasetMapping]) -> orm.Object:
182
+ def get_object(self, cldf: Dataset | DatasetMapping) -> orm.Object:
185
183
  """
186
184
  Resolve the reference in a CLDF Markdown link to an ORM object in the CLDF `Dataset`.
187
185
  """
@@ -219,8 +217,8 @@ class CLDFMarkdownText:
219
217
  :cvar metadata_component: Name of the special "Metadata" component.
220
218
  """
221
219
  def __init__(self,
222
- text: Union[pathlib.Path, str],
223
- dataset_mapping: Optional[Union[str, Dataset, dict]] = None,
220
+ text: pathlib.Path | str,
221
+ dataset_mapping: Optional[str | Dataset | dict] = None,
224
222
  download_dir: Optional[pathlib.Path] = None):
225
223
  """
226
224
  :param text: CLDF Markdown text either to be read from a path or specified as `str`.
@@ -230,7 +228,7 @@ class CLDFMarkdownText:
230
228
  """
231
229
  p = frontmatter.loads(text) if isinstance(text, str) else frontmatter.load(str(text))
232
230
  self.metadata: dict[str, Any] = p.metadata
233
- self.dataset_mapping: Mapping[Union[str, None], Dataset] = DatasetMapping(
231
+ self.dataset_mapping: Mapping[str | None, Dataset] = DatasetMapping(
234
232
  p.get(DATASETS_MAPPING),
235
233
  dataset_mapping,
236
234
  text.parent if isinstance(text, pathlib.Path) else None,
@@ -249,7 +247,7 @@ class CLDFMarkdownText:
249
247
  """
250
248
  return f'---\n{yaml.dump(self.metadata)}---'
251
249
 
252
- def get_object(self, ml: CLDFMarkdownLink) -> Union[list, orm.Object, Source, dict]:
250
+ def get_object(self, ml: CLDFMarkdownLink) -> list | orm.Object | Source | dict:
253
251
  """
254
252
  Resolve the reference in a CLDF Markdown link to the matching object from a mapped dataset.
255
253
 
@@ -279,13 +277,13 @@ class CLDFMarkdownText:
279
277
  return list(self._datadict[ml.prefix][key].values()) if ml.all \
280
278
  else self._datadict[ml.prefix][key][ml.objid]
281
279
 
282
- def _render_link(self, link: CLDFMarkdownLink) -> Union[str, CLDFMarkdownLink]:
280
+ def _render_link(self, link: CLDFMarkdownLink) -> str | CLDFMarkdownLink:
283
281
  """Dispatches to custom rendering in case of CLDF links."""
284
282
  if link.is_cldf_link:
285
283
  return self.render_link(link)
286
284
  return link
287
285
 
288
- def render_link(self, cldf_link: CLDFMarkdownLink) -> Union[str, CLDFMarkdownLink]:
286
+ def render_link(self, cldf_link: CLDFMarkdownLink) -> str | CLDFMarkdownLink:
289
287
  """
290
288
  CLDF Markdown renderers must implement this method.
291
289
  """
@@ -4,12 +4,12 @@ Functionality to access and manipulate files.
4
4
  import re
5
5
  import math
6
6
  import string
7
- from typing import Union, Optional
7
+ from typing import Optional
8
8
  import pathlib
9
9
  import itertools
10
10
 
11
11
 
12
- PathType = Union[str, pathlib.Path]
12
+ PathType = str | pathlib.Path
13
13
 
14
14
 
15
15
  def splitfile(p: PathType, chunksize: int, total: Optional[int] = None) -> list[pathlib.Path]:
@@ -24,7 +24,7 @@ or instantiate a :class:`.File` from a :class:`pycldf.orm.Object`:
24
24
  import io
25
25
  import json
26
26
  import base64
27
- from typing import Union, TYPE_CHECKING, Optional, Callable
27
+ from typing import TYPE_CHECKING, Optional, Callable
28
28
  import pathlib
29
29
  import zipfile
30
30
  import functools
@@ -47,7 +47,7 @@ if TYPE_CHECKING:
47
47
 
48
48
  __all__ = ['Mimetype', 'MediaTable', 'File']
49
49
 
50
- StrOrBytes = Union[str, bytes]
50
+ StrOrBytes = str | bytes
51
51
 
52
52
 
53
53
  class File: # pylint: disable=too-many-instance-attributes
@@ -91,7 +91,7 @@ class File: # pylint: disable=too-many-instance-attributes
91
91
 
92
92
  @classmethod
93
93
  def from_dataset(
94
- cls, ds: 'Dataset', row_or_object: Union[dict, orm.Media]) -> 'File':
94
+ cls, ds: 'Dataset', row_or_object: dict | orm.Media) -> 'File':
95
95
  """
96
96
  Factory method to instantiate a `File` bypassing the `Media` wrapper.
97
97
  """
@@ -350,7 +350,7 @@ class Mimetype:
350
350
  return self.type == 'text'
351
351
 
352
352
  @property
353
- def extension(self) -> Union[None, str]:
353
+ def extension(self) -> None | str:
354
354
  """Return a suitable filename extension for the mimetype."""
355
355
  return mimetypes.guess_extension(f'{self.type}/{self.subtype}')
356
356
 
@@ -360,7 +360,7 @@ class Mimetype:
360
360
  return data.decode(self.encoding)
361
361
  return data
362
362
 
363
- def write(self, data: StrOrBytes, p: Optional[pathlib.Path] = None) -> Union[int, StrOrBytes]:
363
+ def write(self, data: StrOrBytes, p: Optional[pathlib.Path] = None) -> int | StrOrBytes:
364
364
  """The mimetype dictates how/if to encode data."""
365
365
  res = data.encode(self.encoding) if self.is_text else data
366
366
  return p.write_bytes(res) if p else res
@@ -2,7 +2,7 @@
2
2
  Functionality to manage modules, i.e. `Dataset` subclasses implementing particular CLDF modules.
3
3
  """
4
4
  import dataclasses
5
- from typing import Union, Optional, Type
5
+ from typing import Optional, Type
6
6
 
7
7
  from csvw.metadata import TableGroup
8
8
 
@@ -33,7 +33,7 @@ class Module:
33
33
  """
34
34
  return self.uri.split('#')[1]
35
35
 
36
- def match(self, thing: Union[TableGroup, str]) -> bool:
36
+ def match(self, thing: TableGroup | str) -> bool:
37
37
  """Check if the module described here matches thing."""
38
38
  if isinstance(thing, TableGroup):
39
39
  return thing.common_props.get('dc:conformsTo') == term_uri(self.id)
@@ -45,7 +45,7 @@ class Module:
45
45
  _modules = []
46
46
 
47
47
 
48
- def get_module_impl(base_class, spec: Union[TableGroup, str]) -> Optional[Type]:
48
+ def get_module_impl(base_class, spec: TableGroup | str) -> Optional[Type]:
49
49
  """
50
50
  Returns an implementation (aka Dataset subclass) for a particular CLDF module.
51
51
  """
@@ -46,7 +46,7 @@ Limitations:
46
46
  * ~35secs iterating over ``pycldf.Dataset.objects('ValueTable')``
47
47
  """
48
48
  import types
49
- from typing import TYPE_CHECKING, Union, Optional, Any
49
+ from typing import TYPE_CHECKING, Optional, Any
50
50
  import decimal
51
51
  import functools
52
52
  import collections
@@ -63,7 +63,7 @@ if TYPE_CHECKING:
63
63
  from pycldf.media import File # pragma: no cover
64
64
 
65
65
 
66
- def to_json(s: Any) -> Union[str, float, None, list, dict]:
66
+ def to_json(s: Any) -> str | float | int | None | list | dict:
67
67
  """Converts `s` to an object that can be serialized as JSON."""
68
68
  if isinstance(s, (list, tuple)):
69
69
  return [to_json(ss) for ss in s]
@@ -162,19 +162,19 @@ class Object: # pylint: disable=too-many-instance-attributes
162
162
  return getattr(col, attr).expand(**variables)
163
163
  return None # pragma: no cover
164
164
 
165
- def aboutUrl(self, col: str = 'id') -> Union[str, None]: # pylint: disable=invalid-name
165
+ def aboutUrl(self, col: str = 'id') -> str | None: # pylint: disable=invalid-name
166
166
  """
167
167
  The table's `aboutUrl` property, expanded with the object's row as context.
168
168
  """
169
169
  return self._expand_uritemplate('aboutUrl', col)
170
170
 
171
- def valueUrl(self, col: str = 'id') -> Union[str, None]: # pylint: disable=invalid-name
171
+ def valueUrl(self, col: str = 'id') -> str | None: # pylint: disable=invalid-name
172
172
  """
173
173
  The table's `valueUrl` property, expanded with the object's row as context.
174
174
  """
175
175
  return self._expand_uritemplate('valueUrl', col)
176
176
 
177
- def propertyUrl(self, col: str = 'id') -> Union[str, None]: # pylint: disable=invalid-name
177
+ def propertyUrl(self, col: str = 'id') -> str | None: # pylint: disable=invalid-name
178
178
  """
179
179
  The table's `propertyUrl` property, expanded with the object's row as context.
180
180
  """
@@ -218,7 +218,7 @@ class Object: # pylint: disable=too-many-instance-attributes
218
218
  'referencing columns other than CLDF id or primary key.')
219
219
  return None # pragma: no cover
220
220
 
221
- def all_related(self, relation: str) -> Union[DictTuple, list]:
221
+ def all_related(self, relation: str) -> DictTuple | list:
222
222
  """
223
223
  CLDF reference properties can be list-valued. This method returns all related objects for
224
224
  such a property.
@@ -227,7 +227,16 @@ class Object: # pylint: disable=too-many-instance-attributes
227
227
  if fks and not isinstance(fks, list):
228
228
  fks = [fks]
229
229
  if fks:
230
- return DictTuple(self.dataset.get_object(TERMS[relation].references, fk) for fk in fks)
230
+ pk = False
231
+ ref = self.dataset.get_foreign_key_reference(self.component_name(), relation)
232
+ if ref and str(ref[1].propertyUrl) != term_uri('id'):
233
+ if [ref[1].name] != self.dataset[TERMS[relation].references].tableSchema.primaryKey:
234
+ raise NotImplementedError(
235
+ 'pycldf does not support foreign key constraints '
236
+ 'referencing columns other than CLDF id or primary key.')
237
+ pk = True
238
+ return DictTuple(
239
+ self.dataset.get_object(TERMS[relation].references, fk, pk=pk) for fk in fks)
231
240
  return []
232
241
 
233
242
 
@@ -237,7 +246,7 @@ class _WithLanguageMixin:
237
246
  return self.related('languageReference')
238
247
 
239
248
  @property
240
- def languages(self) -> Union[DictTuple, list]: # pylint: disable=C0116
249
+ def languages(self) -> DictTuple | list: # pylint: disable=C0116
241
250
  return self.all_related('languageReference')
242
251
 
243
252
 
@@ -247,7 +256,7 @@ class _WithParameterMixin:
247
256
  return self.related('parameterReference')
248
257
 
249
258
  @property
250
- def parameters(self) -> Union[DictTuple, list]: # pylint: disable=C0116
259
+ def parameters(self) -> DictTuple | list: # pylint: disable=C0116
251
260
  return self.all_related('parameterReference')
252
261
 
253
262
 
@@ -379,7 +388,7 @@ class Language(Object):
379
388
  return None # pragma: no cover
380
389
 
381
390
  @property
382
- def as_geojson_feature(self) -> Union[None, dict[str, Any]]:
391
+ def as_geojson_feature(self) -> None | dict[str, Any]:
383
392
  """
384
393
  `dict` suitable for serialization as GeoJSON Feature object, with the point coordinate as
385
394
  geographic data.
@@ -1,18 +1,16 @@
1
1
  """
2
2
  Functionality to create schema objects.
3
3
  """
4
- from typing import Union
5
-
6
4
  from csvw.metadata import Column, Table
7
5
  from clldutils import jsonlib
8
6
 
9
7
  from pycldf.terms import TERMS
10
8
  from pycldf.util import MD_SUFFIX, pkg_path
11
9
 
12
- ColSpecType = Union[str, dict, Column]
13
- TableSpecType = Union[str, dict, Table]
14
- TableType = Union[str, Table]
15
- ColType = Union[str, Column]
10
+ ColSpecType = str | dict | Column
11
+ TableSpecType = str | dict | Table
12
+ TableType = str | Table
13
+ ColType = str | Column
16
14
 
17
15
 
18
16
  def make_column(spec: ColSpecType) -> Column:
@@ -5,14 +5,14 @@ In addition to Python's way of specifying slices as triples of integers (start,
5
5
  allow specification as strings like '1' or '2:5', where the numbers are interpreted as **1-based**
6
6
  indices, specifying **inclusive** boundaries. I.e. '2:5' is equivalent to `slice(1:5).`
7
7
  """
8
- from typing import Union, TypeVar
8
+ from typing import TypeVar
9
9
  import itertools
10
10
  from collections.abc import Sequence, Iterable
11
11
 
12
12
  __all__ = ['multislice', 'multislice_with_split']
13
13
 
14
14
  T = TypeVar('T')
15
- SliceType = Union[str, tuple[int], tuple[int, int], tuple[int, int, int], slice]
15
+ SliceType = str | tuple[int] | tuple[int, int] | tuple[int, int, int] | slice
16
16
 
17
17
 
18
18
  def multislice(sliceable: Sequence[T], *slices: SliceType) -> Sequence[T]:
@@ -3,7 +3,7 @@ Functionality to handle BibTeX source data of Datasets.
3
3
  """
4
4
  import re
5
5
  import types
6
- from typing import Optional, Union, Literal
6
+ from typing import Optional, Literal
7
7
  import pathlib
8
8
  import zipfile
9
9
  import tempfile
@@ -91,7 +91,7 @@ class Source(BaseSource):
91
91
  else:
92
92
  yield database.Person(name)
93
93
 
94
- def refkey(self, year_brackets: Union[None, Literal["round", "square", "curly"]] = 'round'):
94
+ def refkey(self, year_brackets: None | Literal["round", "square", "curly"] = 'round'):
95
95
  """Compute an author-year type reference key for the item."""
96
96
  brackets = {
97
97
  None: ('', ''),
@@ -205,7 +205,7 @@ class Sources:
205
205
  pages = pages[:-1].strip()
206
206
  return sid, pages
207
207
 
208
- def validate(self, refs: Union[str, list[str]]) -> None:
208
+ def validate(self, refs: str | list[str]) -> None:
209
209
  """Make sure refs are valid. If not, raises Exceptions."""
210
210
  if not isinstance(refs, str) and any(r is None for r in refs):
211
211
  raise ValueError('empty reference in ref list (possibly caused by trailing separator)')
@@ -232,14 +232,14 @@ class Sources:
232
232
  self._add_entries(Source('misc', sid, glottolog_id=sid), **kw)
233
233
  yield Reference(self[sid], pages)
234
234
 
235
- def _add_entries(self, data: Union[Source, database.BibliographyData], **kw) -> None:
235
+ def _add_entries(self, data: Source | database.BibliographyData, **kw) -> None:
236
236
  if isinstance(data, Source):
237
237
  entries = [(data.id, data.entry)]
238
238
  elif hasattr(data, 'entries'):
239
239
  entries = data.entries.items()
240
240
  else:
241
241
  msg = (
242
- 'expected `clldutils.source.Source`,'
242
+ 'expected `pycldf.sources.Source`,'
243
243
  ' `pybtex.database.BibliographyData`,'
244
244
  ' or `simplepybtex.database.BibliographyData`;'
245
245
  f' got {type(data)}')
@@ -302,7 +302,7 @@ class Sources:
302
302
  return fname
303
303
  return None
304
304
 
305
- def add(self, *entries: Union[str, Source], **kw) -> None:
305
+ def add(self, *entries: str | Source, **kw) -> None:
306
306
  """
307
307
  Add a source, either specified as BibTeX string or as :class:`Source`.
308
308
  """
@@ -8,7 +8,7 @@ import pathlib
8
8
  import warnings
9
9
  import dataclasses
10
10
  import urllib.parse
11
- from typing import Optional, Union, Callable, Any, TYPE_CHECKING, Literal, get_args
11
+ from typing import Optional, Callable, Any, TYPE_CHECKING, Literal, get_args
12
12
  from collections.abc import Container
13
13
  from xml.etree import ElementTree
14
14
 
@@ -197,7 +197,7 @@ class Terms(dict):
197
197
  return {k: v for k, v in self.items() if v.subtype == 'component'}
198
198
 
199
199
 
200
- def term_uri(name: Union[Term, str], terms: Container[str] = None, ns: str = URL) -> Optional[str]:
200
+ def term_uri(name: Term | str, terms: Container[str] = None, ns: str = URL) -> Optional[str]:
201
201
  """
202
202
  Returns a full term URI associated with `name`.
203
203
 
@@ -43,6 +43,11 @@ class Tree:
43
43
  """
44
44
  Represents a tree object as specified in a row of `TreeTable`.
45
45
  """
46
+ description: Optional[str]
47
+ tree_type: Optional[str]
48
+ tree_is_rooted: Optional[bool]
49
+ tree_branch_length_unit: Optional[str]
50
+
46
51
  def __init__(self, trees: 'TreeTable', row: 'RowType', file: File):
47
52
  self.row: 'RowType' = row
48
53
  self.id: str = row[trees.cols['id'].name]
@@ -1,16 +1,16 @@
1
1
  """
2
2
  Functionality to manipulate URLs.
3
3
  """
4
- from typing import Callable, Union
4
+ from typing import Callable
5
5
  import urllib.parse
6
6
 
7
7
  __all__ = ['update_url', 'sanitize_url', 'url_without_fragment']
8
8
 
9
9
 
10
10
  def update_url(
11
- url: Union[str, urllib.parse.ParseResult],
11
+ url: str | urllib.parse.ParseResult,
12
12
  updater: Callable[[urllib.parse.ParseResult], tuple[str, str, str, str, str]],
13
- ) -> Union[str, None]:
13
+ ) -> str | None:
14
14
  """Generic update function for URLs."""
15
15
  if url is None:
16
16
  return None
@@ -32,6 +32,6 @@ def sanitize_url(url: str) -> str:
32
32
  return update_url(url, fix)
33
33
 
34
34
 
35
- def url_without_fragment(url: Union[str, urllib.parse.ParseResult]) -> str:
35
+ def url_without_fragment(url: str | urllib.parse.ParseResult) -> str:
36
36
  """Removes fragment from URL."""
37
37
  return update_url(url, lambda u: (u.scheme, u.hostname, u.path, u.query, ''))