Skip to content

Commit 5ee443f

Browse files
authored
feat: Improve the douki structure to allow support for more programming languages (#21)
1 parent 3df6765 commit 5ee443f

23 files changed

+2299
-1729
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ quote-style = "single"
114114
[tool.vulture]
115115
exclude = ["tests"]
116116
ignore_decorators = []
117-
ignore_names = []
117+
ignore_names = ["cwd", "paths", "excludes"]
118118
make_whitelist = true
119119
min_confidence = 80
120120
paths = ["./"]

src/douki/_base/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
title: 'Language-agnostic components: YAML parsing, syncing, etc.'
3+
"""
4+
5+
from douki._base.language import BaseLanguage, get_language
6+
7+
__all__ = ['BaseLanguage', 'get_language']

src/douki/_base/config.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
title: Abstract base configuration for language plugins.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from abc import ABC, abstractmethod
8+
from pathlib import Path
9+
from typing import List
10+
11+
12+
class BaseConfig(ABC):
13+
"""
14+
title: Abstract configuration loader for a language backend.
15+
summary: >-
16+
Each language plugin subclasses this to define how exclude patterns and
17+
source files are discovered.
18+
"""
19+
20+
@abstractmethod
21+
def load_exclude_patterns(self, cwd: Path) -> List[str]:
22+
"""
23+
title: Load exclude patterns from a config file.
24+
parameters:
25+
cwd:
26+
type: Path
27+
returns:
28+
type: List[str]
29+
"""
30+
... # pragma: no cover
31+
32+
@abstractmethod
33+
def collect_files(
34+
self, paths: List[Path], excludes: List[str]
35+
) -> List[Path]:
36+
"""
37+
title: Expand paths into source files, filtering excluded ones.
38+
parameters:
39+
paths:
40+
type: List[Path]
41+
excludes:
42+
type: List[str]
43+
returns:
44+
type: List[Path]
45+
"""
46+
... # pragma: no cover

src/douki/_base/defaults.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
title: Language-agnostic defaults for Douki.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from dataclasses import dataclass, field
8+
from typing import Any, Dict, Tuple
9+
10+
11+
@dataclass
12+
class LanguageDefaults:
13+
"""
14+
title: Per-language defaults for docstring field values.
15+
summary: >-
16+
Each language plugin provides an instance of this class. Fields whose
17+
values match these defaults are omitted from the emitted YAML.
18+
attributes:
19+
visibility:
20+
type: str
21+
mutability:
22+
type: str
23+
scope_function:
24+
type: str
25+
description: Default scope for stand-alone functions.
26+
scope_method:
27+
type: str
28+
description: Default scope for class methods.
29+
file_extensions:
30+
type: Tuple[str, Ellipsis]
31+
description: File extensions to collect (e.g. ('.py',)).
32+
config_files:
33+
type: Tuple[str, Ellipsis]
34+
description: Config files to search for exclude patterns.
35+
field_defaults:
36+
type: Dict[str, Any]
37+
description: >-
38+
Mapping of top-level YAML key to the default value that should be
39+
omitted when emitting.
40+
"""
41+
42+
visibility: str = 'public'
43+
mutability: str = 'mutable'
44+
scope_function: str = 'static'
45+
scope_method: str = 'instance'
46+
file_extensions: Tuple[str, ...] = ()
47+
config_files: Tuple[str, ...] = ()
48+
field_defaults: Dict[str, Any] = field(default_factory=dict)
49+
50+
def get_field_defaults(self) -> Dict[str, Any]:
51+
"""
52+
title: >-
53+
Return the mapping of top-level YAML key to the default value that
54+
should be omitted.
55+
returns:
56+
type: Dict[str, Any]
57+
"""
58+
defaults = {
59+
'visibility': self.visibility,
60+
'mutability': self.mutability,
61+
'scope': self.scope_function,
62+
}
63+
defaults.update(self.field_defaults)
64+
return defaults

src/douki/_base/language.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
title: Language plugin interface.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from abc import ABC, abstractmethod
8+
from typing import Dict, Optional, Type
9+
10+
from douki._base.config import BaseConfig
11+
12+
13+
class BaseLanguage(ABC):
14+
"""
15+
title: Abstract base class for a language plugin.
16+
summary: >-
17+
Plugins subclass this to bind their language-specific extractor, sync
18+
logic, and configuration.
19+
"""
20+
21+
@property
22+
@abstractmethod
23+
def name(self) -> str:
24+
"""
25+
title: The name of the language (e.g. 'python').
26+
returns:
27+
type: str
28+
"""
29+
... # pragma: no cover
30+
31+
@property
32+
@abstractmethod
33+
def config(self) -> BaseConfig:
34+
"""
35+
title: Configuration loader for this language.
36+
returns:
37+
type: BaseConfig
38+
"""
39+
... # pragma: no cover
40+
41+
@abstractmethod
42+
def sync_source(
43+
self, source: str, *, migrate: Optional[str] = None
44+
) -> str:
45+
"""
46+
title: Synchronize docstrings in the given source code.
47+
parameters:
48+
source:
49+
type: str
50+
migrate:
51+
type: Optional[str]
52+
optional: true
53+
returns:
54+
type: str
55+
"""
56+
... # pragma: no cover
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# Registry
61+
# ---------------------------------------------------------------------------
62+
63+
_REGISTRY: Dict[str, Type[BaseLanguage]] = {}
64+
65+
66+
def register_language(lang_class: Type[BaseLanguage]) -> None:
67+
"""
68+
title: Register a language plugin class.
69+
parameters:
70+
lang_class:
71+
type: Type[BaseLanguage]
72+
"""
73+
# Create a temporary instance just to get its name
74+
instance = lang_class()
75+
_REGISTRY[instance.name] = lang_class
76+
77+
78+
def get_language(name: str) -> BaseLanguage:
79+
"""
80+
title: Get an initialized language plugin by name.
81+
parameters:
82+
name:
83+
type: str
84+
returns:
85+
type: BaseLanguage
86+
"""
87+
if name not in _REGISTRY:
88+
raise ValueError(f"Language '{name}' is not registered.")
89+
return _REGISTRY[name]()
90+
91+
92+
def get_registered_language_names() -> list[str]:
93+
"""
94+
title: Return a list of registered language plugin names.
95+
returns:
96+
type: list[str]
97+
"""
98+
return list(_REGISTRY.keys())

0 commit comments

Comments
 (0)