Skip to content

Configuration API

hatchling.config

Settings Management

hatchling.config.settings

Modular settings configuration for Hatchling application.

Imports and combines all modular settings classes.

Classes

AppSettings

Bases: BaseModel

Root settings model that aggregates all setting categories.

Implemented as a thread-safe singleton to provide global access to application settings throughout the codebase.

Source code in hatchling/config/settings.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class AppSettings(BaseModel):
    """Root settings model that aggregates all setting categories.

    Implemented as a thread-safe singleton to provide global access
    to application settings throughout the codebase.
    """

    llm: LLMSettings = Field(default_factory=LLMSettings)
    openai: OpenAISettings = Field(default_factory=OpenAISettings)
    ollama: OllamaSettings = Field(default_factory=OllamaSettings)
    paths: PathSettings = Field(default_factory=PathSettings)
    tool_calling: ToolCallingSettings = Field(default_factory=ToolCallingSettings)
    ui: UISettings = Field(default_factory=UISettings)

    def __new__(cls, *args, **kwargs):
        """Ensure only one instance exists (singleton pattern).

        Returns:
            AppSettings: The singleton instance.
        """
        global _app_settings_instance
        if _app_settings_instance is None:
            with _app_settings_lock:
                # Double-check locking pattern
                if _app_settings_instance is None:
                    _app_settings_instance = super().__new__(cls)
        return _app_settings_instance

    @classmethod
    def get_instance(cls) -> 'AppSettings':
        """Get the singleton instance of AppSettings.

        Creates the instance if it doesn't exist.

        Returns:
            AppSettings: The singleton instance.
        """
        global _app_settings_instance
        if _app_settings_instance is None:
            cls()  # This will create the instance via __new__
        return _app_settings_instance

    @classmethod
    def reset_instance(cls) -> None:
        """Reset the singleton instance.

        This method is primarily for testing purposes.
        """
        global _app_settings_instance
        with _app_settings_lock:
            _app_settings_instance = None

    @property
    def api_base(self) -> str:
        """Get the base API URL for the configured LLM provider."""
        if self.llm.provider_enum == ELLMProvider.OPENAI:
            return self.openai.api_base
        elif self.llm.provider_enum == ELLMProvider.OLLAMA:
            return self.ollama.api_base
        else:
            raise ValueError(f"Unsupported LLM provider: {self.llm.provider_enum}")

    class Config:
        extra = "forbid"
Attributes
api_base property

Get the base API URL for the configured LLM provider.

Functions
__new__(*args, **kwargs)

Ensure only one instance exists (singleton pattern).

Returns:

Name Type Description
AppSettings

The singleton instance.

Source code in hatchling/config/settings.py
44
45
46
47
48
49
50
51
52
53
54
55
56
def __new__(cls, *args, **kwargs):
    """Ensure only one instance exists (singleton pattern).

    Returns:
        AppSettings: The singleton instance.
    """
    global _app_settings_instance
    if _app_settings_instance is None:
        with _app_settings_lock:
            # Double-check locking pattern
            if _app_settings_instance is None:
                _app_settings_instance = super().__new__(cls)
    return _app_settings_instance
get_instance() classmethod

Get the singleton instance of AppSettings.

Creates the instance if it doesn't exist.

Returns:

Name Type Description
AppSettings AppSettings

The singleton instance.

Source code in hatchling/config/settings.py
58
59
60
61
62
63
64
65
66
67
68
69
70
@classmethod
def get_instance(cls) -> 'AppSettings':
    """Get the singleton instance of AppSettings.

    Creates the instance if it doesn't exist.

    Returns:
        AppSettings: The singleton instance.
    """
    global _app_settings_instance
    if _app_settings_instance is None:
        cls()  # This will create the instance via __new__
    return _app_settings_instance
reset_instance() classmethod

Reset the singleton instance.

This method is primarily for testing purposes.

Source code in hatchling/config/settings.py
72
73
74
75
76
77
78
79
80
@classmethod
def reset_instance(cls) -> None:
    """Reset the singleton instance.

    This method is primarily for testing purposes.
    """
    global _app_settings_instance
    with _app_settings_lock:
        _app_settings_instance = None

SettingAccessLevel

Bases: str, Enum

Defines access levels for settings.

Source code in hatchling/config/settings.py
18
19
20
21
22
class SettingAccessLevel(str, Enum):
    """Defines access levels for settings."""
    NORMAL = "normal"
    PROTECTED = "protected"
    READ_ONLY = "read_only"

hatchling.config.settings_registry

Settings registry for centralized management of all application settings.

This module provides a central registry that aggregates all settings, enforces access control, and provides listing, getting, setting, resetting, import/export, and search capabilities.

Classes

SettingsRegistry

Central registry for all settings categories and fields.

This class provides a frontend-agnostic API for all settings operations, including access control enforcement, validation, and audit logging.

Source code in hatchling/config/settings_registry.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
class SettingsRegistry:
    """Central registry for all settings categories and fields.

    This class provides a frontend-agnostic API for all settings operations,
    including access control enforcement, validation, and audit logging.
    """

    def __init__(self, app_settings: Optional[AppSettings] = None, load_persistent: bool = True):
        """Initialize the settings registry.

        Args:
            app_settings (AppSettings): The application settings instance to manage.
        """
        self.settings = app_settings or AppSettings()
        self.logger = logging_manager.get_session("hatchling.config.settings_registry")

        # Initialize translation loader with current language
        translation_loader = get_translation_loader()
        current_language = self.settings.ui.language_code
        if current_language != translation_loader.get_current_language():
            translation_loader.set_language(current_language)

        # Overlay with persistent settings if they exist, and if requested
        if load_persistent:
            self.load_persistent_settings()

    def list_settings(self, filter_regex: Optional[str] = None) -> List[Dict[str, Any]]:
        """List all settings with optional filtering.

        Implements staged search: exact match, category match, regex search, then fuzzy search.

        Args:
            filter_regex (str, optional): Filter pattern for settings. Defaults to None.

        Returns:
            List[Dict[str, Any]]: List of setting information dictionaries.
        """
        all_settings = self._get_all_settings_metadata()

        if not filter_regex:
            return all_settings

        # Stage 1: Exact match (category or setting name)
        exact_matches = []
        for setting in all_settings:
            if setting['category_name']+":"+setting['name'] == filter_regex:
                return [setting]

        # Stage 2: Category-wide listing
        category_matches = []
        for setting in all_settings:
            if setting['category_name'] == filter_regex:
                category_matches.append(setting)

        if category_matches:
            return category_matches

        # Stage 3: Regex search
        try:
            regex_pattern = re.compile(filter_regex, re.IGNORECASE)
            regex_matches = []
            for setting in all_settings:
                if (regex_pattern.search(setting['category_name']) or 
                    regex_pattern.search(setting['name']) or 
                    regex_pattern.search(setting['description'])):
                    regex_matches.append(setting)

            if regex_matches:
                return regex_matches
        except re.error:
            # Invalid regex, fall through to fuzzy search
            pass

        # Stage 4: Fuzzy search
        fuzzy_matches = []
        for setting in all_settings:
            search_text = f"{setting['category_name']} {setting['name']} {setting['description']}"
            similarity = SequenceMatcher(None, filter_regex.lower(), search_text.lower()).ratio()
            if similarity > 0.3:  # Minimum similarity threshold
                setting['_similarity'] = similarity
                fuzzy_matches.append(setting)

        # Sort by similarity (highest first)
        fuzzy_matches.sort(key=lambda x: x.get('_similarity', 0), reverse=True)

        # Remove similarity score from results
        for match in fuzzy_matches:
            match.pop('_similarity', None)

        return fuzzy_matches

    def get_setting(self, category: str, name: str) -> Dict[str, Any]:
        """Get metadata and value for a specific setting.

        Args:
            category (str): Setting category name.
            name (str): Setting name.

        Returns:
            Dict[str, Any]: Setting information including current and default values.

        Raises:
            ValueError: If the setting is not found.
        """
        setting_info = self._get_setting_info(category, name)
        if not setting_info:
            raise ValueError(f"Setting '{category}:{name}' not found")

        return setting_info

    def set_setting(self, category: str, name: str, value: Any, force: bool = False) -> bool:
        """Set a setting value with access control enforcement.

        Args:
            category (str): Setting category name.
            name (str): Setting name.
            value (Any): New value for the setting.
            force (bool, optional): Force setting protected values. Defaults to False.

        Returns:
            bool: True if setting was successful.

        Raises:
            ValueError: If setting is not found or access is denied.
            ValidationError: If the value is invalid.
        """
        setting_info = self._get_setting_info(category, name)
        if not setting_info:
            raise ValueError(f"Setting '{category}:{name}' not found")

        access_level = setting_info['access_level']
        old_value = setting_info['current_value']

        # Enforce access control
        if access_level == SettingAccessLevel.READ_ONLY:
            raise ValueError(f"Setting '{category}:{name}' is read-only and cannot be modified")

        if access_level == SettingAccessLevel.PROTECTED and not force:
            raise ValueError(f"Setting '{category}:{name}' is protected. Use --force to override")

        # Validate and set the value
        try:
            self._set_setting_value(category, name, value)
            new_value = self._get_setting_value(category, name)
            # Log the change
            self.logger.info(f"Setting '{category}:{name}' changed from '{old_value}' to '{new_value}'")
            return True
        except ValidationError as e:
            # Re-raise the original ValidationError for correct error reporting
            raise

    def reset_setting(self, category: str, name: str, force: bool = False) -> bool:
        """Reset a setting to its default value.

        Args:
            category (str): Setting category name.
            name (str): Setting name.
            force (bool, optional): Force resetting protected values. Defaults to False.

        Returns:
            bool: True if reset was successful.

        Raises:
            ValueError: If setting is not found or access is denied.
        """
        setting_info = self._get_setting_info(category, name)
        if not setting_info:
            raise ValueError(f"Setting '{category}:{name}' not found")

        default_value = setting_info['default_value']
        return self.set_setting(category, name, default_value, force)

    def make_serializable(self, obj: Any) -> Any:
        """Convert an object to a serializable format.

        Args:
            obj (Any): The object to convert.

        Returns:
            Any: A serializable version of the object.
        """
        if obj is None:
            return "None"
        elif isinstance(obj, dict):
            return {k: self.make_serializable(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [self.make_serializable(i) for i in obj]
        elif isinstance(obj, Path):
            return str(obj)
        elif isinstance(obj, enum.Enum):
            return obj.value
        elif isinstance(obj, ModelInfo):
            return [obj.provider.value, obj.name]
        return obj

    def export_settings(self, format: str = "toml", include_read_only: bool = False) -> str:
        """Export settings to a formatted string.

        Args:
            format (str, optional): Export format ('toml', 'json', 'yaml'). Defaults to "toml".
            include_read_only (bool, optional): Whether to include read-only settings. Defaults to False.

        Returns:
            str: Serialized settings data.

        Raises:
            ValueError: If format is not supported.
        """
        if include_read_only:
            # Export all settings (original behavior)
            settings_dict = self.settings.model_dump()
        else:
            # Export only non-read-only settings
            settings_dict = self._get_exportable_settings()

        settings_dict = self.make_serializable(settings_dict)

        if format.lower() == "toml":
            return toml_write.dumps(settings_dict)
        elif format.lower() == "json":
            return json.dumps(settings_dict, indent=2)
        elif format.lower() == "yaml":
            return yaml.dump(settings_dict, default_flow_style=False)
        else:
            raise ValueError(f"Unsupported export format: {format}")

    def import_settings(self, data: str, format: str = "toml", force: bool = False) -> Dict[str, Any]:
        """Import settings from a formatted string.

        Args:
            data (str): Serialized settings data.
            format (str, optional): Import format ('toml', 'json', 'yaml'). Defaults to "toml".
            force (bool, optional): Force importing protected values. Defaults to False.

        Returns:
            Dict[str, Any]: Import report with success, skipped, and failed settings.

        Raises:
            ValueError: If format is not supported or data is invalid.
        """
        # Parse the data
        try:
            if format.lower() == "toml":
                parsed_data = toml_read.loads(data)
            elif format.lower() == "json":
                parsed_data = json.loads(data)
            elif format.lower() == "yaml":
                parsed_data = yaml.safe_load(data)
            else:
                raise ValueError(f"Unsupported import format: {format}")
        except Exception as e:
            raise ValueError(f"Failed to parse {format} data: {e}")

        report = {
            "successful": [],
            "skipped": [],
            "failed": []
        }

        # Import each category
        for category_name, category_data in parsed_data.items():
            if not isinstance(category_data, dict):
                continue

            for setting_name, value in category_data.items():
                try:
                    self.set_setting(category_name, setting_name, value, force)
                    report["successful"].append(f"{category_name}:{setting_name}")
                except ValueError as e:
                    if "read-only" in str(e):
                        self.logger.warning(f"{category_name}:{setting_name} is read-only and cannot be modified --> skipped")
                        report["skipped"].append(f"{category_name}:{setting_name} ({str(e)})")
                    elif("protected" in str(e) and not force):
                        self.logger.warning(f"{category_name}:{setting_name} is protected and cannot be modified without force --> skipped")
                        report["skipped"].append(f"{category_name}:{setting_name} ({str(e)})")
                    else:
                        self.logger.error(f"Failed to set {category_name}:{setting_name} --> {str(e)}")
                        report["failed"].append(f"{category_name}:{setting_name} ({str(e)})")
                except Exception as e:
                    self.logger.error(f"Unexpected error setting {category_name}:{setting_name} --> {str(e)}")
                    report["failed"].append(f"{category_name}:{setting_name} ({str(e)})")

        # Log the import operation
        self.logger.info(f"Settings import completed: {len(report['successful'])} successful, "
                        f"{len(report['skipped'])} skipped, {len(report['failed'])} failed")

        return report

    def _get_all_settings_metadata(self) -> List[Dict[str, Any]]:
        """Get metadata for all settings with internationalized display names and descriptions.

        Returns:
            List[Dict[str, Any]]: List of metadata dictionaries for each setting.
        """
        from pydantic import BaseModel

        settings_list = []

        # Iterate over the main settings model fields to get category names
        for category_name, _category_model in iter(self.settings):
            if not isinstance(_category_model, BaseModel):
                continue  # Skip non-model fields

            category_model_class = type(_category_model)

            # Get translated category information
            category_display_name = translate(f"settings.{category_name}.category_display_name")
            category_description = translate(f"settings.{category_name}.category_description")

            for field_name, field_info in category_model_class.model_fields.items():
                current_value = getattr(_category_model, field_name)
                default_value = field_info.default

                # Handle default_factory
                if hasattr(field_info, 'default_factory') and field_info.default_factory:
                    default_value = field_info.default_factory()

                # Get access level from field info
                access_level = SettingAccessLevel.NORMAL
                if hasattr(field_info, 'json_schema_extra') and field_info.json_schema_extra:
                    access_level = field_info.json_schema_extra.get('access_level', SettingAccessLevel.NORMAL)
                elif hasattr(field_info, 'extra') and 'access_level' in field_info.extra:
                    access_level = field_info.extra['access_level']

                # Get translated setting information
                setting_name_key = f"settings.{category_name}.{field_name}.name"
                setting_desc_key = f"settings.{category_name}.{field_name}.description"
                setting_hint_key = f"settings.{category_name}.{field_name}.hint"

                setting_display_name = translate(setting_name_key)
                setting_description = translate(setting_desc_key)
                setting_hint = translate(setting_hint_key)

                # Fall back to original description if translation key doesn't exist
                if setting_description == setting_desc_key:
                    setting_description = field_info.description or ""

                settings_list.append({
                    "category_name": category_name,
                    "category_display_name": category_display_name,
                    "category_description": category_description,
                    "name": field_name,
                    "display_name": setting_display_name,
                    "current_value": current_value,
                    "default_value": default_value,
                    "description": setting_description,
                    "hint": setting_hint if setting_hint != setting_hint_key else "",
                    "access_level": access_level,
                    "type": str(field_info.annotation) if hasattr(field_info, 'annotation') else str(type(current_value).__name__)
                })
        return settings_list

    def _get_setting_info(self, category: str, name: str) -> Optional[Dict[str, Any]]:
        """Get information for a specific setting."""
        all_settings = self._get_all_settings_metadata()
        for setting in all_settings:
            if setting['category_name'] == category and setting['name'] == name:
                return setting
        return None

    def _get_setting_value(self, category: str, name: str) -> Any:
        """Get the current value of a setting."""
        category_model = getattr(self.settings, category, None)
        if category_model is None:
            raise ValueError(f"Unknown category: {category}")

        return getattr(category_model, name, None)

    def _set_setting_value(self, category: str, name: str, value: Any) -> None:
        """Set the value of a setting with validation."""
        category_model = getattr(self.settings, category, None)
        if category_model is None:
            raise ValueError(f"Unknown category: {category}")

        if not hasattr(category_model, name):
            raise ValueError(f"Unknown setting: {name}")

        # Use Pydantic's validation by creating a new instance
        category_dict = category_model.model_dump()

        # if value is "None" (as a string), convert it to None
        if isinstance(value, str) and value.lower() == "none":
            value = None

        # Convert to Enum if needed
        field_info = type(category_model).model_fields[name]
        field_type = field_info.annotation
        import enum
        if isinstance(field_type, type) and issubclass(field_type, enum.Enum) and value is not None:
            if not isinstance(value, field_type):
                self.logger.info(f"Converting value '{value}' to enum {field_type.__name__}")
                value = field_type(value)

        if get_origin(field_type) is list and get_args(field_type) and get_args(field_type)[0] is ModelInfo:
            self.logger.info(f"Converting list of models for setting '{name}' in category '{category}'")
            value = [
                ModelInfo(
                    name=item[1] if isinstance(item, (list, tuple)) and len(item) > 1 else None,
                    provider=ELLMProvider(item[0]) if isinstance(item, (list, tuple)) and len(item) > 0 else ELLMProvider.OLLAMA,
                    status=ModelStatus.AVAILABLE
                )
                for item in value if isinstance(item, (list, tuple)) and len(item) >= 2
            ]

        category_dict[name] = value

        # This will raise ValidationError if the value is invalid
        new_category_model = type(category_model)(**category_dict)

        # If validation passes, update the actual model
        setattr(category_model, name, value)

    # Language management methods

    def get_available_languages(self) -> List[Dict[str, str]]:
        """Get list of available languages for the interface.

        Returns:
            List[Dict[str, str]]: List of available languages with code and name.
        """
        translation_loader = get_translation_loader()
        return translation_loader.get_available_languages()

    def get_current_language(self) -> str:
        """Get the current interface language.

        Returns:
            str: Current language code.
        """
        # Get from UI settings
        return self.settings.ui.language_code

    def set_language(self, language_code: str) -> bool:
        """Set the interface language.

        Args:
            language_code (str): Language code to set.

        Returns:
            bool: True if language was successfully set.

        Raises:
            ValueError: If language is not available.
        """
        translation_loader = get_translation_loader()

        # Set in translation loader
        if translation_loader.set_language(language_code):
            # Update UI settings
            try:
                self.set_setting("ui", "language_code", language_code)
                return True
            except Exception as e:
                self.logger.error(f"Failed to update language setting: {e}")
                return False
        return False

    def reload_translations(self) -> None:
        """Reload translation files from disk."""
        translation_loader = get_translation_loader()
        translation_loader.reload_translations()
        self.logger.info("Translations reloaded")

    # File-based import/export methods

    def export_settings_to_file(self, file_path: str, format: Optional[str] = None, include_read_only: bool = False) -> bool:
        """Export settings to a file.

        Args:
            file_path (str): Path to export file.
            format (Optional[str]): Export format. If None, detected from file extension.
            include_read_only (bool, optional): Whether to include read-only settings. Defaults to False.

        Returns:
            bool: True if export was successful.
        """
        try:
            path = Path(file_path)
            allowed_formats = {"json", "toml", "yaml"}
            # Determine format and file path
            if format:
                fmt = format.lower()
                if fmt not in allowed_formats:
                    raise ValueError(f"Unsupported export format: {format}")
                export_path = path.with_suffix(f".{fmt}")
            else:
                suffix = path.suffix.lower()
                if suffix == ".json":
                    fmt = "json"
                elif suffix in (".yaml", ".yml"):
                    fmt = "yaml"
                else:
                    fmt = "toml"
                export_path = path if path.suffix else path.with_suffix(f".{fmt}")

            # Export settings to string
            settings_data = self.export_settings(fmt, include_read_only)

            # Write to file according to format
            if fmt == "json":
                with open(export_path, "w", encoding="utf-8") as f:
                    f.write(settings_data)
            elif fmt == "yaml":
                with open(export_path, "w", encoding="utf-8") as f:
                    f.write(settings_data)
            elif fmt == "toml":
                with open(export_path, "w", encoding="utf-8") as f:
                    f.write(settings_data)
            else:
                raise ValueError(f"Unsupported export format: {fmt}")

            self.logger.info(f"Settings exported to {export_path} in {fmt} format")
            return True

        except Exception as e:
            self.logger.error(f"Failed to export settings to {file_path}: {e}")
            return False

    def import_settings_from_file(self, file_path: str, format: Optional[str] = None, force: bool = False) -> bool:
        """Import settings from a file.

        Args:
            file_path (str): Path to import file.
            format (Optional[str]): Import format. If None, detected from file extension.
            force (bool): Force importing protected values.

        Returns:
            bool: True if import was successful.
        """
        try:
            path = Path(file_path)

            if not path.exists():
                raise FileNotFoundError(f"File not found: {file_path}")

            if format is None:
                # Detect format from file extension
                suffix = path.suffix.lower()
                if suffix == ".json":
                    format = "json"
                elif suffix in (".yaml", ".yml"):
                    format = "yaml"
                else:
                    format = "toml"  # Default

            # Read file content
            with open(path, 'r', encoding='utf-8') as f:
                data = f.read()

            # Import settings from string
            result = self.import_settings(data, format, force)

            self.logger.info(f"Settings imported from {file_path} in {format} format")
            return True

        except Exception as e:
            self.logger.error(f"Failed to import settings from {file_path}: {e}")
            return False

    def _get_exportable_settings(self) -> Dict[str, Any]:
        """Get settings dictionary excluding read-only settings.

        Returns:
            Dict[str, Any]: Settings dictionary with read-only settings filtered out.
        """
        exportable_dict = {}

        # Get all settings metadata to check access levels
        all_settings = self._get_all_settings_metadata()

        # Group settings by category
        settings_by_category = {}
        for setting in all_settings:
            category = setting['category_name']
            if category not in settings_by_category:
                settings_by_category[category] = []
            settings_by_category[category].append(setting)

        # Build exportable dict excluding read-only settings
        for category_name, category_settings in settings_by_category.items():
            category_dict = {}
            category_model = getattr(self.settings, category_name, None)
            if category_model is None:
                continue

            for setting in category_settings:
                setting_name = setting['name']
                access_level = setting['access_level']

                # Only include non-read-only settings
                if access_level != SettingAccessLevel.READ_ONLY:
                    current_value = getattr(category_model, setting_name)
                    category_dict[setting_name] = current_value

            # Only add the category if it has exportable settings
            if category_dict:
                exportable_dict[category_name] = category_dict

        return exportable_dict

    # Persistent settings methods

    def save_persistent_settings(self, format: str = "toml") -> bool:
        """Save current settings to the persistent settings file.

        Args:
            format (str, optional): Format to save in ('toml', 'json', 'yaml'). Defaults to "toml".

        Returns:
            bool: True if save was successful.
        """
        try:
            settings_dir = Path(self.settings.paths.hatchling_settings_dir)
            settings_dir.mkdir(parents=True, exist_ok=True)

            settings_file = settings_dir / f"hatchling_settings.{format}"

            # Export settings excluding read-only (default behavior for persistence)
            return self.export_settings_to_file(str(settings_file), format, include_read_only=False)

        except Exception as e:
            self.logger.error(f"Failed to save persistent settings: {e}")
            return False

    def load_persistent_settings(self, format: str = "toml") -> bool:
        """Load settings from the persistent settings file.

        Args:
            format (str, optional): Format to load from ('toml', 'json', 'yaml'). Defaults to "toml".

        Returns:
            bool: True if load was successful or no settings file exists.
        """
        try:
            settings_dir = Path(self.settings.paths.hatchling_settings_dir)
            settings_file = settings_dir / f"hatchling_settings.{format}"

            if not settings_file.exists():
                self.logger.warning(f"No persistent settings file found at {settings_file}, using defaults")

                # Export default settings if file does not exist
                self.save_persistent_settings(format)

            # Import settings (will automatically skip read-only and protected without force)
            success = self.import_settings_from_file(str(settings_file), format, force=True)
            if success:
                self.logger.info(f"Loaded persistent settings from {settings_file}")
            return success

        except Exception as e:
            self.logger.error(f"Failed to load persistent settings: {e}")
            return False

    def get_persistent_settings_file_path(self, format: str = "toml") -> Path:
        """Get the path to the persistent settings file.

        Args:
            format (str, optional): Format extension. Defaults to "toml".

        Returns:
            Path: Path to the persistent settings file.
        """
        settings_dir = Path(self.settings.paths.hatchling_settings_dir)
        return settings_dir / f"hatchling_settings.{format}"
Functions
__init__(app_settings=None, load_persistent=True)

Initialize the settings registry.

Parameters:

Name Type Description Default
app_settings AppSettings

The application settings instance to manage.

None
Source code in hatchling/config/settings_registry.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, app_settings: Optional[AppSettings] = None, load_persistent: bool = True):
    """Initialize the settings registry.

    Args:
        app_settings (AppSettings): The application settings instance to manage.
    """
    self.settings = app_settings or AppSettings()
    self.logger = logging_manager.get_session("hatchling.config.settings_registry")

    # Initialize translation loader with current language
    translation_loader = get_translation_loader()
    current_language = self.settings.ui.language_code
    if current_language != translation_loader.get_current_language():
        translation_loader.set_language(current_language)

    # Overlay with persistent settings if they exist, and if requested
    if load_persistent:
        self.load_persistent_settings()
export_settings(format='toml', include_read_only=False)

Export settings to a formatted string.

Parameters:

Name Type Description Default
format str

Export format ('toml', 'json', 'yaml'). Defaults to "toml".

'toml'
include_read_only bool

Whether to include read-only settings. Defaults to False.

False

Returns:

Name Type Description
str str

Serialized settings data.

Raises:

Type Description
ValueError

If format is not supported.

Source code in hatchling/config/settings_registry.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def export_settings(self, format: str = "toml", include_read_only: bool = False) -> str:
    """Export settings to a formatted string.

    Args:
        format (str, optional): Export format ('toml', 'json', 'yaml'). Defaults to "toml".
        include_read_only (bool, optional): Whether to include read-only settings. Defaults to False.

    Returns:
        str: Serialized settings data.

    Raises:
        ValueError: If format is not supported.
    """
    if include_read_only:
        # Export all settings (original behavior)
        settings_dict = self.settings.model_dump()
    else:
        # Export only non-read-only settings
        settings_dict = self._get_exportable_settings()

    settings_dict = self.make_serializable(settings_dict)

    if format.lower() == "toml":
        return toml_write.dumps(settings_dict)
    elif format.lower() == "json":
        return json.dumps(settings_dict, indent=2)
    elif format.lower() == "yaml":
        return yaml.dump(settings_dict, default_flow_style=False)
    else:
        raise ValueError(f"Unsupported export format: {format}")
export_settings_to_file(file_path, format=None, include_read_only=False)

Export settings to a file.

Parameters:

Name Type Description Default
file_path str

Path to export file.

required
format Optional[str]

Export format. If None, detected from file extension.

None
include_read_only bool

Whether to include read-only settings. Defaults to False.

False

Returns:

Name Type Description
bool bool

True if export was successful.

Source code in hatchling/config/settings_registry.py
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
def export_settings_to_file(self, file_path: str, format: Optional[str] = None, include_read_only: bool = False) -> bool:
    """Export settings to a file.

    Args:
        file_path (str): Path to export file.
        format (Optional[str]): Export format. If None, detected from file extension.
        include_read_only (bool, optional): Whether to include read-only settings. Defaults to False.

    Returns:
        bool: True if export was successful.
    """
    try:
        path = Path(file_path)
        allowed_formats = {"json", "toml", "yaml"}
        # Determine format and file path
        if format:
            fmt = format.lower()
            if fmt not in allowed_formats:
                raise ValueError(f"Unsupported export format: {format}")
            export_path = path.with_suffix(f".{fmt}")
        else:
            suffix = path.suffix.lower()
            if suffix == ".json":
                fmt = "json"
            elif suffix in (".yaml", ".yml"):
                fmt = "yaml"
            else:
                fmt = "toml"
            export_path = path if path.suffix else path.with_suffix(f".{fmt}")

        # Export settings to string
        settings_data = self.export_settings(fmt, include_read_only)

        # Write to file according to format
        if fmt == "json":
            with open(export_path, "w", encoding="utf-8") as f:
                f.write(settings_data)
        elif fmt == "yaml":
            with open(export_path, "w", encoding="utf-8") as f:
                f.write(settings_data)
        elif fmt == "toml":
            with open(export_path, "w", encoding="utf-8") as f:
                f.write(settings_data)
        else:
            raise ValueError(f"Unsupported export format: {fmt}")

        self.logger.info(f"Settings exported to {export_path} in {fmt} format")
        return True

    except Exception as e:
        self.logger.error(f"Failed to export settings to {file_path}: {e}")
        return False
get_available_languages()

Get list of available languages for the interface.

Returns:

Type Description
List[Dict[str, str]]

List[Dict[str, str]]: List of available languages with code and name.

Source code in hatchling/config/settings_registry.py
438
439
440
441
442
443
444
445
def get_available_languages(self) -> List[Dict[str, str]]:
    """Get list of available languages for the interface.

    Returns:
        List[Dict[str, str]]: List of available languages with code and name.
    """
    translation_loader = get_translation_loader()
    return translation_loader.get_available_languages()
get_current_language()

Get the current interface language.

Returns:

Name Type Description
str str

Current language code.

Source code in hatchling/config/settings_registry.py
447
448
449
450
451
452
453
454
def get_current_language(self) -> str:
    """Get the current interface language.

    Returns:
        str: Current language code.
    """
    # Get from UI settings
    return self.settings.ui.language_code
get_persistent_settings_file_path(format='toml')

Get the path to the persistent settings file.

Parameters:

Name Type Description Default
format str

Format extension. Defaults to "toml".

'toml'

Returns:

Name Type Description
Path Path

Path to the persistent settings file.

Source code in hatchling/config/settings_registry.py
677
678
679
680
681
682
683
684
685
686
687
def get_persistent_settings_file_path(self, format: str = "toml") -> Path:
    """Get the path to the persistent settings file.

    Args:
        format (str, optional): Format extension. Defaults to "toml".

    Returns:
        Path: Path to the persistent settings file.
    """
    settings_dir = Path(self.settings.paths.hatchling_settings_dir)
    return settings_dir / f"hatchling_settings.{format}"
get_setting(category, name)

Get metadata and value for a specific setting.

Parameters:

Name Type Description Default
category str

Setting category name.

required
name str

Setting name.

required

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Setting information including current and default values.

Raises:

Type Description
ValueError

If the setting is not found.

Source code in hatchling/config/settings_registry.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def get_setting(self, category: str, name: str) -> Dict[str, Any]:
    """Get metadata and value for a specific setting.

    Args:
        category (str): Setting category name.
        name (str): Setting name.

    Returns:
        Dict[str, Any]: Setting information including current and default values.

    Raises:
        ValueError: If the setting is not found.
    """
    setting_info = self._get_setting_info(category, name)
    if not setting_info:
        raise ValueError(f"Setting '{category}:{name}' not found")

    return setting_info
import_settings(data, format='toml', force=False)

Import settings from a formatted string.

Parameters:

Name Type Description Default
data str

Serialized settings data.

required
format str

Import format ('toml', 'json', 'yaml'). Defaults to "toml".

'toml'
force bool

Force importing protected values. Defaults to False.

False

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Import report with success, skipped, and failed settings.

Raises:

Type Description
ValueError

If format is not supported or data is invalid.

Source code in hatchling/config/settings_registry.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def import_settings(self, data: str, format: str = "toml", force: bool = False) -> Dict[str, Any]:
    """Import settings from a formatted string.

    Args:
        data (str): Serialized settings data.
        format (str, optional): Import format ('toml', 'json', 'yaml'). Defaults to "toml".
        force (bool, optional): Force importing protected values. Defaults to False.

    Returns:
        Dict[str, Any]: Import report with success, skipped, and failed settings.

    Raises:
        ValueError: If format is not supported or data is invalid.
    """
    # Parse the data
    try:
        if format.lower() == "toml":
            parsed_data = toml_read.loads(data)
        elif format.lower() == "json":
            parsed_data = json.loads(data)
        elif format.lower() == "yaml":
            parsed_data = yaml.safe_load(data)
        else:
            raise ValueError(f"Unsupported import format: {format}")
    except Exception as e:
        raise ValueError(f"Failed to parse {format} data: {e}")

    report = {
        "successful": [],
        "skipped": [],
        "failed": []
    }

    # Import each category
    for category_name, category_data in parsed_data.items():
        if not isinstance(category_data, dict):
            continue

        for setting_name, value in category_data.items():
            try:
                self.set_setting(category_name, setting_name, value, force)
                report["successful"].append(f"{category_name}:{setting_name}")
            except ValueError as e:
                if "read-only" in str(e):
                    self.logger.warning(f"{category_name}:{setting_name} is read-only and cannot be modified --> skipped")
                    report["skipped"].append(f"{category_name}:{setting_name} ({str(e)})")
                elif("protected" in str(e) and not force):
                    self.logger.warning(f"{category_name}:{setting_name} is protected and cannot be modified without force --> skipped")
                    report["skipped"].append(f"{category_name}:{setting_name} ({str(e)})")
                else:
                    self.logger.error(f"Failed to set {category_name}:{setting_name} --> {str(e)}")
                    report["failed"].append(f"{category_name}:{setting_name} ({str(e)})")
            except Exception as e:
                self.logger.error(f"Unexpected error setting {category_name}:{setting_name} --> {str(e)}")
                report["failed"].append(f"{category_name}:{setting_name} ({str(e)})")

    # Log the import operation
    self.logger.info(f"Settings import completed: {len(report['successful'])} successful, "
                    f"{len(report['skipped'])} skipped, {len(report['failed'])} failed")

    return report
import_settings_from_file(file_path, format=None, force=False)

Import settings from a file.

Parameters:

Name Type Description Default
file_path str

Path to import file.

required
format Optional[str]

Import format. If None, detected from file extension.

None
force bool

Force importing protected values.

False

Returns:

Name Type Description
bool bool

True if import was successful.

Source code in hatchling/config/settings_registry.py
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def import_settings_from_file(self, file_path: str, format: Optional[str] = None, force: bool = False) -> bool:
    """Import settings from a file.

    Args:
        file_path (str): Path to import file.
        format (Optional[str]): Import format. If None, detected from file extension.
        force (bool): Force importing protected values.

    Returns:
        bool: True if import was successful.
    """
    try:
        path = Path(file_path)

        if not path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        if format is None:
            # Detect format from file extension
            suffix = path.suffix.lower()
            if suffix == ".json":
                format = "json"
            elif suffix in (".yaml", ".yml"):
                format = "yaml"
            else:
                format = "toml"  # Default

        # Read file content
        with open(path, 'r', encoding='utf-8') as f:
            data = f.read()

        # Import settings from string
        result = self.import_settings(data, format, force)

        self.logger.info(f"Settings imported from {file_path} in {format} format")
        return True

    except Exception as e:
        self.logger.error(f"Failed to import settings from {file_path}: {e}")
        return False
list_settings(filter_regex=None)

List all settings with optional filtering.

Implements staged search: exact match, category match, regex search, then fuzzy search.

Parameters:

Name Type Description Default
filter_regex str

Filter pattern for settings. Defaults to None.

None

Returns:

Type Description
List[Dict[str, Any]]

List[Dict[str, Any]]: List of setting information dictionaries.

Source code in hatchling/config/settings_registry.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def list_settings(self, filter_regex: Optional[str] = None) -> List[Dict[str, Any]]:
    """List all settings with optional filtering.

    Implements staged search: exact match, category match, regex search, then fuzzy search.

    Args:
        filter_regex (str, optional): Filter pattern for settings. Defaults to None.

    Returns:
        List[Dict[str, Any]]: List of setting information dictionaries.
    """
    all_settings = self._get_all_settings_metadata()

    if not filter_regex:
        return all_settings

    # Stage 1: Exact match (category or setting name)
    exact_matches = []
    for setting in all_settings:
        if setting['category_name']+":"+setting['name'] == filter_regex:
            return [setting]

    # Stage 2: Category-wide listing
    category_matches = []
    for setting in all_settings:
        if setting['category_name'] == filter_regex:
            category_matches.append(setting)

    if category_matches:
        return category_matches

    # Stage 3: Regex search
    try:
        regex_pattern = re.compile(filter_regex, re.IGNORECASE)
        regex_matches = []
        for setting in all_settings:
            if (regex_pattern.search(setting['category_name']) or 
                regex_pattern.search(setting['name']) or 
                regex_pattern.search(setting['description'])):
                regex_matches.append(setting)

        if regex_matches:
            return regex_matches
    except re.error:
        # Invalid regex, fall through to fuzzy search
        pass

    # Stage 4: Fuzzy search
    fuzzy_matches = []
    for setting in all_settings:
        search_text = f"{setting['category_name']} {setting['name']} {setting['description']}"
        similarity = SequenceMatcher(None, filter_regex.lower(), search_text.lower()).ratio()
        if similarity > 0.3:  # Minimum similarity threshold
            setting['_similarity'] = similarity
            fuzzy_matches.append(setting)

    # Sort by similarity (highest first)
    fuzzy_matches.sort(key=lambda x: x.get('_similarity', 0), reverse=True)

    # Remove similarity score from results
    for match in fuzzy_matches:
        match.pop('_similarity', None)

    return fuzzy_matches
load_persistent_settings(format='toml')

Load settings from the persistent settings file.

Parameters:

Name Type Description Default
format str

Format to load from ('toml', 'json', 'yaml'). Defaults to "toml".

'toml'

Returns:

Name Type Description
bool bool

True if load was successful or no settings file exists.

Source code in hatchling/config/settings_registry.py
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
def load_persistent_settings(self, format: str = "toml") -> bool:
    """Load settings from the persistent settings file.

    Args:
        format (str, optional): Format to load from ('toml', 'json', 'yaml'). Defaults to "toml".

    Returns:
        bool: True if load was successful or no settings file exists.
    """
    try:
        settings_dir = Path(self.settings.paths.hatchling_settings_dir)
        settings_file = settings_dir / f"hatchling_settings.{format}"

        if not settings_file.exists():
            self.logger.warning(f"No persistent settings file found at {settings_file}, using defaults")

            # Export default settings if file does not exist
            self.save_persistent_settings(format)

        # Import settings (will automatically skip read-only and protected without force)
        success = self.import_settings_from_file(str(settings_file), format, force=True)
        if success:
            self.logger.info(f"Loaded persistent settings from {settings_file}")
        return success

    except Exception as e:
        self.logger.error(f"Failed to load persistent settings: {e}")
        return False
make_serializable(obj)

Convert an object to a serializable format.

Parameters:

Name Type Description Default
obj Any

The object to convert.

required

Returns:

Name Type Description
Any Any

A serializable version of the object.

Source code in hatchling/config/settings_registry.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def make_serializable(self, obj: Any) -> Any:
    """Convert an object to a serializable format.

    Args:
        obj (Any): The object to convert.

    Returns:
        Any: A serializable version of the object.
    """
    if obj is None:
        return "None"
    elif isinstance(obj, dict):
        return {k: self.make_serializable(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [self.make_serializable(i) for i in obj]
    elif isinstance(obj, Path):
        return str(obj)
    elif isinstance(obj, enum.Enum):
        return obj.value
    elif isinstance(obj, ModelInfo):
        return [obj.provider.value, obj.name]
    return obj
reload_translations()

Reload translation files from disk.

Source code in hatchling/config/settings_registry.py
481
482
483
484
485
def reload_translations(self) -> None:
    """Reload translation files from disk."""
    translation_loader = get_translation_loader()
    translation_loader.reload_translations()
    self.logger.info("Translations reloaded")
reset_setting(category, name, force=False)

Reset a setting to its default value.

Parameters:

Name Type Description Default
category str

Setting category name.

required
name str

Setting name.

required
force bool

Force resetting protected values. Defaults to False.

False

Returns:

Name Type Description
bool bool

True if reset was successful.

Raises:

Type Description
ValueError

If setting is not found or access is denied.

Source code in hatchling/config/settings_registry.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def reset_setting(self, category: str, name: str, force: bool = False) -> bool:
    """Reset a setting to its default value.

    Args:
        category (str): Setting category name.
        name (str): Setting name.
        force (bool, optional): Force resetting protected values. Defaults to False.

    Returns:
        bool: True if reset was successful.

    Raises:
        ValueError: If setting is not found or access is denied.
    """
    setting_info = self._get_setting_info(category, name)
    if not setting_info:
        raise ValueError(f"Setting '{category}:{name}' not found")

    default_value = setting_info['default_value']
    return self.set_setting(category, name, default_value, force)
save_persistent_settings(format='toml')

Save current settings to the persistent settings file.

Parameters:

Name Type Description Default
format str

Format to save in ('toml', 'json', 'yaml'). Defaults to "toml".

'toml'

Returns:

Name Type Description
bool bool

True if save was successful.

Source code in hatchling/config/settings_registry.py
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
def save_persistent_settings(self, format: str = "toml") -> bool:
    """Save current settings to the persistent settings file.

    Args:
        format (str, optional): Format to save in ('toml', 'json', 'yaml'). Defaults to "toml".

    Returns:
        bool: True if save was successful.
    """
    try:
        settings_dir = Path(self.settings.paths.hatchling_settings_dir)
        settings_dir.mkdir(parents=True, exist_ok=True)

        settings_file = settings_dir / f"hatchling_settings.{format}"

        # Export settings excluding read-only (default behavior for persistence)
        return self.export_settings_to_file(str(settings_file), format, include_read_only=False)

    except Exception as e:
        self.logger.error(f"Failed to save persistent settings: {e}")
        return False
set_language(language_code)

Set the interface language.

Parameters:

Name Type Description Default
language_code str

Language code to set.

required

Returns:

Name Type Description
bool bool

True if language was successfully set.

Raises:

Type Description
ValueError

If language is not available.

Source code in hatchling/config/settings_registry.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
def set_language(self, language_code: str) -> bool:
    """Set the interface language.

    Args:
        language_code (str): Language code to set.

    Returns:
        bool: True if language was successfully set.

    Raises:
        ValueError: If language is not available.
    """
    translation_loader = get_translation_loader()

    # Set in translation loader
    if translation_loader.set_language(language_code):
        # Update UI settings
        try:
            self.set_setting("ui", "language_code", language_code)
            return True
        except Exception as e:
            self.logger.error(f"Failed to update language setting: {e}")
            return False
    return False
set_setting(category, name, value, force=False)

Set a setting value with access control enforcement.

Parameters:

Name Type Description Default
category str

Setting category name.

required
name str

Setting name.

required
value Any

New value for the setting.

required
force bool

Force setting protected values. Defaults to False.

False

Returns:

Name Type Description
bool bool

True if setting was successful.

Raises:

Type Description
ValueError

If setting is not found or access is denied.

ValidationError

If the value is invalid.

Source code in hatchling/config/settings_registry.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def set_setting(self, category: str, name: str, value: Any, force: bool = False) -> bool:
    """Set a setting value with access control enforcement.

    Args:
        category (str): Setting category name.
        name (str): Setting name.
        value (Any): New value for the setting.
        force (bool, optional): Force setting protected values. Defaults to False.

    Returns:
        bool: True if setting was successful.

    Raises:
        ValueError: If setting is not found or access is denied.
        ValidationError: If the value is invalid.
    """
    setting_info = self._get_setting_info(category, name)
    if not setting_info:
        raise ValueError(f"Setting '{category}:{name}' not found")

    access_level = setting_info['access_level']
    old_value = setting_info['current_value']

    # Enforce access control
    if access_level == SettingAccessLevel.READ_ONLY:
        raise ValueError(f"Setting '{category}:{name}' is read-only and cannot be modified")

    if access_level == SettingAccessLevel.PROTECTED and not force:
        raise ValueError(f"Setting '{category}:{name}' is protected. Use --force to override")

    # Validate and set the value
    try:
        self._set_setting_value(category, name, value)
        new_value = self._get_setting_value(category, name)
        # Log the change
        self.logger.info(f"Setting '{category}:{name}' changed from '{old_value}' to '{new_value}'")
        return True
    except ValidationError as e:
        # Re-raise the original ValidationError for correct error reporting
        raise

Functions

hatchling.config.settings_access_level

Classes

SettingAccessLevel

Bases: str, Enum

Defines access levels for settings.

Source code in hatchling/config/settings_access_level.py
3
4
5
6
7
class SettingAccessLevel(str, Enum):
    """Defines access levels for settings."""
    NORMAL = "normal"
    PROTECTED = "protected"
    READ_ONLY = "read_only"

LLM Settings

hatchling.config.llm_settings

Settings for LLM (Large Language Model) configuration.

Classes

LLMSettings

Bases: BaseModel

Settings for LLM (Large Language Model) configuration.

Source code in hatchling/config/llm_settings.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class LLMSettings(BaseModel):
    """Settings for LLM (Large Language Model) configuration."""

    # Provider selection
    provider_enum: ELLMProvider = Field(
        default_factory=lambda: LLMSettings.to_provider_enum(os.environ.get("LLM_PROVIDER", "ollama")),
        description="LLM provider to use ('ollama' or 'openai').",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    model: str = Field(
        default_factory=lambda: os.environ.get("LLM_MODEL", "llama3.2"),
        description="Default LLM to use for the selected provider.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    @staticmethod
    def extract_provider_model_list(s: str) -> List[Tuple[ELLMProvider, str]]:
        """
        Extract a list of (provider_name, model_name) tuples from a string using regex.

        Args:
            s (str): Input string containing tuples in the format (provider,model).

        Returns:
            List[tuple[str, str]]: List of extracted tuples.
        """
        pattern = r"\(\s*([a-zA-Z0-9_]+)\s*,\s*([a-zA-Z0-9_.-]+)\s*\)"
        res = [(
            LLMSettings.to_provider_enum(match[0]),
            match[1]
        ) for match in re.findall(pattern, s)]

        return res

    models: List[ModelInfo] = Field(
        default_factory=lambda: [
            ModelInfo(name=model[1], provider=model[0], status=ModelStatus.AVAILABLE)
            for model in LLMSettings.extract_provider_model_list(
                os.environ.get("LLM_MODELS", "") if os.environ.get("LLM_MODELS") else "[(ollama, llama3.2), (openai, gpt-4.1-nano)]"
            )
        ],
        description="List of LLMs the user can choose from.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    @property
    def provider_name(self) -> str:
        """Return the current LLM provider."""
        return self.provider_enum.value

    @property
    def provider_names(self) -> List[str]:
        """Return a list of all available LLM providers."""
        return [provider.value for provider in ELLMProvider]

    @property
    def provider_enums(self) -> List[ELLMProvider]:
        """Return a list of all available LLM provider enums."""
        return List(ELLMProvider)

    @staticmethod
    def to_provider_enum(provider_name: str) -> ELLMProvider:
        """Convert the provider name to its corresponding enum."""
        return ELLMProvider(provider_name)

    class Config:
        extra = "forbid"
Attributes
provider_enums property

Return a list of all available LLM provider enums.

provider_name property

Return the current LLM provider.

provider_names property

Return a list of all available LLM providers.

Functions
extract_provider_model_list(s) staticmethod

Extract a list of (provider_name, model_name) tuples from a string using regex.

Parameters:

Name Type Description Default
s str

Input string containing tuples in the format (provider,model).

required

Returns:

Type Description
List[Tuple[ELLMProvider, str]]

List[tuple[str, str]]: List of extracted tuples.

Source code in hatchling/config/llm_settings.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@staticmethod
def extract_provider_model_list(s: str) -> List[Tuple[ELLMProvider, str]]:
    """
    Extract a list of (provider_name, model_name) tuples from a string using regex.

    Args:
        s (str): Input string containing tuples in the format (provider,model).

    Returns:
        List[tuple[str, str]]: List of extracted tuples.
    """
    pattern = r"\(\s*([a-zA-Z0-9_]+)\s*,\s*([a-zA-Z0-9_.-]+)\s*\)"
    res = [(
        LLMSettings.to_provider_enum(match[0]),
        match[1]
    ) for match in re.findall(pattern, s)]

    return res
to_provider_enum(provider_name) staticmethod

Convert the provider name to its corresponding enum.

Source code in hatchling/config/llm_settings.py
113
114
115
116
@staticmethod
def to_provider_enum(provider_name: str) -> ELLMProvider:
    """Convert the provider name to its corresponding enum."""
    return ELLMProvider(provider_name)

ModelInfo dataclass

Information about an LLM model.

Source code in hatchling/config/llm_settings.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@dataclass
class ModelInfo:
    """Information about an LLM model."""
    name: str
    provider: ELLMProvider
    status: ModelStatus
    size: Optional[int] = None
    modified_at: Optional[str] = None
    digest: Optional[str] = None
    details: Optional[Dict[str, Any]] = None
    error_message: Optional[str] = None

    def to_dict(self) -> dict:
        """Return a dictionary representation of the model."""
        return {
            "name": self.name,
            "provider": self.provider.value,
            "status": self.status.value,
            "size": self.size,
            "modified_at": self.modified_at,
            "digest": self.digest,
            "details": self.details,
            "error_message": self.error_message,
        }
Functions
to_dict()

Return a dictionary representation of the model.

Source code in hatchling/config/llm_settings.py
37
38
39
40
41
42
43
44
45
46
47
48
def to_dict(self) -> dict:
    """Return a dictionary representation of the model."""
    return {
        "name": self.name,
        "provider": self.provider.value,
        "status": self.status.value,
        "size": self.size,
        "modified_at": self.modified_at,
        "digest": self.digest,
        "details": self.details,
        "error_message": self.error_message,
    }

ModelStatus

Bases: Enum

Status of a model.

Source code in hatchling/config/llm_settings.py
17
18
19
20
21
22
class ModelStatus(Enum):
    """Status of a model."""
    AVAILABLE = "available"
    NOT_AVAILABLE = "not_available"
    DOWNLOADING = "downloading"
    ERROR = "error"

hatchling.config.openai_settings

Settings for configuring OpenAI LLMs.

Contains configuration options for connecting to and controlling OpenAI LLM generation.

Classes

OpenAISettings

Bases: BaseModel

Settings for configuring OpenAI LLMs.

Includes connection and generation parameters for OpenAI.

Source code in hatchling/config/openai_settings.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class OpenAISettings(BaseModel):
    """Settings for configuring OpenAI LLMs.

    Includes connection and generation parameters for OpenAI.
    """

    api_key: Optional[str] = Field(
        default_factory=lambda: os.environ.get("OPENAI_API_KEY"),
        description="The API key used to authenticate with OpenAI services.",
        json_schema_extra={"access_level": SettingAccessLevel.PROTECTED},
    )

    api_base: str = Field(
        default="https://api.openai.com/v1",
        description="The base URL for OpenAI API requests.",
        json_schema_extra={"access_level": SettingAccessLevel.READ_ONLY},
    )

    timeout: int = Field(
        default_factory=lambda: int(os.environ.get("OPENAI_TIMEOUT", 60)),
        description="Timeout in seconds for OpenAI API requests.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    max_completion_tokens: int = Field(
        default_factory=lambda: int(os.environ.get("OPENAI_MAX_COMPLETION_TOKENS", 2048)),
        description="The maximum number of tokens for OpenAI completions. This includes visible and reasoning tokens (when enabled). The higher the value, the more tokens can be generated, but it may increase costs.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    temperature: float = Field(
        default_factory=lambda: float(os.environ.get("OPENAI_TEMPERATURE", 0.7)),
        description="Sampling temperature for OpenAI completions.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    top_p: float = Field(
        default_factory=lambda: float(os.environ.get("OPENAI_TOP_P", 1.0)),
        description="Nucleus sampling parameter for OpenAI completions.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    tool_choice: Optional[OpenAIToolChoice] = Field(
        default_factory=lambda: os.environ.get("OPENAI_TOOL_CHOICE", OpenAIToolChoice.AUTO),
        description="The tool choice for OpenAI API requests.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    class Config:
        extra = "forbid"

OpenAIToolChoice

Bases: Enum

Enum for OpenAI tool choice options.

Source code in hatchling/config/openai_settings.py
13
14
15
16
17
class OpenAIToolChoice(Enum):
    """Enum for OpenAI tool choice options."""
    AUTO = "auto"
    NONE = "none"
    REQUIRED = "required"

hatchling.config.ollama_settings

Settings for LLM (Large Language Model) configuration.

Contains configuration options for connecting to and controlling Ollama LLM generation.

Classes

OllamaSettings

Bases: BaseModel

Settings for LLM (Large Language Model) configuration.

Includes connection and generation parameters for Ollama.

Source code in hatchling/config/ollama_settings.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class OllamaSettings(BaseModel):
    """Settings for LLM (Large Language Model) configuration.

    Includes connection and generation parameters for Ollama.
    """

    # Ollama Connection settings
    ip: str = Field(
        default_factory=lambda: os.environ.get("OLLAMA_IP", "localhost"),
        description="IP address for the Ollama API endpoint.",
        json_schema_extra={"access_level": SettingAccessLevel.PROTECTED},
    )

    port: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_PORT", 11434)),
        description="Port for the Ollama API endpoint.",
        json_schema_extra={"access_level": SettingAccessLevel.PROTECTED},
    )

    # Ollama Model settings

    num_ctx: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_NUM_CTX", 4096)),
        description="Sets the size of the context window used to generate the next token. (Default: 4096)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    repeat_last_n: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_REPEAT_LAST_N", 64)),
        description="Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    repeat_penalty: float = Field(
        default_factory=lambda: float(os.environ.get("OLLAMA_REPEAT_PENALTY", 1.1)),
        description="Sets how strongly to penalize repetitions. Higher values penalize more strongly. (Default: 1.1)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    temperature: float = Field(
        default_factory=lambda: float(os.environ.get("OLLAMA_TEMPERATURE", 0.8)),
        description="The temperature of the model. Higher values make answers more creative. (Default: 0.8)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    timeout: float = Field(
        default_factory=lambda: float(os.environ.get("OLLAMA_TIMEOUT", 30.0)),
        description="Timeout in seconds for Ollama API requests. (Default: 30.0)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    seed: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_SEED", 0)),
        description="Sets the random number seed to use for generation. (Default: 0)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    stop: Optional[List[str]] = Field(
        default=None,
        description="Sets the stop sequences to use. When encountered, generation stops. Multiple patterns allowed.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    num_predict: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_NUM_PREDICT", -1)),
        description="Maximum number of tokens to predict when generating text. (Default: -1, infinite generation)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    top_k: int = Field(
        default_factory=lambda: int(os.environ.get("OLLAMA_TOP_K", 40)),
        description="Reduces the probability of generating nonsense. Higher values give more diverse answers. (Default: 40)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    top_p: float = Field(
        default_factory=lambda: float(os.environ.get("OLLAMA_TOP_P", 0.9)),
        description="Works with top-k. Higher values lead to more diverse text. (Default: 0.9)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )
    min_p: float = Field(
        default_factory=lambda: float(os.environ.get("OLLAMA_MIN_P", 0.0)),
        description="Alternative to top_p, ensures a balance of quality and variety. (Default: 0.0)",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    @property
    def api_base(self) -> str:
        return f"http://{self.ip}:{self.port}"

    class Config:
        extra = "forbid"

Other Settings

hatchling.config.path_settings

Settings for file and directory paths.

Handles configuration for environment directories and related paths.

Classes

PathSettings

Bases: BaseModel

Settings for file and directory paths.

Source code in hatchling/config/path_settings.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class PathSettings(BaseModel):
    """Settings for file and directory paths."""

    hatchling_source_dir: Union[str, Path] = Field(
        default_factory=lambda: Path(os.environ.get("HATCHLING_SOURCE_DIR", str(Path.home() / "Hatchling"))),
        description="Directory where Hatchling source code is located.",
        json_schema_extra={"access_level": SettingAccessLevel.READ_ONLY},
    )

    envs_dir: Union[str, Path] = Field(
        default_factory=lambda: Path(os.environ.get("HATCH_ENVS_DIR", str(Path.home() / ".hatch" / "envs"))),
        description="Directory for Hatch environments.",
        json_schema_extra={"access_level": SettingAccessLevel.READ_ONLY},
    )

    hatchling_cache_dir: Union[str, Path] = Field(
        default_factory=lambda: Path(os.environ.get("HATCHLING_CACHE_DIR", str(Path.home() / ".hatch"))),
        description="Directory for Hatchling cache and data storage.",
        json_schema_extra={"access_level": SettingAccessLevel.READ_ONLY},
    )

    hatchling_settings_dir: Union[str, Path] = Field(
        default_factory=lambda: Path(os.environ.get("HATCHLING_SETTINGS_DIR", 
                                                   str(Path(os.environ.get("HATCHLING_CACHE_DIR", str(Path.home() / ".hatch"))) / "settings"))),
        description="Directory for Hatchling settings storage.",
        json_schema_extra={"access_level": SettingAccessLevel.READ_ONLY},
    )

    @field_validator('envs_dir', mode='before')
    @classmethod
    def validate_envs_dir(cls, v):
        """Validate and resolve environment directory path. Always return a Path object.

        Args:
            v (str or Path): The input value for the environment directory.

        Returns:
            Path: The resolved Path object.

        Raises:
            TypeError: If the input is not a str or Path.
        """
        if isinstance(v, Path):
            return v
        if isinstance(v, str):
            if os.path.isabs(v):
                return Path(v)
            else:
                return Path.home() / v
        raise TypeError(f"envs_dir must be a str or Path, got {type(v)}")

    @field_validator('hatchling_cache_dir', mode='before')
    @classmethod
    def validate_cache_dir(cls, v):
        """Validate and resolve cache directory path. Always return a Path object.

        Args:
            v (str or Path): The input value for the cache directory.

        Returns:
            Path: The resolved Path object.

        Raises:
            TypeError: If the input is not a str or Path.
        """
        if isinstance(v, Path):
            return v
        if isinstance(v, str):
            if os.path.isabs(v):
                return Path(v)
            else:
                return Path.home() / v
        raise TypeError(f"hatchling_cache_dir must be a str or Path, got {type(v)}")

    @field_validator('hatchling_settings_dir', mode='before')
    @classmethod
    def validate_settings_dir(cls, v):
        """Validate and resolve settings directory path. Always return a Path object.

        Args:
            v (str or Path): The input value for the settings directory.

        Returns:
            Path: The resolved Path object.

        Raises:
            TypeError: If the input is not a str or Path.
        """
        if isinstance(v, Path):
            return v
        if isinstance(v, str):
            if os.path.isabs(v):
                return Path(v)
            else:
                return Path.home() / v
        raise TypeError(f"hatchling_settings_dir must be a str or Path, got {type(v)}")

    class Config:
        extra = "forbid"
Functions
validate_cache_dir(v) classmethod

Validate and resolve cache directory path. Always return a Path object.

Parameters:

Name Type Description Default
v str or Path

The input value for the cache directory.

required

Returns:

Name Type Description
Path

The resolved Path object.

Raises:

Type Description
TypeError

If the input is not a str or Path.

Source code in hatchling/config/path_settings.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@field_validator('hatchling_cache_dir', mode='before')
@classmethod
def validate_cache_dir(cls, v):
    """Validate and resolve cache directory path. Always return a Path object.

    Args:
        v (str or Path): The input value for the cache directory.

    Returns:
        Path: The resolved Path object.

    Raises:
        TypeError: If the input is not a str or Path.
    """
    if isinstance(v, Path):
        return v
    if isinstance(v, str):
        if os.path.isabs(v):
            return Path(v)
        else:
            return Path.home() / v
    raise TypeError(f"hatchling_cache_dir must be a str or Path, got {type(v)}")
validate_envs_dir(v) classmethod

Validate and resolve environment directory path. Always return a Path object.

Parameters:

Name Type Description Default
v str or Path

The input value for the environment directory.

required

Returns:

Name Type Description
Path

The resolved Path object.

Raises:

Type Description
TypeError

If the input is not a str or Path.

Source code in hatchling/config/path_settings.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@field_validator('envs_dir', mode='before')
@classmethod
def validate_envs_dir(cls, v):
    """Validate and resolve environment directory path. Always return a Path object.

    Args:
        v (str or Path): The input value for the environment directory.

    Returns:
        Path: The resolved Path object.

    Raises:
        TypeError: If the input is not a str or Path.
    """
    if isinstance(v, Path):
        return v
    if isinstance(v, str):
        if os.path.isabs(v):
            return Path(v)
        else:
            return Path.home() / v
    raise TypeError(f"envs_dir must be a str or Path, got {type(v)}")
validate_settings_dir(v) classmethod

Validate and resolve settings directory path. Always return a Path object.

Parameters:

Name Type Description Default
v str or Path

The input value for the settings directory.

required

Returns:

Name Type Description
Path

The resolved Path object.

Raises:

Type Description
TypeError

If the input is not a str or Path.

Source code in hatchling/config/path_settings.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@field_validator('hatchling_settings_dir', mode='before')
@classmethod
def validate_settings_dir(cls, v):
    """Validate and resolve settings directory path. Always return a Path object.

    Args:
        v (str or Path): The input value for the settings directory.

    Returns:
        Path: The resolved Path object.

    Raises:
        TypeError: If the input is not a str or Path.
    """
    if isinstance(v, Path):
        return v
    if isinstance(v, str):
        if os.path.isabs(v):
            return Path(v)
        else:
            return Path.home() / v
    raise TypeError(f"hatchling_settings_dir must be a str or Path, got {type(v)}")

hatchling.config.tool_calling_settings

Settings for tool calling behavior and limits.

Configures limits and parameters for tool call operations.

Classes

ToolCallingSettings

Bases: BaseModel

Settings for tool calling behavior and limits.

Source code in hatchling/config/tool_calling_settings.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ToolCallingSettings(BaseModel):
    """Settings for tool calling behavior and limits."""

    max_iterations: int = Field(
        default=5,
        ge=1,
        description="Maximum number of tool call iterations.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    max_working_time: Optional[float] = Field(
        default=60.0,
        description="Maximum time in seconds for tool operations. If None, no limit is enforced. This is checked upon tool call, not during the tool call. Hence if a single tool call takes longer than this, it will not be interrupted.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    max_tool_working_time: Optional[float] = Field(
        default=12.0,
        description="Maximum time in seconds for a single tool operation. If None, no limit is enforced.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    @field_validator("max_working_time", "max_tool_working_time")
    @classmethod
    def _positive_or_none(cls, v):
        if v is not None and v <= 0.0:
            raise ValueError("Value must be positive or None")
        return v

    class Config:
        extra = "forbid"

hatchling.config.ui_settings

Settings for user interface configuration.

Handles UI-related configuration such as language selection.

Classes

UISettings

Bases: BaseModel

Settings for user interface configuration.

Source code in hatchling/config/ui_settings.py
11
12
13
14
15
16
17
18
19
20
21
class UISettings(BaseModel):
    """Settings for user interface configuration."""

    language_code: str = Field(
        default_factory=lambda: os.environ.get("HATCHLING_DEFAULT_LANGUAGE", "en"),
        description="Language code for user interface localization.",
        json_schema_extra={"access_level": SettingAccessLevel.NORMAL},
    )

    class Config:
        extra = "forbid"

Internationalization

hatchling.config.i18n

Internationalization (i18n) loader for Hatchling.

This module provides translation loading and management functionality, enabling runtime language switching and fallback to English for missing keys.

Classes

TranslationLoader

Manages loading and accessing translation files for internationalization.

This class handles loading translation files from the languages directory, provides translation lookup with fallback to English, and supports runtime language switching.

Source code in hatchling/config/i18n.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class TranslationLoader:
    """Manages loading and accessing translation files for internationalization.

    This class handles loading translation files from the languages directory,
    provides translation lookup with fallback to English, and supports runtime
    language switching.
    """

    def __init__(self, languages_dir: Optional[Path] = None, default_language_code: str = "en"):
        """Initialize the translation loader.

        Args:
            languages_dir (Optional[Path]): Directory containing translation files.
                                          Defaults to hatchling/config/languages/
            default_language_code (str): Default language code. Defaults to "en".
        """
        if languages_dir is None:
            # Default to the languages directory relative to this file
            current_dir = Path(__file__).parent
            languages_dir = current_dir / "languages"

        self.languages_dir = Path(languages_dir)
        self.default_language_code = default_language_code
        self.current_language_code = default_language_code

        # Cache for loaded translations
        self._translations_cache: Dict[str, Dict[str, Any]] = {}
        self._cache_lock = Lock()

        # Logger for translation operations
        self.logger = logging.getLogger(__name__)

        # Load default language on initialization
        self.get_available_languages()  # Pre-load available languages

    def get_available_languages(self) -> List[Dict[str, str]]:
        """Get list of available language files.

        Returns:
            List[Dict[str, str]]: List of dictionaries with language info.
                                 Each dict contains 'code', 'name', and 'file' keys.
        """
        if not self.languages_dir.exists():
            self.logger.warning(f"Languages directory not found: {self.languages_dir}")
            return []

        languages = []
        for file_path in self.languages_dir.glob("*.toml"):
            language_code = file_path.stem
            try:
                if language_code not in self._translations_cache:
                    translations = self._load_language(language_code)
                translations = self._translations_cache.get(language_code, {})
                meta = translations.get("meta", {})
                language_name = meta.get("language_name", language_code.capitalize())

                languages.append({
                    "code": language_code,
                    "name": language_name,
                    "file": str(file_path)
                })
            except Exception as e:
                self.logger.warning(f"Failed to load language metadata for {language_code}: {e}")
                # Add basic info even if file has issues
                languages.append({
                    "code": language_code,
                    "name": language_code.capitalize(),
                    "file": str(file_path)
                })

        return sorted(languages, key=lambda x: x["code"])

    def set_language(self, language_code: str) -> bool:
        """Set the current language.

        Args:
            language_code (str): Language code to set as current.

        Returns:
            bool: True if language was successfully set, False otherwise.
        """
        if self._load_language(language_code):
            with self._cache_lock:
                self.current_language_code = language_code
            self.logger.debug(f"Language changed to: {language_code}")
            return True
        return False

    def get_current_language(self) -> str:
        """Get the current language code.

        Returns:
            str: Current language code.
        """
        return self.current_language_code

    def translate(self, key: str, language_code: Optional[str] = None, **kwargs) -> str:
        """Get translated string for the given key.

        Args:
            key (str): Translation key in dot notation (e.g., "settings.llm.model.name")
            language_code (Optional[str]): Language code to use. Defaults to current language.
            **kwargs: Format arguments for string formatting.

        Returns:
            str: Translated string, or the key itself if translation not found.
        """
        if language_code is None:
            language_code = self.current_language_code

        # Ensure the language is loaded
        if language_code not in self._translations_cache:
            if not self._load_language(language_code):
                # Fall back to default language
                language_code = self.default_language_code

        with self._cache_lock:
            translations = self._translations_cache.get(language_code, {})

        # Navigate through the nested dictionary using dot notation
        value = translations
        for part in key.split('.'):
            if isinstance(value, dict) and part in value:
                value = value[part]
            else:
                # Key not found, try fallback to English
                if language_code != self.default_language_code:
                    return self.translate(key, self.default_language_code, **kwargs)
                else:
                    # Even English doesn't have the key, return the key itself
                    self.logger.warning(f"Translation key not found: {key}")
                    return key

        # If we have a string, format it with any provided arguments
        if isinstance(value, str) and kwargs:
            try:
                return value.format(**kwargs)
            except (KeyError, ValueError) as e:
                self.logger.warning(f"Failed to format translation '{key}': {e}")
                return value

        return str(value) if value is not None else key

    def _load_language(self, language_code: str) -> bool:
        """Load a specific language into the cache.

        Args:
            language_code (str): Language code to load.

        Returns:
            bool: True if language was successfully loaded, False otherwise.
        """
        if language_code in self._translations_cache:
            return True

        language_file = self.languages_dir / f"{language_code}.toml"
        if not language_file.exists():
            self.logger.warning(f"Language file not found: {language_file}")
            return False

        try:
            translations = self._load_translation_file(language_file)
            with self._cache_lock:
                self._translations_cache[language_code] = translations
            self.logger.info(f"Loaded language: {language_code}")
            return True
        except Exception as e:
            self.logger.error(f"Failed to load language {language_code}: {e}")
            return False

    def _load_translation_file(self, file_path: Path) -> Dict[str, Any]:
        """Load a TOML translation file.

        Args:
            file_path (Path): Path to the translation file.

        Returns:
            Dict[str, Any]: Parsed translation data.

        Raises:
            Exception: If file cannot be read or parsed.
        """
        with open(file_path, 'rb') as f:
            return tomli.load(f)

    def reload_translations(self) -> None:
        """Reload all cached translations from disk."""
        with self._cache_lock:
            cached_languages = list(self._translations_cache.keys())
            self._translations_cache.clear()

        # Reload all previously cached languages
        for language_code in cached_languages:
            self._load_language(language_code)

        self.logger.info("Reloaded all translations")
Functions
__init__(languages_dir=None, default_language_code='en')

Initialize the translation loader.

Parameters:

Name Type Description Default
languages_dir Optional[Path]

Directory containing translation files. Defaults to hatchling/config/languages/

None
default_language_code str

Default language code. Defaults to "en".

'en'
Source code in hatchling/config/i18n.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(self, languages_dir: Optional[Path] = None, default_language_code: str = "en"):
    """Initialize the translation loader.

    Args:
        languages_dir (Optional[Path]): Directory containing translation files.
                                      Defaults to hatchling/config/languages/
        default_language_code (str): Default language code. Defaults to "en".
    """
    if languages_dir is None:
        # Default to the languages directory relative to this file
        current_dir = Path(__file__).parent
        languages_dir = current_dir / "languages"

    self.languages_dir = Path(languages_dir)
    self.default_language_code = default_language_code
    self.current_language_code = default_language_code

    # Cache for loaded translations
    self._translations_cache: Dict[str, Dict[str, Any]] = {}
    self._cache_lock = Lock()

    # Logger for translation operations
    self.logger = logging.getLogger(__name__)

    # Load default language on initialization
    self.get_available_languages()  # Pre-load available languages
get_available_languages()

Get list of available language files.

Returns:

Type Description
List[Dict[str, str]]

List[Dict[str, str]]: List of dictionaries with language info. Each dict contains 'code', 'name', and 'file' keys.

Source code in hatchling/config/i18n.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def get_available_languages(self) -> List[Dict[str, str]]:
    """Get list of available language files.

    Returns:
        List[Dict[str, str]]: List of dictionaries with language info.
                             Each dict contains 'code', 'name', and 'file' keys.
    """
    if not self.languages_dir.exists():
        self.logger.warning(f"Languages directory not found: {self.languages_dir}")
        return []

    languages = []
    for file_path in self.languages_dir.glob("*.toml"):
        language_code = file_path.stem
        try:
            if language_code not in self._translations_cache:
                translations = self._load_language(language_code)
            translations = self._translations_cache.get(language_code, {})
            meta = translations.get("meta", {})
            language_name = meta.get("language_name", language_code.capitalize())

            languages.append({
                "code": language_code,
                "name": language_name,
                "file": str(file_path)
            })
        except Exception as e:
            self.logger.warning(f"Failed to load language metadata for {language_code}: {e}")
            # Add basic info even if file has issues
            languages.append({
                "code": language_code,
                "name": language_code.capitalize(),
                "file": str(file_path)
            })

    return sorted(languages, key=lambda x: x["code"])
get_current_language()

Get the current language code.

Returns:

Name Type Description
str str

Current language code.

Source code in hatchling/config/i18n.py
104
105
106
107
108
109
110
def get_current_language(self) -> str:
    """Get the current language code.

    Returns:
        str: Current language code.
    """
    return self.current_language_code
reload_translations()

Reload all cached translations from disk.

Source code in hatchling/config/i18n.py
201
202
203
204
205
206
207
208
209
210
211
def reload_translations(self) -> None:
    """Reload all cached translations from disk."""
    with self._cache_lock:
        cached_languages = list(self._translations_cache.keys())
        self._translations_cache.clear()

    # Reload all previously cached languages
    for language_code in cached_languages:
        self._load_language(language_code)

    self.logger.info("Reloaded all translations")
set_language(language_code)

Set the current language.

Parameters:

Name Type Description Default
language_code str

Language code to set as current.

required

Returns:

Name Type Description
bool bool

True if language was successfully set, False otherwise.

Source code in hatchling/config/i18n.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def set_language(self, language_code: str) -> bool:
    """Set the current language.

    Args:
        language_code (str): Language code to set as current.

    Returns:
        bool: True if language was successfully set, False otherwise.
    """
    if self._load_language(language_code):
        with self._cache_lock:
            self.current_language_code = language_code
        self.logger.debug(f"Language changed to: {language_code}")
        return True
    return False
translate(key, language_code=None, **kwargs)

Get translated string for the given key.

Parameters:

Name Type Description Default
key str

Translation key in dot notation (e.g., "settings.llm.model.name")

required
language_code Optional[str]

Language code to use. Defaults to current language.

None
**kwargs

Format arguments for string formatting.

{}

Returns:

Name Type Description
str str

Translated string, or the key itself if translation not found.

Source code in hatchling/config/i18n.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def translate(self, key: str, language_code: Optional[str] = None, **kwargs) -> str:
    """Get translated string for the given key.

    Args:
        key (str): Translation key in dot notation (e.g., "settings.llm.model.name")
        language_code (Optional[str]): Language code to use. Defaults to current language.
        **kwargs: Format arguments for string formatting.

    Returns:
        str: Translated string, or the key itself if translation not found.
    """
    if language_code is None:
        language_code = self.current_language_code

    # Ensure the language is loaded
    if language_code not in self._translations_cache:
        if not self._load_language(language_code):
            # Fall back to default language
            language_code = self.default_language_code

    with self._cache_lock:
        translations = self._translations_cache.get(language_code, {})

    # Navigate through the nested dictionary using dot notation
    value = translations
    for part in key.split('.'):
        if isinstance(value, dict) and part in value:
            value = value[part]
        else:
            # Key not found, try fallback to English
            if language_code != self.default_language_code:
                return self.translate(key, self.default_language_code, **kwargs)
            else:
                # Even English doesn't have the key, return the key itself
                self.logger.warning(f"Translation key not found: {key}")
                return key

    # If we have a string, format it with any provided arguments
    if isinstance(value, str) and kwargs:
        try:
            return value.format(**kwargs)
        except (KeyError, ValueError) as e:
            self.logger.warning(f"Failed to format translation '{key}': {e}")
            return value

    return str(value) if value is not None else key

Functions

get_available_languages()

Convenience function to get available languages using the global loader.

Returns:

Type Description
List[Dict[str, str]]

List[Dict[str, str]]: List of available languages.

Source code in hatchling/config/i18n.py
271
272
273
274
275
276
277
def get_available_languages() -> List[Dict[str, str]]:
    """Convenience function to get available languages using the global loader.

    Returns:
        List[Dict[str, str]]: List of available languages.
    """
    return get_translation_loader().get_available_languages()

get_current_language()

Convenience function to get current language using the global loader.

Returns:

Name Type Description
str str

Current language code.

Source code in hatchling/config/i18n.py
280
281
282
283
284
285
286
def get_current_language() -> str:
    """Convenience function to get current language using the global loader.

    Returns:
        str: Current language code.
    """
    return get_translation_loader().get_current_language()

get_translation_loader()

Get the global translation loader instance.

Returns:

Name Type Description
TranslationLoader TranslationLoader

The global translation loader instance.

Source code in hatchling/config/i18n.py
218
219
220
221
222
223
224
225
226
227
def get_translation_loader() -> TranslationLoader:
    """Get the global translation loader instance.

    Returns:
        TranslationLoader: The global translation loader instance.
    """
    global _translation_loader
    if _translation_loader is None:
        _translation_loader = TranslationLoader()
    return _translation_loader

init_translation_loader(languages_dir=None, default_language_code='en')

Initialize the global translation loader.

Parameters:

Name Type Description Default
languages_dir Optional[Path]

Directory containing translation files.

None
default_language_code str

Default language code.

'en'

Returns:

Name Type Description
TranslationLoader TranslationLoader

The initialized translation loader.

Source code in hatchling/config/i18n.py
230
231
232
233
234
235
236
237
238
239
240
241
242
def init_translation_loader(languages_dir: Optional[Path] = None, default_language_code: str = "en") -> TranslationLoader:
    """Initialize the global translation loader.

    Args:
        languages_dir (Optional[Path]): Directory containing translation files.
        default_language_code (str): Default language code.

    Returns:
        TranslationLoader: The initialized translation loader.
    """
    global _translation_loader
    _translation_loader = TranslationLoader(languages_dir, default_language_code)
    return _translation_loader

set_language(language_code)

Convenience function to set language using the global loader.

Parameters:

Name Type Description Default
language_code str

Language code to set.

required

Returns:

Name Type Description
bool bool

True if language was successfully set.

Source code in hatchling/config/i18n.py
259
260
261
262
263
264
265
266
267
268
def set_language(language_code: str) -> bool:
    """Convenience function to set language using the global loader.

    Args:
        language_code (str): Language code to set.

    Returns:
        bool: True if language was successfully set.
    """
    return get_translation_loader().set_language(language_code)

translate(key, language_code=None, **kwargs)

Convenience function to translate a key using the global loader.

Parameters:

Name Type Description Default
key str

Translation key in dot notation.

required
language_code Optional[str]

Language code to use.

None
**kwargs

Format arguments for string formatting.

{}

Returns:

Name Type Description
str str

Translated string.

Source code in hatchling/config/i18n.py
245
246
247
248
249
250
251
252
253
254
255
256
def translate(key: str, language_code: Optional[str] = None, **kwargs) -> str:
    """Convenience function to translate a key using the global loader.

    Args:
        key (str): Translation key in dot notation.
        language_code (Optional[str]): Language code to use.
        **kwargs: Format arguments for string formatting.

    Returns:
        str: Translated string.
    """
    return get_translation_loader().translate(key, language_code, **kwargs)