Passed
Push — main ( 2e1b6b...3a0c28 )
by Douglas
02:06
created

TargetTraversalStrategy.__str__()   A

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 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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, MutableMapping, Optional, Sequence, Set
0 ignored issues
show
Unused Code introduced by
Unused Dict imported from typing
Loading history...
Unused Code introduced by
Unused Optional imported from typing
Loading history...
8
from typing import Tuple as Tup
9
from typing import Type
10
11
import decorateme
0 ignored issues
show
introduced by
Unable to import 'decorateme'
Loading history...
12
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
13
from pocketutils.core.enums import CleverEnum
0 ignored issues
show
introduced by
Unable to import 'pocketutils.core.enums'
Loading history...
14
from pocketutils.core.exceptions import ParsingError
0 ignored issues
show
introduced by
Unable to import 'pocketutils.core.exceptions'
Loading history...
15
from pocketutils.tools.reflection_tools import ReflectionTools
0 ignored issues
show
introduced by
Unable to import 'pocketutils.tools.reflection_tools'
Loading history...
16
17
from mandos.model.apis.chembl_api import ChemblApi
18
from mandos.model.apis.chembl_support.chembl_target_graphs import (
19
    ChemblTargetGraph,
20
    TargetEdgeReqs,
21
    TargetNode,
22
    TargetRelType,
23
)
24
from mandos.model.apis.chembl_support.chembl_targets import ChemblTarget, TargetType
25
from mandos.model.utils.setup import MandosResources, logger
26
27
28
class Acceptance(CleverEnum):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
29
    always = enum.auto()
30
    never = enum.auto()
31
    at_start = enum.auto()
32
    at_end = enum.auto()
33
34
35
class TargetTraversalStrategy(metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
36
    """ """
37
38
    @classmethod
39
    def api(cls) -> ChemblApi:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
40
        raise NotImplementedError()
41
42
    def traverse(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
43
        return self.__call__(target)
44
45
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
46
        """
47
        Run the strategy.
48
        """
49
        raise NotImplementedError()
50
51
    def __repr__(self) -> str:
52
        return self.__class__.__name__ + "(" + str(self.api()) + ")"
53
54
    def __str__(self) -> str:
55
        return repr(self)
56
57
58
class NullTargetTraversalStrategy(TargetTraversalStrategy, metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
59
    """ """
60
61
    @classmethod
62
    def api(cls) -> ChemblApi:
63
        raise NotImplementedError()
64
65
    def traverse(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
66
        return self.__call__(target)
67
68
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTargetGraph]:
69
        return [target]
70
71
72
class StandardTargetTraversalStrategy(TargetTraversalStrategy, metaclass=abc.ABCMeta):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
73
    """ """
74
75
    @classmethod
76
    def edges(cls) -> Set[TargetEdgeReqs]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
77
        raise NotImplementedError()
78
79
    @classmethod
80
    def acceptance(cls) -> Mapping[TargetEdgeReqs, Acceptance]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
81
        raise NotImplementedError()
82
83
    def __call__(self, target: ChemblTargetGraph) -> Sequence[ChemblTarget]:
84
        logger.debug(f"Starting {self} on {target.target}")
85
        found = target.traverse(self.edges())
86
        return [f.target for f in found if self.accept(f)]
87
88
    def accept(self, target: TargetNode) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
89
        if target.link_reqs is None:
90
            # typically for root nodes -- we'll have a self-loop to check, too
91
            logger.trace(f"Rejected probable root: {target}")
92
            return False
93
        acceptance_type = self.acceptance()[target.link_reqs]
94
        b = (
0 ignored issues
show
Coding Style Naming introduced by
Variable name "b" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' 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...
95
            acceptance_type is Acceptance.always
96
            or (acceptance_type is Acceptance.at_start and target.is_start)
97
            or (acceptance_type is Acceptance.at_end and target.is_end)
98
        )
99
        x = "accepted" if b else "rejected"
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' 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...
100
        logger.trace(f"{x.capitalize()}: {target}")
101
        return b
102
103
104
class StandardStrategyParser:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
105
    @classmethod
106
    def read_lines(cls, path: Path) -> Sequence[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
107
        return [
108
            line
109
            for line in path.read_text(encoding="utf8").splitlines()
110
            if not line.startswith("#") and len(line.strip()) > 0
111
        ]
112
113
    @classmethod
114
    def parse(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (24/15).
Loading history...
115
        cls, lines: Sequence[str]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
116
    ) -> Tup[Set[TargetEdgeReqs], Mapping[TargetEdgeReqs, Acceptance]]:
117
        pat_type = r"(@?[a-z_]+)"
118
        pat_rel = r"([<>~=.*])"
119
        pat_accept = r"(?:accept:([\-*^$]))?"
120
        pat_src_words = r"(?:src:'''(.+?)''')?"
121
        pat_dest_words = r"(?:dest:'''(.+?)''')?"
122
        comment = r"(?:#(.*))?"
123
        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...
124
        pat = regex.compile(pat, flags=regex.V1)
125
        to_rel = {
126
            ">": TargetRelType.superset_of,
127
            "<": TargetRelType.subset_of,
128
            "~": TargetRelType.overlaps_with,
129
            "=": TargetRelType.equivalent_to,
130
            "*": TargetRelType.any_link,
131
            ".": TargetRelType.self_link,
132
        }
133
        to_accept = {
134
            "*": Acceptance.always,
135
            "-": Acceptance.never,
136
            "^": Acceptance.at_start,
137
            "$": Acceptance.at_end,
138
        }
139
        edges = set()
140
        edge_to_acceptance: MutableMapping[TargetEdgeReqs, Acceptance] = {}
141
        for line in lines:
142
            match = pat.fullmatch(line)
143
            if match is None:
144
                raise ParsingError(f"Could not parse line '{line}'")
145
            try:
146
                sources = TargetType.resolve(match.group(1))
147
                rel = to_rel[match.group(2)]
148
                dests = TargetType.resolve(match.group(3))
149
                accept = to_accept[match.group(4).lower()]
150
                src_pat = (
151
                    None
152
                    if match.group(5) is None or match.group(5) == ""
153
                    else regex.compile(match.group(5), flags=regex.V1)
154
                )
155
                dest_pat = (
156
                    None
157
                    if match.group(6) is None or match.group(6) == ""
158
                    else regex.compile(match.group(6), flags=regex.V1)
159
                )
160
            except (KeyError, TypeError, sre_compile.error):
161
                raise AssertionError(f"Could not parse line '{line}'")
162
            for source in sources:
163
                for dest in dests:
164
                    edge = TargetEdgeReqs(
165
                        src_type=source,
166
                        src_pattern=src_pat,
167
                        rel_type=rel,
168
                        dest_type=dest,
169
                        dest_pattern=dest_pat,
170
                    )
171
                    edges.add(edge)
172
                    edge_to_acceptance[edge] = accept
173
        return edges, edge_to_acceptance
174
175
176
class TargetTraversalStrategies:
177
    """
178
    Factory.
179
    """
180
181
    @classmethod
182
    def standard_strategies(cls) -> Set[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
183
        return {p.stem for p in MandosResources.dir("strategies").iterdir() if p.suffix == ".strat"}
184
185
    @classmethod
186
    def by_name(cls, name: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
187
        if name == "@null":  # just slightly more efficient
188
            return cls.null(api)
189
        if MandosResources.contains("strategies", name, suffix=".strat"):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
190
            return cls.from_resource(name, api)
191
        elif name.endswith(".strat"):
192
            return cls.from_file(Path(name), api)
193
        return cls.by_classname(name, api)
194
195
    @classmethod
196
    def by_classname(cls, fully_qualified: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
197
        clazz = ReflectionTools.injection(fully_qualified, TargetTraversalStrategy)
198
        logger.info(f"Loaded strategy {clazz} by injection: {fully_qualified}")
199
        return cls.create(clazz, api)
200
201
    @classmethod
202
    def from_resource(cls, name: str, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
203
        path = MandosResources.file("strategies", name, suffix=".strat")
204
        return cls.from_file(path, api)
205
206
    @classmethod
207
    def from_file(cls, path: Path, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
208
        lines = StandardStrategyParser.read_lines(path)
209
        logger.debug(f"Loaded strategy from {path} with {len(lines)} lines")
210
        return cls._from_lines(lines, api, name="TraversalStrategy" + path.stem.capitalize())
211
212
    @classmethod
213
    def from_lines(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
214
        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...
215
    ) -> TargetTraversalStrategy:
216
        return cls._from_lines(lines, api, name=name)
217
218
    @classmethod
219
    def _from_lines(
220
        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...
221
    ) -> TargetTraversalStrategy:
222
        edges, accept = StandardStrategyParser.parse(lines)
223
        logger.info(f"Loaded strategy {name} with {len(edges)} edge types")
224
        for edge in edges:
225
            logger.trace(f"Edge: {edge} ({accept[edge]})")
226
227
        class Strategy(StandardTargetTraversalStrategy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
228
            @classmethod
229
            def edges(cls) -> Set[TargetEdgeReqs]:
230
                return edges
231
232
            @classmethod
233
            def acceptance(cls) -> Mapping[TargetEdgeReqs, Acceptance]:
234
                return accept
235
236
            @classmethod
237
            def api(cls) -> ChemblApi:
238
                return api
239
240
        Strategy.__name__ = name
241
        return Strategy()
242
243
    @classmethod
244
    def null(cls, api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
245
        class NullStrategy(NullTargetTraversalStrategy):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
246
            @classmethod
247
            def api(cls) -> ChemblApi:
248
                return api
249
250
        NullStrategy.__name__ = "NullStrategy"
251
        return NullStrategy()
252
253
    # noinspection PyAbstractClass
254
    @classmethod
255
    def create(cls, clz: Type[TargetTraversalStrategy], api: ChemblApi) -> TargetTraversalStrategy:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
256
        @decorateme.auto_utils()
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...
257
        class X(clz):
258
            @classmethod
259
            def api(cls) -> ChemblApi:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
260
                return api
261
262
        X.__name__ = clz.__name__
263
        return X()
264
265
266
__all__ = ["TargetTraversalStrategy", "TargetTraversalStrategies"]
267