From 96af6809c768054a75582a11bfc655913b6a3f17 Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:41:49 +1000 Subject: [PATCH 1/3] Migrated to ruff/ty --- .github/workflows/linttest.yml | 188 +++---------------- pyproject.toml | 37 ++-- src/assertical/asserts/pandas.py | 16 +- src/assertical/asserts/time.py | 6 +- src/assertical/asserts/type.py | 24 +-- src/assertical/fake/generator.py | 38 ++-- src/assertical/fake/http.py | 11 +- src/assertical/fixtures/environment.py | 3 +- src/assertical/fixtures/fastapi.py | 5 +- src/assertical/fixtures/generator.py | 2 +- src/assertical/fixtures/postgres.py | 3 +- src/assertical/snapshot.py | 5 +- tests/asserts/test_assert_type.py | 3 +- tests/fake/test_generator_common.py | 9 +- tests/fake/test_generator_complex_example.py | 19 +- tests/fake/test_generator_dataclass.py | 3 +- tests/fake/test_generator_py310_optional.py | 2 +- tests/fake/test_generator_pydantic.py | 1 - tests/fake/test_generator_pydantic_xml.py | 5 +- tests/fake/test_generator_sql_alchemy.py | 27 +-- tests/fixtures/test_environment.py | 4 +- tests/fixtures/test_generator_fixtures.py | 2 +- tests/test_snapshot.py | 2 +- 23 files changed, 149 insertions(+), 266 deletions(-) diff --git a/.github/workflows/linttest.yml b/.github/workflows/linttest.yml index f5bc334..b0d724a 100644 --- a/.github/workflows/linttest.yml +++ b/.github/workflows/linttest.yml @@ -8,179 +8,53 @@ on: jobs: # This will run bandit and produce a security report if there are any failures - bandit: + linting: runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + pyver: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Security check - Bandit - uses: joshvote/bandit-report-artifacts@v0.0.6 - with: - project_path: . - ignore_failure: false - config_file: pyproject.toml - - - name: Security check report artifacts - uses: actions/upload-artifact@v4 - if: failure() - with: - name: Security report - path: output/security_report.txt - overwrite: true - - # This will run the latest flake8 with python 3.11 and report on any errors - flake8_py311: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ matrix.pyver }} + - name: Install linters + run: pip install -e .[dev,test] - - name: Setup flake8 annotations - uses: rbialon/flake8-annotations@v1 + - name: Lint (ruff) + run: ruff check . + - name: Format check + run: ruff format --check . + - name: Typing (ty) + run: ty check . + - name: Security scan (bandit) + run: bandit -r src/ - - name: Lint with flake8 - if: always() - run: | - pip install flake8 - flake8 . --count --statistics - - # This will run the latest black to see if there are any code formatting errors - black_formatting: + pytest: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: psf/black@stable - - # This will run the latest mypy with python3.11 to see if there are any type errors - mypy_py311: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Setup Python - uses: actions/setup-python@v4.2.0 - with: - python-version: "3.11" + continue-on-error: true + strategy: + fail-fast: false + matrix: + pyver: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - - name: Install Dependencies - run: | - pip install .[all] - - - name: Add mypy annotator - uses: pr-annotators/mypy-pr-annotator@v1.0.0 - - - name: Run mypy - run: | - mypy src/ - - # This will run the pytest with python3.9 - pytest_py39: - runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: ${{ matrix.pyver }} - name: Install Dependencies run: | - pip install .[all] + pip install .[test] - name: Run Pytest - run: | - pytest --junit-xml=.test_report.xml - - - name: Upload Results - uses: test-summary/action@v1 - with: - paths: .test_report.xml - if: always() - - # This will run the pytest with python3.10 - pytest_py310: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Setup Python - uses: actions/setup-python@v4.2.0 - with: - python-version: "3.10" - - - name: Install Dependencies - run: | - pip install .[all] - - - name: Run Pytest - run: | - pytest --junit-xml=.test_report.xml - - - name: Upload Results - uses: test-summary/action@v1 - with: - paths: .test_report.xml - if: always() - - # This will run the pytest with python3.11 - pytest_py311: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Setup Python - uses: actions/setup-python@v4.2.0 - with: - python-version: "3.11" - - - name: Install Dependencies - run: | - pip install .[all] - - - name: Run Pytest - run: | - pytest --junit-xml=.test_report.xml - - - name: Upload Results - uses: test-summary/action@v1 - with: - paths: .test_report.xml - if: always() - - - # This will run the pytest with python3.12 - pytest_py312: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Setup Python - uses: actions/setup-python@v4.2.0 - with: - python-version: "3.12" - - - name: Install Dependencies - run: | - pip install .[all] - - - name: Run Pytest - run: | - pytest --junit-xml=.test_report.xml - - - name: Upload Results - uses: test-summary/action@v1 - with: - paths: .test_report.xml - if: always() \ No newline at end of file + run: pytest diff --git a/pyproject.toml b/pyproject.toml index 8f527d9..dc31a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,31 @@ -[tool.black] -line-length = 120 - [tool.pytest.ini_options] pythonpath = ["src/"] testpaths = "tests" -[tool.isort] -profile = "black" - [tool.bandit] exclude_dirs = ["tests"] skips = ["B101"] -[tool.mypy] -exclude = ["tests"] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -namespace_packages = true -warn_redundant_casts = true -warn_unused_ignores = true +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "S", "C90", "B", "UP", "ANN", "N"] +ignore = ["E721", "B017", "UP035", "E731", "UP045"] + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint.per-file-ignores] +"src/**" = ["ANN401"] +"**/asserts/**" = ["S101", "B011"] +"tests/**" = ["S", "ANN", "N"] + +[tool.ty.src] +include = ["src", "tests"] +[tool.ty.rules] +invalid-type-form = "ignore" [build-system] requires = ["setuptools >= 40.9.0", "wheel"] @@ -60,7 +63,7 @@ Homepage = "https://github.com/bsgip/assertical" [project.optional-dependencies] all = ["assertical[dev,fastapi,pandas,pydantic,postgres,xml]"] -dev = ["bandit", "flake8", "mypy", "black", "coverage"] +dev = ["bandit", "ruff", "ty", "coverage"] fastapi = ["fastapi[standard]", "asgi_lifespan", "uvicorn"] pandas = ["pandas", "pandas_stubs", "numpy"] pydantic = ["pydantic"] diff --git a/src/assertical/asserts/pandas.py b/src/assertical/asserts/pandas.py index 4b3635c..8b50691 100644 --- a/src/assertical/asserts/pandas.py +++ b/src/assertical/asserts/pandas.py @@ -63,15 +63,15 @@ def print_val(v: Any) -> str: query = " & ".join([f"`{k}`=={print_val(v)}" for k, v in col_values.items()]) try: count = len(df.query(query)) - except Exception: - raise AssertionError(f"Column(s) don't exist. col_values: {col_values}") + except Exception as exc: + raise AssertionError(f"Column(s) don't exist. col_values: {col_values}") from exc if expected_min_count is not None: - assert ( - count >= expected_min_count - ), f"Expected at least {expected_min_count} match(es) for {query}\n{df[list(col_values.keys())].to_string()}" + assert count >= expected_min_count, ( + f"Expected at least {expected_min_count} match(es) for {query}\n{df[list(col_values.keys())].to_string()}" + ) if expected_max_count is not None: - assert ( - count <= expected_max_count - ), f"Expected at most {expected_max_count} match(es) for {query}\n{df[list(col_values.keys())].to_string()}" + assert count <= expected_max_count, ( + f"Expected at most {expected_max_count} match(es) for {query}\n{df[list(col_values.keys())].to_string()}" + ) diff --git a/src/assertical/asserts/time.py b/src/assertical/asserts/time.py index 2b0b4c3..91ffdec 100644 --- a/src/assertical/asserts/time.py +++ b/src/assertical/asserts/time.py @@ -14,9 +14,9 @@ def assert_fuzzy_datetime_match( actual_time = datetime.fromtimestamp(float(actual_time)) delta_seconds = expected_time.timestamp() - actual_time.timestamp() - assert ( - abs(delta_seconds) <= fuzziness_seconds - ), f"Expected {expected_time} to be within {fuzziness_seconds} of {actual_time} but it was {delta_seconds}" + assert abs(delta_seconds) <= fuzziness_seconds, ( + f"Expected {expected_time} to be within {fuzziness_seconds} of {actual_time} but it was {delta_seconds}" + ) def assert_nowish(expected_time: Union[int, float, datetime], fuzziness_seconds: int = 20) -> None: diff --git a/src/assertical/asserts/type.py b/src/assertical/asserts/type.py index da8c7fb..358bdbd 100644 --- a/src/assertical/asserts/type.py +++ b/src/assertical/asserts/type.py @@ -6,9 +6,9 @@ def assert_list_type(expected_element_type: type, obj: Any, count: Optional[int] if count is specified - an additional assert will be made on the count of elements in obj""" assert obj is not None - assert ( - isinstance(obj, list) or get_origin(type(obj)) == list - ), f"Expected a list type for obj but got {type(obj)} instead" + assert isinstance(obj, list) or get_origin(type(obj)) == list, ( + f"Expected a list type for obj but got {type(obj)} instead" + ) assert_iterable_type(expected_element_type, obj, count=count) @@ -17,9 +17,9 @@ def assert_set_type(expected_element_type: type, obj: Any, count: Optional[int] if count is specified - an additional assert will be made on the count of elements in obj""" assert obj is not None - assert ( - isinstance(obj, set) or get_origin(type(obj)) == set - ), f"Expected a set type for obj but got {type(obj)} instead" + assert isinstance(obj, set) or get_origin(type(obj)) == set, ( + f"Expected a set type for obj but got {type(obj)} instead" + ) assert_iterable_type(expected_element_type, obj, count=count) @@ -28,9 +28,9 @@ def assert_dict_type(expected_key_type: type, expected_value_type: type, obj: An if count is specified - an additional assert will be made on the count of elements in obj""" assert obj is not None - assert ( - isinstance(obj, dict) or get_origin(type(obj)) == dict - ), f"Expected a dict type for obj but got {type(obj)} instead" + assert isinstance(obj, dict) or get_origin(type(obj)) == dict, ( + f"Expected a dict type for obj but got {type(obj)} instead" + ) assert_iterable_type(expected_key_type, obj.keys(), count=count) assert_iterable_type(expected_value_type, obj.values(), count=count) @@ -49,9 +49,9 @@ def assert_iterable_type(expected_element_type: type, obj: Any, count: Optional[ enumerated_item_count = 0 for i, val in enumerate(obj): enumerated_item_count += 1 - assert isinstance( - val, expected_element_type - ), f"obj[{i}]: Element has type {type(val)} instead of {expected_element_type}" + assert isinstance(val, expected_element_type), ( + f"obj[{i}]: Element has type {type(val)} instead of {expected_element_type}" + ) if count is not None: assert enumerated_item_count == count diff --git a/src/assertical/fake/generator.py b/src/assertical/fake/generator.py index 00143f2..e923001 100644 --- a/src/assertical/fake/generator.py +++ b/src/assertical/fake/generator.py @@ -1,5 +1,6 @@ import inspect import sys +from collections.abc import Generator from dataclasses import dataclass, fields, is_dataclass from datetime import datetime, time, timedelta, timezone from decimal import Decimal @@ -7,7 +8,6 @@ from typing import ( Any, Callable, - Generator, Optional, TypeVar, Union, @@ -17,15 +17,15 @@ ) try: - from types import NoneType + from types import NoneType # type: ignore except ImportError: - NoneType = type(None) # type: ignore + NoneType = type(None) try: - from types import UnionType + from types import UnionType # type: ignore except ImportError: - UnionType = type(None) # type: ignore + UnionType = type(None) try: from pydantic import BaseModel @@ -149,7 +149,8 @@ def get_enum_type(t: Optional[type], include_optional: bool) -> Optional[type]: if is_optional_type(t): optional = True inner_enum_type = get_optional_type_argument(t) - assert inner_enum_type is not None + if inner_enum_type is None: + raise ValueError(f"enum type {t} could not be decomposed from its Optional[enum] value.") t_origin = get_origin(t) is_union = (t_origin == Union or t_origin == UnionType) and len([a for a in get_args(t) if a is not NoneType]) > 1 @@ -158,7 +159,7 @@ def get_enum_type(t: Optional[type], include_optional: bool) -> Optional[type]: arg_enum = get_enum_type(union_arg, include_optional) if arg_enum is not None: if optional and include_optional: - return Optional[arg_enum] # type: ignore + return Optional[arg_enum] else: return arg_enum elif safe_is_subclass(inner_enum_type, Enum): @@ -230,7 +231,7 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional for union_arg in get_args(t): prim_type = get_first_generatable_primitive(union_arg, include_optional=False) if prim_type is not None: - return Optional[prim_type] if include_optional_type else prim_type # type: ignore + return Optional[prim_type] if include_optional_type else prim_type return None @@ -382,9 +383,10 @@ def enumerate_class_properties( # noqa: C901 second_type_to_generate = get_first_generatable_primitive( second_member_type, include_optional=False ) - assert ( - second_type_to_generate is not None - ), f"Error generating member {member_name}. Couldn't find type for {second_member_type}" + if second_type_to_generate is None: + raise ValueError( + f"Error generating member {member_name}. Couldnt find type for {second_member_type}" + ) second_is_primitive = True elif get_generatable_class_base(second_member_type) is not None: second_type_to_generate = ( @@ -403,16 +405,16 @@ def enumerate_class_properties( # noqa: C901 # Currently we're digging around in the guts of the Base registry - there maybe an official way to do this? if t_generatable_base == DeclarativeBase: if isinstance(member_type, str): - member_type = t.registry._class_registry[member_type] + member_type = t.registry._class_registry[member_type] # type: ignore if t_generatable_base == DeclarativeBaseNoMeta: if isinstance(member_type, str): - member_type = t.registry._class_registry[member_type] + member_type = t.registry._class_registry[member_type] # type: ignore if is_generatable_type(member_type): type_to_generate = get_first_generatable_primitive(member_type, include_optional=False) - assert ( - type_to_generate is not None - ), f"Error generating member {member_name}. Couldn't find type for {member_type}" + if type_to_generate is None: + raise ValueError(f"Error generating member {member_name}. Couldn't find type for {member_type}") + is_primitive = True elif get_generatable_class_base(member_type) is not None: type_to_generate = optional_arg_type if is_optional else member_type @@ -442,7 +444,7 @@ def _generate_class_instance_with_seed( # noqa: C901 ) -> tuple[AnyType, int]: """Internal function - performs the work of generate_class_instance but returns each generated type with an updated seed value""" - t = remove_passthrough_type(t) + t = remove_passthrough_type(t) # type: ignore # stop back references from infinite looping if visited_type_stack is None: @@ -733,7 +735,7 @@ def register_value_generator(t: type, generator: Callable[[int], Any]) -> None: register_value_generator(Decimal, lambda seed: Decimal(seed)) register_value_generator( datetime, - lambda seed: (datetime(2010, 1, 1, tzinfo=timezone.utc) + timedelta(days=seed) + timedelta(seconds=seed)), + lambda seed: datetime(2010, 1, 1, tzinfo=timezone.utc) + timedelta(days=seed) + timedelta(seconds=seed), ) register_value_generator(time, lambda seed: time(seed % 24, seed % 60, (seed + 1) % 60)) register_value_generator(timedelta, lambda seed: timedelta(seconds=seed)) diff --git a/src/assertical/fake/http.py b/src/assertical/fake/http.py index b67d67a..c174df4 100644 --- a/src/assertical/fake/http.py +++ b/src/assertical/fake/http.py @@ -2,17 +2,17 @@ from dataclasses import dataclass from datetime import datetime from enum import Enum, auto -from typing import Any, Optional, Union +from typing import Any, Optional, Union, cast from httpx import Response from httpx._types import HeaderTypes, RequestContent # HTTPMethod is only defined in python >= 3.11 try: - from http import HTTPMethod + from http import HTTPMethod # type: ignore except ImportError: - class HTTPMethod(Enum): # type: ignore + class HTTPMethod(Enum): DELETE = auto() GET = auto() HEAD = auto() @@ -58,7 +58,7 @@ def set_results(self, result: Union[Response, Exception, dict, list[Union[Respon self.call_count_by_method_uri = {} if isinstance(result, dict): - self.results_by_uri = result + self.results_by_uri = cast(dict, result) self.result = None else: self.results_by_uri = {} @@ -79,7 +79,7 @@ def _raise_or_return(self, result: Union[Response, Exception, list[Union[Respons if isinstance(result, list): if len(result) > 0: - next_result = result.pop(0) + next_result = cast(Union[Response, Exception], result.pop(0)) return self._raise_or_return(next_result) else: raise Exception("Mocking error - no more responses/errors in list.") @@ -191,7 +191,6 @@ async def wait_for_request(self, timeout_seconds: float) -> bool: Returns True if a request was "consumed" or False if the timeout was hit""" try: - await wait_for(self.request_semaphore.acquire(), timeout_seconds) except TimeoutError: return False diff --git a/src/assertical/fixtures/environment.py b/src/assertical/fixtures/environment.py index 2046a0a..dd7eb7e 100644 --- a/src/assertical/fixtures/environment.py +++ b/src/assertical/fixtures/environment.py @@ -1,6 +1,7 @@ import os +from collections.abc import Generator from contextlib import contextmanager -from typing import Callable, Generator +from typing import Callable from assertical.snapshot import snapshot_kvp_store diff --git a/src/assertical/fixtures/fastapi.py b/src/assertical/fixtures/fastapi.py index da511ee..4c9fbcc 100644 --- a/src/assertical/fixtures/fastapi.py +++ b/src/assertical/fixtures/fastapi.py @@ -1,6 +1,7 @@ import asyncio +from collections.abc import AsyncGenerator, Awaitable from contextlib import asynccontextmanager -from typing import Any, AsyncGenerator, Awaitable, Optional +from typing import Any, Optional import uvicorn from asgi_lifespan import LifespanManager @@ -15,7 +16,7 @@ class UvicornTestServer(uvicorn.Server): Originally adapted from https://github.com/miguelgrinberg/python-socketio/issues/332#issuecomment-712928157""" - def __init__(self, app: FastAPI, host: str, port: int = 13842): + def __init__(self, app: FastAPI, host: str, port: int = 13842) -> None: """Create a Uvicorn test server Args: diff --git a/src/assertical/fixtures/generator.py b/src/assertical/fixtures/generator.py index 3963498..019d659 100644 --- a/src/assertical/fixtures/generator.py +++ b/src/assertical/fixtures/generator.py @@ -1,5 +1,5 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Generator from assertical.fake.generator import ( BASE_CLASS_PUBLIC_MEMBERS, diff --git a/src/assertical/fixtures/postgres.py b/src/assertical/fixtures/postgres.py index 87155bd..173e9cc 100644 --- a/src/assertical/fixtures/postgres.py +++ b/src/assertical/fixtures/postgres.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator, Optional +from typing import Optional from psycopg import Connection from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine diff --git a/src/assertical/snapshot.py b/src/assertical/snapshot.py index 13f67d3..28f2a39 100644 --- a/src/assertical/snapshot.py +++ b/src/assertical/snapshot.py @@ -1,5 +1,6 @@ +from collections.abc import Generator from contextlib import contextmanager -from typing import Callable, Generator, TypeVar, cast +from typing import Callable, TypeVar, cast KeyType = TypeVar("KeyType") ValueType = TypeVar("ValueType") @@ -48,7 +49,7 @@ def snapshot_kvp_store( # Firstly iterate the current environment to see what we need to rectify final_snapshot: dict[KeyType, ValueType] = snapshot() visited_keys = set() - for k, v in final_snapshot.items(): + for k, _ in final_snapshot.items(): visited_keys.add(k) if k in original_snapshot: diff --git a/tests/asserts/test_assert_type.py b/tests/asserts/test_assert_type.py index 6c38e77..37b3868 100644 --- a/tests/asserts/test_assert_type.py +++ b/tests/asserts/test_assert_type.py @@ -1,5 +1,6 @@ +from collections.abc import Iterable from dataclasses import dataclass -from typing import Any, Iterable, Optional +from typing import Any, Optional import pytest diff --git a/tests/fake/test_generator_common.py b/tests/fake/test_generator_common.py index 7b47948..e4e46e7 100644 --- a/tests/fake/test_generator_common.py +++ b/tests/fake/test_generator_common.py @@ -1,8 +1,9 @@ import sys +from collections.abc import Generator from dataclasses import dataclass from datetime import datetime, timedelta -from enum import IntEnum, IntFlag, auto -from typing import Generator, List, Optional, Union +from enum import Enum, IntEnum, IntFlag, auto +from typing import List, Optional, Union import pytest from sqlalchemy.orm import Mapped @@ -147,7 +148,7 @@ def test_get_enum_type_with_enums_py310_optional(t): @pytest.mark.parametrize("t", ALL_ENUM_TYPES) -def test_generate_value_enums(t: type): +def test_generate_value_enums(t: type[Enum]): """Tests that generate_value plays nice with enum values""" COUNT = len(t) * 3 @@ -269,7 +270,7 @@ def test_is_generatable_type(): # check collections assert not is_generatable_type(Optional[list[ReferenceDataclass]]) - assert not is_generatable_type(Optional[List[ReferenceDataclass]]) + assert not is_generatable_type(Optional[List[ReferenceDataclass]]) # noqa: UP006 assert not is_generatable_type(list[ReferenceDataclass]) assert not is_generatable_type(list[int]) assert not is_generatable_type(set[datetime]) diff --git a/tests/fake/test_generator_complex_example.py b/tests/fake/test_generator_complex_example.py index 22e002d..739f694 100644 --- a/tests/fake/test_generator_complex_example.py +++ b/tests/fake/test_generator_complex_example.py @@ -27,7 +27,6 @@ class Student(Base): class ReportCard(Base): - __tablename__ = "_report_card" report_card_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) @@ -76,18 +75,18 @@ def test_generate_complex_relationships(): report_card = student.report_cards[0] assert isinstance(report_card.english_result, EnglishResult) assert isinstance(report_card.math_result, MathResult) - assert report_card.student is None or id(report_card.student) == id( - student - ), "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + assert report_card.student is None or id(report_card.student) == id(student), ( + "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + ) english_result = report_card.english_result - assert english_result.report_card is None or id(english_result.report_card) == id( - report_card - ), "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + assert english_result.report_card is None or id(english_result.report_card) == id(report_card), ( + "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + ) math_result = report_card.math_result - assert math_result.report_card is None or id(math_result.report_card) == id( - report_card - ), "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + assert math_result.report_card is None or id(math_result.report_card) == id(report_card), ( + "Back reference will not be populated by our code - SQLAlchemy might auto fill but that's OK" + ) assert english_result.grade != math_result.grade, "Values should be using unique seeds" diff --git a/tests/fake/test_generator_dataclass.py b/tests/fake/test_generator_dataclass.py index 55e6477..6b0f207 100644 --- a/tests/fake/test_generator_dataclass.py +++ b/tests/fake/test_generator_dataclass.py @@ -1,10 +1,11 @@ from __future__ import annotations import sys +from collections.abc import Generator from dataclasses import dataclass, field from datetime import datetime, time -from typing import Any, Generator, Optional from pathlib import Path +from typing import Any, Optional import pytest diff --git a/tests/fake/test_generator_py310_optional.py b/tests/fake/test_generator_py310_optional.py index 1bbde4f..16e4d15 100644 --- a/tests/fake/test_generator_py310_optional.py +++ b/tests/fake/test_generator_py310_optional.py @@ -5,8 +5,8 @@ if sys.version_info >= (3, 10): # This entire file is only relevant for py310 and greater + from collections.abc import Generator from dataclasses import dataclass - from typing import Generator import pytest diff --git a/tests/fake/test_generator_pydantic.py b/tests/fake/test_generator_pydantic.py index 2b898b9..cb2ccb9 100644 --- a/tests/fake/test_generator_pydantic.py +++ b/tests/fake/test_generator_pydantic.py @@ -10,7 +10,6 @@ class ChildType(BaseModel): - child_id: int period_start: datetime duration_seconds: int diff --git a/tests/fake/test_generator_pydantic_xml.py b/tests/fake/test_generator_pydantic_xml.py index 2b8cfee..7409bb4 100644 --- a/tests/fake/test_generator_pydantic_xml.py +++ b/tests/fake/test_generator_pydantic_xml.py @@ -1,5 +1,6 @@ import sys -from typing import Generator, Optional, Union +from collections.abc import Generator +from typing import Optional, Union import pytest from pydantic_xml import BaseXmlModel, element @@ -224,7 +225,7 @@ def test_check_class_instance_equality(): # check a single property being out expected = generate_class_instance(FurtherXmlClass, seed=1, generate_relationships=False, optional_is_none=True) actual = generate_class_instance(FurtherXmlClass, seed=1, generate_relationships=False, optional_is_none=True) - actual.myStr = actual.myStr + "-changed" + actual.myStr = actual.myStr + "-changed" # type: ignore assert ( len( check_class_instance_equality( diff --git a/tests/fake/test_generator_sql_alchemy.py b/tests/fake/test_generator_sql_alchemy.py index e5388ae..1ea7379 100644 --- a/tests/fake/test_generator_sql_alchemy.py +++ b/tests/fake/test_generator_sql_alchemy.py @@ -1,8 +1,9 @@ import sys +from collections.abc import Generator from datetime import datetime from enum import IntFlag, auto from itertools import product -from typing import Generator, Optional +from typing import Optional import pytest from sqlalchemy import ( @@ -247,9 +248,9 @@ def test_generate_sql_alchemy_instance_multi_relationships(optional_is_none: boo assert p1.children[0].long_name is not None assert p1.children[0].deleted_at is not None assert p1.children[0].parent is not None and p1.children[0].parent == p1, "Backreference should self reference" - assert ( - p1.children[0].created_at != p1.children[0].deleted_at - ), "Checking that fields of the same type get unique values" + assert p1.children[0].created_at != p1.children[0].deleted_at, ( + "Checking that fields of the same type get unique values" + ) assert p1.children[0].long_name != p1.children[0].name, "Checking that fields of the same type get unique values" p2 = generate_class_instance(ParentClass, seed=2, generate_relationships=True, optional_is_none=optional_is_none) @@ -264,18 +265,18 @@ def test_generate_sql_alchemy_instance_multi_relationships(optional_is_none: boo assert p2.children[0].long_name is not None assert p2.children[0].deleted_at is not None assert p2.children[0].parent is not None and p2.children[0].parent == p2, "Backreference should self reference" - assert ( - p2.children[0].created_at != p2.children[0].deleted_at - ), "Checking that fields of the same type get unique values" + assert p2.children[0].created_at != p2.children[0].deleted_at, ( + "Checking that fields of the same type get unique values" + ) assert p2.children[0].long_name != p2.children[0].name, "Checking that fields of the same type get unique values" - assert ( - p1.children[0].created_at != p2.children[0].created_at - ), "Checking that different seed numbers yields different results" + assert p1.children[0].created_at != p2.children[0].created_at, ( + "Checking that different seed numbers yields different results" + ) if not optional_is_none: - assert ( - p1.children[0].deleted_at != p2.children[0].deleted_at - ), "Checking that different seed numbers yields different results" + assert p1.children[0].deleted_at != p2.children[0].deleted_at, ( + "Checking that different seed numbers yields different results" + ) def test_clone_class_instance_sql_alchemy(): diff --git a/tests/fixtures/test_environment.py b/tests/fixtures/test_environment.py index eb4c994..b4490d1 100644 --- a/tests/fixtures/test_environment.py +++ b/tests/fixtures/test_environment.py @@ -11,7 +11,7 @@ NEVER_EXISTS_NAME = "92y539hgf3_123akjh" ALWAYS_EXISTS_NAME = "PATH" if ALWAYS_EXISTS_NAME not in os.environ: - ALWAYS_EXISTS_NAME = os.environ.keys()[0] + ALWAYS_EXISTS_NAME = os.environ.keys()[0] # type: ignore @pytest.mark.parametrize( @@ -36,10 +36,8 @@ def test_environment_snapshot(kvps: list[tuple[str, Optional[str]]]): # update our environment with environment_snapshot(): - # Mess with the environment for k, v in kvps: - # Make the change if v is None: delete_environment_variable(k) diff --git a/tests/fixtures/test_generator_fixtures.py b/tests/fixtures/test_generator_fixtures.py index cc8573d..1e45130 100644 --- a/tests/fixtures/test_generator_fixtures.py +++ b/tests/fixtures/test_generator_fixtures.py @@ -31,7 +31,7 @@ def __init__(self, diff_id: int) -> None: class MyGenerationClass(MyBaseClass): my_id: int = 1 my_str: str = "a" - my_simple_class: MySimpleClass = None + my_simple_class: MySimpleClass = None # type: ignore def __init__(self, my_id: int, my_str: str, my_simple_class: MySimpleClass) -> None: super().__init__() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index d1bdde2..0106f74 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -50,7 +50,7 @@ def test_snapshot_kvp_store_docs_example(): with snapshot_kvp_store( lambda: dict(MY_KEY_VALUE_STORE), # The snapshot function lambda k, v: MY_KEY_VALUE_STORE.update({k: v}), # The update function - lambda k: MY_KEY_VALUE_STORE.pop(k), # The delete function + lambda k: MY_KEY_VALUE_STORE.pop(k), # The delete function # type: ignore ): # Upon entering - nothing has changed assert MY_KEY_VALUE_STORE == {"a": 1, "b": 2} From d650c7491e93a1690d45c1c9815f2321ba649cc3 Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:44:41 +1000 Subject: [PATCH 2/3] Fix action --- .github/workflows/linttest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linttest.yml b/.github/workflows/linttest.yml index b0d724a..db2c6f8 100644 --- a/.github/workflows/linttest.yml +++ b/.github/workflows/linttest.yml @@ -24,7 +24,7 @@ jobs: with: python-version: ${{ matrix.pyver }} - name: Install linters - run: pip install -e .[dev,test] + run: pip install -e .[all] - name: Lint (ruff) run: ruff check . @@ -54,7 +54,7 @@ jobs: - name: Install Dependencies run: | - pip install .[test] + pip install .[all] - name: Run Pytest run: pytest From 5cf687a9a7a5fea15d11d83c23e09d78defc92fd Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:49:01 +1000 Subject: [PATCH 3/3] Fix bandit config --- .github/workflows/linttest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linttest.yml b/.github/workflows/linttest.yml index db2c6f8..a47cb7e 100644 --- a/.github/workflows/linttest.yml +++ b/.github/workflows/linttest.yml @@ -33,7 +33,7 @@ jobs: - name: Typing (ty) run: ty check . - name: Security scan (bandit) - run: bandit -r src/ + run: bandit -c pyproject.toml -r src/ pytest: runs-on: ubuntu-latest