simple-oop 2026.2.1__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.
- simple_oop-2026.2.1/.gitignore +6 -0
- simple_oop-2026.2.1/LICENSE.txt +21 -0
- simple_oop-2026.2.1/PKG-INFO +18 -0
- simple_oop-2026.2.1/README.md +3 -0
- simple_oop-2026.2.1/makefile +29 -0
- simple_oop-2026.2.1/pyproject.toml +23 -0
- simple_oop-2026.2.1/simple_oop/__init__.py +132 -0
- simple_oop-2026.2.1/simple_oop/__main__.py +3 -0
- simple_oop-2026.2.1/simple_oop/code_gen.py +81 -0
- simple_oop-2026.2.1/simple_oop/config.py +89 -0
- simple_oop-2026.2.1/simple_oop/discovery.py +63 -0
- simple_oop-2026.2.1/simple_oop/node.py +107 -0
- simple_oop-2026.2.1/simple_oop/version.py +1 -0
- simple_oop-2026.2.1/vinc.json +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 J. Günthner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-oop
|
|
3
|
+
Version: 2026.2.1
|
|
4
|
+
Summary: Placeholder description
|
|
5
|
+
Author-email: Guenthner <guenthner.jonathan@gmail.com>
|
|
6
|
+
License-File: LICENSE.txt
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: colorama
|
|
12
|
+
Requires-Dist: jinja2
|
|
13
|
+
Requires-Dist: pydantic
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Placeholder
|
|
17
|
+
|
|
18
|
+
Placeholder description
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
py=.venv/bin/python
|
|
2
|
+
pip=.venv/bin/pip
|
|
3
|
+
user=guenthner
|
|
4
|
+
program-name=simple-oop
|
|
5
|
+
|
|
6
|
+
install_dependencies:
|
|
7
|
+
$(pip) install build hatchling twine
|
|
8
|
+
|
|
9
|
+
set_user:
|
|
10
|
+
cp ~/.pypirc_$(user) ~/.pypirc
|
|
11
|
+
|
|
12
|
+
build: clean version
|
|
13
|
+
$(py) -m build
|
|
14
|
+
|
|
15
|
+
version:
|
|
16
|
+
vinc
|
|
17
|
+
|
|
18
|
+
clean:
|
|
19
|
+
mkdir dist -p
|
|
20
|
+
touch dist/fuck
|
|
21
|
+
rm dist/*
|
|
22
|
+
|
|
23
|
+
upload: set_user build
|
|
24
|
+
$(py) -m twine upload --repository pypi dist/* $(flags)
|
|
25
|
+
|
|
26
|
+
reload: upload
|
|
27
|
+
pipx upgrade $(program-name)
|
|
28
|
+
pipx upgrade $(program-name)
|
|
29
|
+
$(program-name) --version . .
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simple-oop"
|
|
7
|
+
version = "2026.2.1" # version
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Guenthner", email = "guenthner.jonathan@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Placeholder description"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent"
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["pydantic", "jinja2", "colorama"]
|
|
20
|
+
keywords = []
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
simple-oop = "simple_oop:command_entry_point"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import colorama
|
|
10
|
+
from colorama import Fore
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from .config import Config, VariableType
|
|
14
|
+
from .discovery import discover
|
|
15
|
+
from .node import NodeContext
|
|
16
|
+
from .version import program_version
|
|
17
|
+
|
|
18
|
+
colorama.init(autoreset=True)
|
|
19
|
+
|
|
20
|
+
from .code_gen import TemplateEnvironment # I put this here to stop it from being moved (professional, I know)
|
|
21
|
+
|
|
22
|
+
PROGRAM_NAME = "simple-oop"
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(PROGRAM_NAME)
|
|
25
|
+
console = logging.StreamHandler()
|
|
26
|
+
log.addHandler(console)
|
|
27
|
+
log.setLevel(logging.DEBUG)
|
|
28
|
+
console.setFormatter(
|
|
29
|
+
logging.Formatter(
|
|
30
|
+
f"{{asctime}} [{Fore.YELLOW}{{levelname:>5}}{Fore.RESET}] {Fore.BLUE}{{name}}{Fore.RESET}: {{message}}",
|
|
31
|
+
style="{", datefmt="W%W %a %I:%M"))
|
|
32
|
+
|
|
33
|
+
colorama.init(autoreset=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def command_entry_point():
|
|
37
|
+
try:
|
|
38
|
+
main()
|
|
39
|
+
except KeyboardInterrupt:
|
|
40
|
+
log.warning("Program was interrupted by user")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Mode(ABC):
|
|
44
|
+
modes: list[type['Mode']] = []
|
|
45
|
+
name: Union[None, str] = None
|
|
46
|
+
description: Union[None, str] = None
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def create_parser(cls, obj) -> ArgumentParser:
|
|
51
|
+
parser = obj.add_parser(cls.name, description=cls.description, help=cls.description)
|
|
52
|
+
parser.set_defaults(mode=cls)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def call(cls, args):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@Mode.modes.append
|
|
62
|
+
class Generate(Mode):
|
|
63
|
+
name = "generate"
|
|
64
|
+
description = "TODO" # TODO
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def create_parser(cls, obj) -> ArgumentParser:
|
|
68
|
+
parser = super().create_parser(obj)
|
|
69
|
+
parser.add_argument("-w", "--working-directory", type=Path, default=Path(os.getcwd()))
|
|
70
|
+
parser.add_argument("--dump-tree", type=str, default=None)
|
|
71
|
+
return parser
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def call(cls, args: argparse.Namespace) -> None:
|
|
75
|
+
os.chdir(args.working_directory.resolve())
|
|
76
|
+
|
|
77
|
+
config_file = Path("simple-oop/config.json")
|
|
78
|
+
assert config_file.exists(), f"File {config_file} does not exist at {os.getcwd()}"
|
|
79
|
+
try:
|
|
80
|
+
c = Config.model_validate_json(config_file.read_text())
|
|
81
|
+
except ValidationError as e:
|
|
82
|
+
print(e)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if args.verbose:
|
|
86
|
+
log.debug("Using following config:")
|
|
87
|
+
print(c.model_dump_json(indent=4))
|
|
88
|
+
|
|
89
|
+
ctx = NodeContext(c)
|
|
90
|
+
|
|
91
|
+
discover(ctx, c.input_directories[0])
|
|
92
|
+
|
|
93
|
+
if ctx.errors > 0:
|
|
94
|
+
log.error(f"Discovery failed with {ctx.errors} error(s)")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if args.verbose:
|
|
98
|
+
log.debug("Found the following type structure:")
|
|
99
|
+
ctx.print_types()
|
|
100
|
+
|
|
101
|
+
if args.dump_tree is not None:
|
|
102
|
+
assert args.dump_tree in c.variables
|
|
103
|
+
assert c.variables[args.dump_tree] == VariableType.NODE
|
|
104
|
+
|
|
105
|
+
roots = [n for n in ctx.nodes.values() if n.variables[args.dump_tree] is None]
|
|
106
|
+
for r in roots:
|
|
107
|
+
r.print_tree(args.dump_tree)
|
|
108
|
+
|
|
109
|
+
gen = TemplateEnvironment(ctx)
|
|
110
|
+
|
|
111
|
+
for template in c.templates:
|
|
112
|
+
gen.generate(template)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
parser = ArgumentParser(prog=PROGRAM_NAME,
|
|
117
|
+
description="Placeholder description",
|
|
118
|
+
allow_abbrev=True, add_help=True, exit_on_error=True)
|
|
119
|
+
|
|
120
|
+
parser.add_argument('-v', '--verbose', action='store_true', help="Show more output")
|
|
121
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {program_version}")
|
|
122
|
+
|
|
123
|
+
subparsers = parser.add_subparsers(title="Modes", description="Possible modes of operation", required=True)
|
|
124
|
+
for mode in Mode.modes:
|
|
125
|
+
mode.create_parser(subparsers)
|
|
126
|
+
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
log.setLevel(logging.DEBUG if args.verbose else logging.INFO)
|
|
130
|
+
log.debug("Starting program...")
|
|
131
|
+
|
|
132
|
+
args.mode.call(args)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from jinja2 import FileSystemLoader, Environment, TemplateNotFound
|
|
6
|
+
|
|
7
|
+
from simple_oop import NodeContext
|
|
8
|
+
from simple_oop.config import TemplateConfig
|
|
9
|
+
from simple_oop.node import Node
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("simple-oop")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TemplateEnvironment:
|
|
15
|
+
generated_file_marker = "// autogenerated by simple-oop\n"
|
|
16
|
+
|
|
17
|
+
def __init__(self, ctx: NodeContext):
|
|
18
|
+
self.ctx = ctx
|
|
19
|
+
self.env = Environment(loader=FileSystemLoader(ctx.config.template_directory), autoescape=False)
|
|
20
|
+
|
|
21
|
+
def check_file(self, file: Path) -> bool:
|
|
22
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
if file.exists() and not file.read_text().startswith(self.generated_file_marker):
|
|
24
|
+
log.error(f"Any files generated by simple-oop must start with "
|
|
25
|
+
f"\"{self.generated_file_marker}\" otherwise they will "
|
|
26
|
+
f"not be overwritten, you probably deleted that line. It is "
|
|
27
|
+
f"normally automatically prepended")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def render_filename(self, filename: str, node: Node) -> str:
|
|
33
|
+
return self.env.from_string(filename).render(node=node)
|
|
34
|
+
|
|
35
|
+
def generate(self, template_config: TemplateConfig):
|
|
36
|
+
try:
|
|
37
|
+
template = Template(self, template_config)
|
|
38
|
+
except TemplateNotFound as e:
|
|
39
|
+
print(e)
|
|
40
|
+
return
|
|
41
|
+
out = self.ctx.config.output_directory
|
|
42
|
+
|
|
43
|
+
if template.config.foreach is None:
|
|
44
|
+
template.render(out / template_config.name, {
|
|
45
|
+
"nodes": self.ctx.nodes.values()
|
|
46
|
+
})
|
|
47
|
+
else:
|
|
48
|
+
for node in self.ctx.nodes.values():
|
|
49
|
+
if template.node_matches(node):
|
|
50
|
+
filename = self.render_filename(template_config.name, node)
|
|
51
|
+
template.render(out / filename, {
|
|
52
|
+
"nodes": self.ctx.nodes.values(),
|
|
53
|
+
"node": node
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Template:
|
|
58
|
+
def __init__(self, env: TemplateEnvironment, config: TemplateConfig):
|
|
59
|
+
self.env: TemplateEnvironment = env
|
|
60
|
+
self.config: TemplateConfig = config
|
|
61
|
+
self.jinja_template: Any
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self.jinja_template = self.env.env.get_template(config.name)
|
|
65
|
+
except TemplateNotFound as e:
|
|
66
|
+
self.jinja_template = None
|
|
67
|
+
raise e
|
|
68
|
+
|
|
69
|
+
def render(self, file: Path, variables: dict[str, Any]):
|
|
70
|
+
if not self.env.check_file(file):
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
file.write_text(self.env.generated_file_marker + self.jinja_template.render(**variables))
|
|
74
|
+
log.info(f"Generated {file.name}")
|
|
75
|
+
|
|
76
|
+
def node_matches(self, node: Node):
|
|
77
|
+
assert self.config.foreach is not None
|
|
78
|
+
for r in self.config.foreach:
|
|
79
|
+
if not node[r]:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Any, Optional
|
|
7
|
+
|
|
8
|
+
from colorama import Fore
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, BeforeValidator, AfterValidator
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("simple-oop")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def coerce_list_to_str(value) -> str:
|
|
15
|
+
if isinstance(value, str):
|
|
16
|
+
return value
|
|
17
|
+
if isinstance(value, list):
|
|
18
|
+
value = "".join(value)
|
|
19
|
+
log.debug(f"Joining regex to {value}")
|
|
20
|
+
return value
|
|
21
|
+
raise ValueError(f"Value {value} is not a string or a list of strings")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def require_exists(path: Path) -> Path:
|
|
25
|
+
path = Path(os.curdir) / path
|
|
26
|
+
assert path.exists(), f"Path {path} does not exist"
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def require_is_dir(path: Path) -> Path:
|
|
31
|
+
assert path.is_dir(), f"Path {path} is not a directory"
|
|
32
|
+
return path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
EXTENDED_REGEX = Annotated[re.Pattern, BeforeValidator(coerce_list_to_str)]
|
|
36
|
+
PATH_EXISTS = Annotated[Path, AfterValidator(require_exists)]
|
|
37
|
+
DIRECTORY_EXISTS = Annotated[PATH_EXISTS, AfterValidator(require_is_dir)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class VariableType(Enum):
|
|
41
|
+
STRING = "string"
|
|
42
|
+
BOOL = "bool"
|
|
43
|
+
NODE = "node"
|
|
44
|
+
|
|
45
|
+
def default(self) -> Any:
|
|
46
|
+
match self:
|
|
47
|
+
case self.BOOL:
|
|
48
|
+
return False
|
|
49
|
+
case _:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def color(self) -> str:
|
|
53
|
+
match self:
|
|
54
|
+
case self.STRING:
|
|
55
|
+
return Fore.YELLOW
|
|
56
|
+
case self.BOOL:
|
|
57
|
+
return Fore.BLUE
|
|
58
|
+
case self.NODE:
|
|
59
|
+
return Fore.GREEN
|
|
60
|
+
|
|
61
|
+
def format(self, name: str, value: Any) -> str:
|
|
62
|
+
match self:
|
|
63
|
+
case self.STRING:
|
|
64
|
+
return f"{self.color()}{value}{Fore.RESET} ({name}) "
|
|
65
|
+
case self.BOOL:
|
|
66
|
+
assert isinstance(value, bool), f"Value {value} is not a bool for field {name}"
|
|
67
|
+
if value:
|
|
68
|
+
return f"{self.color()}{name}{Fore.RESET} "
|
|
69
|
+
else:
|
|
70
|
+
return ""
|
|
71
|
+
case self.NODE:
|
|
72
|
+
return f"{self.color()}{name}->{str(value)}{Fore.RESET}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TemplateConfig(BaseModel):
|
|
76
|
+
model_config = ConfigDict(frozen=True)
|
|
77
|
+
name: str
|
|
78
|
+
foreach: Optional[list[str]] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Config(BaseModel):
|
|
82
|
+
model_config = ConfigDict(frozen=True)
|
|
83
|
+
regex: EXTENDED_REGEX
|
|
84
|
+
file_regex: EXTENDED_REGEX
|
|
85
|
+
variables: dict[str, VariableType]
|
|
86
|
+
input_directories: list[DIRECTORY_EXISTS]
|
|
87
|
+
output_directory: DIRECTORY_EXISTS
|
|
88
|
+
template_directory: DIRECTORY_EXISTS
|
|
89
|
+
templates: list[TemplateConfig]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from colorama import Fore
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .node import NodeContext
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("simple-oop")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_match_variables(config: Config, g: dict[str, str], file: Path, match_string: str) -> None:
|
|
14
|
+
if "name" not in g:
|
|
15
|
+
print(f"The regex must always fill the \"name\" field, "
|
|
16
|
+
f"this did not happen in {file} for:\n"
|
|
17
|
+
f"{match_string}")
|
|
18
|
+
raise ValueError()
|
|
19
|
+
|
|
20
|
+
for name, value in g.items():
|
|
21
|
+
if name not in config.variables:
|
|
22
|
+
print(f"Any variable used in the regex must be declared in the variables list.\n"
|
|
23
|
+
f"The variable {Fore.RED}{name}{Fore.RESET} was not declared.")
|
|
24
|
+
raise ValueError()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_match(ctx: NodeContext, file: Path, m) -> None:
|
|
28
|
+
g = m.groupdict()
|
|
29
|
+
match_string = m.string[m.start(0):m.end(0)]
|
|
30
|
+
|
|
31
|
+
check_match_variables(ctx.config, g, file, match_string)
|
|
32
|
+
node = ctx.node(g["name"])
|
|
33
|
+
|
|
34
|
+
node.declare(file, match_string)
|
|
35
|
+
|
|
36
|
+
for name, _type in ctx.config.variables.items():
|
|
37
|
+
node.set_variable(name, g.get(name, None), _type)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def discover_file(ctx: NodeContext, path: Path):
|
|
41
|
+
if not ctx.config.file_regex.fullmatch(path.name):
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
string = path.read_text("utf-8")
|
|
45
|
+
except UnicodeDecodeError:
|
|
46
|
+
log.error(f"Failed to decode file \"{path}\" using utf-8")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
for m in ctx.config.regex.finditer(string):
|
|
50
|
+
try:
|
|
51
|
+
parse_match(ctx, path, m)
|
|
52
|
+
except ValueError as e:
|
|
53
|
+
if str(e) != "":
|
|
54
|
+
raise e
|
|
55
|
+
ctx.errors += 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def discover(ctx: NodeContext, path: Path):
|
|
59
|
+
if path.is_file():
|
|
60
|
+
discover_file(ctx, path)
|
|
61
|
+
else:
|
|
62
|
+
for file in path.iterdir():
|
|
63
|
+
discover(ctx, file)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
|
|
7
|
+
from colorama import Fore
|
|
8
|
+
|
|
9
|
+
from simple_oop import Config
|
|
10
|
+
from simple_oop.config import VariableType
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("simple-oop")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Node:
|
|
16
|
+
def __init__(self, ctx: 'NodeContext', name: str) -> None:
|
|
17
|
+
self.ctx: 'NodeContext' = ctx
|
|
18
|
+
|
|
19
|
+
self.file: Optional[Path] = None
|
|
20
|
+
self.declaration: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
self.variables: dict[str, Any] = {name: _type.default() for name, _type in ctx.config.variables.items()}
|
|
23
|
+
self.variables["name"] = name
|
|
24
|
+
|
|
25
|
+
self.references: dict[str, list['Node']] = defaultdict(list)
|
|
26
|
+
|
|
27
|
+
def declare(self, file: Path, declaration: str) -> None:
|
|
28
|
+
assert (self.declaration is None) == (self.file is None)
|
|
29
|
+
|
|
30
|
+
if self.declaration is not None:
|
|
31
|
+
print(f"{Fore.YELLOW}Node {Fore.RED}{self.variables["name"]}{Fore.YELLOW} was declared twice.{Fore.RESET}\n"
|
|
32
|
+
f"Once in {self.file}:\n"
|
|
33
|
+
f"{self.declaration}\n"
|
|
34
|
+
f"Then in {file}:\n"
|
|
35
|
+
f"{declaration}")
|
|
36
|
+
raise ValueError
|
|
37
|
+
|
|
38
|
+
self.file = file
|
|
39
|
+
self.declaration = declaration
|
|
40
|
+
|
|
41
|
+
def set_variable(self, name: str, value: Optional[str], _type: VariableType) -> None:
|
|
42
|
+
actual_value: Any
|
|
43
|
+
match _type:
|
|
44
|
+
case VariableType.BOOL:
|
|
45
|
+
actual_value = value is not None
|
|
46
|
+
case VariableType.NODE if value is not None:
|
|
47
|
+
actual_value = self.ctx.node(value)
|
|
48
|
+
actual_value.references[name].append(self)
|
|
49
|
+
case _:
|
|
50
|
+
actual_value = value
|
|
51
|
+
|
|
52
|
+
self.variables[name] = actual_value
|
|
53
|
+
|
|
54
|
+
def print(self, skip_types=frozenset()):
|
|
55
|
+
for name, _type in self.ctx.config.variables.items():
|
|
56
|
+
if _type in skip_types: continue
|
|
57
|
+
value = self.variables[name]
|
|
58
|
+
print(_type.format(name, value), end="")
|
|
59
|
+
|
|
60
|
+
print()
|
|
61
|
+
|
|
62
|
+
def print_tree(self, tree_field, indent=0) -> None:
|
|
63
|
+
print(end=" " * 4 * indent)
|
|
64
|
+
self.print(frozenset([VariableType.NODE]))
|
|
65
|
+
for child in self.references[tree_field]:
|
|
66
|
+
child.print_tree(tree_field, indent + 1)
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return str(self)
|
|
70
|
+
|
|
71
|
+
def __str__(self) -> str:
|
|
72
|
+
return self.variables["name"]
|
|
73
|
+
|
|
74
|
+
def __getitem__(self, item: str):
|
|
75
|
+
invert: bool = False
|
|
76
|
+
|
|
77
|
+
if item.startswith("not"):
|
|
78
|
+
invert = True
|
|
79
|
+
item = item.removeprefix("not").lstrip()
|
|
80
|
+
|
|
81
|
+
if item not in self.ctx.config.variables:
|
|
82
|
+
log.error(
|
|
83
|
+
f"Variable {item} used in a template declaration or "
|
|
84
|
+
f"template and was not declared in \"variables\"")
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
value = self.variables[item]
|
|
88
|
+
if invert:
|
|
89
|
+
return not value
|
|
90
|
+
else:
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NodeContext:
|
|
95
|
+
def __init__(self, config: Config):
|
|
96
|
+
self.config: Config = config
|
|
97
|
+
self.nodes: dict[str, Node] = {}
|
|
98
|
+
self.errors: int = 0
|
|
99
|
+
|
|
100
|
+
def node(self, name: str) -> Node:
|
|
101
|
+
if name not in self.nodes:
|
|
102
|
+
self.nodes[name] = Node(self, name)
|
|
103
|
+
return self.nodes[name]
|
|
104
|
+
|
|
105
|
+
def print_types(self):
|
|
106
|
+
for node in self.nodes.values():
|
|
107
|
+
node.print()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
program_version = "2026.2.1"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "<YEAR>.<MONTH>.<COUNTER>",
|
|
3
|
+
"version": "2026.2.1",
|
|
4
|
+
"YEAR": 2026,
|
|
5
|
+
"MONTH": 2,
|
|
6
|
+
"COUNTER": 1,
|
|
7
|
+
"targets": {
|
|
8
|
+
"pyproject.toml": {
|
|
9
|
+
"find": "\"<VERSION>\"\\s*#\\s*version",
|
|
10
|
+
"replace": "\"<VERSION>\" # version"
|
|
11
|
+
},
|
|
12
|
+
"simple_oop/version.py": {
|
|
13
|
+
"find": ".+",
|
|
14
|
+
"replace": "program_version = \"<VERSION>\""
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|