Passed
Push — main ( cee75c...37036d )
by Douglas
02:08
created

StandardStrategyParser.parse()   C

Complexity

Conditions 8

Size

Total Lines 61
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 55
nop 2
dl 0
loc 61
rs 6.606
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
from __future__ import annotations
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
3
import abc
4
import enum
5
import sre_compile
6
from pathlib import Path
7
from typing import Dict, Mapping, Optional, Sequence, Set, MutableMapping
0 ignored issues
show
Unused Code introduced by
Unused Dict imported from typing
Loading history...
8
from typing import Tuple as Tup
9
from typing import Type
10
11
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
12
13
from mandos.model.utils.resources import MandosResources
14
from mandos.model.utils.reflection_utils import ReflectionUtils
15
from mandos.model.apis.chembl_api import ChemblApi
16
from mandos.model.apis.chembl_support.chembl_target_graphs import (
17
    ChemblTargetGraph,
18
    TargetEdgeReqs,
19
    TargetNode,
20
    TargetRelType,
21
)
22
from mandos.model.apis.chembl_support.chembl_targets import ChemblTarget, TargetType
23
24
25
class Acceptance(enum.Enum):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
26
    always = enum.auto()
27
    never = enum.auto()
28
    at_start = enum.auto()
29
    at_end = enum.auto()
30
31
32
class TargetTraversalStrategy(metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
33
    """ """
34
35
    @classmethod
36
    def api(cls) -> ChemblApi:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
37
        raise NotImplementedError()
38
39
    def traverse(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
40
        return self.__call__(target)
41
42
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
43
        """
44
45
        Returns:
46
47
        """
48
        raise NotImplementedError()
49
50
51
class NullTargetTraversalStrategy(TargetTraversalStrategy, metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
52
    """ """
53
54
    @classmethod
55
    def api(cls) -> ChemblApi:
56
        raise NotImplementedError()
57
58
    def traverse(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
59
        return self.__call__(target)
60
61
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
62
        return [target]
63
64
65
class StandardTargetTraversalStrategy(TargetTraversalStrategy, metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
66
    """ """
67
68
    @classmethod
69
    def edges(cls) -> Set[TargetEdgeReqs]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
70
        raise NotImplementedError()
71
72
    @classmethod
73
    def acceptance(cls) -> Mapping[TargetEdgeReqs, Acceptance]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
74
        raise NotImplementedError()
75
76
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTarget]:
77
        found = target.traverse(self.edges())
78
        return [f.target for f in found if self.accept(f)]
79
80
    def accept(self, target: TargetNode) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
81
        if target.link_reqs is None:
82
            # typically for root nodes -- we'll have a self-loop to check, too
83
            return False
84
        acceptance_type = self.acceptance()[target.link_reqs]
85
        return (
86
            acceptance_type is Acceptance.always
87
            or (acceptance_type is Acceptance.at_start and target.is_start)
88
            or (acceptance_type is Acceptance.at_end and target.is_end)
89
        )
90
91
92
class StandardStrategyParser:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
93
    @classmethod
94
    def read_lines(cls, path: Path) -> Sequence[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
95
        return [
96
            line
97
            for line in path.read_text(encoding="utf8").splitlines()
98
            if not line.startswith("#") and len(line.strip()) > 0
99
        ]
100
101
    @classmethod
102
    def parse(
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (24/15).
Loading history...
introduced by
Missing function or method docstring
Loading history...
103
        cls, lines: Sequence[str]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
104
    ) -> Tup[Set[TargetEdgeReqs], Mapping[TargetEdgeReqs, Acceptance]]:
105
        pat_type = r"(@?[a-z_]+)"
106
        pat_rel = r"([<>~=.*])"
107
        pat_accept = r"(?:accept:([\-*^$]))?"
108
        pat_src_words = r"(?:src:'''(.+?)''')?"
109
        pat_dest_words = r"(?:dest:'''(.+?)''')?"
110
        comment = r"(?:#(.*))?"
111
        pat = f"^ *{pat_type} *{pat_rel} *{pat_type} *{pat_accept} *{pat_src_words} *{pat_dest_words} *{comment} *$"
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (116/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
112
        pat = regex.compile(pat, flags=regex.V1)
113
        to_rel = {
114
            ">": TargetRelType.superset_of,
115
            "<": TargetRelType.subset_of,
116
            "~": TargetRelType.overlaps_with,
117
            "=": TargetRelType.equivalent_to,
118
            "*": TargetRelType.any_link,
119
            ".": TargetRelType.self_link,
120
        }
121
        to_accept = {
122
            "*": Acceptance.always,
123
            "-": Acceptance.never,
124
            "^": Acceptance.at_start,
125
            "$": Acceptance.at_end,
126
        }
127
        edges = set()
128
        edge_to_acceptance: MutableMapping[TargetEdgeReqs, Acceptance] = {}
129
        for line in lines:
130
            match = pat.fullmatch(line)
131
            if match is None:
132
                raise AssertionError(f"Could not parse line '{line}'")
133
            try:
134
                sources = TargetType.resolve(match.group(1))
135
                rel = to_rel[match.group(2)]
136
                dests = TargetType.resolve(match.group(3))
137
                accept = to_accept[match.group(4).lower()]
138
                src_pat = (
139
                    None
140
                    if match.group(5) is None or match.group(5) == ""
141
                    else regex.compile(match.group(5), flags=regex.V1)
142
                )
143
                dest_pat = (
144
                    None
145
                    if match.group(6) is None or match.group(6) == ""
146
                    else regex.compile(match.group(6), flags=regex.V1)
147
                )
148
            except (KeyError, TypeError, sre_compile.error):
149
                raise AssertionError(f"Could not parse line '{line}'")
150
            for source in sources:
151
                for dest in dests:
152
                    edge = TargetEdgeReqs(
153
                        src_type=source,
154
                        src_pattern=src_pat,
155
                        rel_type=rel,
156
                        dest_type=dest,
157
                        dest_pattern=dest_pat,
158
                    )
159
                    edges.add(edge)
160
                    edge_to_acceptance[edge] = accept
161
        return edges, edge_to_acceptance
162
163
164
class TargetTraversalStrategies:
165
    """
166
    Factory.
167
    """
168
169
    @classmethod
170
    def standard_strategies(cls) -> Set[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
171
        return {
172
            p.stem for p in MandosResources.path("strategies").iterdir() if p.suffix == ".strat"
173
        }
174
175
    @classmethod
176
    def by_name(cls, name: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
177
        if name == "@null":  # just slightly more efficient
178
            return cls.null(api)
179
        if MandosResources.contains("strategies", name, suffix=".strat"):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
180
            return cls.from_resource(name, api)
181
        elif name.endswith(".strat"):
182
            return cls.from_file(Path(name), api)
183
        return cls.by_classname(name, api)
184
185
    @classmethod
186
    def by_classname(cls, fully_qualified: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
187
        clazz = ReflectionUtils.injection(fully_qualified, TargetTraversalStrategy)
188
        return cls.create(clazz, api)
189
190
    @classmethod
191
    def from_resource(cls, name: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
192
        path = MandosResources.path("strategies", name, suffix=".strat")
193
        lines = StandardStrategyParser.read_lines(path)
194
        return cls._from_lines(lines, api, path.stem)
195
196
    @classmethod
197
    def from_file(cls, path: Path, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
198
        lines = StandardStrategyParser.read_lines(path)
199
        return cls._from_lines(lines, api, path.stem)
200
201
    @classmethod
202
    def from_lines(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
203
        cls, lines: Sequence[str], api: ChemblApi, name: Optional[str]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
204
    ) -> TargetTraversalStrategy:
205
        return cls._from_lines(lines, api, "" if name is None else name)
206
207
    @classmethod
208
    def _from_lines(
209
        cls, lines: Sequence[str], api: ChemblApi, name: str
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
210
    ) -> TargetTraversalStrategy:
211
        edges, accept = StandardStrategyParser.parse(lines)
212
213
        class Strategy(StandardTargetTraversalStrategy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
214
            @classmethod
215
            def edges(cls) -> Set[TargetEdgeReqs]:
216
                return edges
217
218
            @classmethod
219
            def acceptance(cls) -> Mapping[TargetEdgeReqs, Acceptance]:
220
                return accept
221
222
            @classmethod
223
            def api(cls) -> ChemblApi:
224
                return api
225
226
        Strategy.__name__ = StandardTargetTraversalStrategy.__class__.__name__ + "_" + name
227
        return Strategy()
228
229
    @classmethod
230
    def null(cls, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
231
        class NullStrategy(NullTargetTraversalStrategy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
232
            @classmethod
233
            def api(cls) -> ChemblApi:
234
                return api
235
236
        return NullStrategy()
237
238
    # noinspection PyAbstractClass
239
    @classmethod
240
    def create(cls, clz: Type[TargetTraversalStrategy], api: ChemblApi) -> TargetTraversalStrategy:
241
        """
242
        Factory method.
243
244
        Args:
245
            clz:
246
            api:
247
248
        Returns:
249
250
        """
251
252
        class X(clz):
0 ignored issues
show
Coding Style Naming introduced by
Class name "X" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
introduced by
Missing class docstring
Loading history...
253
            @classmethod
254
            def api(cls) -> ChemblApi:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
255
                return api
256
257
        X.__name__ = clz.__name__
258
        return X()
259
260
261
__all__ = ["TargetTraversalStrategy", "TargetTraversalStrategies"]
262