more fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2023-05-05 12:19:19 -07:00
parent a6638fb08a
commit c209414459
7 changed files with 437 additions and 106 deletions

View File

@ -1,4 +1,4 @@
name: Black name: black
on: on:
push: push:
branches: main branches: main
@ -35,4 +35,4 @@ jobs:
- name: Run black - name: Run black
shell: bash shell: bash
run: | run: |
poetry run black --check --diff . poetry run black --check --diff . generate/generate.py docs/conf.py kittycad/client_test.py kittycad/examples_test.py

38
.github/workflows/mypy.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: mypy
on:
push:
branches: main
paths:
- '**.py'
- .github/workflows/mypy.yml
- 'pyproject.toml'
pull_request:
paths:
- '**.py'
- .github/workflows/mypy.yml
- 'pyproject.toml'
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
# Installation instructions are from: https://python-poetry.org/docs/
- name: Install dependencies
shell: bash
run: |
pip install \
poetry
- name: Build
shell: bash
run: |
poetry install
poetry build
- name: Run mypy
shell: bash
run: |
poetry run mypy .

View File

@ -1,4 +1,4 @@
name: Ruff name: ruff
on: on:
push: push:
branches: main branches: main
@ -16,4 +16,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1 - name: Set up Python
uses: actions/setup-python@v4
# Installation instructions are from: https://python-poetry.org/docs/
- name: Install dependencies
shell: bash
run: |
pip install \
poetry
- name: Build
shell: bash
run: |
poetry install
poetry build
- name: Run ruff
shell: bash
run: |
poetry run ruff check .

View File

@ -129,15 +129,59 @@ def generatePaths(cwd: str, parser: dict) -> dict:
return data return data
def generateTypeAndExamplePython(schema: dict, data: dict) -> Tuple[str, str, str]: def generateTypeAndExamplePython(
name: str, schema: dict, data: dict
) -> Tuple[str, str, str]:
parameter_type = "" parameter_type = ""
parameter_example = "" parameter_example = ""
example_imports = "" example_imports = ""
if "type" in schema: if "type" in schema:
if "format" in schema and schema["format"] == "uuid": if "format" in schema and schema["format"] == "uuid":
if name != "":
parameter_type = name
example_imports = example_imports + (
"from kittycad.models."
+ camel_to_snake(parameter_type)
+ " import "
+ parameter_type
+ "\n"
)
parameter_example = parameter_type + '("<uuid>")'
else:
parameter_type = "str" parameter_type = "str"
parameter_example = '"<uuid>"' parameter_example = '"<uuid>"'
elif (
schema["type"] == "string" and "enum" in schema and len(schema["enum"]) > 0
):
if name == "":
logging.error("schema: %s", json.dumps(schema, indent=4))
raise Exception("Unknown type name for enum")
parameter_type = name
example_imports = example_imports + (
"from kittycad.models."
+ camel_to_snake(parameter_type)
+ " import "
+ parameter_type
+ "\n"
)
parameter_example = (
parameter_type + "." + camel_to_screaming_snake(schema["enum"][0])
)
elif schema["type"] == "string": elif schema["type"] == "string":
if name != "":
parameter_type = name
example_imports = example_imports + (
"from kittycad.models."
+ camel_to_snake(parameter_type)
+ " import "
+ parameter_type
+ "\n"
)
parameter_example = parameter_type + '("<string>")'
else:
parameter_type = "str" parameter_type = "str"
parameter_example = '"<string>"' parameter_example = '"<string>"'
elif schema["type"] == "integer": elif schema["type"] == "integer":
@ -150,11 +194,26 @@ def generateTypeAndExamplePython(schema: dict, data: dict) -> Tuple[str, str, st
): ):
parameter_type = "float" parameter_type = "float"
parameter_example = "3.14" parameter_example = "3.14"
elif schema["type"] == "array" and "items" in schema:
items_type, items_example, items_imports = generateTypeAndExamplePython(
"", schema["items"], data
)
example_imports = example_imports + items_imports
parameter_type = "List[" + items_type + "]"
if "minItems" in schema and schema["minItems"] > 1:
parameter_example = "["
for i in range(schema["minItems"] - 1):
parameter_example = parameter_example + items_example + ", "
parameter_example = parameter_example + "]"
else: else:
parameter_example = "[" + items_example + "]"
elif schema["type"] == "object" and "properties" in schema:
if name == "":
logging.error("schema: %s", json.dumps(schema, indent=4)) logging.error("schema: %s", json.dumps(schema, indent=4))
raise Exception("Unknown parameter type") raise Exception("Unknown type name for object")
elif "$ref" in schema:
parameter_type = schema["$ref"].replace("#/components/schemas/", "") parameter_type = name
example_imports = example_imports + ( example_imports = example_imports + (
"from kittycad.models." "from kittycad.models."
+ camel_to_snake(parameter_type) + camel_to_snake(parameter_type)
@ -162,21 +221,53 @@ def generateTypeAndExamplePython(schema: dict, data: dict) -> Tuple[str, str, st
+ parameter_type + parameter_type
+ "\n" + "\n"
) )
parameter_example = name + "("
for property_name in schema["properties"]:
prop = schema["properties"][property_name]
if "nullable" in prop:
# We don't care if it's nullable
continue
else:
(
prop_type,
prop_example,
prop_imports,
) = generateTypeAndExamplePython("", prop, data)
example_imports = example_imports + prop_imports
parameter_example = parameter_example + (
"\n" + property_name + "=" + prop_example + ",\n"
)
parameter_example = parameter_example + ")"
elif (
schema["type"] == "object"
and "additionalProperties" in schema
and schema["additionalProperties"] is not False
):
items_type, items_example, items_imports = generateTypeAndExamplePython(
"", schema["additionalProperties"], data
)
example_imports = example_imports + items_imports
parameter_type = "Dict[str, " + items_type + "]"
parameter_example = '{"<string>": ' + items_example + "}"
else:
logging.error("schema: %s", json.dumps(schema, indent=4))
raise Exception("Unknown parameter type")
elif "oneOf" in schema and len(schema["oneOf"]) > 0:
# Check if each of these only has a object w 1 property.
if isNestedObjectOneOf(schema):
properties = schema["oneOf"][0]["properties"]
for prop in properties:
return generateTypeAndExamplePython(prop, properties[prop], data)
break
return generateTypeAndExamplePython(name, schema["oneOf"][0], data)
elif "$ref" in schema:
parameter_type = schema["$ref"].replace("#/components/schemas/", "")
# Get the schema for the reference. # Get the schema for the reference.
ref_schema = data["components"]["schemas"][parameter_type] ref_schema = data["components"]["schemas"][parameter_type]
if "type" in ref_schema and ref_schema["type"] == "object":
parameter_example = parameter_type + "()" return generateTypeAndExamplePython(parameter_type, ref_schema, data)
elif (
"type" in ref_schema
and ref_schema["type"] == "string"
and "enum" in ref_schema
):
parameter_example = (
parameter_type + "." + camel_to_screaming_snake(ref_schema["enum"][0])
)
else:
logging.error("schema: %s", json.dumps(ref_schema, indent=4))
raise Exception("Unknown ref schema")
else: else:
logging.error("schema: %s", json.dumps(schema, indent=4)) logging.error("schema: %s", json.dumps(schema, indent=4))
raise Exception("Unknown parameter type") raise Exception("Unknown parameter type")
@ -202,7 +293,7 @@ def generatePath(path: str, name: str, method: str, endpoint: dict, data: dict)
endpoint_refs = getEndpointRefs(endpoint, data) endpoint_refs = getEndpointRefs(endpoint, data)
parameter_refs = getParameterRefs(endpoint) parameter_refs = getParameterRefs(endpoint)
request_body_refs = getRequestBodyRefs(endpoint) request_body_refs = getRequestBodyRefs(endpoint)
request_body_type = getRequestBodyType(endpoint) (request_body_type, request_body_schema) = getRequestBodyTypeSchema(endpoint, data)
success_type = "" success_type = ""
if len(endpoint_refs) > 0: if len(endpoint_refs) > 0:
@ -234,7 +325,7 @@ from kittycad.types import Response
parameter_type, parameter_type,
parameter_example, parameter_example,
more_example_imports, more_example_imports,
) = generateTypeAndExamplePython(parameter["schema"], data) ) = generateTypeAndExamplePython("", parameter["schema"], data)
example_imports = example_imports + more_example_imports example_imports = example_imports + more_example_imports
if "nullable" in parameter["schema"] and parameter["schema"]["nullable"]: if "nullable" in parameter["schema"] and parameter["schema"]["nullable"]:
@ -254,17 +345,20 @@ from kittycad.types import Response
params_str += optional_arg params_str += optional_arg
if request_body_type: if request_body_type:
if request_body_type != "bytes": if request_body_type == "str":
params_str += "body=" + request_body_type + ",\n" params_str += "body='<string>',\n"
example_imports = example_imports + ( elif request_body_type == "bytes":
"from kittycad.models."
+ camel_to_snake(request_body_type)
+ " import "
+ request_body_type
+ "\n"
)
else:
params_str += "body=bytes('some bytes', 'utf-8'),\n" params_str += "body=bytes('some bytes', 'utf-8'),\n"
else:
# Generate an example for the schema.
rbs: dict = request_body_schema
(
body_type,
body_example,
more_example_imports,
) = generateTypeAndExamplePython(request_body_type, rbs, data)
params_str += "body=" + body_example + ",\n"
example_imports = example_imports + more_example_imports
example_variable = "" example_variable = ""
if ( if (
@ -346,6 +440,7 @@ async def test_"""
# Make pretty. # Make pretty.
line_length = 82 line_length = 82
short_sync_example = example_imports + short_sync_example
cleaned_example = black.format_str( cleaned_example = black.format_str(
isort.api.sort_code_string( isort.api.sort_code_string(
short_sync_example, short_sync_example,
@ -1048,15 +1143,7 @@ def generateEnumType(
def generateOneOfType(path: str, name: str, schema: dict, data: dict): def generateOneOfType(path: str, name: str, schema: dict, data: dict):
logging.info("generating type: ", name, " at: ", path) logging.info("generating type: ", name, " at: ", path)
is_enum_with_docs = False if isEnumWithDocsOneOf(schema):
for one_of in schema["oneOf"]:
if one_of["type"] == "string" and "enum" in one_of and len(one_of["enum"]) == 1:
is_enum_with_docs = True
else:
is_enum_with_docs = False
break
if is_enum_with_docs:
additional_docs = [] additional_docs = []
enum = [] enum = []
# We want to treat this as an enum with additional docs. # We want to treat this as an enum with additional docs.
@ -1085,26 +1172,7 @@ def generateOneOfType(path: str, name: str, schema: dict, data: dict):
f.write("from ." + camel_to_snake(ref_name) + " import " + ref_name + "\n") f.write("from ." + camel_to_snake(ref_name) + " import " + ref_name + "\n")
all_options.append(ref_name) all_options.append(ref_name)
is_nested_object = False if isNestedObjectOneOf(schema):
for one_of in schema["oneOf"]:
# Check if each are an object w 1 property in it.
if (
one_of["type"] == "object"
and "properties" in one_of
and len(one_of["properties"]) == 1
):
for prop_name in one_of["properties"]:
nested_object = one_of["properties"][prop_name]
if "type" in nested_object and nested_object["type"] == "object":
is_nested_object = True
else:
is_nested_object = False
break
else:
is_nested_object = False
break
if is_nested_object:
# We want to write each of the nested objects. # We want to write each of the nested objects.
for one_of in schema["oneOf"]: for one_of in schema["oneOf"]:
# Get the nested object. # Get the nested object.
@ -1847,9 +1915,9 @@ def getRequestBodyRefs(endpoint: dict) -> List[str]:
return refs return refs
def getRequestBodyType(endpoint: dict) -> Optional[str]: def getRequestBodyTypeSchema(
type_name = None endpoint: dict, data: dict
) -> Tuple[Optional[str], Optional[dict]]:
if "requestBody" in endpoint: if "requestBody" in endpoint:
requestBody = endpoint["requestBody"] requestBody = endpoint["requestBody"]
if "content" in requestBody: if "content" in requestBody:
@ -1859,21 +1927,29 @@ def getRequestBodyType(endpoint: dict) -> Optional[str]:
json = content[content_type]["schema"] json = content[content_type]["schema"]
if "$ref" in json: if "$ref" in json:
ref = json["$ref"].replace("#/components/schemas/", "") ref = json["$ref"].replace("#/components/schemas/", "")
return ref type_schema = data["components"]["schemas"][ref]
return ref, type_schema
elif json != {}:
logging.error("not a ref: ", json)
raise Exception("not a ref")
elif content_type == "text/plain": elif content_type == "text/plain":
return "bytes" return "str", None
elif content_type == "application/octet-stream": elif content_type == "application/octet-stream":
return "bytes" return "bytes", None
elif content_type == "application/x-www-form-urlencoded": elif content_type == "application/x-www-form-urlencoded":
json = content[content_type]["schema"] form = content[content_type]["schema"]
if "$ref" in json: if "$ref" in form:
ref = json["$ref"].replace("#/components/schemas/", "") ref = form["$ref"].replace("#/components/schemas/", "")
return ref type_schema = data["components"]["schemas"][ref]
return ref, type_schema
elif form != {}:
logging.error("not a ref: ", form)
raise Exception("not a ref")
else: else:
logging.error("unsupported content type: ", content_type) logging.error("unsupported content type: ", content_type)
raise Exception("unsupported content type") raise Exception("unsupported content type")
return type_name return None, None
def to_camel_case(s: str): def to_camel_case(s: str):
@ -1931,6 +2007,41 @@ def getOneOfRefType(schema: dict) -> str:
raise Exception("Cannot get oneOf ref type for schema: ", schema) raise Exception("Cannot get oneOf ref type for schema: ", schema)
def isNestedObjectOneOf(schema: dict) -> bool:
is_nested_object = False
for one_of in schema["oneOf"]:
# Check if each are an object w 1 property in it.
if (
one_of["type"] == "object"
and "properties" in one_of
and len(one_of["properties"]) == 1
):
for prop_name in one_of["properties"]:
nested_object = one_of["properties"][prop_name]
if "type" in nested_object and nested_object["type"] == "object":
is_nested_object = True
else:
is_nested_object = False
break
else:
is_nested_object = False
break
return is_nested_object
def isEnumWithDocsOneOf(schema: dict) -> bool:
is_enum_with_docs = False
for one_of in schema["oneOf"]:
if one_of["type"] == "string" and "enum" in one_of and len(one_of["enum"]) == 1:
is_enum_with_docs = True
else:
is_enum_with_docs = False
break
return is_enum_with_docs
# generate a random letter in the range A - Z # generate a random letter in the range A - Z
# do not use O or I. # do not use O or I.
def randletter(): def randletter():

View File

@ -15,7 +15,7 @@ poetry run python generate/generate.py
# Format and lint. # Format and lint.
poetry run isort . poetry run isort .
poetry run black . generate/generate.py docs/conf.py kittycad/client_test.py poetry run black . generate/generate.py docs/conf.py kittycad/client_test.py kittycad/examples_test.py
poetry run ruff check --fix . poetry run ruff check --fix .
# We ignore errors here but we should eventually fix them. # We ignore errors here but we should eventually fix them.
poetry run mypy . poetry run mypy .

View File

@ -103,6 +103,8 @@ from kittycad.models.api_call_status import ApiCallStatus
from kittycad.models.billing_info import BillingInfo from kittycad.models.billing_info import BillingInfo
from kittycad.models.code_language import CodeLanguage from kittycad.models.code_language import CodeLanguage
from kittycad.models.created_at_sort_mode import CreatedAtSortMode from kittycad.models.created_at_sort_mode import CreatedAtSortMode
from kittycad.models.draw_circle import DrawCircle
from kittycad.models.drawing_cmd_id import DrawingCmdId
from kittycad.models.drawing_cmd_req import DrawingCmdReq from kittycad.models.drawing_cmd_req import DrawingCmdReq
from kittycad.models.drawing_cmd_req_batch import DrawingCmdReqBatch from kittycad.models.drawing_cmd_req_batch import DrawingCmdReqBatch
from kittycad.models.email_authentication_form import EmailAuthenticationForm from kittycad.models.email_authentication_form import EmailAuthenticationForm
@ -629,13 +631,17 @@ def test_auth_email():
auth_email.sync( auth_email.sync(
client=client, client=client,
body=EmailAuthenticationForm, body=EmailAuthenticationForm(
email="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
auth_email.sync_detailed( auth_email.sync_detailed(
client=client, client=client,
body=EmailAuthenticationForm, body=EmailAuthenticationForm(
email="<string>",
),
) )
@ -648,13 +654,17 @@ async def test_auth_email_async():
await auth_email.asyncio( await auth_email.asyncio(
client=client, client=client,
body=EmailAuthenticationForm, body=EmailAuthenticationForm(
email="<string>",
),
) )
# OR run async with more info # OR run async with more info
await auth_email.asyncio_detailed( await auth_email.asyncio_detailed(
client=client, client=client,
body=EmailAuthenticationForm, body=EmailAuthenticationForm(
email="<string>",
),
) )
@ -745,13 +755,33 @@ def test_cmd():
cmd.sync( cmd.sync(
client=client, client=client,
body=DrawingCmdReq, body=DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
cmd.sync_detailed( cmd.sync_detailed(
client=client, client=client,
body=DrawingCmdReq, body=DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
),
) )
@ -764,13 +794,33 @@ async def test_cmd_async():
await cmd.asyncio( await cmd.asyncio(
client=client, client=client,
body=DrawingCmdReq, body=DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
),
) )
# OR run async with more info # OR run async with more info
await cmd.asyncio_detailed( await cmd.asyncio_detailed(
client=client, client=client,
body=DrawingCmdReq, body=DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
),
) )
@ -781,13 +831,43 @@ def test_cmd_batch():
cmd_batch.sync( cmd_batch.sync(
client=client, client=client,
body=DrawingCmdReqBatch, body=DrawingCmdReqBatch(
cmds={
"<string>": DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
)
},
file_id="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
cmd_batch.sync_detailed( cmd_batch.sync_detailed(
client=client, client=client,
body=DrawingCmdReqBatch, body=DrawingCmdReqBatch(
cmds={
"<string>": DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
)
},
file_id="<string>",
),
) )
@ -800,13 +880,43 @@ async def test_cmd_batch_async():
await cmd_batch.asyncio( await cmd_batch.asyncio(
client=client, client=client,
body=DrawingCmdReqBatch, body=DrawingCmdReqBatch(
cmds={
"<string>": DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
)
},
file_id="<string>",
),
) )
# OR run async with more info # OR run async with more info
await cmd_batch.asyncio_detailed( await cmd_batch.asyncio_detailed(
client=client, client=client,
body=DrawingCmdReqBatch, body=DrawingCmdReqBatch(
cmds={
"<string>": DrawingCmdReq(
cmd=DrawCircle(
center=[
3.14,
3.14,
],
radius=3.14,
),
cmd_id=DrawingCmdId("<string>"),
file_id="<string>",
)
},
file_id="<string>",
),
) )
@ -2601,13 +2711,27 @@ def test_update_user_self():
update_user_self.sync( update_user_self.sync(
client=client, client=client,
body=UpdateUser, body=UpdateUser(
company="<string>",
discord="<string>",
first_name="<string>",
github="<string>",
last_name="<string>",
phone="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
update_user_self.sync_detailed( update_user_self.sync_detailed(
client=client, client=client,
body=UpdateUser, body=UpdateUser(
company="<string>",
discord="<string>",
first_name="<string>",
github="<string>",
last_name="<string>",
phone="<string>",
),
) )
@ -2620,13 +2744,27 @@ async def test_update_user_self_async():
await update_user_self.asyncio( await update_user_self.asyncio(
client=client, client=client,
body=UpdateUser, body=UpdateUser(
company="<string>",
discord="<string>",
first_name="<string>",
github="<string>",
last_name="<string>",
phone="<string>",
),
) )
# OR run async with more info # OR run async with more info
await update_user_self.asyncio_detailed( await update_user_self.asyncio_detailed(
client=client, client=client,
body=UpdateUser, body=UpdateUser(
company="<string>",
discord="<string>",
first_name="<string>",
github="<string>",
last_name="<string>",
phone="<string>",
),
) )
@ -3025,13 +3163,19 @@ def test_create_payment_information_for_user():
create_payment_information_for_user.sync( create_payment_information_for_user.sync(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
create_payment_information_for_user.sync_detailed( create_payment_information_for_user.sync_detailed(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
@ -3044,13 +3188,19 @@ async def test_create_payment_information_for_user_async():
await create_payment_information_for_user.asyncio( await create_payment_information_for_user.asyncio(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
# OR run async with more info # OR run async with more info
await create_payment_information_for_user.asyncio_detailed( await create_payment_information_for_user.asyncio_detailed(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
@ -3061,13 +3211,19 @@ def test_update_payment_information_for_user():
update_payment_information_for_user.sync( update_payment_information_for_user.sync(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
# OR if you need more info (e.g. status_code) # OR if you need more info (e.g. status_code)
update_payment_information_for_user.sync_detailed( update_payment_information_for_user.sync_detailed(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
@ -3080,13 +3236,19 @@ async def test_update_payment_information_for_user_async():
await update_payment_information_for_user.asyncio( await update_payment_information_for_user.asyncio(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )
# OR run async with more info # OR run async with more info
await update_payment_information_for_user.asyncio_detailed( await update_payment_information_for_user.asyncio_detailed(
client=client, client=client,
body=BillingInfo, body=BillingInfo(
name="<string>",
phone="<string>",
),
) )

View File

@ -96,6 +96,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
exclude = [] exclude = []
show_error_codes = true show_error_codes = true
ignore_missing_imports = true ignore_missing_imports = true
check_untyped_defs = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--doctest-modules" addopts = "--doctest-modules"