cmdkit 2.7.6__tar.gz → 2.8.0__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.
@@ -1,28 +1,31 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cmdkit
3
- Version: 2.7.6
3
+ Version: 2.8.0
4
4
  Summary: A command-line utility toolkit for Python.
5
- Home-page: https://cmdkit.readthedocs.io
6
- License: Apache-2.0
7
5
  Keywords: command-line,utility,toolkit
8
6
  Author: Geoffrey Lentner
9
- Author-email: glentner@purdue.edu
10
- Requires-Python: >=3.10,<4.0
7
+ Author-email: Geoffrey Lentner <glentner@purdue.edu>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
11
10
  Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
13
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19
- Provides-Extra: toml
20
- Provides-Extra: yaml
21
- Requires-Dist: pyyaml (>=6.0.1,<7.0.0) ; extra == "yaml"
22
- Requires-Dist: tomli (>=2.1.0,<3.0.0) ; extra == "toml"
23
- Requires-Dist: tomli-w (>=1.1.0,<2.0.0) ; extra == "toml"
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: License :: OSI Approved :: Apache Software License
20
+ Requires-Dist: tomli>=2.1.0,<3.0.0 ; extra == 'toml'
21
+ Requires-Dist: tomli-w>=1.1.0,<2.0.0 ; extra == 'toml'
22
+ Requires-Dist: pyyaml>=6.0.1,<7.0.0 ; extra == 'yaml'
23
+ Requires-Python: >=3.9, <4.0
24
+ Project-URL: Homepage, https://cmdkit.readthedocs.io
24
25
  Project-URL: Documentation, https://cmdkit.readthedocs.io
25
26
  Project-URL: Repository, https://github.com/glentner/cmdkit
27
+ Provides-Extra: toml
28
+ Provides-Extra: yaml
26
29
  Description-Content-Type: text/x-rst
27
30
 
28
31
  CmdKit
@@ -91,4 +94,3 @@ Contributions are welcome in the form of suggestions for additional features, pu
91
94
  new features or bug fixes, etc. If you find bugs or have questions, open an *Issue* here. If and
92
95
  when the project grows, a code of conduct will be provided along side a more comprehensive set of
93
96
  guidelines for contributing; until then, just be nice.
94
-
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "cmdkit"
3
+ version = "2.8.0"
4
+ description = "A command-line utility toolkit for Python."
5
+ readme = "README.rst"
6
+ license = "Apache-2.0"
7
+ license-files = ["LICENSE"]
8
+ keywords = [
9
+ "command-line",
10
+ "utility",
11
+ "toolkit",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "License :: OSI Approved :: Apache Software License",
24
+ ]
25
+ requires-python = ">=3.9,<4.0"
26
+ dependencies = []
27
+
28
+ [[project.authors]]
29
+ name = "Geoffrey Lentner"
30
+ email = "glentner@purdue.edu"
31
+
32
+ [project.optional-dependencies]
33
+ toml = [
34
+ "tomli>=2.1.0,<3.0.0",
35
+ "tomli-w>=1.1.0,<2.0.0",
36
+ ]
37
+ yaml = ["pyyaml>=6.0.1,<7.0.0"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://cmdkit.readthedocs.io"
41
+ Documentation = "https://cmdkit.readthedocs.io"
42
+ Repository = "https://github.com/glentner/cmdkit"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "tomli>=2.1.0,<3.0.0",
47
+ "tomli-w>=1.1.0,<2.0.0",
48
+ "pyyaml>=6.0.1,<7.0.0",
49
+ "pytest>=7.4.4,<8.0.0",
50
+ "hypothesis>=6.92.6,<7.0.0",
51
+ "sphinx-autobuild>=2021.3.14,<2022.0.0",
52
+ ]
53
+ docs = [
54
+ "sphinx>=7.2.6,<8.0.0",
55
+ "sphinx-sitemap>=2.5.1,<3.0.0",
56
+ "sphinx-autobuild>=2021.3.14,<2022.0.0",
57
+ "sphinx-copybutton>=0.5.2,<0.6.0",
58
+ "sphinxext-opengraph>=0.9.1,<0.10.0",
59
+ "sphinxcontrib-details-directive>=0.1.0,<0.2.0",
60
+ "sphinx-toolbox>=3.5.0,<4.0.0",
61
+ "pydata-sphinx-theme>=0.15.1,<0.16.0",
62
+ "enum-tools[sphinx]>=0.10.0,<0.11.0",
63
+ "docutils>=0.21.1,<0.22.0",
64
+ "setuptools<82",
65
+ ]
66
+
67
+ [build-system]
68
+ requires = ["uv_build>=0.13.7,<0.14.0"]
69
+ build-backend = "uv_build"
70
+
71
+ [tool.pytest.ini_options]
72
+ addopts = ["--strict-markers"]
73
+ markers = [
74
+ "unit: Unit tests are short, interface driven tests on discrete components.",
75
+ "integration: Integration tests are often longer and deal with the interaction between systems.",
76
+ "parameterize: Place holder for parameterized tests (not a real type).",
77
+ ]
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "cmdkit"
3
+ version = "2.8.0"
4
+ description = "A command-line utility toolkit for Python."
5
+ readme = "README.rst"
6
+ license = "Apache-2.0"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Geoffrey Lentner", email = "glentner@purdue.edu" },
10
+ ]
11
+ keywords = ["command-line", "utility", "toolkit"]
12
+ classifiers = [
13
+ "Development Status :: 5 - Production/Stable",
14
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "License :: OSI Approved :: Apache Software License",
23
+ ]
24
+ requires-python = ">=3.9,<4.0"
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ toml = ["tomli>=2.1.0,<3.0.0", "tomli-w>=1.1.0,<2.0.0"]
29
+ yaml = ["pyyaml>=6.0.1,<7.0.0"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://cmdkit.readthedocs.io"
33
+ Documentation = "https://cmdkit.readthedocs.io"
34
+ Repository = "https://github.com/glentner/cmdkit"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "tomli>=2.1.0,<3.0.0",
39
+ "tomli-w>=1.1.0,<2.0.0",
40
+ "pyyaml>=6.0.1,<7.0.0",
41
+ "pytest>=7.4.4,<8.0.0",
42
+ "hypothesis>=6.92.6,<7.0.0",
43
+ "sphinx-autobuild>=2021.3.14,<2022.0.0",
44
+ ]
45
+ docs = [
46
+ "sphinx>=7.2.6,<8.0.0",
47
+ "sphinx-sitemap>=2.5.1,<3.0.0",
48
+ "sphinx-autobuild>=2021.3.14,<2022.0.0",
49
+ "sphinx-copybutton>=0.5.2,<0.6.0",
50
+ "sphinxext-opengraph>=0.9.1,<0.10.0",
51
+ "sphinxcontrib-details-directive>=0.1.0,<0.2.0",
52
+ "sphinx-toolbox>=3.5.0,<4.0.0",
53
+ "pydata-sphinx-theme>=0.15.1,<0.16.0",
54
+ "enum-tools[sphinx]>=0.10.0,<0.11.0",
55
+ "docutils>=0.21.1,<0.22.0",
56
+ # sphinxcontrib-details-directive imports pkg_resources, which setuptools 82 removed;
57
+ # a uv environment holds only what is declared, so setuptools is named here
58
+ "setuptools<82",
59
+ ]
60
+
61
+ [build-system]
62
+ requires = ["uv_build>=0.13.7,<0.14.0"]
63
+ build-backend = "uv_build"
64
+
65
+ [tool.pytest.ini_options]
66
+ addopts = ["--strict-markers", ]
67
+ markers = [
68
+ "unit: Unit tests are short, interface driven tests on discrete components.",
69
+ "integration: Integration tests are often longer and deal with the interaction between systems.",
70
+ "parameterize: Place holder for parameterized tests (not a real type).",
71
+ ]
@@ -30,4 +30,7 @@ __all__ = [
30
30
  __version__ = get_version('cmdkit')
31
31
 
32
32
  # null-handler for library interface
33
- Logger.with_name(__name__).addHandler(NullHandler())
33
+ _null_handler = NullHandler()
34
+ _null_handler._cmdkit_library_handler = True
35
+ Logger.with_name(__name__).addHandler(_null_handler)
36
+ del _null_handler
@@ -10,6 +10,7 @@ from typing import List, Dict, Callable, NamedTuple, Type, TypeVar
10
10
 
11
11
  # standard libs
12
12
  import abc
13
+ import sys
13
14
  import logging
14
15
 
15
16
  # internal libs
@@ -27,6 +28,41 @@ TAppGrp = TypeVar('TAppGrp', bound='ApplicationGroup')
27
28
  log = logging.getLogger(__name__)
28
29
 
29
30
 
31
+ def _hidden_by_null_handler(logger: logging.Logger, level: int, msg: object) -> bool:
32
+ """
33
+ True if the :class:`logging.NullHandler` cmdkit attaches to its logger at import is the only
34
+ reason Python's last-resort handler did not print a record with `msg` at `level` from `logger`.
35
+
36
+ This follows :meth:`logging.Logger.handle` and :meth:`logging.Logger.callHandlers` in order,
37
+ and is false wherever Python would print nothing anyway: the logger is not enabled for `level`
38
+ (a disabled logger, a logger level above `level`, or :func:`logging.disable`), the logger's
39
+ own filters reject the record, or :data:`logging.lastResort` is unset or above `level`.
40
+ Otherwise the logger and its ancestors are searched for as long as they propagate. Any handler
41
+ found, whatever its level, other than cmdkit's own (recognised by the mark set on it where the
42
+ package attaches it) means the application has configured logging: false. That includes a
43
+ :class:`logging.NullHandler` the application adds to the `cmdkit` logger itself to silence it.
44
+ If no handler is found at all, Python's last resort has already printed the record: also
45
+ false, so the message appears only once.
46
+ """
47
+ if not logger.isEnabledFor(level):
48
+ return False
49
+ if not logger.filter(logger.makeRecord(logger.name, level, '(unknown file)', 0, msg, (), None)):
50
+ return False
51
+ if logging.lastResort is None or level < logging.lastResort.level:
52
+ return False
53
+ found = False
54
+ current = logger
55
+ while current is not None:
56
+ for handler in current.handlers:
57
+ if getattr(handler, '_cmdkit_library_handler', False) is not True:
58
+ return False
59
+ found = True
60
+ if not current.propagate:
61
+ break
62
+ current = current.parent
63
+ return found
64
+
65
+
30
66
  # NOTE: In next major release this will be made into an Enum
31
67
  class ExitStatus(NamedTuple):
32
68
  """Collection of exit status values."""
@@ -96,8 +132,22 @@ class Application(abc.ABC):
96
132
  Entry-point for application.
97
133
  This is a try-except block that handles standard scenarios.
98
134
 
135
+ An :class:`~cmdkit.cli.ArgumentError` is passed to :data:`~Application.log_critical`
136
+ and ``exit_status.bad_argument`` is returned. If ``log_critical`` is the default and the
137
+ :class:`~logging.NullHandler` cmdkit attaches to its own logger is the only reason
138
+ Python's last-resort handler did not print the record (no handler of the application's
139
+ is found, and the ``cmdkit.app`` logger is neither silenced nor filtering the record
140
+ out), the message is also written as is to ``sys.stderr``, followed by a newline.
141
+ Where no handler is found at all, Python's last resort prints the record itself, and
142
+ that line stands alone. A :class:`~logging.NullHandler` the application adds to the
143
+ ``cmdkit`` logger is a handler of the application's, and keeps the message silent.
144
+ Without a ``sys.stderr`` (it is ``None``) the message is skipped, and a failure to
145
+ write it (a closed stream or a broken pipe) is ignored, so ``main`` still returns
146
+ ``exit_status.bad_argument``.
147
+
99
148
  See Also:
100
149
  :data:`~Application.exceptions`
150
+ :data:`~Application.log_critical`
101
151
  """
102
152
  try:
103
153
  if not cmdline:
@@ -124,6 +174,12 @@ class Application(abc.ABC):
124
174
 
125
175
  except ArgumentError as error:
126
176
  cls.log_critical(error)
177
+ if cls.log_critical == log.critical and _hidden_by_null_handler(log, logging.CRITICAL, error):
178
+ if sys.stderr is not None:
179
+ try:
180
+ print(error, file=sys.stderr)
181
+ except (OSError, ValueError):
182
+ pass
127
183
  return exit_status.bad_argument
128
184
 
129
185
  except KeyboardInterrupt:
@@ -10,7 +10,7 @@ local files and your environment.
10
10
 
11
11
  # type annotations
12
12
  from __future__ import annotations
13
- from typing import Tuple, List, Dict, TypeVar, Callable, Union, Any
13
+ from typing import Tuple, List, Dict, TypeVar, Callable, Union, Any, Mapping
14
14
 
15
15
  # standard libs
16
16
  from collections import Counter
@@ -33,6 +33,85 @@ class ConfigurationError(Exception):
33
33
  """Exception specific to configuration errors."""
34
34
 
35
35
 
36
+ _MISSING = object()
37
+
38
+
39
+ def _variant_siblings(key: Any) -> List[str]:
40
+ """The other keys that are a variant of a name `key` is a variant of."""
41
+ if not isinstance(key, str):
42
+ return []
43
+ names = [key]
44
+ if key.endswith('_env'):
45
+ names.append(key[:-4])
46
+ if key.endswith('_eval'):
47
+ names.append(key[:-5])
48
+ return [other for name in names for other in (name, f'{name}_env', f'{name}_eval') if other != key]
49
+
50
+
51
+ def _drop_displaced_variants(target: dict, new: Mapping) -> None:
52
+ """
53
+ Remove from `target` every variant of a name that `new` sets a different variant of,
54
+ at every level where the depth-first update will descend.
55
+ """
56
+ for key, value in new.items():
57
+ for other in _variant_siblings(key):
58
+ if other in target and other not in new:
59
+ del target[other]
60
+ if isinstance(value, dict) and isinstance(target.get(key), dict):
61
+ _drop_displaced_variants(target[key], value)
62
+
63
+
64
+ def _apply_layer(target: NSCoreMixin, new: Mapping) -> None:
65
+ """Depth-first update of `target` with `new`, where the variants `new` sets displace the others."""
66
+ _drop_displaced_variants(target, new)
67
+ NSCoreMixin.update(target, new)
68
+
69
+
70
+ def _value_at(mapping: Any, path: Tuple[Any, ...]) -> Any:
71
+ """The value at `path` through nested mappings, or `_MISSING`."""
72
+ for key in path:
73
+ if not isinstance(mapping, dict) or key not in mapping:
74
+ return _MISSING
75
+ mapping = mapping[key]
76
+ return mapping
77
+
78
+
79
+ def _lacks(mapping: Any, path: Tuple[Any, ...]) -> bool:
80
+ """Whether item access along `path` raises `KeyError`: a mapping on the way lacks the next key."""
81
+ for key in path:
82
+ if not isinstance(mapping, dict):
83
+ return False
84
+ if key not in mapping:
85
+ return True
86
+ mapping = mapping[key]
87
+ return False
88
+
89
+
90
+ def _depth_held(mapping: Any, path: Tuple[Any, ...]) -> int:
91
+ """How many leading keys of `path` are held through nested mappings."""
92
+ for depth, key in enumerate(path):
93
+ if not isinstance(mapping, dict) or key not in mapping:
94
+ return depth
95
+ mapping = mapping[key]
96
+ return len(path)
97
+
98
+
99
+ def _displaced(sources: List[Mapping], path: Tuple[Any, ...]) -> bool:
100
+ """
101
+ Whether a source sets another variant of a key on `path` in that key's place,
102
+ and not the key itself, above a source that holds `path` down to that key.
103
+ """
104
+ held = set()
105
+ for source in sources:
106
+ for depth, key in enumerate(path):
107
+ parent = _value_at(source, path[:depth])
108
+ if depth in held and isinstance(parent, dict) and key not in parent \
109
+ and any(other in parent for other in _variant_siblings(key)):
110
+ return True
111
+ held.update(range(_depth_held(source, path)))
112
+ return False
113
+
114
+
36
115
  class Configuration(NSCoreMixin):
37
116
  """
38
117
  An ordered collection of `Namespace` dictionaries.
@@ -54,6 +133,24 @@ class Configuration(NSCoreMixin):
54
133
 
55
134
  >>> conf.namespaces.one.x
56
135
  1
136
+
137
+ A parameter `name` and its `name_env` and `name_eval` variants are one key across
138
+ the layers: the highest layer that sets any of them decides, and only the variants
139
+ it sets remain in the merged view. Within a single layer, two variants of one name
140
+ are still ambiguous.
141
+
142
+ Example:
143
+ >>> conf = Configuration(one=Namespace({'db': {'password': 'secret'}}),
144
+ ... two=Namespace({'db': {'password_eval': 'echo other'}}))
145
+
146
+ >>> dict(conf)
147
+ {'db': Namespace({'password_eval': 'echo other'})}
148
+
149
+ >>> conf.db.password
150
+ 'other'
151
+
152
+ >>> conf.which('db', 'password_eval')
153
+ 'two'
57
154
  """
58
155
 
59
156
  local: Namespace # NOTE: used to track changes to the 'self'
@@ -89,7 +186,7 @@ class Configuration(NSCoreMixin):
89
186
  for name, mapping in others.items():
90
187
  if name != '_':
91
188
  self.namespaces[name] = Namespace(mapping)
92
- super().update(self.namespaces[name])
189
+ _apply_layer(self, self.namespaces[name])
93
190
  else:
94
191
  self.local.update(mapping)
95
192
 
@@ -180,6 +277,8 @@ class Configuration(NSCoreMixin):
180
277
  Namespace({'x': 1, 'y': 3})
181
278
  """
182
279
  namespaces = Namespace({**self.namespaces, '_': self.local})
280
+ if _lacks(self, path) and _displaced(list(namespaces.values()), path):
281
+ raise KeyError(f'Not found: {path}')
183
282
  for label in reversed(list(namespaces.keys())):
184
283
  try:
185
284
  sub = namespaces[label]
@@ -231,7 +330,7 @@ class Configuration(NSCoreMixin):
231
330
 
232
331
  def __setattr__(self, name: str, value: Any) -> None:
233
332
  """Intercept parameter assignment."""
234
- if name in self:
333
+ if name in self or any(other in self for other in _variant_siblings(name)):
235
334
  self.update({name: value})
236
335
  else:
237
336
  super().__setattr__(name, value)
@@ -247,6 +346,9 @@ class Configuration(NSCoreMixin):
247
346
  Instead, overrides have been implemented to capture these changes in a `local` namespace.
248
347
  If you ask :func:`which` namespace a parameter has come from, and it was an in-place change,
249
348
  it will be considered a member of the "_" namespace.
349
+ An in-place change follows the same rule as the layers for `_env` and `_eval`
350
+ variants: setting one variant of a name replaces any other variant of that name,
351
+ both in the "_" namespace and in the merged view.
250
352
 
251
353
  Example:
252
354
  >>> conf = Configuration(a=Namespace(x=1))
@@ -268,8 +370,8 @@ class Configuration(NSCoreMixin):
268
370
  >>> dict(conf)
269
371
  {'x': 2, 'y': 3}
270
372
  """
271
- self.local.update(*args, **kwargs)
272
- super().update(*args, **kwargs)
373
+ _apply_layer(self.local, dict(*args, **kwargs))
374
+ _apply_layer(self, dict(*args, **kwargs))
273
375
 
274
376
  def pop(self, *args, **kwargs) -> Any:
275
377
  """
@@ -1,70 +0,0 @@
1
- [tool.poetry]
2
- name = "cmdkit"
3
- version = "2.7.6"
4
- description = "A command-line utility toolkit for Python."
5
- readme = "README.rst"
6
- license = "Apache-2.0"
7
- homepage = "https://cmdkit.readthedocs.io"
8
- documentation = "https://cmdkit.readthedocs.io"
9
- repository = "https://github.com/glentner/cmdkit"
10
- authors = [
11
- "Geoffrey Lentner <glentner@purdue.edu>"
12
- ]
13
- keywords = ["command-line", "utility", "toolkit"]
14
- classifiers = [
15
- "Development Status :: 5 - Production/Stable",
16
- "Topic :: Software Development :: Libraries :: Application Frameworks",
17
- "Programming Language :: Python :: 3",
18
- "Programming Language :: Python :: 3.10",
19
- "Programming Language :: Python :: 3.11",
20
- "Programming Language :: Python :: 3.12",
21
- "Programming Language :: Python :: 3.13",
22
- "License :: OSI Approved :: Apache Software License",
23
- ]
24
-
25
- [tool.poetry.dependencies]
26
- python = ">=3.10,<4.0"
27
- tomli = {version = "^2.1.0", optional = true}
28
- tomli-w = {version = "^1.1.0", optional = true}
29
- pyyaml = {version = "^6.0.1", optional = true}
30
-
31
- [tool.poetry.extras]
32
- toml = ['tomli', 'tomli-w']
33
- yaml = ['pyyaml']
34
-
35
- [tool.poetry.group.docs.dependencies]
36
- sphinx = "^7.2.6"
37
- sphinx-sitemap = "^2.5.1"
38
- sphinx-autobuild = "^2021.3.14"
39
- sphinx-copybutton = "^0.5.2"
40
- sphinxext-opengraph = "^0.9.1"
41
- sphinxcontrib-details-directive = "^0.1.0"
42
- sphinx-toolbox = "^3.5.0"
43
- pydata-sphinx-theme = "^0.15.1"
44
- enum-tools = {extras = ["sphinx"], version = "^0.10.0"}
45
- docutils = "^0.21.1" # NOTE: https://github.com/python-poetry/poetry/issues/9293
46
-
47
- [tool.poetry.group.dev.dependencies]
48
- tomli = "^2.1.0"
49
- tomli-w = "^1.1.0"
50
- pyyaml = "^6.0.1"
51
- pytest = "^7.4.4"
52
- pytest-cov = "^4.1.0"
53
- hypothesis = "^6.92.6"
54
- ruff = "^0.1.11"
55
- pyright = "^1.1.345"
56
- pynvim = "^0.5.0"
57
- ipython = "^8.20.0"
58
- sphinx-autobuild = "^2021.3.14"
59
-
60
- [build-system]
61
- requires = ["poetry-core>=1.0.0"]
62
- build-backend = "poetry.core.masonry.api"
63
-
64
- [tool.pytest.ini_options]
65
- addopts = ["--strict-markers", ]
66
- markers = [
67
- "unit: Unit tests are short, interface driven tests on discrete components.",
68
- "integration: Integration tests are often longer and deal with the interaction between systems.",
69
- "parameterize: Place holder for parameterized tests (not a real type).",
70
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes