Passed
Push — issue790-automatically-add-met... ( 845f53...afe4e3 )
by Juho
04:41
created

annif.config.AnnifConfigCFG._read_config()   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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