chutils 2.7.2__tar.gz → 2.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chutils-2.7.2 → chutils-2.7.4}/PKG-INFO +3 -2
- {chutils-2.7.2 → chutils-2.7.4}/README.md +2 -1
- {chutils-2.7.2 → chutils-2.7.4}/pyproject.toml +1 -1
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli_utils.py +35 -3
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/core.py +43 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/getters.py +25 -23
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/providers.py +1 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/ast_indexer.py +53 -5
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/models.py +5 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/env.py +4 -1
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/core.py +10 -0
- {chutils-2.7.2 → chutils-2.7.4}/LICENSE +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/__init__.pyi +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/base.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/decorator.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/in_memory.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/utils.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli_booster.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/base.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/config.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/dev.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/init.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/paths.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/secrets.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/template.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/utils.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/validate.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/GEMINI.md +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/diagnostics.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/generator.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/manager.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/schema.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/utils.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/watcher.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/context.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/decorators.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/exceptions.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/features.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/fs.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/lifecycle.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/GEMINI.md +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/formatters.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/handlers.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/masking.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/GEMINI.md +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/core.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/providers.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/testing/__init__.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/testing/fixtures.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/time.py +0 -0
- {chutils-2.7.2 → chutils-2.7.4}/src/chutils/tracing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chutils
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.4
|
|
4
4
|
Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -83,7 +83,8 @@ Every time you start a new project, you have to solve the same tasks:
|
|
|
83
83
|
file. It uses **lazy initialization** — no heavy operations until you actually need them.
|
|
84
84
|
- **⚙️ Flexible Configuration:** Support for `YAML` and `INI` formats. Simple functions for retrieving typed data.
|
|
85
85
|
- **✍️ Advanced Logger:** The `setup_logger()` function configures logging to the console and rotating files out of the
|
|
86
|
-
box.
|
|
86
|
+
box. Includes **automatic secret masking** and **smart console width detection** for IDEs (PyCharm, etc.).
|
|
87
|
+
It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
|
|
87
88
|
- **🔒 Secure Secret Storage:** The `secret_manager` module provides a simple interface for saving and retrieving secrets
|
|
88
89
|
via the system `keyring`, with a fallback to `.env` files.
|
|
89
90
|
- **🚀 CLI Booster:** Turn any function into a CLI tool in seconds using the `@cli_command` decorator with automatic type
|
|
@@ -31,7 +31,8 @@ Every time you start a new project, you have to solve the same tasks:
|
|
|
31
31
|
file. It uses **lazy initialization** — no heavy operations until you actually need them.
|
|
32
32
|
- **⚙️ Flexible Configuration:** Support for `YAML` and `INI` formats. Simple functions for retrieving typed data.
|
|
33
33
|
- **✍️ Advanced Logger:** The `setup_logger()` function configures logging to the console and rotating files out of the
|
|
34
|
-
box.
|
|
34
|
+
box. Includes **automatic secret masking** and **smart console width detection** for IDEs (PyCharm, etc.).
|
|
35
|
+
It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
|
|
35
36
|
- **🔒 Secure Secret Storage:** The `secret_manager` module provides a simple interface for saving and retrieving secrets
|
|
36
37
|
via the system `keyring`, with a fallback to `.env` files.
|
|
37
38
|
- **🚀 CLI Booster:** Turn any function into a CLI tool in seconds using the `@cli_command` decorator with automatic type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "chutils"
|
|
3
|
-
version = "2.7.
|
|
3
|
+
version = "2.7.4"
|
|
4
4
|
description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
|
|
5
5
|
authors = [{name = "Chu4hel", email = "sergeiivanov636@gmail.com"}]
|
|
6
6
|
license = "MIT"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import re
|
|
3
|
+
import shutil
|
|
2
4
|
import sys
|
|
3
|
-
from typing import Any
|
|
5
|
+
from typing import Any, Optional
|
|
4
6
|
|
|
5
7
|
from .env import RICH_AVAILABLE, is_rich_enabled
|
|
6
8
|
|
|
@@ -18,6 +20,10 @@ class FallbackConsole:
|
|
|
18
20
|
def __init__(self, stderr: bool = False):
|
|
19
21
|
self._is_stderr = stderr
|
|
20
22
|
|
|
23
|
+
@property
|
|
24
|
+
def file(self):
|
|
25
|
+
return sys.stderr if self._is_stderr else sys.stdout
|
|
26
|
+
|
|
21
27
|
def _strip_markup(self, text: str) -> str:
|
|
22
28
|
"""Удаляет простейшие теги rich типа [bold]."""
|
|
23
29
|
return re.sub(r"\[/?[\w\s,=#]*\]", "", text)
|
|
@@ -56,6 +62,32 @@ class FallbackConsole:
|
|
|
56
62
|
|
|
57
63
|
_console = None
|
|
58
64
|
_err_console = None
|
|
65
|
+
_console_width: Optional[int] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_console_width(width: int):
|
|
69
|
+
"""
|
|
70
|
+
Устанавливает ширину консоли и сбрасывает кэшированные экземпляры консолей.
|
|
71
|
+
"""
|
|
72
|
+
global _console_width, _console, _err_console
|
|
73
|
+
_console_width = width
|
|
74
|
+
_console = None
|
|
75
|
+
_err_console = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_default_width() -> Optional[int]:
|
|
79
|
+
"""Определяет ширину консоли по умолчанию с учетом IDE."""
|
|
80
|
+
if _console_width is not None:
|
|
81
|
+
return _console_width
|
|
82
|
+
|
|
83
|
+
# Пытаемся определить размер терминала
|
|
84
|
+
width, _ = shutil.get_terminal_size(fallback=(80, 24))
|
|
85
|
+
|
|
86
|
+
# Специфичное поведение для PyCharm (часто ограничивает ширину в 80 символов при запуске логов)
|
|
87
|
+
if os.getenv("PYCHARM_HOSTED") == "1" and width == 80:
|
|
88
|
+
return 140
|
|
89
|
+
|
|
90
|
+
return width
|
|
59
91
|
|
|
60
92
|
|
|
61
93
|
def get_console(stderr: bool = False) -> Any:
|
|
@@ -68,7 +100,7 @@ def get_console(stderr: bool = False) -> Any:
|
|
|
68
100
|
if _err_console is not None:
|
|
69
101
|
return _err_console
|
|
70
102
|
if is_rich_enabled():
|
|
71
|
-
_err_console = Console(stderr=True)
|
|
103
|
+
_err_console = Console(stderr=True, width=_get_default_width())
|
|
72
104
|
else:
|
|
73
105
|
_err_console = FallbackConsole(stderr=True)
|
|
74
106
|
return _err_console
|
|
@@ -77,7 +109,7 @@ def get_console(stderr: bool = False) -> Any:
|
|
|
77
109
|
return _console
|
|
78
110
|
|
|
79
111
|
if is_rich_enabled():
|
|
80
|
-
_console = Console()
|
|
112
|
+
_console = Console(width=_get_default_width())
|
|
81
113
|
else:
|
|
82
114
|
_console = FallbackConsole()
|
|
83
115
|
return _console
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import logging
|
|
10
|
+
import os
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, Optional, Dict, TYPE_CHECKING, TypeVar, Type, Union, Tuple
|
|
12
13
|
|
|
@@ -131,6 +132,48 @@ def get_config(
|
|
|
131
132
|
except Exception as e:
|
|
132
133
|
logger.error("Ошибка загрузки удаленной конфигурации с %s: %s", remote_url, e)
|
|
133
134
|
|
|
135
|
+
# 5. Переменные окружения (CH_SECTION_KEY)
|
|
136
|
+
disable_env_override = os.getenv("CH_DISABLE_ENV_OVERRIDE", "").lower() in ("true", "1", "yes", "y")
|
|
137
|
+
if not disable_env_override:
|
|
138
|
+
env_overrides = {}
|
|
139
|
+
for env_key, env_value in os.environ.items():
|
|
140
|
+
if env_key.startswith("CH_") and env_key not in ("CH_ENV", "CH_DISABLE_ENV_OVERRIDE",
|
|
141
|
+
"CH_DISABLE_KEYRING_WARNING"):
|
|
142
|
+
parts = env_key[3:].split('_', 1)
|
|
143
|
+
if len(parts) == 2:
|
|
144
|
+
section, key = parts
|
|
145
|
+
section_lower = section.lower()
|
|
146
|
+
key_lower = key.lower()
|
|
147
|
+
|
|
148
|
+
# Попытка найти существующий регистр секции
|
|
149
|
+
actual_sec = section_lower
|
|
150
|
+
for existing_sec in config_data.keys():
|
|
151
|
+
if existing_sec.lower() == section_lower:
|
|
152
|
+
actual_sec = existing_sec
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# Попытка найти существующий регистр ключа
|
|
156
|
+
actual_key = key_lower
|
|
157
|
+
if actual_sec in config_data and isinstance(config_data[actual_sec], dict):
|
|
158
|
+
for existing_key in config_data[actual_sec].keys():
|
|
159
|
+
if existing_key.lower() == key_lower:
|
|
160
|
+
actual_key = existing_key
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if actual_sec not in env_overrides:
|
|
164
|
+
env_overrides[actual_sec] = {}
|
|
165
|
+
env_overrides[actual_sec][actual_key] = env_value
|
|
166
|
+
|
|
167
|
+
# Специфический ключ для secrets
|
|
168
|
+
secrets_env = os.getenv("CH_DISABLE_KEYRING_WARNING")
|
|
169
|
+
if secrets_env is not None:
|
|
170
|
+
if "secrets" not in env_overrides:
|
|
171
|
+
env_overrides["secrets"] = {}
|
|
172
|
+
env_overrides["secrets"]["disable_keyring"] = secrets_env
|
|
173
|
+
|
|
174
|
+
if env_overrides:
|
|
175
|
+
utils.deep_merge(config_data, env_overrides)
|
|
176
|
+
|
|
134
177
|
# Записываем переменные окружения в трассировку
|
|
135
178
|
if _cm.tracing_enabled:
|
|
136
179
|
_cm.trace_env_vars()
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
import os
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any, Optional, List, Dict, TYPE_CHECKING, TypeVar, Type, overload, Union
|
|
12
11
|
|
|
@@ -29,9 +28,7 @@ def get_config_value(section: str, key: str, fallback: Any = None, config: Optio
|
|
|
29
28
|
Получает произвольное значение из конфигурации.
|
|
30
29
|
|
|
31
30
|
Если значение не найдено или оно пустое, возвращает `fallback`.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Также поддерживает универсальное переопределение через переменные окружения
|
|
31
|
+
Поддерживает универсальное переопределение через переменные окружения
|
|
35
32
|
по шаблону `CH_[SECTION]_[KEY]`, если не установлено `CH_DISABLE_ENV_OVERRIDE=true`.
|
|
36
33
|
|
|
37
34
|
Args:
|
|
@@ -43,27 +40,24 @@ def get_config_value(section: str, key: str, fallback: Any = None, config: Optio
|
|
|
43
40
|
Returns:
|
|
44
41
|
Значение из конфигурации или `fallback`.
|
|
45
42
|
"""
|
|
46
|
-
# 1. Проверка глобального флага отключения переопределения через ENV
|
|
47
|
-
disable_env_override = os.getenv("CH_DISABLE_ENV_OVERRIDE", "").lower() in ("true", "1", "yes", "y")
|
|
48
|
-
|
|
49
|
-
if not disable_env_override:
|
|
50
|
-
# 2. Проверка универсального переопределения CH_[SECTION]_[KEY]
|
|
51
|
-
# Используем верхний регистр для поиска в ENV согласно спецификации
|
|
52
|
-
env_key = f"CH_{section.upper()}_{key.upper()}"
|
|
53
|
-
env_value = os.getenv(env_key)
|
|
54
|
-
if env_value is not None:
|
|
55
|
-
return env_value
|
|
56
|
-
|
|
57
|
-
# Проверка переменных окружения для специфических ключей (FR3: приоритет над конфигом)
|
|
58
|
-
if section == "secrets" and key == "disable_keyring":
|
|
59
|
-
env_val = os.getenv("CH_DISABLE_KEYRING_WARNING")
|
|
60
|
-
if env_val is not None:
|
|
61
|
-
return env_val
|
|
62
|
-
|
|
63
43
|
if config is None:
|
|
64
44
|
config = get_config()
|
|
65
45
|
|
|
66
|
-
|
|
46
|
+
section_data = config.get(section)
|
|
47
|
+
if section_data is None:
|
|
48
|
+
for k, v in config.items():
|
|
49
|
+
if k.lower() == section.lower():
|
|
50
|
+
section_data = v
|
|
51
|
+
break
|
|
52
|
+
else:
|
|
53
|
+
section_data = {}
|
|
54
|
+
|
|
55
|
+
value = section_data.get(key)
|
|
56
|
+
if value is None:
|
|
57
|
+
for k, v in section_data.items():
|
|
58
|
+
if k.lower() == key.lower():
|
|
59
|
+
value = v
|
|
60
|
+
break
|
|
67
61
|
|
|
68
62
|
# Если значение не найдено или является пустой строкой, возвращаем fallback
|
|
69
63
|
if value is None or value == '':
|
|
@@ -211,7 +205,15 @@ def get_config_section(
|
|
|
211
205
|
if config is None:
|
|
212
206
|
config = get_config()
|
|
213
207
|
|
|
214
|
-
section_data = config.get(section_name
|
|
208
|
+
section_data = config.get(section_name)
|
|
209
|
+
if section_data is None:
|
|
210
|
+
# Case-insensitive fallback
|
|
211
|
+
for k, v in config.items():
|
|
212
|
+
if k.lower() == section_name.lower():
|
|
213
|
+
section_data = v
|
|
214
|
+
break
|
|
215
|
+
else:
|
|
216
|
+
section_data = fallback if fallback is not None else {}
|
|
215
217
|
|
|
216
218
|
if model is not None:
|
|
217
219
|
if not utils._check_pydantic():
|
|
@@ -166,6 +166,7 @@ class IniConfigProvider(ConfigProvider):
|
|
|
166
166
|
try:
|
|
167
167
|
with open(path, 'r', encoding='utf-8') as f:
|
|
168
168
|
parser = configparser.ConfigParser()
|
|
169
|
+
parser.optionxform = str # Сохраняем регистр ключей
|
|
169
170
|
parser.read_string(f.read())
|
|
170
171
|
flat_ini_config = {s: dict(parser.items(s)) for s in parser.sections()}
|
|
171
172
|
return self._nest_func(flat_ini_config)
|
|
@@ -22,6 +22,8 @@ class Indexer:
|
|
|
22
22
|
self.project_root = self.root_path
|
|
23
23
|
|
|
24
24
|
self._graph_map: Dict[str, Dict[str, int]] = {} # {source: {target: weight}}
|
|
25
|
+
self._current_imports: Dict[str, str] = {}
|
|
26
|
+
"""Карта импортов текущего модуля {asname: full_path}"""
|
|
25
27
|
self._public_symbols = self._discover_public_api()
|
|
26
28
|
|
|
27
29
|
@property
|
|
@@ -162,11 +164,13 @@ class Indexer:
|
|
|
162
164
|
)
|
|
163
165
|
|
|
164
166
|
# Анализ зависимостей
|
|
167
|
+
self._current_imports = {}
|
|
165
168
|
if tree:
|
|
166
169
|
for item in tree.body:
|
|
167
170
|
if isinstance(item, ast.Import):
|
|
168
171
|
for alias in item.names:
|
|
169
172
|
self._record_dependency(rel_path, alias.name)
|
|
173
|
+
self._current_imports[alias.asname or alias.name] = alias.name
|
|
170
174
|
elif isinstance(item, ast.ImportFrom):
|
|
171
175
|
is_relative = item.level > 0
|
|
172
176
|
base = item.module if item.module else "." * item.level
|
|
@@ -177,6 +181,9 @@ class Indexer:
|
|
|
177
181
|
self._record_dependency(rel_path, base, force_internal=is_relative)
|
|
178
182
|
continue
|
|
179
183
|
|
|
184
|
+
full_name = f"{base}.{alias.name}" if not is_relative else f"{base}{alias.name}"
|
|
185
|
+
self._current_imports[alias.asname or alias.name] = full_name
|
|
186
|
+
|
|
180
187
|
if is_relative:
|
|
181
188
|
# Для относительных импортов в тестах ожидается база ('.', '..')
|
|
182
189
|
self._record_dependency(rel_path, base, force_internal=True)
|
|
@@ -224,6 +231,10 @@ class Indexer:
|
|
|
224
231
|
))
|
|
225
232
|
return symbols
|
|
226
233
|
|
|
234
|
+
def _resolve_base_class(self, base_name: str) -> str:
|
|
235
|
+
"""Разрешает имя базового класса в полный путь импорта."""
|
|
236
|
+
return self._current_imports.get(base_name, base_name)
|
|
237
|
+
|
|
227
238
|
def _build_symbol(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef], sym_type: str) -> Symbol:
|
|
228
239
|
"""Создает объект Symbol из узла AST."""
|
|
229
240
|
docstring = ast.get_docstring(node) or ""
|
|
@@ -248,15 +259,21 @@ class Indexer:
|
|
|
248
259
|
|
|
249
260
|
# Декораторы
|
|
250
261
|
for dec in node.decorator_list:
|
|
262
|
+
dec_name = ""
|
|
251
263
|
if isinstance(dec, ast.Name):
|
|
252
|
-
|
|
264
|
+
dec_name = dec.id
|
|
253
265
|
elif isinstance(dec, ast.Attribute) and isinstance(dec.value, ast.Name):
|
|
254
|
-
|
|
266
|
+
dec_name = f"{dec.value.id}.{dec.attr}"
|
|
255
267
|
elif isinstance(dec, ast.Call):
|
|
256
268
|
if isinstance(dec.func, ast.Name):
|
|
257
|
-
|
|
269
|
+
dec_name = dec.func.id
|
|
258
270
|
elif isinstance(dec.func, ast.Attribute) and isinstance(dec.func.value, ast.Name):
|
|
259
|
-
|
|
271
|
+
dec_name = f"{dec.func.value.id}.{dec.func.attr}"
|
|
272
|
+
|
|
273
|
+
if dec_name:
|
|
274
|
+
breadcrumbs.decorators.append(dec_name)
|
|
275
|
+
if dec_name == "abstractmethod" or dec_name.endswith(".abstractmethod"):
|
|
276
|
+
breadcrumbs.is_abstract = True
|
|
260
277
|
|
|
261
278
|
# Теги из docstring (:tag:)
|
|
262
279
|
tags = re.findall(r":([\w-]+):", docstring)
|
|
@@ -267,7 +284,7 @@ class Indexer:
|
|
|
267
284
|
if "heavy" in breadcrumbs.tags:
|
|
268
285
|
breadcrumbs.is_heavy = True
|
|
269
286
|
|
|
270
|
-
|
|
287
|
+
symbol = Symbol(
|
|
271
288
|
name=node.name,
|
|
272
289
|
type=sym_type,
|
|
273
290
|
signature=signature,
|
|
@@ -277,3 +294,34 @@ class Indexer:
|
|
|
277
294
|
line_number=node.lineno,
|
|
278
295
|
layer=self._get_layer(node.name, docstring)
|
|
279
296
|
)
|
|
297
|
+
|
|
298
|
+
if isinstance(node, ast.ClassDef):
|
|
299
|
+
# Извлекаем базы
|
|
300
|
+
for base in node.bases:
|
|
301
|
+
if isinstance(base, ast.Name):
|
|
302
|
+
symbol.bases.append(self._resolve_base_class(base.id))
|
|
303
|
+
elif isinstance(base, ast.Attribute):
|
|
304
|
+
# Случай типа pydantic.BaseModel
|
|
305
|
+
parts = []
|
|
306
|
+
curr = base
|
|
307
|
+
while isinstance(curr, ast.Attribute):
|
|
308
|
+
parts.append(curr.attr)
|
|
309
|
+
curr = curr.value
|
|
310
|
+
if isinstance(curr, ast.Name):
|
|
311
|
+
parts.append(curr.id)
|
|
312
|
+
symbol.bases.append(".".join(reversed(parts)))
|
|
313
|
+
|
|
314
|
+
# Извлекаем методы
|
|
315
|
+
for item in node.body:
|
|
316
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
317
|
+
# Фильтрация: оставляем публичные, защищенные (_) и __init__.
|
|
318
|
+
# Отбрасываем остальные dunder-методы (__dunder__) и приватные (__private).
|
|
319
|
+
name = item.name
|
|
320
|
+
if name.startswith("__") and name != "__init__":
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
method_symbol = self._build_symbol(item, "method")
|
|
324
|
+
|
|
325
|
+
symbol.children.append(method_symbol)
|
|
326
|
+
|
|
327
|
+
return symbol
|
|
@@ -12,6 +12,7 @@ class Breadcrumbs(BaseModel):
|
|
|
12
12
|
is_async: bool = False
|
|
13
13
|
is_thread_safe: bool = False
|
|
14
14
|
is_heavy: bool = False
|
|
15
|
+
is_abstract: bool = False
|
|
15
16
|
decorators: List[str] = Field(default_factory=list)
|
|
16
17
|
tags: List[str] = Field(default_factory=list)
|
|
17
18
|
custom_metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -29,6 +30,10 @@ class Symbol(BaseModel):
|
|
|
29
30
|
docstring: Optional[str] = None
|
|
30
31
|
breadcrumbs: Breadcrumbs = Field(default_factory=Breadcrumbs)
|
|
31
32
|
line_number: int = 0
|
|
33
|
+
bases: List[str] = Field(default_factory=list)
|
|
34
|
+
"""Базовые классы (для классов)"""
|
|
35
|
+
children: List["Symbol"] = Field(default_factory=list)
|
|
36
|
+
"""Вложенные символы (например, методы класса)"""
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
class Node(BaseModel):
|
|
@@ -6,7 +6,10 @@ import os
|
|
|
6
6
|
|
|
7
7
|
def _is_installed(package_name: str) -> bool:
|
|
8
8
|
"""Проверяет наличие пакета в системе без его импорта."""
|
|
9
|
-
|
|
9
|
+
try:
|
|
10
|
+
return importlib.util.find_spec(package_name) is not None
|
|
11
|
+
except (ImportError, ModuleNotFoundError):
|
|
12
|
+
return False
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
RICH_AVAILABLE = _is_installed("rich")
|
|
@@ -264,6 +264,16 @@ def setup_logger(
|
|
|
264
264
|
|
|
265
265
|
final_logger_settings = {**default_settings, **specific_settings}
|
|
266
266
|
|
|
267
|
+
# --- Настройка ширины консоли ---
|
|
268
|
+
cli_settings = cfg.get('CLI', {})
|
|
269
|
+
config_width = cli_settings.get('console_width')
|
|
270
|
+
if config_width is not None:
|
|
271
|
+
try:
|
|
272
|
+
from chutils.cli_utils import set_console_width
|
|
273
|
+
set_console_width(int(config_width))
|
|
274
|
+
except (ValueError, TypeError, ImportError):
|
|
275
|
+
pass
|
|
276
|
+
|
|
267
277
|
# --- Определение флага асинхронности ---
|
|
268
278
|
if use_async is not None:
|
|
269
279
|
final_use_async = use_async
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|