deproc-core 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.
- deproc_core-0.1.0/PKG-INFO +19 -0
- deproc_core-0.1.0/deproc/core/__init__.py +5 -0
- deproc_core-0.1.0/deproc/core/context.py +207 -0
- deproc_core-0.1.0/deproc/core/discovery.py +23 -0
- deproc_core-0.1.0/deproc/core/interfaces/__init__.py +11 -0
- deproc_core-0.1.0/deproc/core/interfaces/linker.py +15 -0
- deproc_core-0.1.0/deproc/core/interfaces/parser/__init__.py +5 -0
- deproc_core-0.1.0/deproc/core/interfaces/parser/base.py +11 -0
- deproc_core-0.1.0/deproc/core/interfaces/parser/models.py +142 -0
- deproc_core-0.1.0/deproc/core/interfaces/resolver.py +16 -0
- deproc_core-0.1.0/deproc/core/interfaces/symbol_cache.py +20 -0
- deproc_core-0.1.0/deproc/core/runtime/__init__.py +7 -0
- deproc_core-0.1.0/deproc/core/runtime/registries/__init__.py +5 -0
- deproc_core-0.1.0/deproc/core/runtime/registries/entity.py +42 -0
- deproc_core-0.1.0/deproc_core.egg-info/PKG-INFO +19 -0
- deproc_core-0.1.0/deproc_core.egg-info/SOURCES.txt +22 -0
- deproc_core-0.1.0/deproc_core.egg-info/dependency_links.txt +1 -0
- deproc_core-0.1.0/deproc_core.egg-info/top_level.txt +1 -0
- deproc_core-0.1.0/pyproject.toml +33 -0
- deproc_core-0.1.0/setup.cfg +4 -0
- deproc_core-0.1.0/tests/test_context.py +307 -0
- deproc_core-0.1.0/tests/test_discovery.py +59 -0
- deproc_core-0.1.0/tests/test_entity_registry.py +104 -0
- deproc_core-0.1.0/tests/test_models.py +173 -0
|
@@ -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,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,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,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,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,22 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
deproc/core/__init__.py
|
|
3
|
+
deproc/core/context.py
|
|
4
|
+
deproc/core/discovery.py
|
|
5
|
+
deproc/core/interfaces/__init__.py
|
|
6
|
+
deproc/core/interfaces/linker.py
|
|
7
|
+
deproc/core/interfaces/resolver.py
|
|
8
|
+
deproc/core/interfaces/symbol_cache.py
|
|
9
|
+
deproc/core/interfaces/parser/__init__.py
|
|
10
|
+
deproc/core/interfaces/parser/base.py
|
|
11
|
+
deproc/core/interfaces/parser/models.py
|
|
12
|
+
deproc/core/runtime/__init__.py
|
|
13
|
+
deproc/core/runtime/registries/__init__.py
|
|
14
|
+
deproc/core/runtime/registries/entity.py
|
|
15
|
+
deproc_core.egg-info/PKG-INFO
|
|
16
|
+
deproc_core.egg-info/SOURCES.txt
|
|
17
|
+
deproc_core.egg-info/dependency_links.txt
|
|
18
|
+
deproc_core.egg-info/top_level.txt
|
|
19
|
+
tests/test_context.py
|
|
20
|
+
tests/test_discovery.py
|
|
21
|
+
tests/test_entity_registry.py
|
|
22
|
+
tests/test_models.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deproc
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "deproc-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Language-agnostic core contracts and runtime for deproc plugins."
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Gurveer Singh Virk", email = "gurveervirk.15517@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
license = {text = "Apache-2.0"}
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/gurveervirk/deproc"
|
|
29
|
+
Repository = "https://github.com/gurveervirk/deproc"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["deproc.core*"]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from deproc.core.context import Context
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
class TestSetLanguage:
|
|
6
|
+
def test_set_language_adds_to_selected(self):
|
|
7
|
+
ctx = Context()
|
|
8
|
+
ctx.set_language("python", [".py", ".pyi"], aliases=["py"])
|
|
9
|
+
assert ctx.selected_languages == {"python"}
|
|
10
|
+
assert ctx.selected_file_extensions == {".py", ".pyi"}
|
|
11
|
+
|
|
12
|
+
def test_set_language_normalizes_extension(self):
|
|
13
|
+
ctx = Context()
|
|
14
|
+
ctx.set_language("python", ["py", "pyi"])
|
|
15
|
+
assert ctx.selected_file_extensions == {".py", ".pyi"}
|
|
16
|
+
|
|
17
|
+
def test_set_language_normalizes_language(self):
|
|
18
|
+
ctx = Context()
|
|
19
|
+
ctx.set_language("Python", [".py"])
|
|
20
|
+
assert ctx.selected_languages == {"python"}
|
|
21
|
+
|
|
22
|
+
def test_set_language_without_aliases(self):
|
|
23
|
+
ctx = Context()
|
|
24
|
+
ctx.set_language("python", [".py"])
|
|
25
|
+
assert ctx.selected_languages == {"python"}
|
|
26
|
+
|
|
27
|
+
def test_multiple_languages(self):
|
|
28
|
+
ctx = Context()
|
|
29
|
+
ctx.set_language("python", [".py"])
|
|
30
|
+
ctx.set_language("javascript", [".js"])
|
|
31
|
+
assert ctx.selected_languages == {"python", "javascript"}
|
|
32
|
+
assert ctx.selected_file_extensions == {".py", ".js"}
|
|
33
|
+
|
|
34
|
+
class TestSelectLanguages:
|
|
35
|
+
def setup_method(self):
|
|
36
|
+
self.ctx = Context()
|
|
37
|
+
self.ctx.set_language("python", [".py"], aliases=["py"])
|
|
38
|
+
self.ctx.set_language("javascript", [".js"])
|
|
39
|
+
|
|
40
|
+
def test_exclude_language(self):
|
|
41
|
+
self.ctx.select_languages(["!python"])
|
|
42
|
+
assert self.ctx.selected_languages == {"javascript"}
|
|
43
|
+
|
|
44
|
+
def test_include_language(self):
|
|
45
|
+
self.ctx.select_languages(["!python"])
|
|
46
|
+
self.ctx.select_languages(["python"])
|
|
47
|
+
assert self.ctx.selected_languages == {"python", "javascript"}
|
|
48
|
+
|
|
49
|
+
def test_select_by_alias(self):
|
|
50
|
+
self.ctx.select_languages(["!python"])
|
|
51
|
+
self.ctx.select_languages(["py"])
|
|
52
|
+
assert "python" in self.ctx.selected_languages
|
|
53
|
+
|
|
54
|
+
def test_unknown_language_ignored(self):
|
|
55
|
+
self.ctx.select_languages(["!python"])
|
|
56
|
+
self.ctx.select_languages(["unknown"])
|
|
57
|
+
assert self.ctx.selected_languages == {"javascript"}
|
|
58
|
+
|
|
59
|
+
class TestSelectFileExtensions:
|
|
60
|
+
def setup_method(self):
|
|
61
|
+
self.ctx = Context()
|
|
62
|
+
self.ctx.set_language("python", [".py", ".pyi"])
|
|
63
|
+
|
|
64
|
+
def test_exclude_extension(self):
|
|
65
|
+
self.ctx.select_file_extensions(["!.py"])
|
|
66
|
+
assert self.ctx.selected_file_extensions == {".pyi"}
|
|
67
|
+
|
|
68
|
+
def test_include_extension(self):
|
|
69
|
+
self.ctx.select_file_extensions(["!.py"])
|
|
70
|
+
self.ctx.select_file_extensions([".py"])
|
|
71
|
+
assert self.ctx.selected_file_extensions == {".py", ".pyi"}
|
|
72
|
+
|
|
73
|
+
def test_unknown_extension_ignored(self):
|
|
74
|
+
self.ctx.select_file_extensions(["!.py"])
|
|
75
|
+
self.ctx.select_file_extensions([".unknown"])
|
|
76
|
+
assert self.ctx.selected_file_extensions == {".pyi"}
|
|
77
|
+
|
|
78
|
+
class TestSetParser:
|
|
79
|
+
def setup_method(self):
|
|
80
|
+
self.ctx = Context()
|
|
81
|
+
self.ctx.set_language("python", [".py"])
|
|
82
|
+
|
|
83
|
+
def test_set_and_get_parser(self):
|
|
84
|
+
self.ctx.set_parser("python", "fake_parser")
|
|
85
|
+
assert self.ctx.get_parser("python") == "fake_parser"
|
|
86
|
+
|
|
87
|
+
def test_has_parser(self):
|
|
88
|
+
assert not self.ctx.has_parser("python")
|
|
89
|
+
self.ctx.set_parser("python", "fake_parser")
|
|
90
|
+
assert self.ctx.has_parser("python")
|
|
91
|
+
|
|
92
|
+
def test_remove_parser(self):
|
|
93
|
+
self.ctx.set_parser("python", "fake_parser")
|
|
94
|
+
self.ctx.remove_parser("python")
|
|
95
|
+
assert not self.ctx.has_parser("python")
|
|
96
|
+
|
|
97
|
+
def test_set_parser_raises_for_unregistered_language(self):
|
|
98
|
+
with pytest.raises(KeyError, match="not registered"):
|
|
99
|
+
self.ctx.set_parser("unknown", "fake_parser")
|
|
100
|
+
|
|
101
|
+
def test_get_parser_missing_language_logs_warning(self, caplog):
|
|
102
|
+
caplog.set_level("WARNING")
|
|
103
|
+
self.ctx.get_parser("unknown")
|
|
104
|
+
assert "not registered" in caplog.text
|
|
105
|
+
|
|
106
|
+
def test_has_parser_missing_language_logs_warning(self, caplog):
|
|
107
|
+
caplog.set_level("WARNING")
|
|
108
|
+
self.ctx.has_parser("unknown")
|
|
109
|
+
assert "not registered" in caplog.text
|
|
110
|
+
|
|
111
|
+
class TestSetResolver:
|
|
112
|
+
def setup_method(self):
|
|
113
|
+
self.ctx = Context()
|
|
114
|
+
self.ctx.set_language("python", [".py"])
|
|
115
|
+
|
|
116
|
+
def test_set_and_get_resolver(self):
|
|
117
|
+
self.ctx.set_resolver("python", "fake_resolver")
|
|
118
|
+
assert self.ctx.get_resolver("python") == "fake_resolver"
|
|
119
|
+
|
|
120
|
+
def test_has_resolver(self):
|
|
121
|
+
assert not self.ctx.has_resolver("python")
|
|
122
|
+
self.ctx.set_resolver("python", "fake_resolver")
|
|
123
|
+
assert self.ctx.has_resolver("python")
|
|
124
|
+
|
|
125
|
+
def test_remove_resolver(self):
|
|
126
|
+
self.ctx.set_resolver("python", "fake_resolver")
|
|
127
|
+
self.ctx.remove_resolver("python")
|
|
128
|
+
assert not self.ctx.has_resolver("python")
|
|
129
|
+
|
|
130
|
+
def test_set_resolver_raises_for_unregistered_language(self):
|
|
131
|
+
with pytest.raises(KeyError, match="not registered"):
|
|
132
|
+
self.ctx.set_resolver("unknown", "fake_resolver")
|
|
133
|
+
|
|
134
|
+
def test_get_resolver_missing_language_logs_warning(self, caplog):
|
|
135
|
+
caplog.set_level("WARNING")
|
|
136
|
+
self.ctx.get_resolver("unknown")
|
|
137
|
+
assert "not registered" in caplog.text
|
|
138
|
+
|
|
139
|
+
class TestSetLinker:
|
|
140
|
+
def setup_method(self):
|
|
141
|
+
self.ctx = Context()
|
|
142
|
+
self.ctx.set_language("python", [".py"])
|
|
143
|
+
|
|
144
|
+
def test_set_and_get_linker(self):
|
|
145
|
+
self.ctx.set_linker("python", "fake_linker")
|
|
146
|
+
assert self.ctx.get_linker("python") == "fake_linker"
|
|
147
|
+
|
|
148
|
+
def test_has_linker(self):
|
|
149
|
+
assert not self.ctx.has_linker("python")
|
|
150
|
+
self.ctx.set_linker("python", "fake_linker")
|
|
151
|
+
assert self.ctx.has_linker("python")
|
|
152
|
+
|
|
153
|
+
def test_remove_linker(self):
|
|
154
|
+
self.ctx.set_linker("python", "fake_linker")
|
|
155
|
+
self.ctx.remove_linker("python")
|
|
156
|
+
assert not self.ctx.has_linker("python")
|
|
157
|
+
|
|
158
|
+
def test_set_linker_raises_for_unregistered_language(self):
|
|
159
|
+
with pytest.raises(KeyError, match="not registered"):
|
|
160
|
+
self.ctx.set_linker("unknown", "fake_linker")
|
|
161
|
+
|
|
162
|
+
def test_get_linker_missing_language_logs_warning(self, caplog):
|
|
163
|
+
caplog.set_level("WARNING")
|
|
164
|
+
self.ctx.get_linker("unknown")
|
|
165
|
+
assert "not registered" in caplog.text
|
|
166
|
+
|
|
167
|
+
class TestSetSymbolCache:
|
|
168
|
+
def setup_method(self):
|
|
169
|
+
self.ctx = Context()
|
|
170
|
+
self.ctx.set_language("python", [".py"])
|
|
171
|
+
self.cache = MagicMock()
|
|
172
|
+
self.cache.language = "python"
|
|
173
|
+
|
|
174
|
+
def test_set_and_get_symbol_cache(self):
|
|
175
|
+
self.ctx.set_symbol_cache(self.cache)
|
|
176
|
+
assert self.ctx.get_symbol_cache("python") is self.cache
|
|
177
|
+
|
|
178
|
+
def test_has_symbol_cache(self):
|
|
179
|
+
assert not self.ctx.has_symbol_cache("python")
|
|
180
|
+
self.ctx.set_symbol_cache(self.cache)
|
|
181
|
+
assert self.ctx.has_symbol_cache("python")
|
|
182
|
+
|
|
183
|
+
def test_remove_symbol_cache(self):
|
|
184
|
+
self.ctx.set_symbol_cache(self.cache)
|
|
185
|
+
self.ctx.remove_symbol_cache("python")
|
|
186
|
+
assert not self.ctx.has_symbol_cache("python")
|
|
187
|
+
|
|
188
|
+
def test_set_symbol_cache_raises_for_unregistered_language(self):
|
|
189
|
+
cache = MagicMock()
|
|
190
|
+
cache.language = "unknown"
|
|
191
|
+
with pytest.raises(KeyError, match="not registered"):
|
|
192
|
+
self.ctx.set_symbol_cache(cache)
|
|
193
|
+
|
|
194
|
+
def test_get_symbol_cache_missing_language_logs_warning(self, caplog):
|
|
195
|
+
caplog.set_level("WARNING")
|
|
196
|
+
self.ctx.get_symbol_cache("unknown")
|
|
197
|
+
assert "not registered" in caplog.text
|
|
198
|
+
|
|
199
|
+
class TestCopyFrom:
|
|
200
|
+
def test_copy_preserves_language_config(self):
|
|
201
|
+
ctx = Context()
|
|
202
|
+
ctx.set_language("python", [".py"], aliases=["py"])
|
|
203
|
+
ctx.set_parser("python", "fake_parser")
|
|
204
|
+
copy = Context(copy_from=ctx)
|
|
205
|
+
assert copy.selected_languages == {"python"}
|
|
206
|
+
assert copy.selected_file_extensions == {".py"}
|
|
207
|
+
assert copy.get_parser("python") == "fake_parser"
|
|
208
|
+
|
|
209
|
+
def test_copy_is_independent(self):
|
|
210
|
+
ctx = Context()
|
|
211
|
+
ctx.set_language("python", [".py"])
|
|
212
|
+
ctx.set_parser("python", "fake_parser")
|
|
213
|
+
copy = Context(copy_from=ctx)
|
|
214
|
+
copy.set_parser("python", "different_parser")
|
|
215
|
+
assert ctx.get_parser("python") == "fake_parser"
|
|
216
|
+
assert copy.get_parser("python") == "different_parser"
|
|
217
|
+
|
|
218
|
+
def test_copy_by_default_empty(self):
|
|
219
|
+
ctx = Context()
|
|
220
|
+
assert ctx.selected_languages == set()
|
|
221
|
+
assert ctx.selected_file_extensions == set()
|
|
222
|
+
|
|
223
|
+
class TestReset:
|
|
224
|
+
def test_reset_restores_all_languages(self):
|
|
225
|
+
ctx = Context()
|
|
226
|
+
ctx.set_language("python", [".py"])
|
|
227
|
+
ctx.set_language("javascript", [".js"])
|
|
228
|
+
ctx.select_languages(["!python"])
|
|
229
|
+
ctx.reset()
|
|
230
|
+
assert ctx.selected_languages == {"python", "javascript"}
|
|
231
|
+
|
|
232
|
+
def test_reset_restores_all_file_extensions(self):
|
|
233
|
+
ctx = Context()
|
|
234
|
+
ctx.set_language("python", [".py", ".pyi"])
|
|
235
|
+
ctx.select_file_extensions(["!.py"])
|
|
236
|
+
ctx.reset()
|
|
237
|
+
assert ctx.selected_file_extensions == {".py", ".pyi"}
|
|
238
|
+
|
|
239
|
+
def test_reset_skips_languages_when_flag_false(self):
|
|
240
|
+
ctx = Context()
|
|
241
|
+
ctx.set_language("python", [".py"])
|
|
242
|
+
ctx.select_languages(["!python"])
|
|
243
|
+
ctx.reset(include_languages=False)
|
|
244
|
+
assert ctx.selected_languages == set()
|
|
245
|
+
|
|
246
|
+
class TestSkipPaths:
|
|
247
|
+
def test_default_is_empty(self):
|
|
248
|
+
ctx = Context()
|
|
249
|
+
assert ctx.skip_paths == set()
|
|
250
|
+
|
|
251
|
+
def test_set_skip_paths(self):
|
|
252
|
+
ctx = Context()
|
|
253
|
+
ctx.set_skip_paths({".venv", ".git"})
|
|
254
|
+
assert ctx.skip_paths == {".venv", ".git"}
|
|
255
|
+
|
|
256
|
+
def test_set_skip_paths_replaces(self):
|
|
257
|
+
ctx = Context()
|
|
258
|
+
ctx.set_skip_paths({".venv"})
|
|
259
|
+
ctx.set_skip_paths({".git"})
|
|
260
|
+
assert ctx.skip_paths == {".git"}
|
|
261
|
+
|
|
262
|
+
def test_copy_preserves_skip_paths(self):
|
|
263
|
+
ctx = Context()
|
|
264
|
+
ctx.set_skip_paths({".venv", "__pycache__"})
|
|
265
|
+
copy = Context(copy_from=ctx)
|
|
266
|
+
assert copy.skip_paths == {".venv", "__pycache__"}
|
|
267
|
+
|
|
268
|
+
def test_copy_is_independent(self):
|
|
269
|
+
ctx = Context()
|
|
270
|
+
ctx.set_skip_paths({".venv"})
|
|
271
|
+
copy = Context(copy_from=ctx)
|
|
272
|
+
copy.set_skip_paths({".git"})
|
|
273
|
+
assert ctx.skip_paths == {".venv"}
|
|
274
|
+
assert copy.skip_paths == {".git"}
|
|
275
|
+
|
|
276
|
+
class TestEmptyContext:
|
|
277
|
+
def test_empty_selected_languages(self):
|
|
278
|
+
ctx = Context()
|
|
279
|
+
assert ctx.selected_languages == set()
|
|
280
|
+
|
|
281
|
+
def test_empty_selected_file_extensions(self):
|
|
282
|
+
ctx = Context()
|
|
283
|
+
assert ctx.selected_file_extensions == set()
|
|
284
|
+
|
|
285
|
+
def test_get_parser_returns_none(self):
|
|
286
|
+
ctx = Context()
|
|
287
|
+
assert ctx.get_parser("anything") is None
|
|
288
|
+
|
|
289
|
+
def test_has_parser_returns_false(self):
|
|
290
|
+
ctx = Context()
|
|
291
|
+
assert not ctx.has_parser("anything")
|
|
292
|
+
|
|
293
|
+
def test_remove_parser_noop(self):
|
|
294
|
+
ctx = Context()
|
|
295
|
+
ctx.remove_parser("anything")
|
|
296
|
+
|
|
297
|
+
def test_get_resolver_returns_none(self):
|
|
298
|
+
ctx = Context()
|
|
299
|
+
assert ctx.get_resolver("anything") is None
|
|
300
|
+
|
|
301
|
+
def test_get_linker_returns_none(self):
|
|
302
|
+
ctx = Context()
|
|
303
|
+
assert ctx.get_linker("anything") is None
|
|
304
|
+
|
|
305
|
+
def test_get_symbol_cache_returns_none(self):
|
|
306
|
+
ctx = Context()
|
|
307
|
+
assert ctx.get_symbol_cache("anything") is None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from deproc.core.context import Context
|
|
2
|
+
from deproc.core.discovery import find_source_files
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class TestFindSourceFiles:
|
|
6
|
+
def test_skips_dirs_in_skip_paths(self, tmp_path):
|
|
7
|
+
(tmp_path / "a.py").write_text("")
|
|
8
|
+
(tmp_path / ".venv").mkdir()
|
|
9
|
+
(tmp_path / ".venv" / "b.py").write_text("")
|
|
10
|
+
(tmp_path / ".git").mkdir()
|
|
11
|
+
(tmp_path / ".git" / "c.py").write_text("")
|
|
12
|
+
|
|
13
|
+
ctx = Context(base_path=str(tmp_path))
|
|
14
|
+
ctx.set_language("python", [".py"])
|
|
15
|
+
ctx.set_skip_paths({".venv", ".git"})
|
|
16
|
+
|
|
17
|
+
files = find_source_files(ctx)
|
|
18
|
+
paths = {Path(f).name for f in files}
|
|
19
|
+
assert paths == {"a.py"}
|
|
20
|
+
|
|
21
|
+
def test_empty_skip_paths_finds_all(self, tmp_path):
|
|
22
|
+
(tmp_path / "a.py").write_text("")
|
|
23
|
+
(tmp_path / ".venv").mkdir()
|
|
24
|
+
(tmp_path / ".venv" / "b.py").write_text("")
|
|
25
|
+
|
|
26
|
+
ctx = Context(base_path=str(tmp_path))
|
|
27
|
+
ctx.set_language("python", [".py"])
|
|
28
|
+
|
|
29
|
+
files = find_source_files(ctx)
|
|
30
|
+
assert len(files) == 2
|
|
31
|
+
|
|
32
|
+
def test_glob_pattern_in_skip_paths(self, tmp_path):
|
|
33
|
+
(tmp_path / ".venv").mkdir()
|
|
34
|
+
(tmp_path / ".venv" / "b.py").write_text("")
|
|
35
|
+
(tmp_path / ".venv2").mkdir()
|
|
36
|
+
(tmp_path / ".venv2" / "c.py").write_text("")
|
|
37
|
+
(tmp_path / "a.py").write_text("")
|
|
38
|
+
|
|
39
|
+
ctx = Context(base_path=str(tmp_path))
|
|
40
|
+
ctx.set_language("python", [".py"])
|
|
41
|
+
ctx.set_skip_paths({".venv*"})
|
|
42
|
+
|
|
43
|
+
files = find_source_files(ctx)
|
|
44
|
+
paths = {Path(f).name for f in files}
|
|
45
|
+
assert paths == {"a.py"}
|
|
46
|
+
|
|
47
|
+
def test_glob_pattern_does_not_affect_non_matching(self, tmp_path):
|
|
48
|
+
(tmp_path / ".venv").mkdir()
|
|
49
|
+
(tmp_path / ".venv" / "b.py").write_text("")
|
|
50
|
+
(tmp_path / "vendor").mkdir()
|
|
51
|
+
(tmp_path / "vendor" / "a.py").write_text("")
|
|
52
|
+
|
|
53
|
+
ctx = Context(base_path=str(tmp_path))
|
|
54
|
+
ctx.set_language("python", [".py"])
|
|
55
|
+
ctx.set_skip_paths({"vendor*"})
|
|
56
|
+
|
|
57
|
+
files = find_source_files(ctx)
|
|
58
|
+
paths = {Path(f).name for f in files}
|
|
59
|
+
assert paths == {"b.py"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for EntityRegistry."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from deproc.core.runtime.registries.entity import EntityRegistry
|
|
4
|
+
from deproc.core.interfaces.parser.models import Entity
|
|
5
|
+
|
|
6
|
+
@dataclass(kw_only=True)
|
|
7
|
+
class _FakeFqnEntity(Entity):
|
|
8
|
+
fqn: str
|
|
9
|
+
|
|
10
|
+
class TestEntityRegistryValues:
|
|
11
|
+
def test_empty_registry_values(self):
|
|
12
|
+
registry = EntityRegistry()
|
|
13
|
+
assert list(registry.values()) == []
|
|
14
|
+
|
|
15
|
+
def test_values_after_add(self):
|
|
16
|
+
registry = EntityRegistry()
|
|
17
|
+
e1 = Entity(id="id_1")
|
|
18
|
+
e2 = Entity(id="id_2")
|
|
19
|
+
registry.add(e1)
|
|
20
|
+
registry.add(e2)
|
|
21
|
+
assert list(registry.values()) == [e1, e2]
|
|
22
|
+
|
|
23
|
+
def test_values_after_add_all(self):
|
|
24
|
+
registry = EntityRegistry()
|
|
25
|
+
e1 = Entity(id="id_1")
|
|
26
|
+
e2 = Entity(id="id_2")
|
|
27
|
+
registry.add_all([e1, e2])
|
|
28
|
+
assert list(registry.values()) == [e1, e2]
|
|
29
|
+
|
|
30
|
+
class TestEntityRegistryGet:
|
|
31
|
+
def test_get_returns_entity(self):
|
|
32
|
+
registry = EntityRegistry()
|
|
33
|
+
e = Entity(id="id_1")
|
|
34
|
+
registry.add(e)
|
|
35
|
+
assert registry.get("id_1") is e
|
|
36
|
+
|
|
37
|
+
def test_get_missing_returns_none(self):
|
|
38
|
+
registry = EntityRegistry()
|
|
39
|
+
assert registry.get("nonexistent") is None
|
|
40
|
+
|
|
41
|
+
def test_get_missing_returns_default(self):
|
|
42
|
+
registry = EntityRegistry()
|
|
43
|
+
assert registry.get("nonexistent", "fallback") == "fallback"
|
|
44
|
+
|
|
45
|
+
def test_get_with_none_default(self):
|
|
46
|
+
registry = EntityRegistry()
|
|
47
|
+
assert registry.get("nonexistent", None) is None
|
|
48
|
+
|
|
49
|
+
def test_get_default_not_used_when_found(self):
|
|
50
|
+
registry = EntityRegistry()
|
|
51
|
+
e = Entity(id="id_1")
|
|
52
|
+
registry.add(e)
|
|
53
|
+
assert registry.get("id_1", "fallback") is e
|
|
54
|
+
|
|
55
|
+
class TestEntityRegistryContains:
|
|
56
|
+
def test_contains_added(self):
|
|
57
|
+
registry = EntityRegistry()
|
|
58
|
+
e = Entity(id="id_1")
|
|
59
|
+
registry.add(e)
|
|
60
|
+
assert "id_1" in registry
|
|
61
|
+
|
|
62
|
+
def test_not_contains_missing(self):
|
|
63
|
+
registry = EntityRegistry()
|
|
64
|
+
assert "nonexistent" not in registry
|
|
65
|
+
|
|
66
|
+
class TestEntityRegistryFqnMapping:
|
|
67
|
+
def test_add_entity_with_fqn_updates_mapping(self):
|
|
68
|
+
registry = EntityRegistry()
|
|
69
|
+
e = _FakeFqnEntity(id="id_1", fqn="mymodule.MyClass")
|
|
70
|
+
registry.add(e)
|
|
71
|
+
assert registry.get_ids_by_fqn("mymodule.MyClass") == {"id_1"}
|
|
72
|
+
|
|
73
|
+
def test_add_entity_without_fqn_does_not_update_mapping(self):
|
|
74
|
+
registry = EntityRegistry()
|
|
75
|
+
e = Entity(id="id_1")
|
|
76
|
+
registry.add(e)
|
|
77
|
+
assert registry.get_ids_by_fqn("anything") == set()
|
|
78
|
+
|
|
79
|
+
def test_get_ids_by_fqn_returns_empty_for_missing(self):
|
|
80
|
+
registry = EntityRegistry()
|
|
81
|
+
assert registry.get_ids_by_fqn("nonexistent") == set()
|
|
82
|
+
|
|
83
|
+
def test_get_ids_by_fqn_multiple_entities_same_fqn(self):
|
|
84
|
+
registry = EntityRegistry()
|
|
85
|
+
e1 = _FakeFqnEntity(id="id_1", fqn="mymodule.MyClass")
|
|
86
|
+
e2 = _FakeFqnEntity(id="id_2", fqn="mymodule.MyClass")
|
|
87
|
+
registry.add(e1)
|
|
88
|
+
registry.add(e2)
|
|
89
|
+
assert registry.get_ids_by_fqn("mymodule.MyClass") == {"id_1", "id_2"}
|
|
90
|
+
|
|
91
|
+
def test_remove_entity_cleans_up_fqn_mapping(self):
|
|
92
|
+
registry = EntityRegistry()
|
|
93
|
+
e = _FakeFqnEntity(id="id_1", fqn="mymodule.MyClass")
|
|
94
|
+
registry.add(e)
|
|
95
|
+
registry.remove("id_1")
|
|
96
|
+
assert registry.get_ids_by_fqn("mymodule.MyClass") == set()
|
|
97
|
+
assert "id_1" not in registry
|
|
98
|
+
|
|
99
|
+
def test_remove_entity_without_fqn_does_not_error(self):
|
|
100
|
+
registry = EntityRegistry()
|
|
101
|
+
e = Entity(id="id_1")
|
|
102
|
+
registry.add(e)
|
|
103
|
+
registry.remove("id_1")
|
|
104
|
+
assert "id_1" not in registry
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for deterministic entity ID generation."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
from deproc.core.interfaces.parser.models import (
|
|
5
|
+
Entity,
|
|
6
|
+
ImportStatement,
|
|
7
|
+
Node,
|
|
8
|
+
SourceFile,
|
|
9
|
+
SourceRange,
|
|
10
|
+
SimpleBinding,
|
|
11
|
+
VariableDeclaration,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_PARENT = uuid4().hex
|
|
15
|
+
_OTHER_PARENT = uuid4().hex
|
|
16
|
+
|
|
17
|
+
@dataclass(kw_only=True)
|
|
18
|
+
class _EntityWithSource(Entity):
|
|
19
|
+
source_range: SourceRange
|
|
20
|
+
|
|
21
|
+
class TestEntityDeterministicId:
|
|
22
|
+
def test_same_parent_and_source_range_produces_same_id(self):
|
|
23
|
+
sr = SourceRange(lineno=10, end_lineno=20, col_offset=1, end_col_offset=30)
|
|
24
|
+
e1 = _EntityWithSource(parent_id=_PARENT, source_range=sr)
|
|
25
|
+
e2 = _EntityWithSource(parent_id=_PARENT, source_range=sr)
|
|
26
|
+
assert e1.id == e2.id
|
|
27
|
+
|
|
28
|
+
def test_different_parent_id_produces_different_id(self):
|
|
29
|
+
sr = SourceRange(lineno=10, end_lineno=20, col_offset=1, end_col_offset=30)
|
|
30
|
+
e1 = _EntityWithSource(parent_id=_PARENT, source_range=sr)
|
|
31
|
+
e2 = _EntityWithSource(parent_id=_OTHER_PARENT, source_range=sr)
|
|
32
|
+
assert e1.id != e2.id
|
|
33
|
+
|
|
34
|
+
def test_different_source_range_produces_different_id(self):
|
|
35
|
+
sr1 = SourceRange(lineno=1, end_lineno=5, col_offset=0, end_col_offset=10)
|
|
36
|
+
sr2 = SourceRange(lineno=2, end_lineno=5, col_offset=0, end_col_offset=10)
|
|
37
|
+
e1 = _EntityWithSource(parent_id=_PARENT, source_range=sr1)
|
|
38
|
+
e2 = _EntityWithSource(parent_id=_PARENT, source_range=sr2)
|
|
39
|
+
assert e1.id != e2.id
|
|
40
|
+
|
|
41
|
+
def test_different_subclass_types_produce_different_ids(self):
|
|
42
|
+
sr = SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=5)
|
|
43
|
+
|
|
44
|
+
@dataclass(kw_only=True)
|
|
45
|
+
class SubA(Entity):
|
|
46
|
+
source_range: SourceRange
|
|
47
|
+
|
|
48
|
+
@dataclass(kw_only=True)
|
|
49
|
+
class SubB(Entity):
|
|
50
|
+
source_range: SourceRange
|
|
51
|
+
|
|
52
|
+
a = SubA(parent_id=_PARENT, source_range=sr)
|
|
53
|
+
b = SubB(parent_id=_PARENT, source_range=sr)
|
|
54
|
+
assert a.id != b.id
|
|
55
|
+
|
|
56
|
+
def test_entity_without_parent_id_is_not_deterministic(self):
|
|
57
|
+
e1 = _EntityWithSource(
|
|
58
|
+
parent_id=None,
|
|
59
|
+
source_range=SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=5),
|
|
60
|
+
)
|
|
61
|
+
e2 = _EntityWithSource(
|
|
62
|
+
parent_id=None,
|
|
63
|
+
source_range=SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=5),
|
|
64
|
+
)
|
|
65
|
+
assert e1.id != e2.id
|
|
66
|
+
|
|
67
|
+
def test_entity_without_source_range_is_not_deterministic(self):
|
|
68
|
+
e1 = Entity(parent_id=_PARENT)
|
|
69
|
+
e2 = Entity(parent_id=_PARENT)
|
|
70
|
+
assert e1.id != e2.id
|
|
71
|
+
|
|
72
|
+
def test_explicit_id_is_preserved(self):
|
|
73
|
+
e = Entity(id="my-explicit-id")
|
|
74
|
+
assert e.id == "my-explicit-id"
|
|
75
|
+
|
|
76
|
+
def test_explicit_id_skips_computation(self):
|
|
77
|
+
sr = SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=5)
|
|
78
|
+
e = _EntityWithSource(id="explicit", parent_id=_PARENT, source_range=sr)
|
|
79
|
+
assert e.id == "explicit"
|
|
80
|
+
|
|
81
|
+
class TestNodeDeterministicId:
|
|
82
|
+
def test_same_path_produces_same_id(self):
|
|
83
|
+
n1 = Node(path="src/main.py")
|
|
84
|
+
n2 = Node(path="src/main.py")
|
|
85
|
+
assert n1.id == n2.id
|
|
86
|
+
|
|
87
|
+
def test_different_path_produces_different_id(self):
|
|
88
|
+
n1 = Node(path="src/main.py")
|
|
89
|
+
n2 = Node(path="src/utils.py")
|
|
90
|
+
assert n1.id != n2.id
|
|
91
|
+
|
|
92
|
+
def test_node_id_is_32_hex_chars(self):
|
|
93
|
+
n = Node(path="src/main.py")
|
|
94
|
+
assert isinstance(n.id, str)
|
|
95
|
+
assert len(n.id) == 32
|
|
96
|
+
|
|
97
|
+
def test_source_file_id_is_deterministic(self):
|
|
98
|
+
sf1 = SourceFile(path="foo/bar.py", docstring_range=None, source="")
|
|
99
|
+
sf2 = SourceFile(path="foo/bar.py", docstring_range=None, source="")
|
|
100
|
+
assert sf1.id == sf2.id
|
|
101
|
+
|
|
102
|
+
def test_source_file_with_different_path_has_different_id(self):
|
|
103
|
+
sf1 = SourceFile(path="foo/bar.py", docstring_range=None, source="")
|
|
104
|
+
sf2 = SourceFile(path="foo/baz.py", docstring_range=None, source="")
|
|
105
|
+
assert sf1.id != sf2.id
|
|
106
|
+
|
|
107
|
+
def test_node_uses_path_not_parent_id(self):
|
|
108
|
+
@dataclass(kw_only=True)
|
|
109
|
+
class NodeSubclass(Node):
|
|
110
|
+
source_range: SourceRange
|
|
111
|
+
|
|
112
|
+
sr = SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=5)
|
|
113
|
+
n1 = NodeSubclass(path="foo.py", parent_id=_PARENT, source_range=sr)
|
|
114
|
+
n2 = NodeSubclass(path="foo.py", parent_id=_OTHER_PARENT, source_range=sr)
|
|
115
|
+
assert n1.id == n2.id
|
|
116
|
+
|
|
117
|
+
def test_source_file_different_source_different_id(self):
|
|
118
|
+
sf1 = SourceFile(path="foo.py", docstring_range=None, source="a")
|
|
119
|
+
sf2 = SourceFile(path="foo.py", docstring_range=None, source="b")
|
|
120
|
+
assert sf1.id != sf2.id
|
|
121
|
+
|
|
122
|
+
def test_source_file_same_source_same_id(self):
|
|
123
|
+
sf1 = SourceFile(path="foo.py", docstring_range=None, source="hello")
|
|
124
|
+
sf2 = SourceFile(path="foo.py", docstring_range=None, source="hello")
|
|
125
|
+
assert sf1.id == sf2.id
|
|
126
|
+
|
|
127
|
+
class TestSourceRangeSourceId:
|
|
128
|
+
def test_source_id_defaults_to_none(self):
|
|
129
|
+
sr = SourceRange(lineno=1, end_lineno=10, col_offset=0, end_col_offset=5)
|
|
130
|
+
assert sr.source_id is None
|
|
131
|
+
|
|
132
|
+
def test_source_id_set_via_keyword(self):
|
|
133
|
+
sr = SourceRange(lineno=1, end_lineno=10, col_offset=0, end_col_offset=5, source_id="src123")
|
|
134
|
+
assert sr.source_id == "src123"
|
|
135
|
+
|
|
136
|
+
def test_source_id_not_in_entity_id_computation(self):
|
|
137
|
+
sr1 = SourceRange(lineno=5, end_lineno=5, col_offset=0, end_col_offset=10, source_id="file_a")
|
|
138
|
+
sr2 = SourceRange(lineno=5, end_lineno=5, col_offset=0, end_col_offset=10, source_id="file_b")
|
|
139
|
+
e1 = _EntityWithSource(parent_id=_PARENT, source_range=sr1)
|
|
140
|
+
e2 = _EntityWithSource(parent_id=_PARENT, source_range=sr2)
|
|
141
|
+
assert e1.id == e2.id
|
|
142
|
+
|
|
143
|
+
def test_source_id_mutable(self):
|
|
144
|
+
sr = SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=0)
|
|
145
|
+
assert sr.source_id is None
|
|
146
|
+
sr.source_id = "assigned_later"
|
|
147
|
+
assert sr.source_id == "assigned_later"
|
|
148
|
+
|
|
149
|
+
class TestConcreteSubclassDeterminism:
|
|
150
|
+
def test_variable_declaration_with_parent_and_source_is_deterministic(self):
|
|
151
|
+
sr = SourceRange(lineno=5, end_lineno=5, col_offset=0, end_col_offset=10)
|
|
152
|
+
binding = SimpleBinding(name="x", fqn="mod.x")
|
|
153
|
+
v1 = VariableDeclaration(
|
|
154
|
+
parent_id=_PARENT,
|
|
155
|
+
source_range=sr,
|
|
156
|
+
variable_binding=binding,
|
|
157
|
+
value_range=None,
|
|
158
|
+
type_annotation=None,
|
|
159
|
+
)
|
|
160
|
+
v2 = VariableDeclaration(
|
|
161
|
+
parent_id=_PARENT,
|
|
162
|
+
source_range=sr,
|
|
163
|
+
variable_binding=binding,
|
|
164
|
+
value_range=None,
|
|
165
|
+
type_annotation=None,
|
|
166
|
+
)
|
|
167
|
+
assert v1.id == v2.id
|
|
168
|
+
|
|
169
|
+
def test_import_statement_with_parent_and_source_is_deterministic(self):
|
|
170
|
+
sr = SourceRange(lineno=1, end_lineno=1, col_offset=0, end_col_offset=20)
|
|
171
|
+
i1 = ImportStatement(type="from_import", parent_id=_PARENT, source_range=sr)
|
|
172
|
+
i2 = ImportStatement(type="from_import", parent_id=_PARENT, source_range=sr)
|
|
173
|
+
assert i1.id == i2.id
|