schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a11__py3-none-any.whl

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 (92) hide show
  1. schemathesis/__init__.py +3 -7
  2. schemathesis/checks.py +17 -7
  3. schemathesis/cli/commands/__init__.py +51 -3
  4. schemathesis/cli/commands/data.py +10 -0
  5. schemathesis/cli/commands/run/__init__.py +147 -260
  6. schemathesis/cli/commands/run/context.py +2 -3
  7. schemathesis/cli/commands/run/events.py +4 -0
  8. schemathesis/cli/commands/run/executor.py +60 -73
  9. schemathesis/cli/commands/run/filters.py +15 -165
  10. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  11. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  12. schemathesis/cli/commands/run/handlers/output.py +26 -47
  13. schemathesis/cli/commands/run/loaders.py +35 -50
  14. schemathesis/cli/commands/run/validation.py +36 -161
  15. schemathesis/cli/core.py +5 -3
  16. schemathesis/cli/ext/fs.py +7 -5
  17. schemathesis/cli/ext/options.py +0 -21
  18. schemathesis/config/__init__.py +188 -0
  19. schemathesis/config/_auth.py +51 -0
  20. schemathesis/config/_checks.py +268 -0
  21. schemathesis/config/_diff_base.py +99 -0
  22. schemathesis/config/_env.py +21 -0
  23. schemathesis/config/_error.py +156 -0
  24. schemathesis/config/_generation.py +150 -0
  25. schemathesis/config/_health_check.py +24 -0
  26. schemathesis/config/_operations.py +313 -0
  27. schemathesis/config/_output.py +171 -0
  28. schemathesis/config/_parameters.py +19 -0
  29. schemathesis/config/_phases.py +151 -0
  30. schemathesis/config/_projects.py +495 -0
  31. schemathesis/config/_rate_limit.py +17 -0
  32. schemathesis/config/_report.py +116 -0
  33. schemathesis/config/_validator.py +9 -0
  34. schemathesis/config/schema.json +837 -0
  35. schemathesis/core/__init__.py +2 -0
  36. schemathesis/core/compat.py +16 -9
  37. schemathesis/core/errors.py +19 -2
  38. schemathesis/core/failures.py +6 -7
  39. schemathesis/core/hooks.py +20 -0
  40. schemathesis/core/output/__init__.py +14 -37
  41. schemathesis/core/output/sanitization.py +3 -146
  42. schemathesis/core/validation.py +16 -0
  43. schemathesis/engine/__init__.py +2 -4
  44. schemathesis/engine/context.py +41 -43
  45. schemathesis/engine/core.py +7 -5
  46. schemathesis/engine/phases/__init__.py +10 -0
  47. schemathesis/engine/phases/probes.py +8 -8
  48. schemathesis/engine/phases/stateful/_executor.py +68 -43
  49. schemathesis/engine/phases/unit/__init__.py +23 -15
  50. schemathesis/engine/phases/unit/_executor.py +77 -17
  51. schemathesis/engine/phases/unit/_pool.py +1 -1
  52. schemathesis/errors.py +2 -0
  53. schemathesis/filters.py +2 -3
  54. schemathesis/generation/__init__.py +6 -31
  55. schemathesis/generation/case.py +5 -3
  56. schemathesis/generation/coverage.py +153 -123
  57. schemathesis/generation/hypothesis/builder.py +40 -14
  58. schemathesis/generation/meta.py +3 -3
  59. schemathesis/generation/overrides.py +37 -1
  60. schemathesis/generation/stateful/state_machine.py +8 -1
  61. schemathesis/graphql/loaders.py +21 -12
  62. schemathesis/openapi/checks.py +12 -8
  63. schemathesis/openapi/generation/filters.py +10 -8
  64. schemathesis/openapi/loaders.py +22 -13
  65. schemathesis/pytest/lazy.py +2 -5
  66. schemathesis/pytest/plugin.py +11 -2
  67. schemathesis/schemas.py +13 -61
  68. schemathesis/specs/graphql/schemas.py +11 -15
  69. schemathesis/specs/openapi/_hypothesis.py +12 -8
  70. schemathesis/specs/openapi/checks.py +16 -18
  71. schemathesis/specs/openapi/examples.py +4 -3
  72. schemathesis/specs/openapi/formats.py +2 -2
  73. schemathesis/specs/openapi/negative/__init__.py +2 -2
  74. schemathesis/specs/openapi/patterns.py +46 -16
  75. schemathesis/specs/openapi/references.py +2 -3
  76. schemathesis/specs/openapi/schemas.py +11 -20
  77. schemathesis/specs/openapi/stateful/__init__.py +10 -5
  78. schemathesis/transport/prepare.py +7 -6
  79. schemathesis/transport/requests.py +3 -1
  80. schemathesis/transport/wsgi.py +3 -4
  81. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
  82. schemathesis-4.0.0a11.dist-info/RECORD +166 -0
  83. schemathesis/cli/commands/run/checks.py +0 -79
  84. schemathesis/cli/commands/run/hypothesis.py +0 -78
  85. schemathesis/cli/commands/run/reports.py +0 -72
  86. schemathesis/cli/hooks.py +0 -36
  87. schemathesis/engine/config.py +0 -59
  88. schemathesis/experimental/__init__.py +0 -72
  89. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  90. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
  91. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
  92. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -6,42 +6,28 @@ supporting both GraphQL and OpenAPI specifications.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import os
9
10
  import warnings
10
- from dataclasses import dataclass
11
11
  from typing import TYPE_CHECKING, Any, Callable
12
12
 
13
13
  from schemathesis import graphql, openapi
14
- from schemathesis.core import NOT_SET, NotSet
14
+ from schemathesis.config import ProjectConfig
15
15
  from schemathesis.core.errors import LoaderError, LoaderErrorKind
16
16
  from schemathesis.core.fs import file_exists
17
- from schemathesis.core.output import OutputConfig
18
- from schemathesis.generation import GenerationConfig
19
17
 
20
18
  if TYPE_CHECKING:
21
- from schemathesis.engine.config import NetworkConfig
22
19
  from schemathesis.schemas import BaseSchema
23
20
 
24
- Loader = Callable[["AutodetectConfig"], "BaseSchema"]
21
+ Loader = Callable[["ProjectConfig"], "BaseSchema"]
25
22
 
26
23
 
27
- @dataclass
28
- class AutodetectConfig:
29
- location: str
30
- network: NetworkConfig
31
- wait_for_schema: float | None
32
- base_url: str | None | NotSet = NOT_SET
33
- rate_limit: str | None | NotSet = NOT_SET
34
- generation: GenerationConfig | NotSet = NOT_SET
35
- output: OutputConfig | NotSet = NOT_SET
36
-
37
-
38
- def load_schema(config: AutodetectConfig) -> BaseSchema:
24
+ def load_schema(location: str, config: ProjectConfig) -> BaseSchema:
39
25
  """Load API schema automatically based on the provided configuration."""
40
- if is_probably_graphql(config.location):
26
+ if is_probably_graphql(location):
41
27
  # Try GraphQL first, then fallback to Open API
42
- return _try_load_schema(config, graphql, openapi)
28
+ return _try_load_schema(location, config, graphql, openapi)
43
29
  # Try Open API first, then fallback to GraphQL
44
- return _try_load_schema(config, openapi, graphql)
30
+ return _try_load_schema(location, config, openapi, graphql)
45
31
 
46
32
 
47
33
  def should_try_more(exc: LoaderError) -> bool:
@@ -60,27 +46,28 @@ def should_try_more(exc: LoaderError) -> bool:
60
46
  )
61
47
 
62
48
 
63
- def detect_loader(schema_or_location: str | dict[str, Any], module: Any) -> Callable:
49
+ def detect_loader(location: str, module: Any) -> Callable:
64
50
  """Detect API schema loader."""
65
- if isinstance(schema_or_location, str):
66
- if file_exists(schema_or_location):
67
- return module.from_path # type: ignore
68
- return module.from_url # type: ignore
69
- raise NotImplementedError
51
+ if file_exists(location):
52
+ return module.from_path # type: ignore
53
+ return module.from_url # type: ignore
70
54
 
71
55
 
72
- def _try_load_schema(config: AutodetectConfig, first_module: Any, second_module: Any) -> BaseSchema:
56
+ def _try_load_schema(location: str, config: ProjectConfig, first_module: Any, second_module: Any) -> BaseSchema:
73
57
  """Try to load schema with fallback option."""
74
58
  from urllib3.exceptions import InsecureRequestWarning
75
59
 
76
60
  with warnings.catch_warnings():
77
61
  warnings.simplefilter("ignore", InsecureRequestWarning)
78
62
  try:
79
- return _load_schema(config, first_module)
63
+ return _load_schema(location, config, first_module)
80
64
  except LoaderError as exc:
65
+ # If this was the OpenAPI loader on an explicit OpenAPI file, don't fallback
66
+ if first_module is openapi and is_openapi_file(location):
67
+ raise exc
81
68
  if should_try_more(exc):
82
69
  try:
83
- return _load_schema(config, second_module)
70
+ return _load_schema(location, config, second_module)
84
71
  except Exception as second_exc:
85
72
  if is_specific_exception(second_exc):
86
73
  raise second_exc
@@ -88,26 +75,23 @@ def _try_load_schema(config: AutodetectConfig, first_module: Any, second_module:
88
75
  raise exc
89
76
 
90
77
 
91
- def _load_schema(config: AutodetectConfig, module: Any) -> BaseSchema:
78
+ def _load_schema(location: str, config: ProjectConfig, module: Any) -> BaseSchema:
92
79
  """Unified schema loader for both GraphQL and OpenAPI."""
93
- loader = detect_loader(config.location, module)
80
+ loader = detect_loader(location, module)
94
81
 
95
82
  kwargs: dict = {}
96
83
  if loader is module.from_url:
97
84
  if config.wait_for_schema is not None:
98
85
  kwargs["wait_for_schema"] = config.wait_for_schema
99
- kwargs["verify"] = config.network.tls_verify
100
- if config.network.cert:
101
- kwargs["cert"] = config.network.cert
102
- if config.network.auth:
103
- kwargs["auth"] = config.network.auth
104
-
105
- return loader(config.location, **kwargs).configure(
106
- base_url=config.base_url,
107
- rate_limit=config.rate_limit,
108
- output=config.output,
109
- generation=config.generation,
110
- )
86
+ kwargs["verify"] = config.tls_verify
87
+ request_cert = config.request_cert_for()
88
+ if request_cert:
89
+ kwargs["cert"] = request_cert
90
+ auth = config.auth_for()
91
+ if auth is not None:
92
+ kwargs["auth"] = auth
93
+
94
+ return loader(location, config=config._parent, **kwargs)
111
95
 
112
96
 
113
97
  def is_specific_exception(exc: Exception) -> bool:
@@ -120,10 +104,11 @@ def is_specific_exception(exc: Exception) -> bool:
120
104
  )
121
105
 
122
106
 
123
- def is_probably_graphql(schema_or_location: str | dict[str, Any]) -> bool:
107
+ def is_probably_graphql(location: str) -> bool:
124
108
  """Detect whether it is likely that the given location is a GraphQL endpoint."""
125
- if isinstance(schema_or_location, str):
126
- return schema_or_location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
127
- return "__schema" in schema_or_location or (
128
- "data" in schema_or_location and "__schema" in schema_or_location["data"]
129
- )
109
+ return location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
110
+
111
+
112
+ def is_openapi_file(location: str) -> bool:
113
+ name = os.path.basename(location).lower()
114
+ return any(name == f"{base}{ext}" for base in ("openapi", "swagger") for ext in (".json", ".yaml", ".yml"))
@@ -2,24 +2,21 @@ from __future__ import annotations
2
2
 
3
3
  import codecs
4
4
  import operator
5
- import os
6
5
  import pathlib
7
- import re
8
6
  from contextlib import contextmanager
9
- from functools import partial, reduce
10
- from typing import Callable, Generator, Sequence
7
+ from functools import reduce
8
+ from typing import Callable, Generator
11
9
  from urllib.parse import urlparse
12
10
 
13
11
  import click
14
12
 
15
- from schemathesis import errors, experimental
16
- from schemathesis.cli.commands.run.reports import ReportFormat
17
- from schemathesis.cli.constants import DEFAULT_WORKERS
18
- from schemathesis.core import rate_limit, string_to_boolean
13
+ from schemathesis.config import ReportFormat, get_workers_count
14
+ from schemathesis.core import errors, rate_limit, string_to_boolean
19
15
  from schemathesis.core.fs import file_exists
20
- from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
16
+ from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
17
+ from schemathesis.filters import expression_to_filter_function
21
18
  from schemathesis.generation import GenerationMode
22
- from schemathesis.generation.overrides import Override
19
+ from schemathesis.generation.targets import TargetFunction
23
20
 
24
21
  INVALID_DERANDOMIZE_MESSAGE = (
25
22
  "`--generation-deterministic` implies no database, so passing `--generation-database` too is invalid."
@@ -35,27 +32,24 @@ INVALID_BASE_URL_MESSAGE = (
35
32
  "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
36
33
  "Make sure it is a properly formatted URL."
37
34
  )
38
- MISSING_BASE_URL_MESSAGE = "The `--url` option is required when specifying a schema via a file."
39
35
  MISSING_REQUEST_CERT_MESSAGE = "The `--request-cert` option must be specified if `--request-cert-key` is used."
40
36
 
41
37
 
42
- def validate_schema(schema: str, base_url: str | None) -> None:
38
+ def validate_schema_location(ctx: click.core.Context, param: click.core.Parameter, location: str) -> str:
43
39
  try:
44
- netloc = urlparse(schema).netloc
40
+ netloc = urlparse(location).netloc
45
41
  if netloc:
46
- validate_url(schema)
47
- return None
42
+ validate_url(location)
43
+ return location
48
44
  except ValueError as exc:
49
45
  raise click.UsageError(INVALID_SCHEMA_MESSAGE) from exc
50
- if "\x00" in schema or not schema:
46
+ if "\x00" in location or not location:
51
47
  raise click.UsageError(INVALID_SCHEMA_MESSAGE)
52
- exists = file_exists(schema)
53
- if exists or bool(pathlib.Path(schema).suffix):
48
+ exists = file_exists(location)
49
+ if exists or bool(pathlib.Path(location).suffix):
54
50
  if not exists:
55
51
  raise click.UsageError(FILE_DOES_NOT_EXIST_MESSAGE)
56
- if base_url is None:
57
- raise click.UsageError(MISSING_BASE_URL_MESSAGE)
58
- return None
52
+ return location
59
53
  raise click.UsageError(INVALID_SCHEMA_MESSAGE)
60
54
 
61
55
 
@@ -122,82 +116,31 @@ def validate_auth(
122
116
  return None
123
117
 
124
118
 
125
- def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str], override: Override) -> None:
119
+ def validate_auth_overlap(auth: tuple[str, str] | None, headers: dict[str, str]) -> None:
126
120
  auth_is_set = auth is not None
127
121
  header_is_set = "authorization" in {header.lower() for header in headers}
128
- override_is_set = "authorization" in {header.lower() for header in override.headers}
129
- if len([is_set for is_set in (auth_is_set, header_is_set, override_is_set) if is_set]) > 1:
122
+ if len([is_set for is_set in (auth_is_set, header_is_set) if is_set]) > 1:
130
123
  message = "The "
131
124
  used = []
132
125
  if auth_is_set:
133
126
  used.append("`--auth`")
134
127
  if header_is_set:
135
128
  used.append("`--header`")
136
- if override_is_set:
137
- used.append("`--set-header`")
138
129
  message += " and ".join(used)
139
130
  message += " options were both used to set the 'Authorization' header, which is not permitted."
140
131
  raise click.BadParameter(message)
141
132
 
142
133
 
143
- def _validate_and_build_multiple_options(
144
- values: tuple[str, ...], name: str, callback: Callable[[str, str], None]
145
- ) -> dict[str, str]:
146
- output = {}
147
- for raw in values:
134
+ def validate_filter_expression(
135
+ ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
136
+ ) -> Callable | None:
137
+ if raw_value:
148
138
  try:
149
- key, value = raw.split("=", maxsplit=1)
150
- except ValueError as exc:
151
- raise click.BadParameter(f"Expected NAME=VALUE format, received {raw}.") from exc
152
- key = key.strip()
153
- if not key:
154
- raise click.BadParameter(f"{name} parameter name should not be empty.")
155
- if key in output:
156
- raise click.BadParameter(f"{name} parameter {key} is specified multiple times.")
157
- value = value.strip()
158
- callback(key, value)
159
- output[key] = value
160
- return output
161
-
162
-
163
- def validate_unique_filter(values: Sequence[str], arg_name: str) -> None:
164
- if len(values) != len(set(values)):
165
- duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
166
- raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
167
-
168
-
169
- def _validate_set_query(_: str, value: str) -> None:
170
- if contains_unicode_surrogate_pair(value):
171
- raise click.BadParameter("Query parameter value should not contain surrogates.")
172
-
173
-
174
- def validate_set_query(
175
- ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
176
- ) -> dict[str, str]:
177
- return _validate_and_build_multiple_options(raw_value, "Query", _validate_set_query)
178
-
179
-
180
- def validate_set_header(
181
- ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
182
- ) -> dict[str, str]:
183
- return _validate_and_build_multiple_options(raw_value, "Header", partial(_validate_header, where="Header"))
184
-
185
-
186
- def validate_set_cookie(
187
- ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
188
- ) -> dict[str, str]:
189
- return _validate_and_build_multiple_options(raw_value, "Cookie", partial(_validate_header, where="Cookie"))
190
-
191
-
192
- def _validate_set_path(_: str, value: str) -> None:
193
- if contains_unicode_surrogate_pair(value):
194
- raise click.BadParameter("Path parameter value should not contain surrogates.")
195
-
196
-
197
- def validate_set_path(
198
- ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]
199
- ) -> dict[str, str]:
200
- return _validate_and_build_multiple_options(raw_value, "Path", _validate_set_path)
139
+ return expression_to_filter_function(raw_value)
140
+ except ValueError:
141
+ arg_name = param.opts[0]
142
+ raise click.UsageError(f"Invalid expression for {arg_name}: {raw_value}") from None
143
+ return None
201
144
 
202
145
 
203
146
  def _validate_header(key: str, value: str, where: str) -> None:
@@ -225,15 +168,6 @@ def validate_headers(
225
168
  return headers
226
169
 
227
170
 
228
- def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: tuple[str, ...]) -> tuple[str, ...]:
229
- for value in raw_value:
230
- try:
231
- re.compile(value)
232
- except (re.error, OverflowError, RuntimeError) as exc:
233
- raise click.BadParameter(f"Invalid regex: {exc.args[0]}.") # noqa: B904
234
- return raw_value
235
-
236
-
237
171
  def validate_request_cert_key(
238
172
  ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
239
173
  ) -> str | None:
@@ -256,68 +190,21 @@ def validate_preserve_bytes(ctx: click.core.Context, param: click.core.Parameter
256
190
  return True
257
191
 
258
192
 
259
- def convert_experimental(
260
- ctx: click.core.Context, param: click.core.Parameter, value: tuple[str, ...]
261
- ) -> list[experimental.Experiment]:
262
- return [
263
- feature
264
- for feature in experimental.GLOBAL_EXPERIMENTS.available
265
- if feature.label in value or feature.is_env_var_set
266
- ]
267
-
268
-
269
- def reduce_list(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
270
- return reduce(operator.iadd, value, [])
271
-
272
-
273
- def convert_http_methods(
274
- ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
275
- ) -> set[str] | None:
276
- if value is None:
277
- return value
278
- return {item.lower() for item in value}
279
-
280
-
281
- def convert_status_codes(
282
- ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
193
+ def reduce_list(
194
+ ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]] | None
283
195
  ) -> list[str] | None:
284
196
  if not value:
285
- return value
286
-
287
- invalid = []
288
-
289
- for code in value:
290
- if len(code) != 3:
291
- invalid.append(code)
292
- continue
293
-
294
- if code[0] not in {"1", "2", "3", "4", "5"}:
295
- invalid.append(code)
296
- continue
297
-
298
- upper_code = code.upper()
197
+ return None
198
+ return reduce(operator.iadd, value, [])
299
199
 
300
- if "X" in upper_code:
301
- if (
302
- upper_code[1:] == "XX"
303
- or (upper_code[1] == "X" and upper_code[2].isdigit())
304
- or (upper_code[1].isdigit() and upper_code[2] == "X")
305
- ):
306
- continue
307
- else:
308
- invalid.append(code)
309
- continue
310
200
 
311
- if not code.isnumeric():
312
- invalid.append(code)
201
+ def convert_maximize(
202
+ ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]
203
+ ) -> list[TargetFunction]:
204
+ from schemathesis.generation.targets import TARGETS
313
205
 
314
- if invalid:
315
- raise click.UsageError(
316
- f"Invalid status code(s): {', '.join(invalid)}. "
317
- "Use valid 3-digit codes between 100 and 599, "
318
- "or wildcards (e.g., 2XX, 2X0, 20X), where X is a wildcard digit."
319
- )
320
- return value
206
+ names: list[str] = reduce(operator.iadd, value, [])
207
+ return TARGETS.get_by_names(names)
321
208
 
322
209
 
323
210
  def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter, value: str) -> list[GenerationMode]:
@@ -338,18 +225,6 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
338
225
  raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
339
226
 
340
227
 
341
- def get_workers_count() -> int:
342
- """Detect the number of available CPUs for the current process, if possible.
343
-
344
- Use ``DEFAULT_WORKERS`` if not possible to detect.
345
- """
346
- if hasattr(os, "sched_getaffinity"):
347
- # In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
348
- return len(os.sched_getaffinity(0))
349
- # Number of CPUs in the system, or 1 if undetermined
350
- return os.cpu_count() or DEFAULT_WORKERS
351
-
352
-
353
228
  def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
354
229
  if value == "auto":
355
230
  return get_workers_count()
schemathesis/cli/core.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import shutil
3
5
 
@@ -9,9 +11,9 @@ def get_terminal_width() -> int:
9
11
  return shutil.get_terminal_size((80, 24)).columns
10
12
 
11
13
 
12
- def ensure_color(ctx: click.Context, no_color: bool, force_color: bool) -> None:
13
- if force_color:
14
+ def ensure_color(ctx: click.Context, color: bool | None) -> None:
15
+ if color:
14
16
  ctx.color = True
15
- elif no_color or "NO_COLOR" in os.environ:
17
+ elif color is False or "NO_COLOR" in os.environ:
16
18
  ctx.color = False
17
19
  os.environ["NO_COLOR"] = "1"
@@ -1,14 +1,16 @@
1
+ from pathlib import Path
2
+
1
3
  import click
2
4
 
3
5
  from schemathesis.core.fs import ensure_parent
4
6
 
5
7
 
6
- def open_file(file: click.utils.LazyFile) -> None:
8
+ def open_file(file: Path) -> None:
7
9
  try:
8
- ensure_parent(file.name, fail_silently=False)
10
+ ensure_parent(file, fail_silently=False)
9
11
  except OSError as exc:
10
12
  raise click.BadParameter(f"'{file.name}': {exc.strerror}") from exc
11
13
  try:
12
- file.open()
13
- except click.FileError as exc:
14
- raise click.BadParameter(exc.format_message()) from exc
14
+ file.open("w", encoding="utf-8")
15
+ except OSError as exc:
16
+ raise click.BadParameter(f"Could not open file {file.name}: {exc}") from exc
@@ -5,7 +5,6 @@ from typing import Any, NoReturn
5
5
 
6
6
  import click
7
7
 
8
- from schemathesis.core import NOT_SET, NotSet
9
8
  from schemathesis.core.registries import Registry
10
9
 
11
10
 
@@ -64,13 +63,6 @@ class CsvEnumChoice(BaseCsvChoice):
64
63
  self.fail_on_invalid_options(invalid_options, selected)
65
64
 
66
65
 
67
- class CsvListChoice(click.ParamType):
68
- def convert( # type: ignore[return]
69
- self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
70
- ) -> list[str]:
71
- return [item for item in value.split(",") if item]
72
-
73
-
74
66
  class RegistryChoice(BaseCsvChoice):
75
67
  def __init__(self, registry: Registry, with_all: bool = False) -> None:
76
68
  self.registry = registry
@@ -91,16 +83,3 @@ class RegistryChoice(BaseCsvChoice):
91
83
  if not invalid_options and selected:
92
84
  return selected
93
85
  self.fail_on_invalid_options(invalid_options, selected)
94
-
95
-
96
- class OptionalInt(click.types.IntRange):
97
- def convert( # type: ignore
98
- self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
99
- ) -> int | NotSet:
100
- if value.lower() == "none":
101
- return NOT_SET
102
- try:
103
- int(value)
104
- return super().convert(value, param, ctx)
105
- except ValueError:
106
- self.fail(f"{value} is not a valid integer or None.", param, ctx)