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.
@@ -0,0 +1,6 @@
1
+ .git
2
+ .idea
3
+ .venv
4
+ dist
5
+ test
6
+ __pycache__/
@@ -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,3 @@
1
+ # Placeholder
2
+
3
+ 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,3 @@
1
+ from . import command_entry_point
2
+
3
+ command_entry_point()
@@ -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
+ }