socx-cli 0.13.5__tar.gz → 0.13.7__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 (117) hide show
  1. {socx_cli-0.13.5 → socx_cli-0.13.7}/PKG-INFO +2 -2
  2. {socx_cli-0.13.5 → socx_cli-0.13.7}/pyproject.toml +2 -2
  3. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/_config.py +12 -57
  4. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/_settings.py +149 -21
  5. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/_paths.py +1 -1
  6. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/enums.py +7 -3
  7. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/singleton/singleton.py +1 -1
  8. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/progress.py +52 -53
  9. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/regression.py +191 -237
  10. socx_cli-0.13.7/socx/regression/test.py +583 -0
  11. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/cli.yaml +6 -1
  12. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/console.yaml +6 -1
  13. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/logging.yaml +1 -1
  14. socx_cli-0.13.7/socx/static/settings/regression.yaml +113 -0
  15. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/rich_click.yaml +7 -8
  16. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/_run.py +13 -16
  17. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/callbacks.py +1 -1
  18. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/tui.py +1 -1
  19. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/app.py +41 -29
  20. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/details.py +44 -24
  21. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/dialog.py +81 -3
  22. socx_cli-0.13.7/socx_tui/regression/mixins/configurable.py +22 -0
  23. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/widget.py +93 -46
  24. socx_cli-0.13.7/socx_tui/static/tcss/regression/app.tcss +224 -0
  25. socx_cli-0.13.5/socx/regression/test.py +0 -351
  26. socx_cli-0.13.5/socx/static/settings/regression.yaml +0 -40
  27. socx_cli-0.13.5/socx_tui/static/tcss/regression/app.tcss +0 -107
  28. {socx_cli-0.13.5 → socx_cli-0.13.7}/.gitignore +0 -0
  29. {socx_cli-0.13.5 → socx_cli-0.13.7}/LICENSE +0 -0
  30. {socx_cli-0.13.5 → socx_cli-0.13.7}/README.md +0 -0
  31. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/__init__.py +0 -0
  32. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/__main__.py +0 -0
  33. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/__init__.py +0 -0
  34. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/_cli.py +0 -0
  35. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/_jinja.py +0 -0
  36. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/callbacks.py +0 -0
  37. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/cfg.py +0 -0
  38. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/cli.py +0 -0
  39. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/params.py +0 -0
  40. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/cli/types.py +0 -0
  41. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/__init__.py +0 -0
  42. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/converters.py +0 -0
  43. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/encoders.py +0 -0
  44. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/formatters.py +0 -0
  45. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/serializers.py +0 -0
  46. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/config/validators.py +0 -0
  47. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/__init__.py +0 -0
  48. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/encoder.py +0 -0
  49. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/funcs.py +0 -0
  50. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/metadata.py +0 -0
  51. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/paths.py +0 -0
  52. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/__init__.py +0 -0
  53. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/git/__init__.py +0 -0
  54. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/git/git.py +0 -0
  55. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/git/manifest.py +0 -0
  56. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/plugin.py +0 -0
  57. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/schema/types.py +0 -0
  58. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/core/serializer.py +0 -0
  59. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/git/__init__.py +0 -0
  60. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/git/_git.py +0 -0
  61. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/git/_manifest.py +0 -0
  62. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/git/_ssh.py +0 -0
  63. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/io/__init__.py +0 -0
  64. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/io/console.py +0 -0
  65. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/io/decorators.py +0 -0
  66. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/io/log.py +0 -0
  67. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/__init__.py +0 -0
  68. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/mixins/__init__.py +0 -0
  69. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/mixins/proxy.py +0 -0
  70. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/mixins/uid.py +0 -0
  71. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/singleton/__init__.py +0 -0
  72. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/visitor/__init__.py +0 -0
  73. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/visitor/protocol.py +0 -0
  74. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/patterns/visitor/traversal.py +0 -0
  75. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/__init__.py +0 -0
  76. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/status.py +0 -0
  77. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/validator.py +0 -0
  78. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/regression/visitor.py +0 -0
  79. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/git.yaml +0 -0
  80. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/plugins.yaml +0 -0
  81. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/settings/settings.yaml +1 -1
  82. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/static/sql/socx.sql +0 -0
  83. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/utils/__init__.py +0 -0
  84. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx/utils/decorators.py +0 -0
  85. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/config/__init__.py +0 -0
  86. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/config/_config.py +0 -0
  87. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/config/edit.py +0 -0
  88. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/__init__.py +0 -0
  89. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/arguments.py +0 -0
  90. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/callbacks.py +0 -0
  91. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/cli.py +0 -0
  92. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/manifest.py +0 -0
  93. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/renderables.py +0 -0
  94. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/summary.py +0 -0
  95. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/git/utils.py +0 -0
  96. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/plugin/__init__.py +0 -0
  97. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/plugin/example.py +0 -0
  98. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/plugin/schema.py +0 -0
  99. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/__init__.py +0 -0
  100. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/cli.py +0 -0
  101. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/regression/run.py +0 -0
  102. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/version/__init__.py +0 -0
  103. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_plugins/version/__main__.py +0 -0
  104. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/__init__.py +0 -0
  105. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/__init__.py +0 -0
  106. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/__main__.py +0 -0
  107. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/bindings/__init__.py +0 -0
  108. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/bindings/vim/__init__.py +0 -0
  109. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/bindings/vim/mode.py +0 -0
  110. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/bindings/vim/vim.py +0 -0
  111. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/containers.py +0 -0
  112. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/mixins/__init__.py +0 -0
  113. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/mixins/composable.py +0 -0
  114. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/preview.py +0 -0
  115. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/table.py +0 -0
  116. {socx_cli-0.13.5 → socx_cli-0.13.7}/socx_tui/regression/tree.py +0 -0
  117. {socx_cli-0.13.5 → socx_cli-0.13.7}/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.5
3
+ Version: 0.13.7
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
@@ -28,7 +28,7 @@ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA
28
28
  Classifier: Topic :: Software Development
29
29
  Classifier: Topic :: Utilities
30
30
  Requires-Python: >=3.12
31
- Requires-Dist: anyio>=4.12.1
31
+ Requires-Dist: anyio[trio]>=4.12.1
32
32
  Requires-Dist: click
33
33
  Requires-Dist: copier~=9.11
34
34
  Requires-Dist: declare
@@ -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.5"
32
+ version = "0.13.7"
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" }]
@@ -82,7 +82,7 @@ dependencies = [
82
82
  "pydantic-extra-types>=2.11.0",
83
83
  "pydantic-settings>=2.12.0",
84
84
  "sqlmodel>=0.0.31",
85
- "anyio>=4.12.1",
85
+ "anyio[trio]>=4.12.1",
86
86
  ]
87
87
 
88
88
  [project.urls]
@@ -8,9 +8,7 @@ import contextvars as ctx
8
8
  from textwrap import dedent
9
9
  from typing import Any
10
10
  from pathlib import Path
11
- from collections import ChainMap
12
11
 
13
- from dynaconf.utils import ensure_a_list
14
12
  from werkzeug.local import LocalProxy
15
13
 
16
14
  from socx.config import converters
@@ -109,70 +107,27 @@ def get_settings(
109
107
  path: str | Path | None = None,
110
108
  /,
111
109
  user_overrides: bool = False,
112
- local_overrides: bool = False,
110
+ project_overrides: bool = False,
113
111
  extra_overrides: list[str | Path] | None = None,
114
112
  **kwargs: Any,
115
113
  ) -> Settings:
116
114
  """Create a configured ``Settings`` instance, including overrides."""
117
- from socx.core import paths
118
- from socx.core import metadata
119
- from socx.config.serializers import ModuleSerializer
120
-
121
- if isinstance(path, str):
122
- path = Path(path)
123
-
124
- includes = []
125
- settings_file = ensure_a_list(path or paths.APP_CONFIG_FILE)
126
-
127
- if user_overrides:
128
- includes.extend(get_user_config_files())
129
-
130
- if local_overrides:
131
- includes.extend(get_local_config_files())
132
-
133
- if extra_overrides:
134
- includes.extend(
135
- Path(p) if isinstance(p, str) else p for p in extra_overrides
136
- )
137
-
138
- root = includes[-1].parent if includes else Path.cwd()
139
- settings_kwargs = dict(
140
- preload=settings_file, root_path=root, settings_file=includes
141
- )
142
- kwargs = dict(
143
- ChainMap(
144
- kwargs,
145
- settings_kwargs,
146
- ModuleSerializer.serialize(paths),
147
- ModuleSerializer.serialize(metadata),
148
- ),
115
+ return Settings(
116
+ user_overrides=user_overrides,
117
+ project_overrides=project_overrides,
118
+ **kwargs,
149
119
  )
150
- rv = Settings(**kwargs)
151
- return rv
152
120
 
153
121
 
154
122
  converters.init()
155
-
156
123
  _tokens = []
157
-
158
124
  _settings_cv: ctx.ContextVar[Settings] = ctx.ContextVar("settings")
159
-
160
- _default_settings: Settings = get_settings()
161
-
162
- try:
163
- _user_settings: Settings = get_settings(user_overrides=True)
164
- except Exception:
165
- _user_settings = _default_settings
166
-
167
- try:
168
- _local_settings: Settings = get_settings(
169
- user_overrides=True, local_overrides=True
170
- )
171
- except Exception:
172
- _local_settings = _default_settings
173
- _local_settings.update(_user_settings)
174
-
175
- settings: SettingsProxy = LocalProxy( # type: ignore[assignment]
125
+ _default_settings: Settings = Settings(
126
+ project_overrides=False, user_overrides=False
127
+ )
128
+ _user_settings: Settings = Settings(project_overrides=False)
129
+ _local_settings: Settings = Settings()
130
+ settings: SettingsProxy = LocalProxy(
176
131
  _settings_cv,
177
132
  unbound_message=dedent("""
178
133
  Working outside of application context.
@@ -180,7 +135,7 @@ settings: SettingsProxy = LocalProxy( # type: ignore[assignment]
180
135
  Attempted to use functionality that expected a current application to
181
136
  be set. To solve this, set up an app context.
182
137
  """),
183
- )
138
+ ) # ty:ignore[invalid-assignment]
184
139
 
185
140
  if "--no-configure" in sys.argv or "-NC" in sys.argv:
186
141
  _tokens.append(_settings_cv.set(_default_settings))
@@ -9,14 +9,16 @@ from collections import ChainMap
9
9
  from collections.abc import Callable
10
10
 
11
11
  from dynaconf import LazySettings, get_history
12
+ from dynaconf.loaders import env_loader
12
13
  from dynaconf.base import SourceMetadata, ensure_a_list
13
14
  from dynaconf.utils.boxing import DynaBox
14
15
  from dynaconf.utils.inspect import get_debug_info, _get_data_by_key
15
16
  from dynaconf.utils.parse_conf import unparse_conf_data
16
17
  from pydantic_core import to_jsonable_python
17
18
 
18
- from socx.config.serializers import SettingsSerializer
19
-
19
+ from socx.core import paths, metadata
20
+ from socx.core.enums import SettingsFormat
21
+ from socx.config.serializers import SettingsSerializer, ModuleSerializer
20
22
 
21
23
  logger = logging.getLogger(__name__)
22
24
 
@@ -29,8 +31,10 @@ VT = TypeVar("VT")
29
31
  SETTINGS_DEFAULTS: dict[str, Any] = dict(
30
32
  env="default",
31
33
  envvar="SOCX_SETTINGS_PATH",
34
+ preload=[paths.APP_CONFIG_FILE],
32
35
  encoding="utf-8",
33
36
  auto_cast=True,
37
+ root_path=paths.PROJECT_ROOT_DIR,
34
38
  load_dotenv=True,
35
39
  environments=False,
36
40
  dotted_lookup=True,
@@ -46,15 +50,51 @@ SETTINGS_DEFAULTS: dict[str, Any] = dict(
46
50
 
47
51
 
48
52
  class Settings(LazySettings):
49
- """Singleton settings instance of loaded `socx` configurations."""
53
+ """Application settings class."""
50
54
 
51
55
  def __init__(self, wrapped=None, **kwargs: Any) -> None:
52
- kwargs = dict(ChainMap(kwargs, SETTINGS_DEFAULTS))
56
+ default_kwargs = SETTINGS_DEFAULTS
57
+ if kwargs.get("project_overrides", True):
58
+ project_files = self.get_project_config_files()
59
+ if project_files:
60
+ default_kwargs["settings_file"] = [project_files[-1]]
61
+ kwargs = dict(
62
+ ChainMap(
63
+ kwargs,
64
+ default_kwargs,
65
+ ModuleSerializer.serialize(paths),
66
+ ModuleSerializer.serialize(metadata),
67
+ )
68
+ )
53
69
  LazySettings.__init__(self, wrapped=wrapped, **kwargs)
54
- if hasattr(self, "dynaconf_include"):
55
- for file in self.dynaconf_include:
56
- if file not in self.loaded_files:
57
- self.load_file(path=file)
70
+
71
+ def _setup(self) -> None:
72
+ super()._setup()
73
+ self._load_overrides()
74
+
75
+ def _load_overrides(self) -> None:
76
+ user_overrides = self.get("user_overrides", True)
77
+ project_overrides = self.get("project_overrides", True)
78
+ includes = self.get_settings_overrides(
79
+ user_overrides=user_overrides, project_overrides=project_overrides
80
+ )
81
+
82
+ if includes:
83
+ for file in includes:
84
+ logger.debug("loading settings overrides from: '%s'")
85
+ try:
86
+ self.load_file(file)
87
+ except Exception:
88
+ logger.exception(
89
+ "Failed to load settings file: '%s'" % str(file)
90
+ )
91
+ continue
92
+ else:
93
+ logger.debug("loaded settings overrides from: '%s'")
94
+
95
+ last_loader = self.loaders and self.loaders[-1]
96
+ if last_loader is env_loader:
97
+ env_loader.load(self._store)
58
98
 
59
99
  def __contains__(self, key):
60
100
  return self.exists(key) or (
@@ -77,19 +117,6 @@ class Settings(LazySettings):
77
117
  """Get the root path of the current settings instance."""
78
118
  return Path(self._root_path)
79
119
 
80
- @root.setter
81
- def root(self, value: str | Path) -> None:
82
- if isinstance(value, str):
83
- value = Path(value)
84
-
85
- if value.is_file() and value.exists():
86
- self.set(
87
- "SETTINGS_FILE_FOR_DYNACONF",
88
- value,
89
- loader_identifier="init_settings_module",
90
- )
91
- self.reload()
92
-
93
120
  @property
94
121
  def history(self) -> tuple[dict[str, Any], ...]:
95
122
  """Get the history of this instance.
@@ -274,6 +301,107 @@ class Settings(LazySettings):
274
301
  rv = cls._transform(rv, fn, skip_values=skip_values)
275
302
  return rv
276
303
 
304
+ @classmethod
305
+ def get_settings_overrides(
306
+ cls, user_overrides: bool = True, project_overrides: bool = True
307
+ ) -> list[Path]:
308
+ """Get a list of project settings file overrides."""
309
+ overrides = []
310
+ if user_overrides:
311
+ overrides.extend(cls.get_user_config_files())
312
+ if project_overrides:
313
+ overrides.extend(cls.get_project_config_files())
314
+ return overrides
315
+
316
+ @classmethod
317
+ def get_project_config_files(cls) -> list[Path]:
318
+ """Get a list of local settings file paths found in parent folders.
319
+
320
+ Description:
321
+ ------------
322
+ After initialization, `socx` searches parent directories for any file
323
+ named '.socx.<ext>' where <ext> is any file extension supported by the
324
+ `socx` configuration system.
325
+
326
+ Local configuration overrides are any files named '.socx.<ext>' which
327
+ found in any of the parent directories of the current working
328
+ directory, where <ext> may be any one of: '.yaml', '.yml', '.json', or
329
+ '.toml'.
330
+
331
+ For reference, it works similar to git:
332
+
333
+ 1. first, default app configurations are loaded to initialize to
334
+ app's core functionality.
335
+
336
+ 2. second, global user configurations are loaded from the user's
337
+ config directory, determined according to the 'XDG Base Directory
338
+ Specification'
339
+ (https://specifications.freedesktop.org/basedir-spec).
340
+
341
+ 3. last, a search for local configuration files is done, matching
342
+ (and loading) any configuration files named '.socx.yaml' found in
343
+ any of the parent directories starting the search at the current
344
+ working
345
+ directory.
346
+
347
+ Returns:
348
+ --------
349
+ An ordered list of `Path` objects pointing at configuration files to
350
+ be loaded in that exact order to preserve the described overrides
351
+ order.
352
+
353
+ """
354
+ from socx.core.paths import LOCAL_CONFIG_FILE, LOCAL_CONFIG_FILENAME
355
+
356
+ rv = []
357
+ for parent in LOCAL_CONFIG_FILE.parents:
358
+ cfg_file = parent / LOCAL_CONFIG_FILENAME
359
+ for member in SettingsFormat:
360
+ rv.extend(
361
+ cfg_file.with_suffix(ext)
362
+ for ext in member.extensions
363
+ if cfg_file.with_suffix(ext).is_file()
364
+ )
365
+ return rv
366
+
367
+ @classmethod
368
+ def get_user_config_files(cls) -> list[Path]:
369
+ """Get a list of all local config files found in parent folders.
370
+
371
+ Description:
372
+ ------------
373
+ User configuration files are any configuration files who's format is
374
+ supported and are located under the XDG_CONFIG_HOME directory.
375
+
376
+ Local configuration overrides are any files named '.socx.yaml' which
377
+ found in any of the parent directories of the current working
378
+ directory.
379
+
380
+ For reference, it works similar to git:
381
+
382
+ 1. first, default app configurations are loaded to initialize to
383
+ app's core functionality.
384
+
385
+ 2. second, global user configurations are loaded from the user's
386
+ config directory, determined according to the 'XDG Base Directory
387
+ Specification'
388
+ (https://specifications.freedesktop.org/basedir-spec).
389
+
390
+ 3. last, a search for local configuration files is done, matching
391
+ (and loading) any configuration files named '.socx.yaml' found in
392
+ any of the parent directories starting the search at the current
393
+ working directory.
394
+
395
+ Returns:
396
+ --------
397
+ An ordered list of `Path` objects pointing at configuration files to be
398
+ loaded in that exact order to preserve the described overrides order.
399
+
400
+ """
401
+ from socx.core.paths import USER_CONFIG_FILE
402
+
403
+ return [USER_CONFIG_FILE] if USER_CONFIG_FILE.exists() else []
404
+
277
405
  @classmethod
278
406
  def _transform(
279
407
  cls,
@@ -36,7 +36,7 @@ def find_project_root(start_path: Path) -> Path:
36
36
 
37
37
  if not start_path.exists():
38
38
  err = f"Path '{start_path}' does not exist."
39
- raise OSError(err)
39
+ raise FileNotFoundError(err)
40
40
 
41
41
  if start_path.is_file():
42
42
  start_path = start_path.parent
@@ -18,15 +18,19 @@ class AutoNumber(int, enum.ReprEnum):
18
18
 
19
19
 
20
20
  class SettingsFormat(AutoNumber):
21
- Ini = ".ini"
22
21
  Json = ".json"
23
22
  Yaml = ".yaml", ".yml"
24
23
  Toml = ".toml"
24
+ Ini = ".ini"
25
25
  Python = ".python"
26
26
 
27
27
  def __init__(self, extension: str, *extensions: str) -> None:
28
- self.extension = extension
29
- self.extensions = [extension, *extensions]
28
+ self.extensions = {extension, *extensions}
29
+
30
+ @classmethod
31
+ @cache
32
+ def all_extensions(cls) -> set[str]:
33
+ return {extension for member in cls for extension in member.extensions}
30
34
 
31
35
  @classmethod
32
36
  @cache
@@ -18,7 +18,7 @@ class _SingletonMeta(type):
18
18
  return cls._instances[cls]
19
19
 
20
20
 
21
- class Singleton(_SingletonMeta("SingletonMeta", (object,), {})):
21
+ class Singleton(_SingletonMeta("_SingletonMeta", (object,), {})):
22
22
  """Mixin class for creating singleton classes.
23
23
 
24
24
  Extending this class enforces the singleton pattern on the subclass.
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import anyio
4
3
  import logging
5
4
  from typing import Any
6
- from collections import ChainMap
7
5
 
6
+ import anyio
7
+ from anyio.abc import TaskStatus
8
8
  from rich.progress import (
9
9
  Progress as BaseProgress,
10
10
  ProgressColumn,
@@ -27,7 +27,6 @@ logger = logging.getLogger(__name__)
27
27
 
28
28
  class PipelineProgress(BaseProgress):
29
29
  def __init__(self, **kwargs: Any) -> None:
30
- kwargs = dict(ChainMap(kwargs, dict(speed_estimate_period=10)))
31
30
  super().__init__(*self.get_default_columns(), **kwargs)
32
31
 
33
32
  @classmethod
@@ -60,23 +59,10 @@ class RegressionProgress:
60
59
  self,
61
60
  include: set[str] | None = None,
62
61
  exclude: set[str] | None = None,
62
+ limiter: anyio.CapacityLimiter | None = None,
63
63
  ) -> None:
64
- """Update progress tasks and flush log messages while running."""
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
79
-
64
+ self.progress.start()
65
+ try:
80
66
  async with anyio.create_task_group() as tg:
81
67
  for obj in self.regression.tests:
82
68
  if exclude is not None and obj.name in exclude:
@@ -86,54 +72,62 @@ class RegressionProgress:
86
72
  continue
87
73
 
88
74
  if isinstance(obj, Regression) and not obj.started:
89
- tg.start_soon(obj.start, name=obj.name)
90
75
  tg.start_soon(
91
76
  self.track_regression,
92
77
  obj,
93
78
  name=f"track_{obj.name}_progress",
94
79
  )
80
+ finally:
81
+ self.progress.stop()
95
82
 
96
- async def track_regression(self, regression: Regression) -> None:
97
- finished = 0
98
- status = TestStatus.Idle
99
- progress = self.progress
83
+ async def track_regression(
84
+ self,
85
+ regression: Regression,
86
+ limiter: anyio.CapacityLimiter | None = None,
87
+ task_status: TaskStatus = anyio.TASK_STATUS_IGNORED,
88
+ ) -> None:
100
89
 
101
- if regression.id not in self.tasks:
102
- self.tasks[regression.id] = progress.add_task(
103
- total=len(regression),
104
- description=(
105
- f"[gray39]{self._get_task_tag(regression)}: "
106
- f"{regression.status.name}"
107
- ),
108
- )
90
+ async with anyio.create_task_group() as tg:
91
+ if regression.id not in self.tasks:
92
+ self.tasks[regression.id] = self.progress.add_task(
93
+ total=len(regression),
94
+ description=(
95
+ f"[gray39]{self._get_task_tag(regression)}: "
96
+ f"{regression.status.name}"
97
+ ),
98
+ )
99
+ tg.start_soon(self._track_regression, regression)
100
+ await tg.start(regression.start, limiter)
101
+ task_status.started()
109
102
 
110
- while not progress.finished:
111
- if regression.finished:
112
- self.update_regression(regression, len(regression))
113
- break
103
+ async def _track_regression(self, regression: Regression) -> None:
104
+ task = self.progress.tasks[self.tasks[regression.id]]
114
105
 
115
- prev_status = status
116
- prev_finished = finished
117
- status = regression.status
118
- finished = self._count_statuses(
106
+ while True:
107
+ finished = await self._count_statuses(
119
108
  regression,
120
109
  TestStatus.Finished,
121
110
  TestStatus.Terminated,
122
111
  )
123
112
 
124
- if prev_finished != finished or prev_status != status:
125
- self.update_regression(regression, finished)
113
+ if finished != task.completed:
114
+ await self.update_regression(regression, finished)
115
+
116
+ if finished == len(regression):
117
+ break
126
118
 
127
- await anyio.sleep(0.5)
119
+ await anyio.sleep(0.05)
128
120
 
129
- def advance_regression(self, regression: Regression, n: int) -> None:
121
+ async def advance_regression(
122
+ self, regression: Regression, n: int = 1
123
+ ) -> None:
130
124
  progress = self.progress
131
125
  tid = self.tasks.get(regression.id)
132
126
  if progress is not None and tid is not None:
133
127
  task = progress.tasks[tid]
134
- self.update_regression(regression, task.completed + n)
128
+ await self.update_regression(regression, task.completed + n)
135
129
 
136
- def update_regression(self, regression: Regression, n: int) -> None:
130
+ async def update_regression(self, regression: Regression, n: int) -> None:
137
131
  progress = self.progress
138
132
  tid = self.tasks.get(regression.id)
139
133
  if progress is not None and tid is not None:
@@ -144,32 +138,37 @@ class RegressionProgress:
144
138
  or regression.is_pending()
145
139
  or regression.is_suspended()
146
140
  ):
147
- task.description = (
141
+ description = (
148
142
  f"[gray39]{self._get_task_tag(regression)}: "
149
143
  f"{regression.status.name}"
150
144
  )
151
145
  elif regression.is_running():
152
- task.description = (
146
+ description = (
153
147
  f"[yellow]{self._get_task_tag(regression)}: "
154
148
  f"{regression.status.name}"
155
149
  )
156
150
  elif regression.finished:
157
- task.description = (
151
+ description = (
158
152
  f"[green]{self._get_task_tag(regression)}: "
159
153
  f"{regression.status.name}"
160
154
  )
161
155
  elif regression.terminated:
162
- task.description = (
156
+ description = (
163
157
  f"[red]{self._get_task_tag(regression)}: "
164
158
  f"{regression.status.name}"
165
159
  )
166
160
 
167
- task.completed = min(n, task.total)
161
+ completed = min(n, task.total)
162
+ progress.update(
163
+ task.id,
164
+ completed=completed,
165
+ description=description,
166
+ )
168
167
 
169
- def _count_statuses(
168
+ async def _count_statuses(
170
169
  self, regression: Regression, *statuses: TestStatus
171
170
  ) -> int:
172
- with self.regression.lock:
171
+ async with self.regression.mutex:
173
172
  return sum(
174
173
  1 for test in regression.tests if test.status in statuses
175
174
  )