deproc-core 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.
@@ -0,0 +1,5 @@
1
+ from .discovery import find_source_files
2
+
3
+ __all__ = [
4
+ "find_source_files",
5
+ ]
deproc/core/context.py ADDED
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .interfaces.linker import Linker
8
+ from .interfaces.parser import SourceParser
9
+ from .interfaces.resolver import Resolver
10
+ from .interfaces.symbol_cache import SymbolCache
11
+
12
+ from .runtime import EntityRegistry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class Context:
17
+ def __init__(self, base_path: str = "", copy_from: Context | None = None):
18
+ if copy_from:
19
+ self.base_path = copy_from.base_path
20
+ self.entity_registry = copy_from.entity_registry
21
+ self._language_aliases = dict(copy_from._language_aliases)
22
+ self._language_extensions = dict(copy_from._language_extensions)
23
+ self._all_languages = set(copy_from._all_languages)
24
+ self._all_file_extensions = set(copy_from._all_file_extensions)
25
+ self._selected_languages = set(copy_from._selected_languages)
26
+ self._selected_file_extensions = set(copy_from._selected_file_extensions)
27
+ self._parsers = dict(copy_from._parsers)
28
+ self._resolvers = dict(copy_from._resolvers)
29
+ self._linkers = dict(copy_from._linkers)
30
+ self.symbol_caches = dict(copy_from.symbol_caches)
31
+ self._skip_paths = set(copy_from._skip_paths)
32
+ else:
33
+ self.base_path = base_path
34
+ self.entity_registry = EntityRegistry()
35
+ self._language_aliases: dict[str, list[str]] = {}
36
+ self._language_extensions: dict[str, list[str]] = {}
37
+ self._all_languages: set[str] = set()
38
+ self._all_file_extensions: set[str] = set()
39
+ self._selected_languages: set[str] = set()
40
+ self._selected_file_extensions: set[str] = set()
41
+ self._skip_paths: set[str] = set()
42
+ self._parsers: dict[str, SourceParser] = {}
43
+ self._resolvers: dict[str, Resolver] = {}
44
+ self._linkers: dict[str, Linker] = {}
45
+ self.symbol_caches: dict[str, SymbolCache] = {}
46
+
47
+ def set_language(
48
+ self,
49
+ language: str,
50
+ file_extensions: list[str],
51
+ aliases: list[str] | None = None,
52
+ ) -> None:
53
+ normalized = language.strip().lower()
54
+ normalized_extensions: list[str] = []
55
+ for ext in file_extensions:
56
+ e = ext.strip().lower()
57
+ if not e.startswith("."):
58
+ e = f".{e}"
59
+ logger.warning("Normalized file extension '%s' to '%s' for language '%s'", ext, e, normalized)
60
+ normalized_extensions.append(e)
61
+ self._language_extensions[normalized] = normalized_extensions
62
+ if aliases:
63
+ self._language_aliases[normalized] = [a.strip().lower() for a in aliases]
64
+ else:
65
+ self._language_aliases[normalized] = []
66
+ self._all_languages.add(normalized)
67
+ self._selected_languages.add(normalized)
68
+ self._all_file_extensions.update(normalized_extensions)
69
+ self._selected_file_extensions.update(normalized_extensions)
70
+
71
+ @property
72
+ def selected_languages(self) -> set[str]:
73
+ return self._selected_languages
74
+
75
+ @property
76
+ def selected_file_extensions(self) -> set[str]:
77
+ return self._selected_file_extensions
78
+
79
+ @property
80
+ def skip_paths(self) -> set[str]:
81
+ return self._skip_paths
82
+
83
+ def set_skip_paths(self, paths: set[str]) -> None:
84
+ self._skip_paths = set(paths)
85
+
86
+ def _normalize_language(self, language: str) -> str:
87
+ normalized = language.strip().lower()
88
+ for canonical, aliases in self._language_aliases.items():
89
+ if normalized == canonical or normalized in aliases:
90
+ return canonical
91
+ return normalized
92
+
93
+ def _normalize_extension(self, extension: str) -> str:
94
+ normalized = extension.strip().lower()
95
+ if not normalized.startswith("."):
96
+ normalized = f".{normalized}"
97
+ return normalized
98
+
99
+ def select_languages(self, languages: list[str]) -> None:
100
+ for lang in languages:
101
+ normalized = self._normalize_language(lang)
102
+ exclude = normalized.startswith("!")
103
+ if exclude:
104
+ normalized = normalized[1:]
105
+ if normalized in self._all_languages:
106
+ if exclude:
107
+ self._selected_languages.discard(normalized)
108
+ else:
109
+ self._selected_languages.add(normalized)
110
+
111
+ def select_file_extensions(self, extensions: list[str]) -> None:
112
+ for ext in extensions:
113
+ raw = ext.strip().lower()
114
+ exclude = raw.startswith("!")
115
+ if exclude:
116
+ raw = raw[1:]
117
+ normalized = self._normalize_extension(raw)
118
+ if normalized in self._all_file_extensions:
119
+ if exclude:
120
+ self._selected_file_extensions.discard(normalized)
121
+ else:
122
+ self._selected_file_extensions.add(normalized)
123
+
124
+ def _require_language(self, language: str) -> None:
125
+ if language not in self._all_languages:
126
+ raise KeyError(f"Language '{language}' is not registered. Call set_language() first.")
127
+
128
+ def _warn_if_language_missing(self, language: str) -> None:
129
+ if language not in self._all_languages:
130
+ logger.warning("Language '%s' is not registered. Did you forget to call set_language()?", language)
131
+
132
+ def set_parser(self, language: str, parser: SourceParser) -> None:
133
+ self._require_language(language)
134
+ self._parsers[language] = parser
135
+
136
+ def get_parser(self, language: str) -> SourceParser | None:
137
+ self._warn_if_language_missing(language)
138
+ return self._parsers.get(language)
139
+
140
+ def has_parser(self, language: str) -> bool:
141
+ self._warn_if_language_missing(language)
142
+ return language in self._parsers
143
+
144
+ def remove_parser(self, language: str) -> None:
145
+ self._warn_if_language_missing(language)
146
+ self._parsers.pop(language, None)
147
+
148
+ def set_resolver(self, language: str, resolver: Resolver) -> None:
149
+ self._require_language(language)
150
+ self._resolvers[language] = resolver
151
+
152
+ def get_resolver(self, language: str) -> Resolver | None:
153
+ self._warn_if_language_missing(language)
154
+ return self._resolvers.get(language)
155
+
156
+ def has_resolver(self, language: str) -> bool:
157
+ self._warn_if_language_missing(language)
158
+ return language in self._resolvers
159
+
160
+ def remove_resolver(self, language: str) -> None:
161
+ self._warn_if_language_missing(language)
162
+ self._resolvers.pop(language, None)
163
+
164
+ def set_linker(self, language: str, linker: Linker) -> None:
165
+ self._require_language(language)
166
+ self._linkers[language] = linker
167
+
168
+ def get_linker(self, language: str) -> Linker | None:
169
+ self._warn_if_language_missing(language)
170
+ return self._linkers.get(language)
171
+
172
+ def has_linker(self, language: str) -> bool:
173
+ self._warn_if_language_missing(language)
174
+ return language in self._linkers
175
+
176
+ def remove_linker(self, language: str) -> None:
177
+ self._warn_if_language_missing(language)
178
+ self._linkers.pop(language, None)
179
+
180
+ def set_symbol_cache(self, cache: SymbolCache) -> None:
181
+ self._require_language(cache.language)
182
+ self.symbol_caches[cache.language] = cache
183
+
184
+ def get_symbol_cache(self, language: str) -> SymbolCache | None:
185
+ self._warn_if_language_missing(language)
186
+ return self.symbol_caches.get(language)
187
+
188
+ def has_symbol_cache(self, language: str) -> bool:
189
+ self._warn_if_language_missing(language)
190
+ return language in self.symbol_caches
191
+
192
+ def remove_symbol_cache(self, language: str) -> None:
193
+ self._warn_if_language_missing(language)
194
+ self.symbol_caches.pop(language, None)
195
+
196
+ def reset(
197
+ self,
198
+ include_languages: bool = True,
199
+ include_file_extensions: bool = True,
200
+ include_symbol_caches: bool = True,
201
+ ) -> None:
202
+ if include_languages:
203
+ self._selected_languages = set(self._all_languages)
204
+ if include_file_extensions:
205
+ self._selected_file_extensions = set(self._all_file_extensions)
206
+ if include_symbol_caches:
207
+ self.symbol_caches = {}
@@ -0,0 +1,23 @@
1
+ import fnmatch
2
+ import os
3
+ from .context import Context
4
+
5
+ def _match_any(name: str, patterns: set[str]) -> bool:
6
+ return any(fnmatch.fnmatch(name, p) for p in patterns)
7
+
8
+ def find_source_files(context: Context) -> list[str]:
9
+ root = context.base_path
10
+ extension_set = set(context.selected_file_extensions)
11
+ skip_dirs = context.skip_paths
12
+ if not extension_set:
13
+ return []
14
+
15
+ matches: list[str] = []
16
+ for dirpath, dirnames, filenames in os.walk(root):
17
+ dirnames[:] = [d for d in dirnames if not _match_any(d, skip_dirs)]
18
+ for f in filenames:
19
+ if any(f.lower().endswith(ext) for ext in extension_set):
20
+ matches.append(os.path.join(dirpath, f))
21
+
22
+ matches.sort()
23
+ return matches
@@ -0,0 +1,11 @@
1
+ from .linker import Linker
2
+ from .parser import SourceParser
3
+ from .resolver import Resolver
4
+ from .symbol_cache import SymbolCache
5
+
6
+ __all__ = [
7
+ "Linker",
8
+ "Resolver",
9
+ "SourceParser",
10
+ "SymbolCache",
11
+ ]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable
3
+
4
+ if TYPE_CHECKING:
5
+ from ..context import Context
6
+
7
+ from .parser.models import SourceFile, Node
8
+
9
+ T_In = TypeVar("T_In", bound=list[SourceFile])
10
+ T_Out = TypeVar("T_Out", bound=list[Node])
11
+
12
+ @runtime_checkable
13
+ class Linker(Protocol[T_In, T_Out]):
14
+ def link_files(self, nodes: T_In, context: Context) -> T_Out:
15
+ ...
@@ -0,0 +1,5 @@
1
+ from .base import SourceParser
2
+
3
+ __all__ = [
4
+ "SourceParser"
5
+ ]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
3
+
4
+ if TYPE_CHECKING:
5
+ from .models import SourceFile
6
+ from ...context import Context
7
+
8
+ @runtime_checkable
9
+ class SourceParser(Protocol):
10
+ def parse_file(self, file_path: str, context: Context) -> SourceFile:
11
+ ...
@@ -0,0 +1,142 @@
1
+ """
2
+ Provisional data models for the parser interface.
3
+ Please use these models either directly in the plugin implementations or as a reference for defining plugin-specific models.
4
+ """
5
+ import hashlib
6
+ from dataclasses import dataclass, field
7
+ from uuid import NAMESPACE_URL, UUID, uuid4, uuid5
8
+
9
+ type SymbolID = str
10
+
11
+ def generate_id() -> SymbolID:
12
+ return uuid4().hex
13
+
14
+ @dataclass(kw_only=True)
15
+ class Entity:
16
+ id: str | None = None
17
+ parent_id: str | None = None
18
+
19
+ def __post_init__(self):
20
+ if self.id is not None:
21
+ return
22
+ self.id = self._compute_id()
23
+
24
+ def _compute_id(self) -> str:
25
+ parent_id = getattr(self, "parent_id", None)
26
+ source_range = getattr(self, "source_range", None)
27
+ if parent_id and source_range:
28
+ namespace = UUID(hex=parent_id)
29
+ name = f"{type(self).__qualname__}:{source_range.lineno}:{source_range.end_lineno}:{source_range.col_offset}:{source_range.end_col_offset}"
30
+ return uuid5(namespace, name).hex
31
+ return uuid4().hex
32
+
33
+ @dataclass(kw_only=True)
34
+ class SourceRange:
35
+ lineno: int
36
+ end_lineno: int
37
+ col_offset: int
38
+ end_col_offset: int
39
+ source_id: str | None = None
40
+
41
+ @dataclass
42
+ class Docstring:
43
+ docstring_range: SourceRange | None
44
+
45
+ @dataclass
46
+ class Signature:
47
+ signature_range: SourceRange
48
+ arguments_range: SourceRange | None
49
+ return_type_range: SourceRange | None
50
+
51
+ @dataclass
52
+ class Annotation:
53
+ source_range: SourceRange
54
+ name: str
55
+
56
+ @dataclass
57
+ class ImportStatement(Entity):
58
+ source_range: SourceRange
59
+ type: str
60
+
61
+ @dataclass
62
+ class Argument:
63
+ name: str
64
+ source_range: SourceRange
65
+ default_value: str | None
66
+ type_annotation: str | None
67
+
68
+ @dataclass(kw_only=True)
69
+ class FunctionLike(Docstring, Entity):
70
+ name: str
71
+ fqn: str
72
+ source_range: SourceRange
73
+ type: str = field(default="FUNCTION")
74
+ signature: Signature
75
+
76
+ @dataclass
77
+ class SimpleBinding:
78
+ name: str
79
+ fqn: str
80
+
81
+ @dataclass
82
+ class ComplexBinding:
83
+ source_range: SourceRange | None
84
+
85
+ @dataclass(kw_only=True)
86
+ class VariableDeclaration(Entity):
87
+ type: str = field(default="VARIABLE")
88
+ source_range: SourceRange
89
+ variable_binding: SimpleBinding | ComplexBinding
90
+ value_range: SourceRange | None
91
+ type_annotation: SourceRange | None
92
+ modifiers: list[str] = field(default_factory=list)
93
+
94
+ @dataclass(kw_only=True)
95
+ class TypeDefinition(Docstring, Entity):
96
+ name: str
97
+ fqn: str
98
+ source_range: SourceRange
99
+ type: str = field(default="TYPE_DEFINITION")
100
+ annotations: list[Annotation] = field(default_factory=list)
101
+ inherits: list[str] = field(default_factory=list)
102
+ method_ids: list[str] = field(default_factory=list)
103
+ inner_type_ids: list[str] = field(default_factory=list)
104
+ property_ids: list[str] = field(default_factory=list)
105
+ visibility: str | None
106
+
107
+ @dataclass
108
+ class ControlFlowBlock(Entity):
109
+ branch: str
110
+ source_range: SourceRange
111
+ condition_range: SourceRange | None
112
+ import_stmt_ids: list[SymbolID] = field(default_factory=list)
113
+ type_ids: list[SymbolID] = field(default_factory=list)
114
+ function_ids: list[SymbolID] = field(default_factory=list)
115
+ variable_ids: list[SymbolID] = field(default_factory=list)
116
+ nested_group_ids: list[SymbolID] = field(default_factory=list)
117
+
118
+ @dataclass
119
+ class ControlFlowGroup(Entity):
120
+ group_type: str
121
+ source_range: SourceRange
122
+ block_ids: list[SymbolID] = field(default_factory=list)
123
+
124
+ @dataclass
125
+ class Node(Entity):
126
+ path: str
127
+
128
+ def _compute_id(self) -> str:
129
+ return uuid5(NAMESPACE_URL, f"file://{self.path}").hex
130
+
131
+ @dataclass(kw_only=True)
132
+ class SourceFile(Docstring, Node):
133
+ import_stmt_ids: list[SymbolID] = field(default_factory=list)
134
+ type_ids: list[SymbolID] = field(default_factory=list)
135
+ function_ids: list[SymbolID] = field(default_factory=list)
136
+ variable_ids: list[SymbolID] = field(default_factory=list)
137
+ control_flow_group_ids: list[SymbolID] = field(default_factory=list)
138
+ source: str
139
+
140
+ def _compute_id(self) -> str:
141
+ h = hashlib.sha256(self.source.encode()).hexdigest()[:16]
142
+ return uuid5(NAMESPACE_URL, f"file://{self.path}#{h}").hex
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+ from typing import (
3
+ Any,
4
+ Protocol,
5
+ TypeVar,
6
+ runtime_checkable
7
+ )
8
+
9
+ TOut = TypeVar("TOut")
10
+
11
+ @runtime_checkable
12
+ class Resolver(Protocol[TOut]):
13
+ def resolve(self, *args: Any, **kwargs: Any) -> TOut:
14
+ ...
15
+
16
+ __all__ = ["Resolver"]
@@ -0,0 +1,20 @@
1
+ from typing import(
2
+ Any,
3
+ Protocol,
4
+ TypeVar,
5
+ runtime_checkable
6
+ )
7
+
8
+ TCache = TypeVar("TCache")
9
+ TReturn = TypeVar("TReturn")
10
+
11
+ @runtime_checkable
12
+ class SymbolCache(Protocol[TCache, TReturn]):
13
+ language: str
14
+ cache: TCache
15
+
16
+ def get(self, *args: Any, **kwargs: Any) -> TReturn:
17
+ ...
18
+
19
+ def set(self, *args: Any, **kwargs: Any) -> None:
20
+ ...
@@ -0,0 +1,7 @@
1
+ from .registries import (
2
+ EntityRegistry
3
+ )
4
+
5
+ __all__ = [
6
+ "EntityRegistry"
7
+ ]
@@ -0,0 +1,5 @@
1
+ from .entity import EntityRegistry
2
+
3
+ __all__ = [
4
+ "EntityRegistry"
5
+ ]
@@ -0,0 +1,42 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass, field
3
+ from ...interfaces.parser.models import (
4
+ Entity,
5
+ SymbolID,
6
+ )
7
+
8
+ @dataclass
9
+ class EntityRegistry:
10
+ entities: dict[SymbolID, Entity] = field(default_factory=dict)
11
+ fqn_to_ids: dict[str, set[SymbolID]] = field(default_factory=lambda: defaultdict(set))
12
+
13
+ def add(self, entity: Entity) -> None:
14
+ self.entities[entity.id] = entity
15
+ fqn = getattr(entity, "fqn", None)
16
+ if fqn:
17
+ self.fqn_to_ids[fqn].add(entity.id)
18
+
19
+ def add_all(self, entities: list[Entity]) -> None:
20
+ for entity in entities:
21
+ self.add(entity)
22
+
23
+ def remove(self, entity_id: SymbolID) -> None:
24
+ entity = self.entities.pop(entity_id, None)
25
+ if entity is not None:
26
+ fqn = getattr(entity, "fqn", None)
27
+ if fqn and entity_id in self.fqn_to_ids.get(fqn, set()):
28
+ self.fqn_to_ids[fqn].discard(entity_id)
29
+ if not self.fqn_to_ids[fqn]:
30
+ del self.fqn_to_ids[fqn]
31
+
32
+ def get_ids_by_fqn(self, fqn: str) -> set[SymbolID]:
33
+ return set(self.fqn_to_ids.get(fqn, set()))
34
+
35
+ def values(self):
36
+ return self.entities.values()
37
+
38
+ def get(self, entity_id: SymbolID, default=None):
39
+ return self.entities.get(entity_id, default)
40
+
41
+ def __contains__(self, entity_id: SymbolID) -> bool:
42
+ return entity_id in self.entities
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: deproc-core
3
+ Version: 0.1.0
4
+ Summary: Language-agnostic core contracts and runtime for deproc plugins.
5
+ Author-email: Gurveer Singh Virk <gurveervirk.15517@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/gurveervirk/deproc
8
+ Project-URL: Repository, https://github.com/gurveervirk/deproc
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
@@ -0,0 +1,17 @@
1
+ deproc/core/__init__.py,sha256=byQSoqypO46cce6J7ykiEgVLzy90xdMvU38k_qigrA0,81
2
+ deproc/core/context.py,sha256=oNRBB2OzXLpYLahe3IyAi2_G4Uu0VSfuiPocBJEoYmU,8283
3
+ deproc/core/discovery.py,sha256=GUxOqSby7tzwGOHhjMN52rPg7gVDEkHMuZMZvSab0PE,743
4
+ deproc/core/interfaces/__init__.py,sha256=fAScn456-iPQ8jhNMV5GcDcM4Hx80R5YJfMR8FQjsPA,209
5
+ deproc/core/interfaces/linker.py,sha256=N1aRQp2L9Alks4_eygKXyxc091atoddt5bJ1wrMzILM,429
6
+ deproc/core/interfaces/resolver.py,sha256=MuAX16ylFqnC12DzjmXglo6NfZZXIEHR3uB--eDlIuo,285
7
+ deproc/core/interfaces/symbol_cache.py,sha256=Ah-FCeAs84OfbgGUm1H5fcBuzQZbnZBOHXEbBz5sfVE,377
8
+ deproc/core/interfaces/parser/__init__.py,sha256=muqAf12npvewkM4cmK38oLyO6SQrn8SCwBjkIatE2jI,64
9
+ deproc/core/interfaces/parser/base.py,sha256=PsbcKn9acd8oa7FR_K_dSJiFUANdITzbHzTLTWwfx18,321
10
+ deproc/core/interfaces/parser/models.py,sha256=kDbbFxBVSjFs-m1_S5uIX4GoxJTjPEpyGS93P6sOnyQ,4147
11
+ deproc/core/runtime/__init__.py,sha256=OQycEB3UA8DMZ2_OBIvjDzxGJubYe48fQAJ2KuqajJM,83
12
+ deproc/core/runtime/registries/__init__.py,sha256=N4VQvTpcvzQTnKM7XvYhnzGqLVONQ69EImfD8K-hX-A,70
13
+ deproc/core/runtime/registries/entity.py,sha256=fMbpIRkJ--t7ig_Q3RP1akQSWw2mHP1e-VMLLQJizgg,1425
14
+ deproc_core-0.1.0.dist-info/METADATA,sha256=y0emwcdJGseLvbkbRBVCOd39aYIYoK37baNfnx_cRtg,870
15
+ deproc_core-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
16
+ deproc_core-0.1.0.dist-info/top_level.txt,sha256=qyPIAnJVTLaewuAbLpShmn-777EtCI7b1b5sGgkzO10,7
17
+ deproc_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ deproc