enumsync 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enumsync/__init__.py +1 -0
- enumsync/store.py +99 -0
- enumsync/sync.py +148 -0
- enumsync-0.1.0.dist-info/METADATA +9 -0
- enumsync-0.1.0.dist-info/RECORD +7 -0
- enumsync-0.1.0.dist-info/WHEEL +4 -0
- enumsync-0.1.0.dist-info/entry_points.txt +3 -0
enumsync/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from enumsync.store import FileStore
|
enumsync/store.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TYPE_CHECKING, Generic, Iterable, TypeVar
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from jinja2 import Template
|
|
6
|
+
|
|
7
|
+
ENABLE_JINJA2 = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
|
|
10
|
+
class Template:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
ENABLE_JINJA2 = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from enum import StrEnum
|
|
16
|
+
|
|
17
|
+
TEnum = TypeVar("TEnum", bound="StrEnum")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileStore(Generic[TEnum]):
|
|
21
|
+
def __init__(self, path: str):
|
|
22
|
+
"""Store object in file
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
path (str): Set `__file__` or its parent directory. Path to the file where the object will be stored
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
There is automatic syncing enum by the store in the file.
|
|
29
|
+
```python
|
|
30
|
+
from enumsync import FileStore
|
|
31
|
+
|
|
32
|
+
store = FileStore(__file__)
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
_path = Path(path)
|
|
36
|
+
if _path.is_dir():
|
|
37
|
+
folder = _path
|
|
38
|
+
file = _path / "__init__.py"
|
|
39
|
+
else:
|
|
40
|
+
folder = _path.parent
|
|
41
|
+
file = Path(path)
|
|
42
|
+
self.file = file
|
|
43
|
+
self.folder = folder
|
|
44
|
+
|
|
45
|
+
def sync(self, output: str | Path | None = None) -> None:
|
|
46
|
+
"""Generate a typed module that mirrors the files in the store folder.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
output: Optional path for the generated module. Relative paths are
|
|
50
|
+
resolved from the store folder.
|
|
51
|
+
"""
|
|
52
|
+
from enumsync.sync import generate_sync_code
|
|
53
|
+
|
|
54
|
+
code = generate_sync_code(self)
|
|
55
|
+
if output is None:
|
|
56
|
+
output_path = self.file
|
|
57
|
+
else:
|
|
58
|
+
output_path = Path(output)
|
|
59
|
+
if not output_path.is_absolute():
|
|
60
|
+
output_path = self.folder / output_path
|
|
61
|
+
|
|
62
|
+
output_path.write_text(code, encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
def get_text(self, value: TEnum) -> str:
|
|
65
|
+
"""Get the file path
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: The file path where the object is stored
|
|
69
|
+
"""
|
|
70
|
+
return (self.folder / str(value)).read_text()
|
|
71
|
+
|
|
72
|
+
def get_template(self, value: TEnum) -> "Template":
|
|
73
|
+
"""Get the template file path
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Template: The template file path where the object is stored
|
|
77
|
+
"""
|
|
78
|
+
if not ENABLE_JINJA2:
|
|
79
|
+
raise ImportError(
|
|
80
|
+
"Jinja2 is not installed. Please install it to use this feature."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
84
|
+
|
|
85
|
+
env = Environment(
|
|
86
|
+
loader=FileSystemLoader(self.folder), autoescape=select_autoescape()
|
|
87
|
+
)
|
|
88
|
+
return env.get_template(str(value))
|
|
89
|
+
|
|
90
|
+
def glob(self, pattern: str) -> Iterable[Path]:
|
|
91
|
+
"""Glob the file path
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
pattern (str): The glob pattern to match files
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Iterable[Path]: An iterable of file paths that match the glob pattern
|
|
98
|
+
"""
|
|
99
|
+
return self.folder.glob(pattern)
|
enumsync/sync.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
from ast import (
|
|
4
|
+
AnnAssign,
|
|
5
|
+
Assign,
|
|
6
|
+
ClassDef,
|
|
7
|
+
Constant,
|
|
8
|
+
Expr,
|
|
9
|
+
ImportFrom,
|
|
10
|
+
List,
|
|
11
|
+
Load,
|
|
12
|
+
Module,
|
|
13
|
+
Name,
|
|
14
|
+
Store,
|
|
15
|
+
Subscript,
|
|
16
|
+
alias,
|
|
17
|
+
)
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from tempfile import TemporaryDirectory
|
|
20
|
+
from typing import TYPE_CHECKING, Iterable
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from enumsync.store import FileStore
|
|
24
|
+
else:
|
|
25
|
+
FileStore = object
|
|
26
|
+
try:
|
|
27
|
+
import ruff
|
|
28
|
+
except ImportError:
|
|
29
|
+
ruff = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_pascal(name: str) -> str:
|
|
33
|
+
parts = re.split(r"[^0-9A-Za-z]+", name)
|
|
34
|
+
return "".join(part.capitalize() for part in parts if part)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
IMPORT_BLOCKS = [
|
|
38
|
+
ImportFrom(module="enum", names=[alias(name="StrEnum")], level=0),
|
|
39
|
+
ImportFrom(module="typing", names=[alias(name="TypeAlias")], level=0),
|
|
40
|
+
ImportFrom(module="enumsync", names=[alias(name="FileStore")], level=0),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ModuleBuilder:
|
|
45
|
+
def define_import_blocks(self) -> list[ImportFrom]:
|
|
46
|
+
return IMPORT_BLOCKS
|
|
47
|
+
|
|
48
|
+
def define_enum_sync_block(
|
|
49
|
+
self, store: FileStore, glob: Iterable[Path]
|
|
50
|
+
) -> ClassDef:
|
|
51
|
+
prefix = to_pascal(store.folder.name) or "Synced"
|
|
52
|
+
enum_body = {
|
|
53
|
+
to_pascal(path.name): path.relative_to(store.folder).as_posix()
|
|
54
|
+
for path in glob
|
|
55
|
+
if path.is_file() and path.name != "__init__.py"
|
|
56
|
+
}
|
|
57
|
+
return ClassDef(
|
|
58
|
+
name=f"{prefix}Enum",
|
|
59
|
+
bases=[Name(id="StrEnum", ctx=Load())],
|
|
60
|
+
keywords=[],
|
|
61
|
+
body=[
|
|
62
|
+
*[
|
|
63
|
+
Assign(
|
|
64
|
+
targets=[Name(id=name, ctx=Store())],
|
|
65
|
+
value=Constant(value=value),
|
|
66
|
+
)
|
|
67
|
+
for name, value in enum_body.items()
|
|
68
|
+
]
|
|
69
|
+
],
|
|
70
|
+
decorator_list=[],
|
|
71
|
+
type_params=[],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def define_comment_block(self) -> Expr:
|
|
75
|
+
return ast.parse(
|
|
76
|
+
'"""This file is auto-generated by enumsync. Do not edit this file directly."""'
|
|
77
|
+
).body[
|
|
78
|
+
0
|
|
79
|
+
] # type: ignore
|
|
80
|
+
|
|
81
|
+
def define_sync_block(self) -> Expr:
|
|
82
|
+
return ast.parse("FileStore(__file__).sync()").body[0] # type: ignore
|
|
83
|
+
|
|
84
|
+
def define_domain_file_sotre(self, store: FileStore) -> AnnAssign:
|
|
85
|
+
prefix = to_pascal(store.folder.name)
|
|
86
|
+
|
|
87
|
+
return AnnAssign(
|
|
88
|
+
target=Name(id=f"{prefix}FileStore", ctx=Store()),
|
|
89
|
+
annotation=Name(id="TypeAlias", ctx=Load()),
|
|
90
|
+
value=Subscript(
|
|
91
|
+
value=Name(id="FileStore", ctx=Load()),
|
|
92
|
+
slice=Name(id=f"{prefix}Enum", ctx=Load()),
|
|
93
|
+
ctx=Load(),
|
|
94
|
+
),
|
|
95
|
+
simple=1,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def define_export_all_block(self, store: FileStore) -> Assign:
|
|
99
|
+
prefix = to_pascal(store.folder.name)
|
|
100
|
+
return Assign(
|
|
101
|
+
targets=[Name(id="__all__", ctx=Store())],
|
|
102
|
+
value=List(
|
|
103
|
+
elts=[
|
|
104
|
+
Constant(value=f"{prefix}Enum"),
|
|
105
|
+
Constant(value=f"{prefix}FileStore"),
|
|
106
|
+
],
|
|
107
|
+
ctx=Load(),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def define(self, store: FileStore) -> Module:
|
|
112
|
+
module = Module(
|
|
113
|
+
body=[
|
|
114
|
+
self.define_comment_block(),
|
|
115
|
+
*self.define_import_blocks(),
|
|
116
|
+
self.define_enum_sync_block(store, store.glob("*.*")),
|
|
117
|
+
self.define_sync_block(),
|
|
118
|
+
self.define_domain_file_sotre(store),
|
|
119
|
+
self.define_export_all_block(store),
|
|
120
|
+
],
|
|
121
|
+
type_ignores=[],
|
|
122
|
+
)
|
|
123
|
+
return ast.fix_missing_locations(module)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def generate_sync_code(store: FileStore) -> str:
|
|
127
|
+
builder = ModuleBuilder()
|
|
128
|
+
module = builder.define(store)
|
|
129
|
+
code = ast.unparse(module)
|
|
130
|
+
try:
|
|
131
|
+
format_code(code)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
return code
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def format_code(code: str) -> str:
|
|
138
|
+
if ruff is None:
|
|
139
|
+
return code
|
|
140
|
+
from ruff import find_ruff_bin
|
|
141
|
+
|
|
142
|
+
with TemporaryDirectory() as temp_dir:
|
|
143
|
+
temp_path = Path(temp_dir) / "temp.py"
|
|
144
|
+
temp_path.write_text(code, encoding="utf-8")
|
|
145
|
+
ruff_bin = find_ruff_bin()
|
|
146
|
+
subprocess.check_call([ruff_bin, "format", temp_path])
|
|
147
|
+
code = temp_path.read_text(encoding="utf-8")
|
|
148
|
+
return code
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
enumsync/__init__.py,sha256=J55nmm8MF_mJ5cH5Seb5kDdhDRAsn7zUOV9KNa1FZsw,37
|
|
2
|
+
enumsync/store.py,sha256=VQWRC7JhsFBI6rJP1B_DvuQdQxI_tbBhxUCyZYh08LI,2911
|
|
3
|
+
enumsync/sync.py,sha256=rUfotjaMBrBVB8Ziqk14ZqAyd-4dCDoGmc2Y-OfVVW4,4373
|
|
4
|
+
enumsync-0.1.0.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
|
|
5
|
+
enumsync-0.1.0.dist-info/entry_points.txt,sha256=xKdNK_NTx-NTHSd7cx81w1DXIb6On9j8jbwo9ie4Cs0,44
|
|
6
|
+
enumsync-0.1.0.dist-info/METADATA,sha256=jgMKlmRh_P-BJ6umNK-G9GlPyvDuEUY9BU7xopgV0YE,214
|
|
7
|
+
enumsync-0.1.0.dist-info/RECORD,,
|