enumsync 0.1.0__tar.gz

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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.3
2
+ Name: enumsync
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: elda27
6
+ Author-email: elda27 <kaz.birdstick@gmail.com>
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/markdown
9
+
File without changes
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "enumsync"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "elda27", email = "kaz.birdstick@gmail.com" }]
7
+ requires-python = ">=3.14"
8
+ dependencies = []
9
+
10
+ [project.scripts]
11
+ enumsync = "enumsync:main"
12
+
13
+ [build-system]
14
+ requires = ["uv_build>=0.9.25,<0.10.0"]
15
+ build-backend = "uv_build"
16
+
17
+ [dependency-groups]
18
+ dev = ["jinja2>=3.1.6", "pytest>=9.0.2", "ruff>=0.15.6", "ty>=0.0.23"]
@@ -0,0 +1 @@
1
+ from enumsync.store import FileStore
@@ -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)
@@ -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