legesher-language-server 0.2.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.
- legesher_language_server/__init__.py +50 -0
- legesher_language_server/__main__.py +140 -0
- legesher_language_server/config.py +333 -0
- legesher_language_server/document.py +195 -0
- legesher_language_server/enhanced_intelligence.py +1018 -0
- legesher_language_server/incremental_translator.py +244 -0
- legesher_language_server/logging_setup.py +335 -0
- legesher_language_server/providers/__init__.py +39 -0
- legesher_language_server/providers/completion.py +516 -0
- legesher_language_server/providers/diagnostic.py +525 -0
- legesher_language_server/providers/hover.py +730 -0
- legesher_language_server/providers/semantic_tokens.py +391 -0
- legesher_language_server/server.py +956 -0
- legesher_language_server/source_map.py +340 -0
- legesher_language_server/type_alias_generator.py +313 -0
- legesher_language_server/utils/__init__.py +5 -0
- legesher_language_server/utils/error_templates.py +404 -0
- legesher_language_server/utils/keyword_docs.py +1333 -0
- legesher_language_server/utils/python_names.py +12 -0
- legesher_language_server-0.2.0.dist-info/METADATA +384 -0
- legesher_language_server-0.2.0.dist-info/RECORD +24 -0
- legesher_language_server-0.2.0.dist-info/WHEEL +4 -0
- legesher_language_server-0.2.0.dist-info/entry_points.txt +5 -0
- legesher_language_server-0.2.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Legesher Language Server.
|
|
2
|
+
|
|
3
|
+
Language Server Protocol (LSP) implementation that gives IDEs multilingual
|
|
4
|
+
Python awareness — hover tooltips, completions, diagnostics, and syntax
|
|
5
|
+
highlighting that follow the user's configured language pack.
|
|
6
|
+
|
|
7
|
+
Launched by IDEs over stdio via the ``legesher-lsp`` console entry point;
|
|
8
|
+
not typically imported directly. Built on pygls 2.0 and supports Python
|
|
9
|
+
3.10 and newer.
|
|
10
|
+
|
|
11
|
+
Capabilities:
|
|
12
|
+
Hover: Translated docstrings, keyword documentation, and cultural
|
|
13
|
+
comments displayed on hover.
|
|
14
|
+
Completion: Localized keyword and builtin completion with fuzzy
|
|
15
|
+
matching against the user's target language pack.
|
|
16
|
+
Diagnostics: Translated error messages with 41 templates across 17
|
|
17
|
+
error types, mapped from Python's syntax/runtime errors.
|
|
18
|
+
Semantic tokens: Syntax highlighting for translated keywords so
|
|
19
|
+
Spanish ``definir`` renders with the same color as English ``def``.
|
|
20
|
+
|
|
21
|
+
Configuration (via environment variables):
|
|
22
|
+
LEGESHER_SOURCE_LANG: Source language code (default: ``"en"``).
|
|
23
|
+
LEGESHER_TARGET_LANG: Target language code (default: ``"en"``).
|
|
24
|
+
LEGESHER_PYTHON_VERSION: Python version; auto-detected if unset.
|
|
25
|
+
LEGESHER_CACHE_SIZE: LRU cache size for pack lookups (default: 1000).
|
|
26
|
+
LEGESHER_CACHE_TTL: Cache TTL in seconds (default: 3600).
|
|
27
|
+
LEGESHER_LOG_LEVEL: Log level (default: ``"INFO"``).
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
Start the server directly from a shell::
|
|
31
|
+
|
|
32
|
+
$ LEGESHER_TARGET_LANG=es legesher-lsp
|
|
33
|
+
|
|
34
|
+
Then point your IDE at the ``legesher-lsp`` binary as an LSP backend
|
|
35
|
+
for Python.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from importlib.metadata import PackageNotFoundError
|
|
39
|
+
from importlib.metadata import version as _distribution_version
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
#: Read from the installed distribution's metadata rather than hand-kept
|
|
43
|
+
#: here, so `--version`, the `initialize` handshake and the release train
|
|
44
|
+
#: all report the version in `pyproject.toml`, which is the wheel's own.
|
|
45
|
+
#: Three literals used to disagree (0.1.0, 0.1.2 and 0.1.3) (CORE-2203).
|
|
46
|
+
__version__ = _distribution_version("legesher-language-server")
|
|
47
|
+
except PackageNotFoundError: # pragma: no cover (running from an uninstalled tree)
|
|
48
|
+
__version__ = "0.0.0.dev0"
|
|
49
|
+
|
|
50
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""CLI entry point for Legesher Language Server."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .config import LanguageServerConfig
|
|
9
|
+
from .logging_setup import LspLogHandler, configure_logging
|
|
10
|
+
from .server import create_server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(log_level: str) -> LspLogHandler:
|
|
14
|
+
"""Configure logging for the language server.
|
|
15
|
+
|
|
16
|
+
Logs go to the client through ``window/logMessage`` with the level they
|
|
17
|
+
were written at, and to stderr only when no connection can carry them.
|
|
18
|
+
Sending them to stdout is not an option, because under stdio transport
|
|
19
|
+
that is the JSON-RPC channel itself, and sending them all to stderr is what
|
|
20
|
+
made a healthy startup look like forty errors (CORE-1979).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The installed handler, which the server is bound to once it exists.
|
|
27
|
+
"""
|
|
28
|
+
return configure_logging(log_level)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
"""Main entry point for language server CLI."""
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description="Legesher Language Server - LSP for multilingual Python",
|
|
35
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
36
|
+
epilog="""
|
|
37
|
+
Examples:
|
|
38
|
+
# Start server with Spanish target language
|
|
39
|
+
legesher-lsp --target-lang es
|
|
40
|
+
|
|
41
|
+
# Start server with French, Python 3.11
|
|
42
|
+
legesher-lsp --target-lang fr --python-version 3.11
|
|
43
|
+
|
|
44
|
+
# Start server with debug logging
|
|
45
|
+
legesher-lsp --target-lang es --log-level DEBUG
|
|
46
|
+
|
|
47
|
+
Environment Variables:
|
|
48
|
+
LEGESHER_SOURCE_LANG Source code language (default: en)
|
|
49
|
+
LEGESHER_TARGET_LANG Target display language (default: es)
|
|
50
|
+
LEGESHER_PYTHON_VERSION Python version (auto-detected if not set)
|
|
51
|
+
LEGESHER_LOG_LEVEL Logging level (default: INFO)
|
|
52
|
+
""",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--source-lang", default=None, help="Source code language (ISO 639-1 code, default: en)"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--target-lang", default=None, help="Target display language (ISO 639-1 code, default: es)"
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--python-version",
|
|
63
|
+
default=None,
|
|
64
|
+
help="Python version (e.g., '3.10', '3.11'). Auto-detected if not specified.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--log-level",
|
|
68
|
+
default=None,
|
|
69
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
70
|
+
help="Logging level (default: INFO)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--stdio",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Use stdio for LSP communication (default mode for IDE integration)",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument("--version", action="version", version=f"legesher-lsp {__version__}")
|
|
78
|
+
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
|
|
81
|
+
# Load config from environment first
|
|
82
|
+
config = LanguageServerConfig.from_env()
|
|
83
|
+
|
|
84
|
+
# Override with CLI arguments if provided
|
|
85
|
+
if args.source_lang:
|
|
86
|
+
config.source_language = args.source_lang
|
|
87
|
+
if args.target_lang:
|
|
88
|
+
# Seeds file_language and working_language too: the flag predates the
|
|
89
|
+
# role split and stands for both (CORE-1760).
|
|
90
|
+
config.apply_legacy_target_language(args.target_lang)
|
|
91
|
+
if args.python_version:
|
|
92
|
+
config.python_version = args.python_version
|
|
93
|
+
if args.log_level:
|
|
94
|
+
config.log_level = args.log_level
|
|
95
|
+
|
|
96
|
+
# Validate language codes
|
|
97
|
+
if not config.validate_languages():
|
|
98
|
+
print(
|
|
99
|
+
f"Error: Invalid language codes. "
|
|
100
|
+
f"Source: '{config.source_language}', Target: '{config.target_language}'. "
|
|
101
|
+
f"Must be 2-letter ISO 639-1 codes.",
|
|
102
|
+
file=sys.stderr,
|
|
103
|
+
)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
# Setup logging
|
|
107
|
+
handler = setup_logging(config.log_level)
|
|
108
|
+
|
|
109
|
+
# Named rather than ``__name__``: launched as ``python -m`` this module is
|
|
110
|
+
# ``__main__``, which sits outside the package logger the trace setting
|
|
111
|
+
# raises and lands in the output channel under a name that says nothing.
|
|
112
|
+
logger = logging.getLogger("legesher_language_server.__main__")
|
|
113
|
+
logger.info(f"Starting Legesher Language Server v{__version__}")
|
|
114
|
+
# Labelled "startup" on purpose. These are the languages the process was
|
|
115
|
+
# launched with, and a client's initialize options can still replace them;
|
|
116
|
+
# the server then reports what it resolved. Two unlabelled reports are what
|
|
117
|
+
# left a log reader guessing which language was actually in effect.
|
|
118
|
+
logger.info(
|
|
119
|
+
f"Startup configuration: source={config.source_language}, "
|
|
120
|
+
f"target={config.target_language}, python={config.detect_python_version()}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Create and start server
|
|
124
|
+
try:
|
|
125
|
+
server = create_server(config)
|
|
126
|
+
# Nothing logged up to here could have reached the client: the
|
|
127
|
+
# transport does not exist until start_io. Binding is what lets those
|
|
128
|
+
# held records be replayed with their real level once it does.
|
|
129
|
+
handler.bind(server)
|
|
130
|
+
server.start_io()
|
|
131
|
+
except KeyboardInterrupt:
|
|
132
|
+
logger.info("Server interrupted by user")
|
|
133
|
+
sys.exit(0)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Server error: {e}", exc_info=True)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main()
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Configuration management for Legesher Language Server.
|
|
2
|
+
|
|
3
|
+
This module provides configuration loading from environment variables,
|
|
4
|
+
command-line arguments, and defaults for the LSP server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Settings renamed in CORE-1890, mapped to what replaced them. Deliberately not
|
|
18
|
+
# honoured — the rename ships without aliases because nothing is published and a
|
|
19
|
+
# compatibility layer would be debt with no debtor. Kept at module level rather
|
|
20
|
+
# than on the model: pydantic reinterprets underscore-prefixed class attributes
|
|
21
|
+
# as private attributes, which are not plain dicts.
|
|
22
|
+
RETIRED_ENV_VARS = {
|
|
23
|
+
"LEGESHER_PYLANCE_ENABLED": "LEGESHER_ENHANCED_INTELLIGENCE_ENABLED",
|
|
24
|
+
"LEGESHER_PYLANCE_PATH": "LEGESHER_LANGUAGE_SERVER_PATH",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LanguageServerConfig(BaseModel):
|
|
29
|
+
"""Configuration for Legesher Language Server.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
source_language: Canonical language the project's code is stored in
|
|
33
|
+
(default: 'en' for English)
|
|
34
|
+
target_language: Deprecated single slot for "the other language"; kept
|
|
35
|
+
in lockstep with file_language so code written against it keeps
|
|
36
|
+
working (CORE-1760)
|
|
37
|
+
file_language: Language a file is written in unless the client says
|
|
38
|
+
otherwise. Each document's own language arrives in a
|
|
39
|
+
``legesher/documentLanguage`` notification; this is the fallback
|
|
40
|
+
for any document the client has not named (CORE-1976)
|
|
41
|
+
working_language: Language the user reads and writes in right now —
|
|
42
|
+
what hover and other render surfaces should target
|
|
43
|
+
python_version: Python version for language pack selection (auto-detected if None)
|
|
44
|
+
cache_size: Maximum number of cached translations (LRU cache)
|
|
45
|
+
cache_ttl: Time-to-live for cache entries in seconds
|
|
46
|
+
enhanced_intelligence_enabled: Whether to enable enhanced Python intelligence
|
|
47
|
+
language_server_path: Path to the Python language server executable
|
|
48
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Language configuration (CORE-1760: the roles are named — canonical /
|
|
52
|
+
# file / working — because one "target" slot cannot say both what a file
|
|
53
|
+
# is written in and what the user wants to read in.)
|
|
54
|
+
source_language: str = Field(default="en", description="Source code language (ISO 639-1 code)")
|
|
55
|
+
target_language: str = Field(
|
|
56
|
+
default="en",
|
|
57
|
+
description="Deprecated alias kept in lockstep with file_language (ISO 639-1 code)",
|
|
58
|
+
)
|
|
59
|
+
file_language: str = Field(
|
|
60
|
+
default="en",
|
|
61
|
+
description="Language of any document the client has not named (ISO 639-1 code)",
|
|
62
|
+
)
|
|
63
|
+
working_language: str = Field(
|
|
64
|
+
default="en",
|
|
65
|
+
description="Language the user reads and writes in right now (ISO 639-1 code)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Python configuration
|
|
69
|
+
python_version: str | None = Field(
|
|
70
|
+
default=None, description="Python version (e.g., '3.10', '3.11'). Auto-detected if None."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Performance configuration
|
|
74
|
+
cache_size: int = Field(default=1000, ge=1, description="Maximum number of cached translations")
|
|
75
|
+
cache_ttl: int = Field(
|
|
76
|
+
default=3600, ge=0, description="Cache time-to-live in seconds (0 = no expiration)"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Enhanced Python intelligence (CORE-360, CORE-1890)
|
|
80
|
+
enhanced_intelligence_enabled: bool = Field(
|
|
81
|
+
default=True, description="Enable enhanced Python intelligence"
|
|
82
|
+
)
|
|
83
|
+
language_server_path: str | None = Field(
|
|
84
|
+
default=None, description="Path to the Python language server (auto-detected if None)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Logging
|
|
88
|
+
log_level: str = Field(default="INFO", description="Logging level")
|
|
89
|
+
|
|
90
|
+
@field_validator("source_language", "target_language", "file_language", "working_language")
|
|
91
|
+
@classmethod
|
|
92
|
+
def validate_language_code(cls, v: str) -> str:
|
|
93
|
+
"""Validate language code format (ISO 639-1/639-2).
|
|
94
|
+
|
|
95
|
+
Prevents cache pollution and path traversal attempts (CORE-446 security).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
v: Language code to validate
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Validated and normalized language code (lowercase)
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If language code format is invalid
|
|
105
|
+
"""
|
|
106
|
+
if not v:
|
|
107
|
+
raise ValueError("Language code cannot be empty")
|
|
108
|
+
|
|
109
|
+
# Check length (2-3 chars for ISO 639-1/639-2)
|
|
110
|
+
if len(v) < 2 or len(v) > 3:
|
|
111
|
+
raise ValueError(f"Language code must be 2-3 characters (ISO 639): '{v}' has {len(v)}")
|
|
112
|
+
|
|
113
|
+
# Must be alphabetic only (no path separators, special chars)
|
|
114
|
+
if not v.isalpha():
|
|
115
|
+
raise ValueError(f"Language code must be alphabetic: '{v}' contains invalid characters")
|
|
116
|
+
|
|
117
|
+
# Convert to lowercase for consistency
|
|
118
|
+
return v.lower()
|
|
119
|
+
|
|
120
|
+
@model_validator(mode="after")
|
|
121
|
+
def keep_file_language_in_lockstep(self) -> "LanguageServerConfig":
|
|
122
|
+
"""Hold the deprecated slot and the file-language role together from construction.
|
|
123
|
+
|
|
124
|
+
The entry points keep them in lockstep, but a model built directly with
|
|
125
|
+
only ``target_language`` left ``file_language`` at "en". That was
|
|
126
|
+
harmless while providers read the deprecated slot. They now serve each
|
|
127
|
+
document in its own language and fall back to ``file_language``
|
|
128
|
+
(CORE-1976), so a configuration written the old way would be served in
|
|
129
|
+
English with nothing to say so. Whichever of the two is given seeds
|
|
130
|
+
the other; when both are given, both are kept as given.
|
|
131
|
+
"""
|
|
132
|
+
given = self.model_fields_set
|
|
133
|
+
if "target_language" in given and "file_language" not in given:
|
|
134
|
+
self.file_language = self.target_language
|
|
135
|
+
elif "file_language" in given and "target_language" not in given:
|
|
136
|
+
self.target_language = self.file_language
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
@field_validator("python_version")
|
|
140
|
+
@classmethod
|
|
141
|
+
def validate_python_version(cls, v: str | None) -> str | None:
|
|
142
|
+
"""Validate Python version format.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
v: Python version string or None
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Validated version string or None
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: If version format is invalid
|
|
152
|
+
"""
|
|
153
|
+
if v is None:
|
|
154
|
+
return v
|
|
155
|
+
|
|
156
|
+
# Must match X.Y or X.Y.Z format
|
|
157
|
+
if not re.match(r"^\d+\.\d+(\.\d+)?$", v):
|
|
158
|
+
raise ValueError(f"Invalid Python version format: '{v}' (expected X.Y or X.Y.Z)")
|
|
159
|
+
|
|
160
|
+
# Reasonable range check
|
|
161
|
+
parts = v.split(".")
|
|
162
|
+
major, minor = int(parts[0]), int(parts[1])
|
|
163
|
+
if major != 3 or minor < 8 or minor > 20:
|
|
164
|
+
raise ValueError(f"Python version out of supported range: {v}")
|
|
165
|
+
|
|
166
|
+
return v
|
|
167
|
+
|
|
168
|
+
@field_validator("cache_size")
|
|
169
|
+
@classmethod
|
|
170
|
+
def validate_cache_size(cls, v: int) -> int:
|
|
171
|
+
"""Validate cache size to prevent resource exhaustion.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
v: Cache size value
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Validated cache size
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValueError: If cache size is out of acceptable range
|
|
181
|
+
"""
|
|
182
|
+
if v < 1 or v > 10000:
|
|
183
|
+
logger.warning(f"Cache size out of range (1-10000): {v}, using 1000")
|
|
184
|
+
return 1000
|
|
185
|
+
return v
|
|
186
|
+
|
|
187
|
+
@field_validator("cache_ttl")
|
|
188
|
+
@classmethod
|
|
189
|
+
def validate_cache_ttl(cls, v: int) -> int:
|
|
190
|
+
"""Validate cache TTL to prevent resource exhaustion.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
v: Cache TTL in seconds
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Validated TTL
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
ValueError: If TTL is out of acceptable range
|
|
200
|
+
"""
|
|
201
|
+
if v < 0 or v > 86400: # Max 24 hours
|
|
202
|
+
logger.warning(f"Cache TTL out of range (0-86400): {v}, using 3600")
|
|
203
|
+
return 3600
|
|
204
|
+
return v
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def _warn_about_retired_variables(cls) -> None:
|
|
208
|
+
"""Say when a retired setting is present but no longer read.
|
|
209
|
+
|
|
210
|
+
Dropping the old names is the right call, but dropping them *silently*
|
|
211
|
+
is not: someone who set ``LEGESHER_PYLANCE_ENABLED=false`` would find
|
|
212
|
+
the feature switched back on, because the replacement defaults to true.
|
|
213
|
+
The value is not honoured — that would reintroduce the alias we
|
|
214
|
+
deliberately avoided — but its presence is announced, so the change is
|
|
215
|
+
something a user can see rather than something they discover.
|
|
216
|
+
"""
|
|
217
|
+
for retired, replacement in RETIRED_ENV_VARS.items():
|
|
218
|
+
if retired in os.environ:
|
|
219
|
+
logger.warning(
|
|
220
|
+
f"{retired} is set but no longer read; it was renamed to "
|
|
221
|
+
f"{replacement} (CORE-1890). The old value is being ignored — "
|
|
222
|
+
"set the new variable to keep your configuration."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def from_env(cls) -> "LanguageServerConfig":
|
|
227
|
+
"""Load configuration from environment variables with validation.
|
|
228
|
+
|
|
229
|
+
Environment variables:
|
|
230
|
+
LEGESHER_SOURCE_LANG: Source language code
|
|
231
|
+
LEGESHER_TARGET_LANG: Target language code
|
|
232
|
+
LEGESHER_PYTHON_VERSION: Python version
|
|
233
|
+
LEGESHER_CACHE_SIZE: Cache size
|
|
234
|
+
LEGESHER_CACHE_TTL: Cache TTL in seconds
|
|
235
|
+
LEGESHER_ENHANCED_INTELLIGENCE_ENABLED: Enable enhanced Python intelligence (true/false)
|
|
236
|
+
LEGESHER_LANGUAGE_SERVER_PATH: Path to the Python language server
|
|
237
|
+
LEGESHER_LOG_LEVEL: Logging level
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Configured LanguageServerConfig instance
|
|
241
|
+
|
|
242
|
+
Note:
|
|
243
|
+
Falls back to safe defaults if environment variables contain invalid values.
|
|
244
|
+
Validation errors are logged as warnings (CORE-446 security).
|
|
245
|
+
"""
|
|
246
|
+
cls._warn_about_retired_variables()
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# Parse cache values with error handling
|
|
250
|
+
try:
|
|
251
|
+
cache_size = int(os.getenv("LEGESHER_CACHE_SIZE", "1000"))
|
|
252
|
+
except ValueError:
|
|
253
|
+
logger.warning("Invalid LEGESHER_CACHE_SIZE, using default 1000")
|
|
254
|
+
cache_size = 1000
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
cache_ttl = int(os.getenv("LEGESHER_CACHE_TTL", "3600"))
|
|
258
|
+
except ValueError:
|
|
259
|
+
logger.warning("Invalid LEGESHER_CACHE_TTL, using default 3600")
|
|
260
|
+
cache_ttl = 3600
|
|
261
|
+
|
|
262
|
+
# LEGESHER_TARGET_LANG predates the role split and stands for all
|
|
263
|
+
# three of the slots it used to mean at once (CORE-1760); passing
|
|
264
|
+
# it once would leave file_language and working_language at "en"
|
|
265
|
+
# while the file is parsed as something else.
|
|
266
|
+
target_lang = os.getenv("LEGESHER_TARGET_LANG", "en")
|
|
267
|
+
|
|
268
|
+
return cls(
|
|
269
|
+
source_language=os.getenv("LEGESHER_SOURCE_LANG", "en"),
|
|
270
|
+
target_language=target_lang,
|
|
271
|
+
file_language=target_lang,
|
|
272
|
+
working_language=target_lang,
|
|
273
|
+
python_version=os.getenv("LEGESHER_PYTHON_VERSION"),
|
|
274
|
+
cache_size=cache_size,
|
|
275
|
+
cache_ttl=cache_ttl,
|
|
276
|
+
enhanced_intelligence_enabled=os.getenv(
|
|
277
|
+
"LEGESHER_ENHANCED_INTELLIGENCE_ENABLED", "true"
|
|
278
|
+
).lower()
|
|
279
|
+
== "true",
|
|
280
|
+
language_server_path=os.getenv("LEGESHER_LANGUAGE_SERVER_PATH"),
|
|
281
|
+
log_level=os.getenv("LEGESHER_LOG_LEVEL", "INFO"),
|
|
282
|
+
)
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
logger.error(f"Invalid configuration: {e}")
|
|
285
|
+
logger.info("Using safe default configuration")
|
|
286
|
+
# Fall back to safe defaults rather than crash
|
|
287
|
+
return cls()
|
|
288
|
+
|
|
289
|
+
def apply_legacy_target_language(self, code: str) -> None:
|
|
290
|
+
"""Set the deprecated target slot and the roles it used to stand for.
|
|
291
|
+
|
|
292
|
+
``target_language`` predates the role split and always meant the file
|
|
293
|
+
and working languages at once (CORE-1760). Every entry point that
|
|
294
|
+
still speaks the old vocabulary — the ``--target-lang`` flag,
|
|
295
|
+
``LEGESHER_TARGET_LANG``, a client sending only ``targetLanguage`` —
|
|
296
|
+
therefore has to seed all three, or the roles this model advertises as
|
|
297
|
+
being in lockstep silently disagree: the file would be parsed as the
|
|
298
|
+
chosen language while ``file_language`` still read ``en``.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
code: Validated ISO 639-1 language code.
|
|
302
|
+
"""
|
|
303
|
+
self.target_language = code
|
|
304
|
+
self.file_language = code
|
|
305
|
+
self.working_language = code
|
|
306
|
+
|
|
307
|
+
def detect_python_version(self) -> str:
|
|
308
|
+
"""Detect or return configured Python version.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Python version string (e.g., '3.10', '3.11')
|
|
312
|
+
"""
|
|
313
|
+
if self.python_version:
|
|
314
|
+
return self.python_version
|
|
315
|
+
|
|
316
|
+
# Use runtime version
|
|
317
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
318
|
+
|
|
319
|
+
def validate_languages(self) -> bool:
|
|
320
|
+
"""Validate that language codes are valid ISO 639-1 codes.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if every language role holds a valid 2-letter code
|
|
324
|
+
"""
|
|
325
|
+
return all(
|
|
326
|
+
len(code) == 2 and code.isalpha()
|
|
327
|
+
for code in (
|
|
328
|
+
self.source_language,
|
|
329
|
+
self.target_language,
|
|
330
|
+
self.file_language,
|
|
331
|
+
self.working_language,
|
|
332
|
+
)
|
|
333
|
+
)
|