backlogops 1.0__tar.gz → 1.2__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 (68) hide show
  1. {backlogops-1.0 → backlogops-1.2}/PKG-INFO +7 -7
  2. {backlogops-1.0 → backlogops-1.2}/README_pypi.md +2 -2
  3. {backlogops-1.0 → backlogops-1.2}/backlogops.egg-info/PKG-INFO +7 -7
  4. {backlogops-1.0 → backlogops-1.2}/backlogops.egg-info/requires.txt +4 -4
  5. {backlogops-1.0 → backlogops-1.2}/setup.py +5 -5
  6. {backlogops-1.0 → backlogops-1.2}/src/backlogops/available_teams_config.py +28 -17
  7. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_ops_config.py +62 -17
  8. {backlogops-1.0 → backlogops-1.2}/src/backlogops/config_editing.py +30 -10
  9. {backlogops-1.0 → backlogops-1.2}/src/backlogops/io_config.py +34 -18
  10. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_io_config.py +12 -6
  11. {backlogops-1.0 → backlogops-1.2}/LICENSE.txt +0 -0
  12. {backlogops-1.0 → backlogops-1.2}/backlogops.egg-info/SOURCES.txt +0 -0
  13. {backlogops-1.0 → backlogops-1.2}/backlogops.egg-info/dependency_links.txt +0 -0
  14. {backlogops-1.0 → backlogops-1.2}/backlogops.egg-info/top_level.txt +0 -0
  15. {backlogops-1.0 → backlogops-1.2}/pyproject.toml +0 -0
  16. {backlogops-1.0 → backlogops-1.2}/setup.cfg +0 -0
  17. {backlogops-1.0 → backlogops-1.2}/src/backlogops/__init__.py +0 -0
  18. {backlogops-1.0 → backlogops-1.2}/src/backlogops/apply_format_rules.py +0 -0
  19. {backlogops-1.0 → backlogops-1.2}/src/backlogops/available_teams.py +0 -0
  20. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog.py +0 -0
  21. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_helpers.py +0 -0
  22. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_in_release_order.py +0 -0
  23. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_ops_wizard.py +0 -0
  24. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_releases.py +0 -0
  25. {backlogops-1.0 → backlogops-1.2}/src/backlogops/backlog_releases_io.py +0 -0
  26. {backlogops-1.0 → backlogops-1.2}/src/backlogops/blo_version_reporter.py +0 -0
  27. {backlogops-1.0 → backlogops-1.2}/src/backlogops/config_descriptions.py +0 -0
  28. {backlogops-1.0 → backlogops-1.2}/src/backlogops/config_file_io.py +0 -0
  29. {backlogops-1.0 → backlogops-1.2}/src/backlogops/date_ranges.py +0 -0
  30. {backlogops-1.0 → backlogops-1.2}/src/backlogops/demo_backlog.py +0 -0
  31. {backlogops-1.0 → backlogops-1.2}/src/backlogops/estimate_ready_date.py +0 -0
  32. {backlogops-1.0 → backlogops-1.2}/src/backlogops/format_rules.py +0 -0
  33. {backlogops-1.0 → backlogops-1.2}/src/backlogops/io_preset_wizard.py +0 -0
  34. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_connect.py +0 -0
  35. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_order_releases.py +0 -0
  36. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_rank_backlog.py +0 -0
  37. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_rank_by_keys.py +0 -0
  38. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_rank_move_keys.py +0 -0
  39. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_read.py +0 -0
  40. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_rename_releases.py +0 -0
  41. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_search.py +0 -0
  42. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_token.py +0 -0
  43. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_update_backlog.py +0 -0
  44. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_update_releases.py +0 -0
  45. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_wizard.py +0 -0
  46. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_write.py +0 -0
  47. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_write_fields.py +0 -0
  48. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_write_format.py +0 -0
  49. {backlogops-1.0 → backlogops-1.2}/src/backlogops/jira_write_releases.py +0 -0
  50. {backlogops-1.0 → backlogops-1.2}/src/backlogops/key_list_io.py +0 -0
  51. {backlogops-1.0 → backlogops-1.2}/src/backlogops/levels.py +0 -0
  52. {backlogops-1.0 → backlogops-1.2}/src/backlogops/move_keys_first.py +0 -0
  53. {backlogops-1.0 → backlogops-1.2}/src/backlogops/name_list_io.py +0 -0
  54. {backlogops-1.0 → backlogops-1.2}/src/backlogops/no_text_io.py +0 -0
  55. {backlogops-1.0 → backlogops-1.2}/src/backlogops/order_by_dependencies.py +0 -0
  56. {backlogops-1.0 → backlogops-1.2}/src/backlogops/person.py +0 -0
  57. {backlogops-1.0 → backlogops-1.2}/src/backlogops/py.typed +0 -0
  58. {backlogops-1.0 → backlogops-1.2}/src/backlogops/release_backlog_updates.py +0 -0
  59. {backlogops-1.0 → backlogops-1.2}/src/backlogops/release_change_io.py +0 -0
  60. {backlogops-1.0 → backlogops-1.2}/src/backlogops/releases.py +0 -0
  61. {backlogops-1.0 → backlogops-1.2}/src/backlogops/rename_list_io.py +0 -0
  62. {backlogops-1.0 → backlogops-1.2}/src/backlogops/table_create.py +0 -0
  63. {backlogops-1.0 → backlogops-1.2}/src/backlogops/table_rows.py +0 -0
  64. {backlogops-1.0 → backlogops-1.2}/src/backlogops/team.py +0 -0
  65. {backlogops-1.0 → backlogops-1.2}/src/backlogops/wizard_forms.py +0 -0
  66. {backlogops-1.0 → backlogops-1.2}/src/backlogops/wizard_helpers.py +0 -0
  67. {backlogops-1.0 → backlogops-1.2}/src/backlogops/wizard_navigator.py +0 -0
  68. {backlogops-1.0 → backlogops-1.2}/src/backlogops/work_hours.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -21,14 +21,14 @@ Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.12
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE.txt
24
- Requires-Dist: config-as-json>=1.5
25
- Requires-Dist: cryptography>=50.0.0
24
+ Requires-Dist: config-as-json>=1.7
25
+ Requires-Dist: cryptography>=50.0.1
26
26
  Requires-Dist: jira[cli,opt]>=3.10.5
27
27
  Requires-Dist: tableio>=1.1
28
- Requires-Dist: tableio-cfg-json>=1.3
28
+ Requires-Dist: tableio-cfg-json>=1.4
29
29
  Requires-Dist: wizard-ui-bridge[textual]>=1.3
30
30
  Requires-Dist: versionreporter>=0.4
31
- Requires-Dist: edit-cfg-json==0.0.4
31
+ Requires-Dist: edit-cfg-json>=0.2.0
32
32
  Dynamic: author
33
33
  Dynamic: license-file
34
34
  Dynamic: requires-dist
@@ -325,10 +325,10 @@ For the full set of public names see the API documentation linked above.
325
325
 
326
326
  ## Test summary
327
327
 
328
- - Test result: 2234 passed, 1 deselected in 45s
328
+ - Test result: 2309 passed, 1 deselected in 58s
329
329
  - No flake8 warnings.
330
330
  - No mypy errors found.
331
331
  - No pylint warnings.
332
332
  - No python layout warnings.
333
- - Built version(s): 1.0
333
+ - Built version(s): 1.2
334
334
  - Build and test using Python 3.14.7
@@ -289,10 +289,10 @@ For the full set of public names see the API documentation linked above.
289
289
 
290
290
  ## Test summary
291
291
 
292
- - Test result: 2234 passed, 1 deselected in 45s
292
+ - Test result: 2309 passed, 1 deselected in 58s
293
293
  - No flake8 warnings.
294
294
  - No mypy errors found.
295
295
  - No pylint warnings.
296
296
  - No python layout warnings.
297
- - Built version(s): 1.0
297
+ - Built version(s): 1.2
298
298
  - Build and test using Python 3.14.7
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backlogops
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: Library with backlog operations.
5
5
  Author: Tom Björkholm
6
6
  Author-email: Tom Björkholm <klausuler_linnet0q@icloud.com>
@@ -21,14 +21,14 @@ Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.12
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE.txt
24
- Requires-Dist: config-as-json>=1.5
25
- Requires-Dist: cryptography>=50.0.0
24
+ Requires-Dist: config-as-json>=1.7
25
+ Requires-Dist: cryptography>=50.0.1
26
26
  Requires-Dist: jira[cli,opt]>=3.10.5
27
27
  Requires-Dist: tableio>=1.1
28
- Requires-Dist: tableio-cfg-json>=1.3
28
+ Requires-Dist: tableio-cfg-json>=1.4
29
29
  Requires-Dist: wizard-ui-bridge[textual]>=1.3
30
30
  Requires-Dist: versionreporter>=0.4
31
- Requires-Dist: edit-cfg-json==0.0.4
31
+ Requires-Dist: edit-cfg-json>=0.2.0
32
32
  Dynamic: author
33
33
  Dynamic: license-file
34
34
  Dynamic: requires-dist
@@ -325,10 +325,10 @@ For the full set of public names see the API documentation linked above.
325
325
 
326
326
  ## Test summary
327
327
 
328
- - Test result: 2234 passed, 1 deselected in 45s
328
+ - Test result: 2309 passed, 1 deselected in 58s
329
329
  - No flake8 warnings.
330
330
  - No mypy errors found.
331
331
  - No pylint warnings.
332
332
  - No python layout warnings.
333
- - Built version(s): 1.0
333
+ - Built version(s): 1.2
334
334
  - Build and test using Python 3.14.7
@@ -1,8 +1,8 @@
1
- config-as-json>=1.5
2
- cryptography>=50.0.0
1
+ config-as-json>=1.7
2
+ cryptography>=50.0.1
3
3
  jira[cli,opt]>=3.10.5
4
4
  tableio>=1.1
5
- tableio-cfg-json>=1.3
5
+ tableio-cfg-json>=1.4
6
6
  wizard-ui-bridge[textual]>=1.3
7
7
  versionreporter>=0.4
8
- edit-cfg-json==0.0.4
8
+ edit-cfg-json>=0.2.0
@@ -5,7 +5,7 @@ from setuptools import setup
5
5
 
6
6
  setup(
7
7
  name='backlogops',
8
- version='1.0',
8
+ version='1.2',
9
9
  description='Library with backlog operations.',
10
10
  author='Tom Björkholm',
11
11
  author_email='klausuler_linnet0q@icloud.com',
@@ -14,13 +14,13 @@ setup(
14
14
  package_dir={'backlogops': 'src/backlogops'},
15
15
  package_data={'backlogops': ['src/py.typed']},
16
16
  install_requires=[
17
- 'config-as-json >= 1.5',
18
- 'cryptography >= 50.0.0',
17
+ 'config-as-json >= 1.7',
18
+ 'cryptography >= 50.0.1',
19
19
  'jira[cli,opt] >= 3.10.5',
20
20
  'tableio >= 1.1',
21
- 'tableio-cfg-json >= 1.3',
21
+ 'tableio-cfg-json >= 1.4',
22
22
  'wizard-ui-bridge[textual] >= 1.3',
23
23
  'versionreporter >= 0.4',
24
- 'edit-cfg-json == 0.0.4'
24
+ 'edit-cfg-json >= 0.2.0',
25
25
  ]
26
26
  )
@@ -117,16 +117,20 @@ class _BridgeConfig(Config):
117
117
  return {}
118
118
 
119
119
  def __init__(self, from_json_data_text: Optional[str],
120
- from_json_filename: Optional[PathOrStr],
121
- stderr_file: TextIO) -> None:
120
+ from_json_filename: Optional[PathOrStr], stderr_file: TextIO,
121
+ *, member_name: Optional[str] = None) -> None:
122
122
  """Run the Config lifecycle for a bridge instance.
123
123
 
124
124
  Each bridge first creates its data class attributes, then calls
125
125
  this constructor to read JSON, apply defaults, and validate.
126
+ ``member_name`` is the path for reaching this object from the top
127
+ level configuration, so that a diagnostic about a value inside it
128
+ names the whole path; it is None for a bridge that is a whole
129
+ configuration file of its own.
126
130
  """
127
131
  Config.__init__(self, from_json_data_text=from_json_data_text,
128
132
  from_json_filename=from_json_filename,
129
- stderr_file=stderr_file)
133
+ stderr_file=stderr_file, member_name=member_name)
130
134
 
131
135
  @staticmethod
132
136
  def _consistency() -> WholeConfigValidationStep:
@@ -154,12 +158,13 @@ class FteExceptionConfig(FteException, _BridgeConfig):
154
158
 
155
159
  def __init__(self, from_json_data_text: Optional[str] = None,
156
160
  from_json_filename: Optional[PathOrStr] = None,
157
- stderr_file: TextIO = sys.stderr) -> None:
161
+ stderr_file: TextIO = sys.stderr, *,
162
+ member_name: Optional[str] = None) -> None:
158
163
  """Create placeholder defaults, then read one FTE exception."""
159
164
  FteException.__init__(self, start_date=date(2000, 1, 1),
160
165
  end_date=date(2000, 1, 1), fte=1.0)
161
166
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
162
- stderr_file)
167
+ stderr_file, member_name=member_name)
163
168
 
164
169
  @override
165
170
  def get_validation_plan(self, stderr_file: TextIO) -> ValidationPlan:
@@ -179,13 +184,14 @@ class ExceptionWorkHoursConfig(ExceptionWorkHours, _BridgeConfig):
179
184
 
180
185
  def __init__(self, from_json_data_text: Optional[str] = None,
181
186
  from_json_filename: Optional[PathOrStr] = None,
182
- stderr_file: TextIO = sys.stderr) -> None:
187
+ stderr_file: TextIO = sys.stderr, *,
188
+ member_name: Optional[str] = None) -> None:
183
189
  """Create placeholder defaults, then read one work-hours exception."""
184
190
  ExceptionWorkHours.__init__(self, start_date=date(2000, 1, 1),
185
191
  end_date=date(2000, 1, 1),
186
192
  hours_per_day=0.0)
187
193
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
188
- stderr_file)
194
+ stderr_file, member_name=member_name)
189
195
 
190
196
  @override
191
197
  def get_validation_plan(self, stderr_file: TextIO) -> ValidationPlan:
@@ -205,11 +211,12 @@ class MembershipConfig(Membership, _BridgeConfig):
205
211
 
206
212
  def __init__(self, from_json_data_text: Optional[str] = None,
207
213
  from_json_filename: Optional[PathOrStr] = None,
208
- stderr_file: TextIO = sys.stderr) -> None:
214
+ stderr_file: TextIO = sys.stderr, *,
215
+ member_name: Optional[str] = None) -> None:
209
216
  """Create placeholder defaults, then read one membership."""
210
217
  Membership.__init__(self, person_name='person')
211
218
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
212
- stderr_file)
219
+ stderr_file, member_name=member_name)
213
220
 
214
221
  @override
215
222
  def nested_configs(self) -> NestedConfigs:
@@ -241,12 +248,13 @@ class TeamConfig(Team, _BridgeConfig):
241
248
 
242
249
  def __init__(self, from_json_data_text: Optional[str] = None,
243
250
  from_json_filename: Optional[PathOrStr] = None,
244
- stderr_file: TextIO = sys.stderr) -> None:
251
+ stderr_file: TextIO = sys.stderr, *,
252
+ member_name: Optional[str] = None) -> None:
245
253
  """Create placeholder defaults, then read one team."""
246
254
  Team.__init__(self, name='team', velocity=0.0, sum_fte_at_velocity=1.0,
247
255
  sprint_length=1)
248
256
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
249
- stderr_file)
257
+ stderr_file, member_name=member_name)
250
258
 
251
259
  @override
252
260
  def nested_configs(self) -> NestedConfigs:
@@ -266,11 +274,12 @@ class PersonConfig(Person, _BridgeConfig):
266
274
 
267
275
  def __init__(self, from_json_data_text: Optional[str] = None,
268
276
  from_json_filename: Optional[PathOrStr] = None,
269
- stderr_file: TextIO = sys.stderr) -> None:
277
+ stderr_file: TextIO = sys.stderr, *,
278
+ member_name: Optional[str] = None) -> None:
270
279
  """Create placeholder defaults, then read one person."""
271
280
  Person.__init__(self, name='person')
272
281
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
273
- stderr_file)
282
+ stderr_file, member_name=member_name)
274
283
 
275
284
  @override
276
285
  def nested_configs(self) -> NestedConfigs:
@@ -291,12 +300,13 @@ class CompanyWorkHoursConfig(CompanyWorkHours, _BridgeConfig):
291
300
 
292
301
  def __init__(self, from_json_data_text: Optional[str] = None,
293
302
  from_json_filename: Optional[PathOrStr] = None,
294
- stderr_file: TextIO = sys.stderr) -> None:
303
+ stderr_file: TextIO = sys.stderr, *,
304
+ member_name: Optional[str] = None) -> None:
295
305
  """Create defaults, then read the company work hours."""
296
306
  CompanyWorkHours.__init__(self)
297
307
  self._unchecked_dicts = ['work_hours']
298
308
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
299
- stderr_file)
309
+ stderr_file, member_name=member_name)
300
310
 
301
311
  @override
302
312
  def nested_configs(self) -> NestedConfigs:
@@ -328,7 +338,8 @@ class AvailableTeamsConfig(AvailableTeams, _BridgeConfig):
328
338
  def __init__(self, *, neutral: Optional[AvailableTeams] = None,
329
339
  from_json_data_text: Optional[str] = None,
330
340
  from_json_filename: Optional[PathOrStr] = None,
331
- stderr_file: TextIO = sys.stderr) -> None:
341
+ stderr_file: TextIO = sys.stderr,
342
+ member_name: Optional[str] = None) -> None:
332
343
  """Create the bridge from a neutral workforce or from JSON.
333
344
 
334
345
  ``AvailableTeams.__init__`` is intentionally not invoked because
@@ -340,7 +351,7 @@ class AvailableTeamsConfig(AvailableTeams, _BridgeConfig):
340
351
  neutral = AvailableTeams(persons={}, teams=[])
341
352
  Config.copy_initial_data(neutral, self)
342
353
  _BridgeConfig.__init__(self, from_json_data_text, from_json_filename,
343
- stderr_file)
354
+ stderr_file, member_name=member_name)
344
355
 
345
356
  @override
346
357
  def nested_configs(self) -> NestedConfigs:
@@ -17,7 +17,10 @@ item levels:
17
17
  :class:`backlogops.jira_io_config.JiraIOConfig`;
18
18
  * ``levels`` is the optional list of backlog item levels. It is omitted
19
19
  from the file while it is ``None``; :meth:`BacklogOpsConfig.get_levels`
20
- then falls back to :data:`backlogops.levels.DEFAULT_LEVELS`.
20
+ then falls back to :data:`backlogops.levels.DEFAULT_LEVELS`. An empty
21
+ list is refused, because it would mean no levels at all where the
22
+ omitted member means the built-in ones, and the configuration editor
23
+ offers giving that member an empty list.
21
24
 
22
25
  Earlier file versions stored the workforce members (``persons``,
23
26
  ``teams`` and ``company_work_hours``) at the top level next to the
@@ -36,7 +39,8 @@ from config_as_json import CallingWholeConfigValidator, Config, \
36
39
  ConfigAutoChangeHook, ConfigNesting, ConfigNestingKind, ConfigPath, \
37
40
  JsonType, MemberValidationStep, MemberValidator, NestedConfigs, \
38
41
  PathOrStr, ReadOldConfiguration, RocfKeyMove, SerializeConverter, \
39
- SerializeConverters, ValidationPlan, WholeConfigValidationStep
42
+ SerializeConverters, ValidationPlan, WholeConfigValidationStep, \
43
+ member_path
40
44
  from backlogops.available_teams import AvailableTeams
41
45
  from backlogops.available_teams_config import AvailableTeamsConfig
42
46
  from backlogops.backlog import Status
@@ -72,19 +76,32 @@ def _as_str_list(name: str, value: object, stderr_file: TextIO) -> list[str]:
72
76
  return result
73
77
 
74
78
 
75
- def _level_from_dict(data: dict[str, object], stderr_file: TextIO) -> Level:
76
- """Return one Level from its JSON object, checking the field shape."""
79
+ def _level_from_dict(name: str, data: dict[str, object],
80
+ stderr_file: TextIO) -> Level:
81
+ """Return one Level from its JSON object, checking the field shape.
82
+
83
+ Args:
84
+ name: Reported path of the level itself, such as ``levels[2]``,
85
+ which each field name is appended to.
86
+ data: The JSON object holding the fields of one level.
87
+ stderr_file: The file to report errors to.
88
+
89
+ Returns:
90
+ The level the JSON object describes.
91
+ """
77
92
  unknown = sorted(set(data) - {'level', 'name', 'aliases'})
78
93
  if unknown:
79
- report_bad_value('level', data, f'unknown level fields {unknown}',
94
+ report_bad_value(name, data, f'unknown level fields {unknown}',
80
95
  stderr_file, 'Level')
81
96
  if 'level' not in data or 'name' not in data:
82
- report_bad_value('level', data, 'requires "level" and "name"',
97
+ report_bad_value(name, data, 'requires "level" and "name"',
83
98
  stderr_file, 'Level')
84
- return Level(level=_as_int('level', data['level'], stderr_file),
85
- name=_as_str('name', data['name'], stderr_file),
86
- aliases=_as_str_list('aliases', data.get('aliases', []),
87
- stderr_file))
99
+ return Level(level=_as_int(member_path(name, 'level'), data['level'],
100
+ stderr_file),
101
+ name=_as_str(member_path(name, 'name'), data['name'],
102
+ stderr_file),
103
+ aliases=_as_str_list(member_path(name, 'aliases'),
104
+ data.get('aliases', []), stderr_file))
88
105
 
89
106
 
90
107
  def _level_from_obj(name: str, value: object, stderr_file: TextIO) -> Level:
@@ -93,7 +110,13 @@ def _level_from_obj(name: str, value: object, stderr_file: TextIO) -> Level:
93
110
  return value
94
111
  if not isinstance(value, dict):
95
112
  report_wrong_type(name, value, dict, stderr_file, 'Level')
96
- return _level_from_dict(value, stderr_file)
113
+ return _level_from_dict(name, value, stderr_file)
114
+
115
+
116
+ _NO_LEVELS = ('no levels at all; a configuration that leaves the levels out '
117
+ 'uses the built-in ones, so take the member away rather than '
118
+ 'emptying it')
119
+ """Why an empty list of levels is not a configuration with no levels."""
97
120
 
98
121
 
99
122
  # pylint: disable-next=too-few-public-methods
@@ -104,13 +127,23 @@ class _LevelsMember(MemberValidator):
104
127
  def validate_member(self, config: Config, member_name: str,
105
128
  member_value: object,
106
129
  stderr_file: TextIO = sys.stderr) -> Optional[object]:
107
- """Return the levels as a list of ``Level``, or ``None``."""
130
+ """Return the levels as a list of ``Level``, or ``None``.
131
+
132
+ Raises:
133
+ TypeError: The member holds something that is no list of
134
+ levels.
135
+ ValueError: The member holds an empty list, which is not a
136
+ configuration of the levels; leaving the member out is.
137
+ """
108
138
  _ = config
109
139
  if member_value is None:
110
140
  return None
111
141
  if not isinstance(member_value, list):
112
142
  report_wrong_type(member_name, member_value, list, stderr_file,
113
143
  'BacklogOps config')
144
+ if not member_value:
145
+ report_bad_value(member_name, member_value, _NO_LEVELS,
146
+ stderr_file, 'BacklogOps config')
114
147
  return [_level_from_obj(f'{member_name}[{index}]', element,
115
148
  stderr_file)
116
149
  for index, element in enumerate(member_value)]
@@ -173,11 +206,15 @@ DEF_STATUS_INPUT_MAP: dict[str, Status] = {
173
206
  class BacklogOpsConfig(Config): # pylint: disable=too-many-instance-attributes
174
207
  """Top-level backlog-ops configuration stored as config-as-json."""
175
208
 
209
+ # The library contract fixes five of these; the workforce is the one
210
+ # argument of this application, and every one of them is keyword only.
211
+ # pylint: disable-next=too-many-arguments
176
212
  def __init__(self, *, available_teams: Optional[AvailableTeams] = None,
177
213
  from_json_data_text: Optional[str] = None,
178
214
  from_json_filename: Optional[PathOrStr] = None,
179
215
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
180
- stderr_file: TextIO = sys.stderr) -> None:
216
+ stderr_file: TextIO = sys.stderr,
217
+ member_name: Optional[str] = None) -> None:
181
218
  """Create defaults, or read the configuration from JSON.
182
219
 
183
220
  The supplied workforce establishes the schema of the nested
@@ -185,7 +222,11 @@ class BacklogOpsConfig(Config): # pylint: disable=too-many-instance-attributes
185
222
  into an :class:`AvailableTeamsConfig`. The presets default to
186
223
  empty maps and the levels default to ``None`` (use the defaults).
187
224
  The ``auto_ch_hook`` is notified when an old file needed
188
- backward-compatible normalization while reading.
225
+ backward-compatible normalization while reading. The default
226
+ nested sections are created under the path of the member holding
227
+ each of them, so that a diagnostic names the whole path even
228
+ before any file is read; ``member_name`` is None for a whole
229
+ configuration file, which is a member of nothing.
189
230
  """
190
231
  self.available_teams: AvailableTeams = (
191
232
  AvailableTeams(persons={}, teams=[]) if available_teams is None
@@ -193,14 +234,18 @@ class BacklogOpsConfig(Config): # pylint: disable=too-many-instance-attributes
193
234
  self.input_configs: dict[str, InputFormatConfig] = {}
194
235
  self.output_configs: dict[str, OutputFormatConfig] = {}
195
236
  self.gui_display: GuiDisplayConfig = GuiDisplayConfig(
196
- stderr_file=stderr_file)
237
+ stderr_file=stderr_file,
238
+ member_name=member_path(member_name, 'gui_display'))
197
239
  self.status_input_map: dict[str, Status] = DEF_STATUS_INPUT_MAP
198
- self.jira: JiraIOConfig = JiraIOConfig(stderr_file=stderr_file)
240
+ self.jira: JiraIOConfig = JiraIOConfig(
241
+ stderr_file=stderr_file,
242
+ member_name=member_path(member_name, 'jira'))
199
243
  self.levels: Optional[list[Level]] = None
200
244
  self._unchecked_dicts = ['status_input_map']
201
245
  Config.__init__(self, from_json_data_text=from_json_data_text,
202
246
  from_json_filename=from_json_filename,
203
- auto_ch_hook=auto_ch_hook, stderr_file=stderr_file)
247
+ auto_ch_hook=auto_ch_hook, stderr_file=stderr_file,
248
+ member_name=member_name)
204
249
 
205
250
  @override
206
251
  def _get_read_old_config(self) -> ReadOldConfiguration:
@@ -27,19 +27,39 @@ toolkit mounts the editor itself — ``edit_cfg_json_tk.TkEditorPanel`` in a
27
27
  window of the application, ``edit_cfg_json_textual.edit`` in a terminal of
28
28
  its own — and hands those two answers to it.
29
29
 
30
- Three things the editor cannot do are worth knowing before it is offered
31
- instead of the wizard, and all three are of ``edit_cfg_json`` itself rather
32
- than of this configuration:
33
-
34
- * A dict whose keys the application validates for itself cannot gain or
35
- lose a key. That is the status map, the column-name maps of a preset and
36
- of the display, and the Jira column and issue-type maps: their values are
37
- editable, and a new entry is the wizard's to create.
38
- * A member left out of the file has no row, so the levels can be edited
39
- only in a configuration that already states them.
30
+ How many things a member holds is the editor's to change as well, so a
31
+ named preset, a Jira connection, a person, a team, a membership, an
32
+ exception, a level and an entry of any of the maps can each be added beside
33
+ the ones that are there, taken out again, and moved within a list. A new
34
+ element is never invented: it is an object of the class the declaration
35
+ names, a copy of what the container holds now, or the empty value that the
36
+ declared type of the member says an element is. That is what the first of
37
+ the notes below follows from.
38
+
39
+ Four things are worth knowing before the editor is offered instead of the
40
+ wizard:
41
+
42
+ * One entry of the Jira column maps and of the issue-type maps is a map of
43
+ its own, which the class does not name a type for, so there is no pattern
44
+ for one until the file holds one to copy: an empty map of those offers
45
+ nothing and says why below itself. Every other container can be given its
46
+ first element, because the class names the type of one or its declared
47
+ type says what an element is.
48
+ * The levels keep a row in a configuration that leaves them out, because a
49
+ member the class omits is still a member. What that row offers is an
50
+ empty list, which is not what leaving the levels out means and is
51
+ refused as such: the row's other control puts the member back to holding
52
+ nothing, which is the built-in levels. Writing the levels the first time
53
+ is the wizard's.
40
54
  * A person is keyed by their own name in lower case, so renaming one means
41
55
  adding an entry under the new key and removing the old one; editing the
42
56
  name alone leaves a configuration the class refuses.
57
+ * The editor reads the declarations of a class and not the insides of its
58
+ validators, so it offers a change that this library then refuses where
59
+ only a validator knows better: a week day taken out of the company
60
+ schedule, or a level that loses its name, because a level is a plain
61
+ object in the file rather than a nested configuration class. What the
62
+ class said is shown at the member it is about.
43
63
  """
44
64
 
45
65
  # Copyright (c) 2026, Tom Björkholm
@@ -48,7 +48,7 @@ from config_as_json import Config, ConfigAutoChangeHook, ConfigNesting, \
48
48
  ConfigNestingKind, ConfigPath, InvalidConfiguration, \
49
49
  MemberValidationStep, MemberValidator, NestedConfigs, ParseConverter, \
50
50
  PathOrStr, ReadOldConfiguration, RocfKeyMove, RocfValueMigration, \
51
- RocfValueWrite, ValidationPlan
51
+ RocfValueWrite, ValidationPlan, member_path
52
52
  from tableio import Capabilities, FileAccess, access_capabilities
53
53
  from tableio_cfg_json import TioJsonConfig, tio_json_config_default
54
54
  from backlogops.backlog import Status
@@ -195,23 +195,25 @@ def _capabilities(file_access: FileAccess, stderr_file: TextIO
195
195
 
196
196
 
197
197
  def _tio_default(file_access: FileAccess, format_name: Optional[str] = None,
198
- stderr_file: TextIO = sys.stderr) -> TioJsonConfig:
198
+ stderr_file: TextIO = sys.stderr, *,
199
+ member_name: Optional[str] = None) -> TioJsonConfig:
199
200
  """Return a default TableIO config for a format and file access."""
200
201
  return tio_json_config_default(
201
202
  capabilities=_capabilities(file_access, stderr_file),
202
203
  file_access=file_access, format_name=format_name,
203
- stderr_file=stderr_file)
204
+ stderr_file=stderr_file, member_name=member_name)
204
205
 
205
206
 
206
207
  def _tio_from_json(file_access: FileAccess, from_json_data_text: Optional[str],
207
208
  from_json_filename: Optional[PathOrStr],
208
- stderr_file: TextIO) -> TioJsonConfig:
209
+ stderr_file: TextIO,
210
+ member_name: Optional[str]) -> TioJsonConfig:
209
211
  """Return a TableIO config read from JSON for a file access mode."""
210
212
  return TioJsonConfig(capabilities=_capabilities(file_access, stderr_file),
211
213
  file_access=file_access,
212
214
  from_json_data_text=from_json_data_text,
213
215
  from_json_filename=from_json_filename,
214
- stderr_file=stderr_file)
216
+ stderr_file=stderr_file, member_name=member_name)
215
217
 
216
218
 
217
219
  class _FormatConfig(Config):
@@ -230,22 +232,31 @@ class _FormatConfig(Config):
230
232
  def __init__(self, from_json_data_text: Optional[str] = None,
231
233
  from_json_filename: Optional[PathOrStr] = None,
232
234
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
233
- stderr_file: TextIO = sys.stderr) -> None:
234
- """Create default settings or read them from a JSON source."""
235
- self.tableio: TioJsonConfig = _tio_default(self._FILE_ACCESS,
236
- stderr_file=stderr_file)
235
+ stderr_file: TextIO = sys.stderr, *,
236
+ member_name: Optional[str] = None) -> None:
237
+ """Create default settings or read them from a JSON source.
238
+
239
+ The default TableIO endpoint is created under the path of the
240
+ ``tableio`` member of this endpoint, so that a TableIO diagnostic
241
+ names which preset it is about even before any file is read.
242
+ """
243
+ self.tableio: TioJsonConfig = _tio_default(
244
+ self._FILE_ACCESS, stderr_file=stderr_file,
245
+ member_name=member_path(member_name, 'tableio'))
237
246
  self._unchecked_dicts = list(self._MAP_NAMES) + \
238
247
  list(self._UNCHECKED_EXTRA)
239
248
  Config.__init__(self, from_json_data_text=from_json_data_text,
240
249
  from_json_filename=from_json_filename,
241
- auto_ch_hook=auto_ch_hook, stderr_file=stderr_file)
250
+ auto_ch_hook=auto_ch_hook, stderr_file=stderr_file,
251
+ member_name=member_name)
242
252
 
243
253
  def _tio_factory(self, *, from_json_data_text: Optional[str] = None,
244
254
  from_json_filename: Optional[PathOrStr] = None,
245
- stderr_file: TextIO = sys.stderr) -> TioJsonConfig:
255
+ stderr_file: TextIO = sys.stderr,
256
+ member_name: Optional[str] = None) -> TioJsonConfig:
246
257
  """Construct the nested TableIO config from JSON when reading."""
247
258
  return _tio_from_json(self._FILE_ACCESS, from_json_data_text,
248
- from_json_filename, stderr_file)
259
+ from_json_filename, stderr_file, member_name)
249
260
 
250
261
  def _map_validator(self) -> MemberValidator:
251
262
  """Return the validator applied to each column-name map member."""
@@ -334,7 +345,8 @@ class InputFormatConfig(_FormatConfig):
334
345
  def __init__(self, from_json_data_text: Optional[str] = None,
335
346
  from_json_filename: Optional[PathOrStr] = None,
336
347
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
337
- stderr_file: TextIO = sys.stderr) -> None:
348
+ stderr_file: TextIO = sys.stderr, *,
349
+ member_name: Optional[str] = None) -> None:
338
350
  """Create the input map defaults, then run the shared constructor."""
339
351
  self.backlog_to_internal = {}
340
352
  self.release_to_internal = {}
@@ -342,7 +354,8 @@ class InputFormatConfig(_FormatConfig):
342
354
  _FormatConfig.__init__(self, from_json_data_text=from_json_data_text,
343
355
  from_json_filename=from_json_filename,
344
356
  auto_ch_hook=auto_ch_hook,
345
- stderr_file=stderr_file)
357
+ stderr_file=stderr_file,
358
+ member_name=member_name)
346
359
 
347
360
  @override
348
361
  def _get_read_old_config(self) -> ReadOldConfiguration:
@@ -381,7 +394,8 @@ class OutputFormatConfig(_FormatConfig):
381
394
  def __init__(self, from_json_data_text: Optional[str] = None,
382
395
  from_json_filename: Optional[PathOrStr] = None,
383
396
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
384
- stderr_file: TextIO = sys.stderr) -> None:
397
+ stderr_file: TextIO = sys.stderr, *,
398
+ member_name: Optional[str] = None) -> None:
385
399
  """Create the output defaults, then run the shared constructor."""
386
400
  self.backlog_to_external = {}
387
401
  self.release_to_external = {}
@@ -389,7 +403,8 @@ class OutputFormatConfig(_FormatConfig):
389
403
  _FormatConfig.__init__(self, from_json_data_text=from_json_data_text,
390
404
  from_json_filename=from_json_filename,
391
405
  auto_ch_hook=auto_ch_hook,
392
- stderr_file=stderr_file)
406
+ stderr_file=stderr_file,
407
+ member_name=member_name)
393
408
 
394
409
  @override
395
410
  def parse_converters(self) -> dict[str, ParseConverter]:
@@ -450,7 +465,8 @@ class GuiDisplayConfig(Config):
450
465
 
451
466
  def __init__(self, from_json_data_text: Optional[str] = None,
452
467
  from_json_filename: Optional[PathOrStr] = None,
453
- stderr_file: TextIO = sys.stderr) -> None:
468
+ stderr_file: TextIO = sys.stderr, *,
469
+ member_name: Optional[str] = None) -> None:
454
470
  """Create the display defaults, then read them from JSON."""
455
471
  self.backlog_to_external = {}
456
472
  self.release_to_external = {}
@@ -458,7 +474,7 @@ class GuiDisplayConfig(Config):
458
474
  self._unchecked_dicts = ['backlog_to_external', 'release_to_external']
459
475
  Config.__init__(self, from_json_data_text=from_json_data_text,
460
476
  from_json_filename=from_json_filename,
461
- stderr_file=stderr_file)
477
+ stderr_file=stderr_file, member_name=member_name)
462
478
 
463
479
  @override
464
480
  def parse_converters(self) -> dict[str, ParseConverter]:
@@ -420,7 +420,8 @@ class JiraConnectConfig(Config):
420
420
  def __init__(self, from_json_data_text: Optional[str] = None,
421
421
  from_json_filename: Optional[PathOrStr] = None,
422
422
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
423
- stderr_file: TextIO = sys.stderr) -> None:
423
+ stderr_file: TextIO = sys.stderr, *,
424
+ member_name: Optional[str] = None) -> None:
424
425
  """Create connection defaults, then read them from JSON."""
425
426
  self.jira_type = JiraType.CLOUD
426
427
  self.base_url = ''
@@ -431,7 +432,8 @@ class JiraConnectConfig(Config):
431
432
  self._cached_token: Optional[str] = None
432
433
  Config.__init__(self, from_json_data_text=from_json_data_text,
433
434
  from_json_filename=from_json_filename,
434
- auto_ch_hook=auto_ch_hook, stderr_file=stderr_file)
435
+ auto_ch_hook=auto_ch_hook, stderr_file=stderr_file,
436
+ member_name=member_name)
435
437
 
436
438
  @override
437
439
  def _omit_none_from_json(self) -> list[str]:
@@ -600,7 +602,8 @@ class JiraPreset(Config):
600
602
  def __init__(self, from_json_data_text: Optional[str] = None,
601
603
  from_json_filename: Optional[PathOrStr] = None,
602
604
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
603
- stderr_file: TextIO = sys.stderr) -> None:
605
+ stderr_file: TextIO = sys.stderr, *,
606
+ member_name: Optional[str] = None) -> None:
604
607
  """Create preset defaults, then read them from JSON."""
605
608
  self.connection_name = ''
606
609
  self.backlog_column_map_name = ''
@@ -611,7 +614,8 @@ class JiraPreset(Config):
611
614
  self.def_filter = ''
612
615
  Config.__init__(self, from_json_data_text=from_json_data_text,
613
616
  from_json_filename=from_json_filename,
614
- auto_ch_hook=auto_ch_hook, stderr_file=stderr_file)
617
+ auto_ch_hook=auto_ch_hook, stderr_file=stderr_file,
618
+ member_name=member_name)
615
619
 
616
620
  @override
617
621
  def _get_read_old_config(self) -> ReadOldConfiguration:
@@ -695,7 +699,8 @@ class JiraIOConfig(Config):
695
699
  def __init__(self, from_json_data_text: Optional[str] = None,
696
700
  from_json_filename: Optional[PathOrStr] = None,
697
701
  auto_ch_hook: Optional[ConfigAutoChangeHook] = None,
698
- stderr_file: TextIO = sys.stderr) -> None:
702
+ stderr_file: TextIO = sys.stderr, *,
703
+ member_name: Optional[str] = None) -> None:
699
704
  """Create empty defaults, then read the jira configuration."""
700
705
  self.connections: dict[str, JiraConnectConfig] = {}
701
706
  self.backlog_column_maps: dict[str, JiraColumnMap] = {}
@@ -706,7 +711,8 @@ class JiraIOConfig(Config):
706
711
  'issue_type_maps']
707
712
  Config.__init__(self, from_json_data_text=from_json_data_text,
708
713
  from_json_filename=from_json_filename,
709
- auto_ch_hook=auto_ch_hook, stderr_file=stderr_file)
714
+ auto_ch_hook=auto_ch_hook, stderr_file=stderr_file,
715
+ member_name=member_name)
710
716
 
711
717
  @override
712
718
  def _get_read_old_config(self) -> ReadOldConfiguration:
File without changes
File without changes
File without changes