socx-cli 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.
src/socx/core/_uid.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from weakref import WeakValueDictionary
4
+ from typing import ClassVar
5
+ from threading import RLock
6
+
7
+
8
+ class _UIDMeta(type):
9
+ __lock: ClassVar[RLock] = RLock()
10
+ __uid_map: ClassVar[dict[str, int]] = {}
11
+ __handles: ClassVar[dict[int, object]] = WeakValueDictionary()
12
+
13
+ def __call__(cls, *args, **kwargs) -> _UIDMeta:
14
+ inst = super().__call__(*args, **kwargs)
15
+ inst.__uid = cls._next_uid(inst)
16
+ cls.__handles[inst.__uid] = inst
17
+ return inst
18
+
19
+ def ref(cls) -> _UIDMeta:
20
+ return cls.__uid
21
+
22
+ @classmethod
23
+ def dref(cls, handle) -> _UIDMeta:
24
+ return cls.__handles.get(handle)
25
+
26
+ def _next_uid(cls, inst) -> int:
27
+ name = inst.__class__.__name__
28
+ with cls.__lock:
29
+ if name not in cls.__uid_map:
30
+ cls.__uid_map[name] = 0
31
+ rv = cls.__uid_map[name]
32
+ cls.__uid_map[name] += 1
33
+ return rv
34
+
35
+
36
+
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+ from dataclasses import field
3
+ from dataclasses import dataclass
4
+
5
+ from ._uid import _UIDMeta
6
+
7
+
8
+ class TypedPtrMixin(metaclass=_UIDMeta):
9
+ """
10
+ It is probably very unsafe in context of software-security, use this
11
+ for ez compute and lazy eval but using it in an object that also encrypts
12
+ ur password would probably be a rly bad idea.
13
+
14
+ Anyway I dont even think I need this I just thought it would be cool to
15
+ write so I did lol.
16
+ """
17
+
18
+ @property
19
+ def ref(self) -> int:
20
+ return _UIDMeta.ref(self)
21
+
22
+ @classmethod
23
+ def dref(cls, ref: int) -> TypedPtrMixin:
24
+ return _UIDMeta.dref(ref)
25
+
26
+
27
+ @dataclass # cuz a true alpha man never implements __repr__ themselves
28
+ class UIDMixin(TypedPtrMixin):
29
+ """
30
+ A mixin class that generates unique instance ids for instances of the same
31
+ class.
32
+ """
33
+
34
+ uid: int = field(init=False, default=property(lambda self: self.ref))
src/socx/formatter.py ADDED
@@ -0,0 +1,61 @@
1
+ import re
2
+ import abc
3
+ from typing import override
4
+ from dataclasses import dataclass
5
+ from jinja2 import Environment
6
+
7
+ from dynaconf.utils.boxing import DynaBox
8
+
9
+ from .parser import SymbolTable
10
+
11
+
12
+ @dataclass
13
+ class Formatter(abc.ABC):
14
+ @abc.abstractmethod
15
+ def format(
16
+ self, tokens: dict[str, DynaBox], matches: list[re.Match]
17
+ ) -> str:
18
+ """Format matches as text."""
19
+ ...
20
+
21
+
22
+ class SystemVerilogFormatter(Formatter):
23
+ @override
24
+ def format(
25
+ self,
26
+ tokens: dict[str, DynaBox],
27
+ matches: list[re.Match],
28
+ sym_table: SymbolTable,
29
+ ) -> str:
30
+ state = True
31
+ env = Environment()
32
+ base = None
33
+ scope = None
34
+ header = ""
35
+ footer = ""
36
+ output = ""
37
+
38
+ for match in matches:
39
+ name = match.lastgroup
40
+ tok = tokens[name]
41
+
42
+ if tok.starts_scope:
43
+ output += header if state else footer
44
+ scope = match.expand(r"\g<device>_\g<mem>")
45
+ header = match.expand(tok.subst)
46
+ footer = match.expand(tok.scope_ender)
47
+ if scope and base and not state:
48
+ template = env.from_string(output)
49
+ output = template.render(base=base)
50
+ state = not state
51
+ base = None
52
+
53
+ if base is None:
54
+ base = match.group("addr")
55
+
56
+ output += match.expand(tok.subst)
57
+
58
+ output += footer
59
+ template = env.from_string(output)
60
+ output = template.render(base=base)
61
+ return output
src/socx/log.py ADDED
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ import os
5
+ import logging
6
+ from weakref import proxy
7
+ from typing import Final, NewType
8
+
9
+ from rich.console import Console
10
+ from rich.logging import RichHandler
11
+
12
+
13
+ __all__ = (
14
+ # types
15
+ "Level",
16
+ # API
17
+ "log",
18
+ "info",
19
+ "warn",
20
+ "error",
21
+ "fatal",
22
+ "debug",
23
+ "warning",
24
+ "exception",
25
+ "critical",
26
+ "level",
27
+ "set_level",
28
+ "has_handlers",
29
+ "add_handler",
30
+ "get_handler",
31
+ "remove_handler",
32
+ "get_handler_names",
33
+ "add_filter",
34
+ "remove_filter",
35
+ "is_enabled_for",
36
+ # defaults
37
+ "DEFAULT_LEVEL",
38
+ "DEFAULT_FORMAT",
39
+ "DEFAULT_HANDLERS",
40
+ "DEFAULT_TIME_FORMAT",
41
+ )
42
+
43
+ Level: NewType = NewType("Level", int | str)
44
+ """
45
+ Union type definition of `int | str` for annotating level arguments.
46
+ """
47
+
48
+ DEFAULT_LEVEL: Final[Level] = os.environ.get("SOCX_VERBOSITY", logging.WARN)
49
+ """
50
+ Default logger level, a.k.a verbosity.
51
+ """
52
+
53
+ DEFAULT_FORMAT: Final[str] = os.environ.get("SOCX_LOG_FORMAT", "%(message)s")
54
+ """
55
+ Default logger message format.
56
+ """
57
+
58
+ DEFAULT_TIME_FORMAT: Final[str] = os.environ.get("SOCX_TIME_FORMAT", "[%X]")
59
+ """
60
+ Default logger date format logs.
61
+ """
62
+
63
+ DEFAULT_CHILD_FORMAT: Final[str] = os.environ.get(
64
+ "SOCX_LOG_FORMAT",
65
+ "%(asctime)s %(levelname)5s - %(filename)5s:%(lineno)-4d - %(message)s",
66
+ )
67
+ """
68
+ Default logger message format.
69
+ """
70
+
71
+ DEFAULT_CHILD_FORMATTER: Final[str] = logging.Formatter(
72
+ DEFAULT_CHILD_FORMAT, DEFAULT_TIME_FORMAT
73
+ )
74
+ """
75
+ Default logger message format.
76
+ """
77
+
78
+ DEFAULT_HANDLERS: Final[list[logging.Handler]] = [
79
+ RichHandler(
80
+ level=DEFAULT_LEVEL,
81
+ console=Console(tab_size=4, markup=True, force_terminal=True),
82
+ show_time=True,
83
+ show_level=True,
84
+ rich_tracebacks=False,
85
+ omit_repeated_times=False,
86
+ tracebacks_word_wrap=False,
87
+ tracebacks_show_locals=False,
88
+ log_time_format=DEFAULT_TIME_FORMAT,
89
+ tracebacks_theme="monokai",
90
+ locals_max_string=None,
91
+ locals_max_length=None,
92
+ ),
93
+ ]
94
+ """
95
+ Default logging handlers of this module's default `logger`.
96
+ """
97
+
98
+
99
+ def _get_logger() -> logging.Logger:
100
+ logging.basicConfig(
101
+ level=DEFAULT_LEVEL,
102
+ format=DEFAULT_FORMAT,
103
+ datefmt=DEFAULT_TIME_FORMAT,
104
+ handlers=DEFAULT_HANDLERS,
105
+ )
106
+ return logging.getLogger(__package__.partition(".")[0])
107
+
108
+
109
+ _logger = _get_logger()
110
+
111
+
112
+ def get_logger(name: str, filename: str | None = None) -> logging.Logger:
113
+ """
114
+ Get a pretty printing log handler.
115
+
116
+ Parameters
117
+ ----------
118
+ name: str
119
+ Name of the logger.
120
+
121
+ filename: str
122
+ Specifies that a FileHandler be created, using the specified filename
123
+
124
+ Returns
125
+ -------
126
+ A pretty printing logging.Logger instance.
127
+
128
+ """
129
+ rv = _logger.getChild(name)
130
+ if filename is not None:
131
+ handler = logging.FileHandler(
132
+ filename=filename, mode="w", encoding="utf-8"
133
+ )
134
+ handler.setFormatter(DEFAULT_CHILD_FORMATTER)
135
+ rv.addHandler(handler)
136
+ return rv
137
+
138
+
139
+ def log(level: Level, msg: str, *args, **kwargs) -> None:
140
+ _logger.log(msg, *args, **kwargs)
141
+
142
+
143
+ def info(msg: str, *args, **kwargs) -> None:
144
+ _logger.info(msg, *args, **kwargs)
145
+
146
+
147
+ def warn(msg: str, *args, **kwargs) -> None:
148
+ _logger.warn(msg, *args, **kwargs)
149
+
150
+
151
+ def error(msg: str, *args, **kwargs) -> None:
152
+ _logger.error(msg, *args, **kwargs)
153
+
154
+
155
+ def fatal(msg: str, *args, **kwargs) -> None:
156
+ _logger.fatal(msg, *args, **kwargs)
157
+
158
+
159
+ def debug(msg: str, *args, **kwargs) -> None:
160
+ _logger.debug(msg, *args, **kwargs)
161
+
162
+
163
+ def warning(msg: str, *args, **kwargs) -> None:
164
+ _logger.warning(msg, *args, **kwargs)
165
+
166
+
167
+ def exception(msg: str, *args, **kwargs) -> None:
168
+ _logger.exception(msg, *args, **kwargs)
169
+
170
+
171
+ def critical(msg: str, *args, **kwargs) -> None:
172
+ _logger.critical(msg, *args, **kwargs)
173
+
174
+
175
+ def level() -> str:
176
+ return _logger.getEffectiveLevel()
177
+
178
+
179
+ def set_level(level: Level) -> None:
180
+ _logger.setLevel(level)
181
+
182
+
183
+ def has_handlers() -> None:
184
+ _logger.hasHandlers()
185
+
186
+
187
+ def add_handler(handler: logging.Handler) -> None:
188
+ _logger.addHandler(handler)
189
+
190
+
191
+ def get_handler(name: str) -> logging.Handler:
192
+ return logging.getHandlerByName(name)
193
+
194
+
195
+ def remove_handler(handler: logging.Handler) -> None:
196
+ _logger.removeHandler(handler)
197
+
198
+
199
+ def get_handler_names() -> logging.Handlers:
200
+ return logging.getHandlerNames()
201
+
202
+
203
+ def add_filter(filter: logging.Filter) -> None: # noqa: A002
204
+ _logger.addFilter(filter)
205
+
206
+
207
+ def remove_filter(filter: logging.Filter) -> None: # noqa: A002
208
+ _logger.removeFilter(filter)
209
+
210
+
211
+ def is_enabled_for(level: Level) -> bool:
212
+ if isinstance(level, str):
213
+ level = logging.getLevelName(level)
214
+ return _logger.isEnabledFor(level)
215
+
216
+ logger = proxy(_logger)
217
+ """
218
+ Default logging handler.
219
+
220
+ Can be used for default logging when no custom behavior is required.
221
+
222
+ If custom logging is needed, use `get_logger` method to get a custom handler
223
+ instead.
224
+ """
src/socx/memory.py ADDED
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ import dataclasses as dc
5
+
6
+ import rich as rich
7
+ import rich.table as table
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class MemorySegment:
12
+ origin: int
13
+ """
14
+ Origin/Base address of the address space.
15
+ """
16
+
17
+ length: int
18
+ """
19
+ Length/Size of the address space in bytes.
20
+ """
21
+
22
+ end: int
23
+ """
24
+ End/Highest memory address of the address space.
25
+ """
26
+
27
+
28
+ @dc.dataclass(frozen=True)
29
+ class DynamicSymbol:
30
+ name: str
31
+ """
32
+ Name identifier aliased with the symbol.
33
+ """
34
+
35
+ addr: int
36
+ """
37
+ Symbol start address in the relevant address space.
38
+ """
39
+
40
+ length: int
41
+ """
42
+ Total byte length symbol takes up in memory address space.
43
+ """
44
+
45
+ index: int
46
+ """
47
+ Index of symbol in symbol table.
48
+ """
49
+
50
+
51
+ @dc.dataclass
52
+ class SymbolTable(t.TypedDict):
53
+ device: str
54
+ """
55
+ Name identifier of the device associated with the address space.
56
+ """
57
+
58
+ segment: MemorySegment
59
+ """
60
+ Address space memory adress specification.
61
+ """
62
+
63
+ symbols: tuple[DynamicSymbol, ...] | None = None
64
+ """
65
+ Tuple listing of all dynamic symbols associated with the device and their
66
+ mapping within the address space.
67
+ """
68
+
69
+
70
+ @table.dataclass
71
+ class RichSymTable:
72
+ device: str
73
+ """
74
+ Name identifier of the device associated with the address space.
75
+ """
76
+
77
+ segment: MemorySegment
78
+ """
79
+ Address space memory adress specification.
80
+ """
81
+
82
+ symbols: tuple[DynamicSymbol, ...] | None = None
83
+ """
84
+ Tuple listing of all dynamic symbols associated with the device and their
85
+ mapping within the address space.
86
+ """
src/socx/parser.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ("Parser", "LstParser", "parse")
4
+
5
+
6
+ import re as re
7
+ import abc as abc
8
+ import json as json
9
+ import typing as t
10
+ import pathlib as pathlib
11
+ import dataclasses as dc
12
+ from pathlib import Path as Path
13
+
14
+ import rich as rich
15
+ import click as click
16
+ from dynaconf.utils.boxing import DynaBox
17
+
18
+ from .console import console as console
19
+ from .config import settings as settings
20
+ from .memory import SymbolTable as SymbolTable
21
+ from .memory import MemorySegment as MemorySegment
22
+ from .memory import DynamicSymbol as DynamicSymbol
23
+ from .tokenizer import Tokenizer as Tokenizer
24
+ from .tokenizer import LstTokenizer as LstTokenizer
25
+ from .validators import PathValidator as PathValidator
26
+
27
+
28
+ class Parser(abc.ABC):
29
+ @abc.abstractmethod
30
+ def parse(self) -> None:
31
+ """Start the parser."""
32
+ ...
33
+
34
+ @property
35
+ @abc.abstractmethod
36
+ def lang(self) -> DynaBox:
37
+ """Return the 'lang' configuration of the parser's source language."""
38
+ ...
39
+
40
+
41
+ @dc.dataclass
42
+ class LstParser(Parser):
43
+ """
44
+ Parses .lst files to functions definitions represented as a
45
+ python object.
46
+
47
+ See DynamicSymbol, MemorySegment, SymbolTable.
48
+ """
49
+
50
+ options: dict[str, str] | None = None
51
+ includes: set[pathlib.Path] | None = None
52
+ excludes: set[pathlib.Path] | None = None
53
+ source_dir: pathlib.Path | None = None
54
+ target_dir: pathlib.Path | None = None
55
+
56
+ def __init__(
57
+ self: t.Self,
58
+ options: dict[str, str] | None = None,
59
+ includes: set[pathlib.Path] | None = None,
60
+ excludes: set[pathlib.Path] | None = None,
61
+ source_dir: pathlib.Path | None = None,
62
+ target_dir: pathlib.Path | None = None,
63
+ ) -> None:
64
+ """
65
+ Initialize the parser.
66
+
67
+ Parameters
68
+ ----------
69
+ source_dir
70
+ Source directory from which sources are included and parsed by the
71
+ parser.
72
+ target_dir
73
+ Target directory to which parsed sources will be saved with as
74
+ configured in convert.toml
75
+ options
76
+ Options for handling the conversion operation. See `convert.toml`
77
+ for additional info.
78
+ """
79
+ if options is None:
80
+ options = self.cfg.options
81
+ if includes is None:
82
+ includes = self.cfg.includes
83
+ if excludes is None:
84
+ excludes = self.cfg.excludes
85
+ if source_dir is None:
86
+ source_dir = self.cfg.source
87
+ if target_dir is None:
88
+ target_dir = self.cfg.target
89
+ self.options = options
90
+ self.includes = includes
91
+ self.excludes = excludes
92
+ self.source_dir = source_dir
93
+ self.target_dir = target_dir
94
+ self.includes = PathValidator._extract_includes(
95
+ self.source_dir, includes, excludes
96
+ )
97
+ self.sym_table = SymbolTable()
98
+
99
+ @property
100
+ def cfg(self) -> DynaBox:
101
+ return settings.convert[self.lang]
102
+
103
+ @property
104
+ def lang(self) -> DynaBox:
105
+ return "lst"
106
+
107
+ def parse(self) -> None:
108
+ """Parse the sources according to initialization configuration."""
109
+ self._parse_sym_table()
110
+
111
+ def _parse_sym_table(self: t.Self) -> SymbolTable:
112
+ memory_map = {}
113
+ base_addr_file = self.cfg.base_addr_map
114
+ base_addr_path = pathlib.Path(self.source_dir) / base_addr_file
115
+ field_names = tuple([field.name for field in dc.fields(MemorySegment)])
116
+ base_addr_map = json.loads(base_addr_path.read_text())
117
+ for name in field_names:
118
+ for device_field, value in base_addr_map.items():
119
+ if name not in device_field:
120
+ continue
121
+ device = str(device_field).removesuffix(f"_{name}")
122
+ if device not in memory_map:
123
+ memory_map[device] = {}
124
+ memory_map[device][name] = int(value, self.cfg.base_addr_base)
125
+ self.sym_table.clear()
126
+ for device in memory_map:
127
+ if all(name in memory_map[device] for name in field_names):
128
+ space = MemorySegment(**dict(memory_map[device].items()))
129
+ self.sym_table[device] = (space, None)
130
+ self.sym_table = self.sym_table
131
+ return self.sym_table
132
+
133
+ def __hash__(self) -> int:
134
+ return hash(tuple(self.includes))
135
+
136
+
137
+ @click.pass_context
138
+ def parse(ctx: click.Context) -> SymbolTable:
139
+ src = ctx.source_dir if hasattr(ctx, "source_dir") else None
140
+ target = ctx.target_dir if hasattr(ctx, "target_dir") else None
141
+ parser = LstParser(src, target)
142
+ parser.parse()
143
+ table = parser.sym_table
144
+ rich.print(parser)
145
+ rich.print(table)
146
+ return table
147
+
148
+
149
+ def write(ctx: click.Context) -> None:
150
+ for asm in ctx.mods:
151
+ output_file = ctx.output / asm.file.with_suffix(".sv")
152
+ output_file.write_text(asm.to_sv())
153
+ print(f"SystemVerilog output successfuly written to {output_file}")
src/socx/reader.py ADDED
@@ -0,0 +1,47 @@
1
+ import abc
2
+ from typing import override
3
+ from pathlib import Path
4
+ from dataclasses import dataclass
5
+
6
+ from .validators import PathValidator
7
+
8
+
9
+ @dataclass
10
+ class Reader(abc.ABC):
11
+ """
12
+ Reads input from one or more sources.
13
+
14
+ Attributes
15
+ ----------
16
+ source: Path
17
+ Source of input to read from.
18
+
19
+ includes: set[Path]
20
+ Names or patterns to include in the reading list.
21
+
22
+ exludes: set[Path]
23
+ Names or patterns to exclude from the reading list.
24
+ """
25
+
26
+ source: Path | None = None
27
+ includes: set[Path] | None = None
28
+ excludes: set[Path] | None = None
29
+
30
+ @abc.abstractmethod
31
+ def read(self) -> dict[Path, str]:
32
+ """Read text from one or more sources into a dictionary."""
33
+ ...
34
+
35
+
36
+ @dataclass
37
+ class FileReader(Reader):
38
+ @override
39
+ def read(self) -> dict[Path, str]:
40
+ return {path: path.read_text() for path in self.paths}
41
+
42
+ @property
43
+ def paths(self) -> set[Path]:
44
+ return PathValidator._extract_includes(
45
+ src=self.source, includes=self.includes, excludes=self.excludes
46
+ )
47
+
@@ -0,0 +1,14 @@
1
+ __all__ = (
2
+ "Test",
3
+ "TestStatus",
4
+ "TestCommand",
5
+ "Regression",
6
+ "RegressionStatus",
7
+ )
8
+
9
+ from .test import Test as Test
10
+ from .test import Status as TestStatus
11
+ from .test import Command as TestCommand
12
+
13
+ from .regression import Status as RegressionStatus
14
+ from .regression import Regression as Regression