tyrannosaurus.context.Context.check_path()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 12
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nop 2
dl 0
loc 12
rs 9.3333
c 0
b 0
f 0
1
"""
2
Holds a "context" of metadata that was read from a pyproject.toml.
3
4
Original source: https://github.com/dmyersturnbull/tyrannosaurus
5
Copyright 2020–2021 Douglas Myers-Turnbull
6
Licensed under the Apache License, Version 2.0 (the "License");
7
you may not use this file except in compliance with the License.
8
You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
9
"""
10
11
from __future__ import annotations
12
13
import logging
14
import os
15
import re
16
import shutil
17
from pathlib import Path
18
from typing import Mapping, Optional, Sequence
19
from typing import Tuple as Tup
20
from typing import Union
21
22
from tyrannosaurus import TyrannoInfo
23
from tyrannosaurus.enums import DevStatus, License, Toml
24
from tyrannosaurus.parser import LiteralParser
25
26
logger = logging.getLogger(__package__)
27
28
29
class Source:
30
    @classmethod
31
    def parse(cls, s: str, toml: Toml) -> Union[str, Sequence]:
32
        from tyrannosaurus import TyrannoInfo
33
34
        project = toml["tool.poetry.name"]
35
        version = toml["tool.poetry.version"]
36
        description = toml["tool.poetry.description"]
37
        authors = toml["tool.poetry.authors"]
38
        keywords = toml["tool.poetry.keywords"]
39
        license_name = toml["tool.poetry.license"]
40
        status = DevStatus.guess_from_version(version)
41
        if isinstance(s, str) and s.startswith("'") and s.endswith("'"):
42
            return (
43
                LiteralParser(
44
                    project=project,
45
                    user=None,
46
                    authors=authors,
47
                    description=description,
48
                    keywords=keywords,
49
                    version=version,
50
                    status=status,
51
                    license_name=license_name,
52
                    tyranno_vr=TyrannoInfo.version,
53
                )
54
                .parse(s)
55
                .strip("'")
56
            )
57
        elif isinstance(s, str):
58
            value = toml[s]
59
            return str(value)
60
        else:
61
            # TODO not great
62
            return list(s)
63
64
65
class Context:
66
    def __init__(self, path: Union[Path, str], data=None, dry_run: bool = False):
67
        self.path = Path(path).resolve()
68
        if data is None:
69
            data = Toml.read(Path(self.path) / "pyproject.toml")
70
        self.data = data
71
        self.options = {k for k, v in data.get("tool.tyrannosaurus.options", {}).items() if v}
72
        self.targets = {k for k, v in data.get("tool.tyrannosaurus.targets", {}).items() if v}
73
        self.sources = {
74
            k: Source.parse(v, data)
75
            for k, v in data.get("tool.tyrannosaurus.sources", {}).items()
76
            if v
77
        }
78
        self.tmp_path = self.path / ".tyrannosaurus"
79
        self.dry_run = dry_run
80
81
    @property
82
    def project(self) -> str:
83
        return str(self.data["tool.poetry.name"])
84
85
    @property
86
    def version(self) -> str:
87
        return str(self.data["tool.poetry.version"])
88
89
    @property
90
    def description(self) -> str:
91
        return str(self.data["tool.poetry.description"])
92
93
    @property
94
    def license(self) -> License:
95
        return License.of(self.data["tool.poetry.license"])
96
97
    @property
98
    def build_sys_reqs(self) -> Mapping[str, str]:
99
        pat = re.compile(r" *^([A-Za-z][A-Za-z0-9-_.]*) *(.*)$")
100
        dct = {}
101
        for entry in self.data["build-system.requires"]:
102
            match = pat.fullmatch(entry)
103
            dct[match.group(1)] = match.group(2)
104
        return dct
105
106
    @property
107
    def deps(self) -> Mapping[str, str]:
108
        return self.data["tool.poetry.dependencies"]
109
110
    @property
111
    def dev_deps(self) -> Mapping[str, str]:
112
        return self.data["tool.poetry.dev-dependencies"]
113
114
    @property
115
    def extras(self) -> Mapping[str, str]:
116
        return self.data["tool.poetry.extras"]
117
118
    def destroy_tmp(self) -> bool:
119
        if not self.dry_run and self.tmp_path.exists():
120
            shutil.rmtree(str(self.tmp_path))
121
            return True
122
        return False
123
124
    def back_up(self, path: Union[Path, str]) -> None:
125
        path = Path(path)
126
        self.check_path(path)
127
        bak = self.get_bak_path(path)
128
        if not self.dry_run:
129
            bak.parent.mkdir(exist_ok=True, parents=True)
130
            shutil.copyfile(str(path), str(bak))
131
            logger.debug(f"Generated backup of {path} to {bak}")
132
133
    def trash(self, path: str, hard_delete: bool) -> Tup[Optional[Path], Optional[Path]]:
134
        return self.delete_exact_path(self.path / path, hard_delete=hard_delete)
135
136
    def delete_exact_path(
137
        self, path: Path, hard_delete: bool
138
    ) -> Tup[Optional[Path], Optional[Path]]:
139
        if not path.exists():
140
            return None, None
141
        self.check_path(path)
142
        if hard_delete:
143
            if not self.dry_run:
144
                shutil.rmtree(path)
145
            logger.debug(f"Deleted {path}")
146
            return path, None
147
        else:
148
            bak = self.get_bak_path(path)
149
            bak.parent.mkdir(exist_ok=True, parents=True)
150
            if not self.dry_run:
151
                os.rename(str(path), str(bak))
152
            logger.debug(f"Trashed {path} to {bak}")
153
            return path, bak
154
155
    def get_bak_path(self, path: Union[Path, str]):
156
        if not str(path).startswith(str(self.path)):
157
            path = self.path / path
158
        path = Path(path).resolve()
159
        suffix = path.suffix + "." + TyrannoInfo.timestamp + ".bak"
160
        return self.tmp_path / path.relative_to(self.path).with_suffix(suffix)
161
162
    def check_path(self, path: Union[Path, str]) -> None:
163
        # none of these should even be possible, but let's be 100% sure
164
        path = Path(path)
165
        if path.resolve() == self.path.resolve():
166
            raise ValueError(f"Cannot touch {path.resolve()}: identical to {self.path.resolve()}")
167
        if not path.exists():
168
            raise FileNotFoundError(f"Path {path} does not exist")
169
        for parent in path.resolve().parents:
170
            if parent.resolve() == self.path.resolve():
171
                return
172
        raise ValueError(
173
            f"Cannot touch {path.resolve()}: not under the parent dir {self.path.resolve()}"
174
        )
175
176
    def item(self, key: str):
177
        return self.data[key]
178
179
    def poetry(self, key: str):
180
        return self.data["tool.poetry." + key]
181
182
    def has_opt(self, key: str):
183
        return key in self.options
184
185
    def source(self, key: str):
186
187
        return self.sources[key]
188
189
    def path_source(self, key: str) -> Path:
190
        output_path = self.path
191
        for s in str(self.source(key)).split("/"):
192
            output_path /= s
193
        return output_path
194
195
    def has_target(self, key: str) -> bool:
196
        return key in self.targets
197
198
199
__all__ = ["Context"]
200