socx-cli 0.13.3__tar.gz → 0.13.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.
Files changed (115) hide show
  1. {socx_cli-0.13.3 → socx_cli-0.13.4}/PKG-INFO +2 -1
  2. {socx_cli-0.13.3 → socx_cli-0.13.4}/pyproject.toml +2 -1
  3. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/__init__.py +0 -8
  4. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/callbacks.py +8 -6
  5. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/_paths.py +8 -2
  6. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/__init__.py +0 -8
  7. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/log.py +164 -62
  8. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/__init__.py +0 -2
  9. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/progress.py +68 -66
  10. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/regression.py +145 -148
  11. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/test.py +38 -19
  12. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/cli.yaml +1 -1
  13. socx_cli-0.13.4/socx/static/settings/logging.yaml +24 -0
  14. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/regression.yaml +4 -0
  15. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/settings.yaml +2 -1
  16. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/callbacks.py +10 -2
  17. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/run.py +5 -2
  18. socx_cli-0.13.4/socx_plugins/regression/tui.py +30 -0
  19. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/app.py +7 -2
  20. socx_cli-0.13.4/socx_tui/regression/details.py +158 -0
  21. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/widget.py +62 -92
  22. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/static/tcss/regression/app.tcss +3 -1
  23. socx_cli-0.13.3/socx_plugins/regression/tui.py +0 -11
  24. socx_cli-0.13.3/socx_tui/regression/details.py +0 -165
  25. {socx_cli-0.13.3 → socx_cli-0.13.4}/.gitignore +0 -0
  26. {socx_cli-0.13.3 → socx_cli-0.13.4}/LICENSE +0 -0
  27. {socx_cli-0.13.3 → socx_cli-0.13.4}/README.md +0 -0
  28. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/__main__.py +0 -0
  29. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/__init__.py +0 -0
  30. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/_cli.py +0 -0
  31. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/_jinja.py +0 -0
  32. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/cfg.py +0 -0
  33. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/cli.py +0 -0
  34. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/params.py +0 -0
  35. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/cli/types.py +0 -0
  36. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/__init__.py +0 -0
  37. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/_config.py +0 -0
  38. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/_settings.py +0 -0
  39. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/converters.py +0 -0
  40. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/encoders.py +0 -0
  41. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/formatters.py +0 -0
  42. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/serializers.py +0 -0
  43. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/config/validators.py +0 -0
  44. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/__init__.py +0 -0
  45. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/encoder.py +0 -0
  46. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/enums.py +0 -0
  47. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/funcs.py +0 -0
  48. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/metadata.py +0 -0
  49. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/paths.py +0 -0
  50. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/__init__.py +0 -0
  51. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/__init__.py +0 -0
  52. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/git.py +0 -0
  53. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/git/manifest.py +0 -0
  54. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/plugin.py +0 -0
  55. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/schema/types.py +0 -0
  56. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/core/serializer.py +0 -0
  57. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/__init__.py +0 -0
  58. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_git.py +0 -0
  59. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_manifest.py +0 -0
  60. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/git/_ssh.py +0 -0
  61. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/console.py +0 -0
  62. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/io/decorators.py +0 -0
  63. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/__init__.py +0 -0
  64. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/__init__.py +0 -0
  65. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/proxy.py +0 -0
  66. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/mixins/uid.py +0 -0
  67. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/singleton/__init__.py +0 -0
  68. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/singleton/singleton.py +0 -0
  69. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/__init__.py +0 -0
  70. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/protocol.py +0 -0
  71. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/patterns/visitor/traversal.py +0 -0
  72. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/status.py +0 -0
  73. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/validator.py +0 -0
  74. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/regression/visitor.py +0 -0
  75. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/console.yaml +0 -0
  76. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/git.yaml +0 -0
  77. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/plugins.yaml +0 -0
  78. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/settings/rich_click.yaml +0 -0
  79. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/static/sql/socx.sql +0 -0
  80. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/utils/__init__.py +0 -0
  81. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx/utils/decorators.py +0 -0
  82. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/__init__.py +0 -0
  83. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/_config.py +0 -0
  84. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/config/edit.py +0 -0
  85. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/__init__.py +0 -0
  86. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/arguments.py +0 -0
  87. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/callbacks.py +0 -0
  88. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/cli.py +0 -0
  89. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/manifest.py +0 -0
  90. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/renderables.py +0 -0
  91. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/summary.py +0 -0
  92. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/git/utils.py +0 -0
  93. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/__init__.py +0 -0
  94. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/example.py +0 -0
  95. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/plugin/schema.py +0 -0
  96. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/__init__.py +0 -0
  97. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/_run.py +0 -0
  98. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/regression/cli.py +0 -0
  99. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/version/__init__.py +0 -0
  100. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_plugins/version/__main__.py +0 -0
  101. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/__init__.py +0 -0
  102. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/__init__.py +0 -0
  103. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/__main__.py +0 -0
  104. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/__init__.py +0 -0
  105. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/__init__.py +0 -0
  106. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/mode.py +0 -0
  107. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/bindings/vim/vim.py +0 -0
  108. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/containers.py +0 -0
  109. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/dialog.py +0 -0
  110. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/mixins/__init__.py +0 -0
  111. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/mixins/composable.py +0 -0
  112. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/preview.py +0 -0
  113. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/table.py +0 -0
  114. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/regression/tree.py +0 -0
  115. {socx_cli-0.13.3 → socx_cli-0.13.4}/socx_tui/static/tcss/regression/preview.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socx-cli
3
- Version: 0.13.3
3
+ Version: 0.13.4
4
4
  Summary: System on chip verification and tooling infrastructure.
5
5
  Project-URL: Issues, https://github.com/sagikimhi/socx-cli/issues
6
6
  Project-URL: Homepage, https://sagikimhi.dev/socx-cli
@@ -31,6 +31,7 @@ Requires-Python: >=3.12
31
31
  Requires-Dist: anyio>=4.12.1
32
32
  Requires-Dist: click
33
33
  Requires-Dist: copier~=9.11
34
+ Requires-Dist: declare
34
35
  Requires-Dist: dynaconf<3.2.12
35
36
  Requires-Dist: gitpython
36
37
  Requires-Dist: hoptex
@@ -29,7 +29,7 @@ socx = 'socx.__main__:main'
29
29
  [project]
30
30
  name = "socx-cli"
31
31
  readme = "README.md"
32
- version = "0.13.3"
32
+ version = "0.13.4"
33
33
  license = "Apache-2.0"
34
34
  authors = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
35
35
  maintainers = [{ name = "Sagi Kimhi", email = "sagi.kim5@gmail.com" }]
@@ -59,6 +59,7 @@ dependencies = [
59
59
  "pip",
60
60
  "rich",
61
61
  "click",
62
+ "declare",
62
63
  "typer~=0.19",
63
64
  "jinja2",
64
65
  "psutil",
@@ -47,7 +47,6 @@ __all__ = (
47
47
  "DEFAULT_HANDLERS",
48
48
  "DEFAULT_TIME_FORMAT",
49
49
  "Level",
50
- "log",
51
50
  "log_it",
52
51
  "logger",
53
52
  "console",
@@ -135,7 +134,6 @@ __all__ = (
135
134
  "Regression",
136
135
  "TestStatus",
137
136
  "TestResult",
138
- "RegressionTree",
139
137
  "RegressionProgress",
140
138
  )
141
139
 
@@ -180,11 +178,6 @@ from socx.core import deduplicate as deduplicate
180
178
 
181
179
  from socx.utils import join_decorators as join_decorators
182
180
 
183
- from socx.io import log as log
184
- from socx.io import DEFAULT_LEVEL as DEFAULT_LEVEL
185
- from socx.io import DEFAULT_FORMAT as DEFAULT_FORMAT
186
- from socx.io import DEFAULT_HANDLERS as DEFAULT_HANDLERS
187
- from socx.io import DEFAULT_TIME_FORMAT as DEFAULT_TIME_FORMAT
188
181
  from socx.io import Level as Level
189
182
  from socx.io import log_it as log_it
190
183
  from socx.io import logger as logger
@@ -264,7 +257,6 @@ from socx.regression import TestBase as TestBase
264
257
  from socx.regression import Regression as Regression
265
258
  from socx.regression import TestStatus as TestStatus
266
259
  from socx.regression import TestResult as TestResult
267
- from socx.regression import RegressionTree as RegressionTree
268
260
  from socx.regression import RegressionProgress as RegressionProgress
269
261
 
270
262
  from socx.cli import FuncType as FuncType
@@ -12,7 +12,7 @@ from rich_click import Context
12
12
  from rich_click import Parameter
13
13
  from rich_click import RichContext
14
14
 
15
- import socx.io.log as log
15
+ from socx.io.log import Level, set_level, get_level, _get_logger
16
16
  from socx.io.console import console
17
17
  from socx.io.decorators import log_it
18
18
  from socx.config._config import settings
@@ -45,10 +45,11 @@ def cwd_cb(ctx: Context, param: Parameter, value: Path) -> Path:
45
45
  @log_it(logger=logger)
46
46
  def debug_cb(_: Context, param: Parameter, value: bool) -> bool:
47
47
  """Enable debug logging and persist the CLI switch to settings."""
48
+ socx_logger = _get_logger()
48
49
  settings.cli.params[param.name] = value
49
50
  if value:
50
- log.set_level(log.Level.DEBUG)
51
- settings.cli.params["verbosity"] = log.get_level().name
51
+ set_level(Level.DEBUG, socx_logger)
52
+ settings.cli.params["verbosity"] = get_level().name
52
53
  return value
53
54
 
54
55
 
@@ -88,10 +89,11 @@ def configure_cb(ctx: Context, param: Parameter, value: str) -> str:
88
89
  @log_it(logger=logger)
89
90
  def verbosity_cb(_: Context, param: Parameter, value: str) -> str:
90
91
  """Update the global log level while respecting existing overrides."""
91
- level = log.Level[value.upper()]
92
+ socx_logger = _get_logger()
93
+ level = Level[value.upper()]
92
94
  if not settings.cli.params.debug:
93
- log.set_level(level)
94
- settings.cli.params[param.name] = log.get_level().name
95
+ set_level(level, socx_logger)
96
+ settings.cli.params[param.name] = get_level().name
95
97
  return settings.cli.params[param.name]
96
98
 
97
99
 
@@ -109,8 +109,11 @@ USER_LOG_DIR: Path = user_log_path(
109
109
  ).resolve()
110
110
  """Absolute path to platform's native application logs directory."""
111
111
 
112
- USER_LOG_FILENAME: str = f"{__appname__}.log"
113
- """File name of application's native log file used for debug and tracing."""
112
+ USER_LOG_FILENAME: str = f"{__appname__}_log.log"
113
+ """File name of application's log file."""
114
+
115
+ USER_ROTATING_LOG_FILENAME: str = f"rotating_{__appname__}_log.log"
116
+ """File name of application's rotating log file."""
114
117
 
115
118
  USER_CONFIG_FILENAME: str = f"{__appname__}.yaml"
116
119
  """File name searched in user's config directory to load user configs."""
@@ -118,6 +121,9 @@ USER_CONFIG_FILENAME: str = f"{__appname__}.yaml"
118
121
  USER_LOG_FILE: Path = USER_LOG_DIR / USER_LOG_FILENAME
119
122
  """Absolute path to application's main log for the current local user."""
120
123
 
124
+ USER_ROTATING_LOG_FILE: Path = USER_LOG_DIR / USER_ROTATING_LOG_FILENAME
125
+ """Absolute path to application's rotating log for the current local user."""
126
+
121
127
  USER_CONFIG_FILE: Path = USER_CONFIG_DIR / USER_CONFIG_FILENAME
122
128
  """Absolute path to application's user config file."""
123
129
 
@@ -1,8 +1,6 @@
1
1
  """Expose logging, console, and decorator utilities for I/O facilities."""
2
2
 
3
3
  __all__ = (
4
- # Modules
5
- "log",
6
4
  # Constants
7
5
  "DEFAULT_LEVEL",
8
6
  "DEFAULT_FORMAT",
@@ -32,12 +30,6 @@ __all__ = (
32
30
  "print_command_outputs",
33
31
  )
34
32
 
35
- from socx.io import log as log
36
-
37
- from socx.io.log import DEFAULT_LEVEL as DEFAULT_LEVEL
38
- from socx.io.log import DEFAULT_FORMAT as DEFAULT_FORMAT
39
- from socx.io.log import DEFAULT_HANDLERS as DEFAULT_HANDLERS
40
- from socx.io.log import DEFAULT_TIME_FORMAT as DEFAULT_TIME_FORMAT
41
33
  from socx.io.log import Level as Level
42
34
  from socx.io.log import logger as logger
43
35
  from socx.io.log import get_level as get_level
@@ -1,19 +1,21 @@
1
1
  """Logging helpers that standardise Rich-powered output across SoCX."""
2
2
 
3
3
  from __future__ import annotations
4
+ from types import ModuleType
4
5
 
5
- import os
6
6
  import enum
7
7
  import logging
8
8
  import logging.handlers
9
- from typing import Any
9
+ from typing import Any, IO
10
10
  from pathlib import Path
11
11
  from collections import ChainMap
12
12
  from collections.abc import Iterable
13
13
 
14
14
  from rich.console import Console
15
15
  from rich.logging import RichHandler
16
- from platformdirs import user_log_path
16
+
17
+ from socx.config._config import settings
18
+ from socx.core.metadata import __appname__
17
19
 
18
20
  __all__ = (
19
21
  # Logging
@@ -39,10 +41,7 @@ __all__ = (
39
41
  # Types
40
42
  "Level",
41
43
  # Defaults
42
- "DEFAULT_LEVEL",
43
- "DEFAULT_FORMAT",
44
44
  "DEFAULT_HANDLERS",
45
- "DEFAULT_TIME_FORMAT",
46
45
  )
47
46
 
48
47
 
@@ -59,95 +58,195 @@ class Level(enum.IntEnum):
59
58
  CRITICAL = logging.CRITICAL
60
59
 
61
60
 
62
- def _get_console_handler(level: Level = Level.INFO) -> logging.Handler:
61
+ def _get_console(
62
+ file: IO | None = None,
63
+ stderr: bool = False,
64
+ markup: bool = True,
65
+ tab_size: int = 4,
66
+ force_terminal: bool = True,
67
+ **kwargs: Any,
68
+ ) -> Console:
69
+ defaults = dict(
70
+ file=file,
71
+ markup=markup,
72
+ stderr=stderr,
73
+ tab_size=tab_size,
74
+ force_terminal=force_terminal,
75
+ )
76
+ kwargs = dict(ChainMap(kwargs, defaults))
77
+ return Console(**kwargs)
78
+
79
+
80
+ def _get_level(level: str | int | Level) -> Level:
81
+ if isinstance(level, str):
82
+ level = Level[level]
83
+ elif isinstance(level, int):
84
+ level = Level(level)
85
+ return level
86
+
87
+
88
+ def _get_console_handler(
89
+ file: IO | None = None,
90
+ level: int | str | Level = Level.INFO,
91
+ stderr: bool = False,
92
+ tab_size: int = 4,
93
+ tracebacks: bool = True,
94
+ force_terminal: bool = True,
95
+ tracebacks_theme: str | None = None,
96
+ tracebacks_suppress: Iterable[ModuleType] | None = None,
97
+ tracebacks_show_locals: bool = True,
98
+ ) -> logging.Handler:
63
99
  """Create a Rich console handler configured for interactive output."""
64
- console = Console(tab_size=4, markup=True, force_terminal=True)
65
- return RichHandler(
66
- level=level,
100
+ import click
101
+ import rich_click
102
+ from socx.io.console import console as _console
103
+
104
+ level = _get_level(level)
105
+ tracebacks_theme = tracebacks_theme or "ansi_dark"
106
+ tracebacks_suppress = tracebacks_suppress or [click, rich_click]
107
+ if file is None and stderr is None:
108
+ console = _console
109
+ else:
110
+ console = _get_console(
111
+ file=file,
112
+ stderr=stderr,
113
+ tab_size=tab_size,
114
+ force_terminal=force_terminal,
115
+ )
116
+ handler = RichHandler(
67
117
  console=console,
68
- tracebacks_suppress=("rich-click", "click"),
69
- rich_tracebacks=True,
70
- tracebacks_show_locals=True,
118
+ rich_tracebacks=tracebacks,
119
+ tracebacks_theme=tracebacks_theme,
120
+ tracebacks_suppress=tracebacks_suppress,
121
+ tracebacks_show_locals=tracebacks_show_locals,
71
122
  )
123
+ formatter = logging.Formatter(**settings.logging.formatters.default)
124
+ handler.name = "console"
125
+ handler.setLevel(level)
126
+ handler.setFormatter(formatter)
127
+ return handler
72
128
 
73
129
 
74
130
  def _get_file_handler(
75
- path: str | Path, level: Level = Level.NOTSET
131
+ path: str | Path,
132
+ mode: str | None = None,
133
+ level: Level = Level.INFO,
134
+ stderr: bool = False,
135
+ tab_size: int = 4,
136
+ tracebacks: bool = True,
137
+ force_terminal: bool = False,
138
+ tracebacks_theme: str | None = None,
139
+ tracebacks_suppress: Iterable[ModuleType] | None = None,
140
+ tracebacks_show_locals: bool = True,
141
+ ) -> logging.Handler:
142
+ import atexit
143
+
144
+ def close_if_open(file: IO) -> None:
145
+ if not file.closed:
146
+ file.close()
147
+
148
+ mode = mode or "a"
149
+ file = open(path, mode=mode) # noqa: SIM115
150
+ atexit.register(close_if_open, file)
151
+ handler = _get_console_handler(
152
+ file=file,
153
+ level=level,
154
+ stderr=stderr,
155
+ tab_size=tab_size,
156
+ tracebacks=tracebacks,
157
+ force_terminal=force_terminal,
158
+ tracebacks_theme=tracebacks_theme,
159
+ tracebacks_suppress=tracebacks_suppress,
160
+ tracebacks_show_locals=tracebacks_show_locals,
161
+ )
162
+ handler.name = "file"
163
+ handler.setFormatter(DEFAULT_CHILD_FORMATTER)
164
+ return handler
165
+
166
+
167
+ def _get_rotating_file_handler(
168
+ path: str | Path,
169
+ level: Level = Level.DEBUG,
170
+ stderr: bool = False,
171
+ mode: str | None = None,
76
172
  ) -> logging.Handler:
77
173
  """Create a Rich handler that writes log output to ``path``."""
174
+
175
+ def MBs(n: int) -> int: # noqa: N802
176
+ return 1024 * 1024 * n
177
+
178
+ mode = mode or "w"
78
179
  handler = logging.handlers.RotatingFileHandler(
79
- path, maxBytes=1024 * 1024 * 5, backupCount=5
180
+ # no particular reason for size or backup count - arbitrarily chosen
181
+ path,
182
+ mode=mode,
183
+ maxBytes=MBs(10),
184
+ backupCount=5,
80
185
  )
186
+ handler.name = "rotating_file"
81
187
  handler.setLevel(level)
82
- handler.setFormatter(
83
- logging.Formatter(DEFAULT_CHILD_FORMAT, DEFAULT_TIME_FORMAT)
84
- )
188
+ handler.setFormatter(DEFAULT_CHILD_FORMATTER)
85
189
  return handler
86
190
 
87
191
 
88
- APP_LIB_NAME = __name__.partition(".")[0]
89
- """Application library namespace used for loggers."""
192
+ def _get_handler(handler: str) -> logging.Handler | None:
193
+ match handler:
194
+ case "file":
195
+ return _get_file_handler(**settings.logging.handlers.file)
196
+ case "console":
197
+ return _get_console_handler(**settings.logging.handlers.console)
198
+ case "rotating_file":
199
+ return _get_rotating_file_handler(
200
+ **settings.logging.handlers.rotating_file
201
+ )
202
+ case _:
203
+ return None
204
+
205
+
206
+ def _get_logger() -> logging.Logger:
207
+ """Initialise and return the module-level root logger."""
208
+ socx_logger = logging.getLogger(__appname__)
209
+
210
+ if not socx_logger.hasHandlers():
211
+ unknown_handlers = []
90
212
 
91
- DEFAULT_ENCODING: str = "utf-8"
92
- """Default text encoding for emitted log files."""
213
+ for handler_name in settings.logging.handlers:
214
+ handler = _get_handler(handler_name)
215
+ if handler is not None:
216
+ socx_logger.addHandler(handler)
217
+ else:
218
+ unknown_handlers.append(handler_name)
93
219
 
94
- DEFAULT_LEVEL: Level = Level[os.environ.get("SOCX_VERBOSITY", "FATAL")]
95
- """Default logger level, a.k.a verbosity."""
220
+ for handler in unknown_handlers:
221
+ msg = f"Ignored unknown handler configuration: '{handler}'"
222
+ socx_logger.warning(msg)
96
223
 
97
- DEFAULT_FORMAT: str = os.environ.get("SOCX_LOG_FORMAT", "%(message)s")
98
- """Default log message format used by the root handler."""
224
+ return socx_logger
99
225
 
100
- DEFAULT_TIME_FORMAT: str = os.environ.get("SOCX_TIME_FORMAT", "[%x %X]")
101
- """Default timestamp format injected into log records."""
102
226
 
103
- DEFAULT_CHILD_FORMAT: str = os.environ.get(
104
- "SOCX_LOG_FORMAT",
105
- "%(asctime)s %(levelname)5s - %(filename)5s:%(lineno)-4d - %(message)s",
227
+ DEFAULT_FORMATTER: logging.Formatter = logging.Formatter(
228
+ **settings.logging.formatters.default
106
229
  )
107
- """Message format for child loggers that also emit timestamps."""
230
+ """Default application logging formatter."""
108
231
 
109
232
  DEFAULT_CHILD_FORMATTER: logging.Formatter = logging.Formatter(
110
- DEFAULT_CHILD_FORMAT, DEFAULT_TIME_FORMAT
233
+ **settings.logging.formatters.child
111
234
  )
112
235
  """Formatter applied to file handlers registered on child loggers."""
113
236
 
114
- DEFAULT_LOG_DIRECTORY: Path = Path(
115
- os.environ.get(
116
- "SOCX_LOG_DIR",
117
- user_log_path(appname=APP_LIB_NAME, ensure_exists=True),
118
- )
119
- )
120
- """Default application log directory."""
121
-
122
- DEFAULT_LOG_FILE: str = os.environ.get("SOCX_LOG_FILE", f"{APP_LIB_NAME}.log")
123
- """Default application log file."""
124
-
125
237
  DEFAULT_HANDLERS: list[logging.Handler] = [
126
- _get_console_handler(DEFAULT_LEVEL),
127
- _get_file_handler(DEFAULT_LOG_DIRECTORY / DEFAULT_LOG_FILE),
238
+ _get_file_handler(**settings.logging.handlers.file),
239
+ _get_console_handler(**settings.logging.handlers.console),
240
+ _get_rotating_file_handler(**settings.logging.handlers.rotating_file),
128
241
  ]
129
242
  """Handlers attached to the module-level logger by default."""
130
243
 
131
- DEFAULT_LOGGING_CONFIG: dict[str, Any] = dict(
132
- level=DEFAULT_LEVEL,
133
- format=DEFAULT_FORMAT,
134
- handlers=DEFAULT_HANDLERS,
135
- encoding=DEFAULT_ENCODING,
136
- datefmt=DEFAULT_TIME_FORMAT,
137
- )
138
-
139
-
140
- def _get_logger(**kwargs: Any) -> logging.Logger:
141
- """Initialise and return the module-level root logger."""
142
- logging.basicConfig(**dict(ChainMap(kwargs, DEFAULT_LOGGING_CONFIG)))
143
- return logging.getLogger(APP_LIB_NAME)
144
-
145
244
 
146
245
  def get_logger(name: str, filename: str | None = None) -> logging.Logger:
147
246
  """Return a child logger configured with optional file output."""
148
247
  rv = logger.getChild(name)
149
248
  if filename is not None:
150
- handler = _get_file_handler(filename)
249
+ handler = _get_rotating_file_handler(filename)
151
250
  handler.setFormatter(DEFAULT_CHILD_FORMATTER)
152
251
  rv.addHandler(handler)
153
252
  return rv
@@ -243,8 +342,11 @@ def get_level(logger_: logging.Logger | None = None) -> Level:
243
342
 
244
343
 
245
344
  def set_level(level: Level, logger_: logging.Logger | None = None) -> None:
246
- """Set the log level on the provided logger (defaults to module logger)."""
345
+ """Set the log level on ``logger_`` and all currently attached handlers."""
247
346
  logger_ = logger_ or logger
347
+ level = level if isinstance(level, str | int) else level.value
348
+ for handler in logger_.handlers:
349
+ handler.setLevel(level)
248
350
  logger_.setLevel(level)
249
351
 
250
352
 
@@ -6,7 +6,6 @@ __all__ = (
6
6
  "TestStatus",
7
7
  "TestResult",
8
8
  "Regression",
9
- "RegressionTree",
10
9
  "RegressionProgress",
11
10
  )
12
11
 
@@ -15,5 +14,4 @@ from socx.regression.test import TestBase as TestBase
15
14
  from socx.regression.test import TestStatus as TestStatus
16
15
  from socx.regression.test import TestResult as TestResult
17
16
  from socx.regression.regression import Regression as Regression
18
- from socx.regression.regression import RegressionTree as RegressionTree
19
17
  from socx.regression.progress import RegressionProgress as RegressionProgress
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import anyio
4
- import anyio.lowlevel
5
4
  import logging
5
+ from typing import Any
6
+ from collections import ChainMap
6
7
 
7
8
  from rich.progress import (
8
9
  Progress as BaseProgress,
@@ -17,30 +18,23 @@ from rich.progress import (
17
18
  Task,
18
19
  )
19
20
 
20
- from socx.io.console import console
21
21
  from socx.regression.test import TestStatus
22
22
  from socx.regression.regression import Regression
23
23
 
24
+
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
 
27
28
  class PipelineProgress(BaseProgress):
28
- def __init__(self) -> None:
29
- super().__init__(
30
- *self.get_default_columns(),
31
- speed_estimate_period=10,
32
- redirect_stderr=True,
33
- redirect_stdout=True,
34
- console=console,
35
- )
29
+ def __init__(self, **kwargs: Any) -> None:
30
+ kwargs = dict(ChainMap(kwargs, dict(speed_estimate_period=10)))
31
+ super().__init__(*self.get_default_columns(), **kwargs)
36
32
 
37
33
  @classmethod
38
34
  def get_default_columns(cls) -> tuple[ProgressColumn, ...]:
39
35
  return (
40
36
  SpinnerColumn(),
41
- TextColumn(
42
- "[progress.description]{task.description}", justify="right"
43
- ),
37
+ TextColumn("[progress.description]{task.description}"),
44
38
  MofNCompleteColumn(),
45
39
  BarColumn(),
46
40
  TaskProgressColumn(),
@@ -56,11 +50,7 @@ class RegressionProgress:
56
50
  self.tasks = {}
57
51
  self.total = len(regression)
58
52
  self.regression = regression
59
- self.progress_map = {}
60
- # for child in self.regression.tests:
61
- # if isinstance(child, Regression):
62
- # self.progress_map[child.id] = PipelineProgress()
63
- self.progress_map[self.regression.id] = PipelineProgress()
53
+ self.progress = PipelineProgress()
64
54
 
65
55
  def __len__(self) -> int:
66
56
  """Get the total number of test items in a regression's progress."""
@@ -72,11 +62,20 @@ class RegressionProgress:
72
62
  exclude: set[str] | None = None,
73
63
  ) -> None:
74
64
  """Update progress tasks and flush log messages while running."""
75
- with (
76
- console.status("regression running..."),
77
- PipelineProgress() as progress,
78
- ):
79
- self.progress_map[self.regression.id] = progress
65
+ with self.progress:
66
+ if include is None and exclude is None:
67
+ async with anyio.create_task_group() as tg:
68
+ for obj in self.regression.tests:
69
+ tg.start_soon(
70
+ self.track_regression,
71
+ obj,
72
+ name=f"track_{obj.name}_progress",
73
+ )
74
+ tg.start_soon(
75
+ self.regression.start,
76
+ name=f"{self.regression.name}",
77
+ )
78
+ return
80
79
 
81
80
  async with anyio.create_task_group() as tg:
82
81
  for obj in self.regression.tests:
@@ -86,41 +85,31 @@ class RegressionProgress:
86
85
  if include is not None and obj.name not in include:
87
86
  continue
88
87
 
89
- if isinstance(obj, Regression):
90
- track_task_func = self.track_regression
91
- track_task_name = f"track_{obj.name}_progress"
88
+ if isinstance(obj, Regression) and not obj.started:
89
+ tg.start_soon(obj.start, name=obj.name)
92
90
  tg.start_soon(
93
- track_task_func, obj, name=track_task_name
91
+ self.track_regression,
92
+ obj,
93
+ name=f"track_{obj.name}_progress",
94
94
  )
95
95
 
96
- run_task_func = obj.start
97
- run_task_name = f"run_{obj.name}"
98
- tg.start_soon(run_task_func, name=run_task_name)
99
-
100
- if include is None and exclude is None:
101
- run_task_func = self.regression.start
102
- run_task_name = f"run_{self.regression.name}"
103
- tg.start_soon(run_task_func, name=run_task_name)
104
-
105
- del self.progress_map[self.regression.id]
106
-
107
96
  async def track_regression(self, regression: Regression) -> None:
108
- progress = self.progress_map[self.regression.id]
97
+ finished = 0
98
+ status = TestStatus.Idle
99
+ progress = self.progress
100
+
109
101
  if regression.id not in self.tasks:
110
102
  self.tasks[regression.id] = progress.add_task(
111
103
  total=len(regression),
112
104
  description=(
113
- f"[light_red]{regression.name}: {regression.status.name}"
105
+ f"[gray39]{self._get_task_tag(regression)}: "
106
+ f"{regression.status.name}"
114
107
  ),
115
108
  )
116
- await self._track_regression(regression)
117
109
 
118
- async def _track_regression(self, regression: Regression) -> None:
119
- finished = 0
120
- status = regression.status
121
- while True:
110
+ while not progress.finished:
122
111
  if regression.finished:
123
- await self.update_regression(regression, len(regression))
112
+ self.update_regression(regression, len(regression))
124
113
  break
125
114
 
126
115
  prev_status = status
@@ -133,39 +122,49 @@ class RegressionProgress:
133
122
  )
134
123
 
135
124
  if prev_finished != finished or prev_status != status:
136
- await self.update_regression(regression, finished)
125
+ self.update_regression(regression, finished)
137
126
 
138
- await anyio.sleep(0.1)
127
+ await anyio.sleep(0.5)
139
128
 
140
- async def advance_regression(self, regression: Regression, n: int) -> None:
141
- progress = self.progress_map.get(self.regression.id)
129
+ def advance_regression(self, regression: Regression, n: int) -> None:
130
+ progress = self.progress
142
131
  tid = self.tasks.get(regression.id)
143
132
  if progress is not None and tid is not None:
144
133
  task = progress.tasks[tid]
145
- await self.update_regression(regression, task.completed + n)
134
+ self.update_regression(regression, task.completed + n)
146
135
 
147
- async def update_regression(self, regression: Regression, n: int) -> None:
148
- progress = self.progress_map.get(self.regression.id)
136
+ def update_regression(self, regression: Regression, n: int) -> None:
137
+ progress = self.progress
149
138
  tid = self.tasks.get(regression.id)
150
139
  if progress is not None and tid is not None:
151
140
  task: Task = progress.tasks[tid]
152
- if task.completed >= n:
153
- return
154
141
 
155
- if task.completed < n:
156
- description = (
157
- f"[yellow]{regression.name}: {regression.status.name}"
142
+ if (
143
+ regression.is_idle()
144
+ or regression.is_pending()
145
+ or regression.is_suspended()
146
+ ):
147
+ task.description = (
148
+ f"[gray39]{self._get_task_tag(regression)}: "
149
+ f"{regression.status.name}"
158
150
  )
159
- else:
160
- description = (
161
- f"[green]{regression.name}: {regression.status.name}"
151
+ elif regression.is_running():
152
+ task.description = (
153
+ f"[yellow]{self._get_task_tag(regression)}: "
154
+ f"{regression.status.name}"
155
+ )
156
+ elif regression.finished:
157
+ task.description = (
158
+ f"[green]{self._get_task_tag(regression)}: "
159
+ f"{regression.status.name}"
160
+ )
161
+ elif regression.terminated:
162
+ task.description = (
163
+ f"[red]{self._get_task_tag(regression)}: "
164
+ f"{regression.status.name}"
162
165
  )
163
166
 
164
- progress.update(
165
- tid,
166
- completed=min(n, task.total),
167
- description=description,
168
- )
167
+ task.completed = min(n, task.total)
169
168
 
170
169
  def _count_statuses(
171
170
  self, regression: Regression, *statuses: TestStatus
@@ -174,3 +173,6 @@ class RegressionProgress:
174
173
  return sum(
175
174
  1 for test in regression.tests if test.status in statuses
176
175
  )
176
+
177
+ def _get_task_tag(self, regression: Regression) -> str:
178
+ return f"{regression.__class__.__name__}({regression.name})"