annif.config.AnnifConfigTOML.__getitem__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
"""Configuration file handling"""
2
3
from __future__ import annotations
4
5
import configparser
6
import os.path
7
from glob import glob
8
9
try:
10
    import tomllib
11
except ImportError:
12
    import tomli as tomllib
13
14
import annif
15
import annif.util
16
from annif.exception import ConfigurationException
17
18
logger = annif.logger
19
20
21
class AnnifConfigCFG:
22
    """Class for reading configuration in CFG/INI format"""
23
24
    def __init__(self, filename: str = None, projstr: str = None) -> None:
25
        self._config = configparser.ConfigParser()
26
        self._config.optionxform = annif.util.identity
27
        if filename is not None:
28
            logger.debug(f"Reading configuration file {filename} in CFG format")
29
            self._read_config(self._config.read, filename)
30
        elif projstr is not None:
31
            logger.debug("Reading configuration from a string in CFG format")
32
            self._read_config(self._config.read_string, projstr)
33
34
    def _read_config(self, read_method, source):
35
        encoding = "utf-8-sig"
36
        try:
37
            read_method(source, encoding)
38
        except (
39
            configparser.DuplicateOptionError,
40
            configparser.DuplicateSectionError,
41
        ) as err:
42
            raise ConfigurationException(err.message)
43
44
    @property
45
    def project_ids(self) -> list[str]:
46
        return self._config.sections()
47
48
    def __getitem__(self, key: str) -> configparser.SectionProxy:
49
        return self._config[key]
50
51
52
class AnnifConfigTOML:
53
    """Class for reading configuration in TOML format"""
54
55
    def __init__(self, filename: str) -> None:
56
        with open(filename, "rb") as projf:
57
            try:
58
                logger.debug(f"Reading configuration file {filename} in TOML format")
59
                self._config = tomllib.load(projf)
60
            except tomllib.TOMLDecodeError as err:
61
                raise ConfigurationException(
62
                    f"Parsing TOML file '{filename}' failed: {err}"
63
                )
64
65
    @property
66
    def project_ids(self):
67
        return self._config.keys()
68
69
    def __getitem__(self, key: str) -> dict[str, str]:
70
        return self._config[key]
71
72
73
class AnnifConfigDirectory:
74
    """Class for reading configuration from directory"""
75
76
    def __init__(self, directory: str) -> None:
77
        files = glob(os.path.join(directory, "*.cfg"))
78
        files.extend(glob(os.path.join(directory, "*.toml")))
79
        logger.debug(f"Reading configuration files in directory {directory}")
80
81
        self._config = dict()
82
        for file in sorted(files):
83
            source_config = parse_config(file)
84
            for proj_id in source_config.project_ids:
85
                self._check_duplicate_project_ids(proj_id, file)
86
                self._config[proj_id] = source_config[proj_id]
87
88
    def _check_duplicate_project_ids(self, proj_id: str, file: str) -> None:
89
        if proj_id in self._config:
90
            # Error message resembles configparser's DuplicateSection message
91
            raise ConfigurationException(
92
                f'While reading from "{file}": project ID "{proj_id}" already '
93
                "exists in another configuration file in the directory."
94
            )
95
96
    @property
97
    def project_ids(self):
98
        return self._config.keys()
99
100
    def __getitem__(self, key: str) -> dict[str, str] | configparser.SectionProxy:
101
        return self._config[key]
102
103
104
def check_config(projects_config_path: str) -> str | None:
105
    if os.path.exists(projects_config_path):
106
        return projects_config_path
107
    else:
108
        logger.warning(
109
            "Project configuration file or directory "
110
            + f'"{projects_config_path}" is missing. Please provide one. '
111
            + "You can set the path to the project configuration "
112
            + "using the ANNIF_PROJECTS environment "
113
            + 'variable or the command-line option "--projects".'
114
        )
115
        return None
116
117
118
def find_config() -> str | None:
119
    for path in ("projects.cfg", "projects.toml", "projects.d"):
120
        if os.path.exists(path):
121
            return path
122
123
    logger.warning(
124
        "Could not find project configuration "
125
        + '"projects.cfg", "projects.toml" or "projects.d". '
126
        + "You can set the path to the project configuration "
127
        + "using the ANNIF_PROJECTS environment "
128
        + 'variable or the command-line option "--projects".'
129
    )
130
    return None
131
132
133
def parse_config(
134
    projects_config_path: str,
135
) -> AnnifConfigDirectory | AnnifConfigCFG | AnnifConfigTOML | None:
136
    if projects_config_path:
137
        projects_config_path = check_config(projects_config_path)
138
    else:
139
        projects_config_path = find_config()
140
141
    if not projects_config_path:  # not found
142
        return None
143
144
    if os.path.isdir(projects_config_path):
145
        return AnnifConfigDirectory(projects_config_path)
146
    elif projects_config_path.endswith(".toml"):  # TOML format
147
        return AnnifConfigTOML(projects_config_path)
148
    else:  # classic CFG/INI style format
149
        return AnnifConfigCFG(projects_config_path)
150