diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9c7333e..e6f543c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint-command: - ruff check --output-format=github . - black --check --diff . - - mypy model_bakery + - ty check model_bakery steps: - uses: actions/checkout@v6 - name: Install uv diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cad67f3..05e5bfea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Changed +- [dev] Replace mypy with ty as the primary type checker with stricter type checks ### Removed diff --git a/Makefile b/Makefile index 15599660..6f4a0030 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,6 @@ release: lint: @black . @ruff check . - @mypy model_bakery + @ty check model_bakery .PHONY: help test release lint diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 5ed9c5aa..78a8d7b9 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -719,7 +719,7 @@ def _handle_m2m(self, instance: Model): m2m_relation.source_field_name: instance, m2m_relation.target_field_name: value, } - make( + make( # ty: ignore[no-matching-overload] cast(type[Model], through_model), _using=self._using, **base_kwargs, diff --git a/model_bakery/generators.py b/model_bakery/generators.py index 70e71d06..f7300027 100644 --- a/model_bakery/generators.py +++ b/model_bakery/generators.py @@ -43,13 +43,13 @@ # PostgreSQL-specific field (only available when psycopg is installed) from django.contrib.postgres.fields import ArrayField except ImportError: - ArrayField = None + ArrayField = None # type: ignore[misc,assignment] try: # PostgreSQL-specific field (only available when psycopg is installed) from django.contrib.postgres.fields import HStoreField except ImportError: - HStoreField = None + HStoreField = None # type: ignore[misc,assignment] try: # PostgreSQL-specific fields (only available when psycopg is installed) @@ -59,9 +59,9 @@ CITextField, ) except ImportError: - CICharField = None - CIEmailField = None - CITextField = None + CICharField = None # type: ignore[misc,assignment] + CIEmailField = None # type: ignore[misc,assignment] + CITextField = None # type: ignore[misc,assignment] try: @@ -74,11 +74,11 @@ IntegerRangeField, ) except ImportError: - BigIntegerRangeField = None - DateRangeField = None - DateTimeRangeField = None - DecimalRangeField = None - IntegerRangeField = None + BigIntegerRangeField = None # type: ignore[misc,assignment] + DateRangeField = None # type: ignore[misc,assignment] + DateTimeRangeField = None # type: ignore[misc,assignment] + DecimalRangeField = None # type: ignore[misc,assignment] + IntegerRangeField = None # type: ignore[misc,assignment] default_mapping = { diff --git a/model_bakery/random_gen.py b/model_bakery/random_gen.py index 0b798dd5..dfee321f 100644 --- a/model_bakery/random_gen.py +++ b/model_bakery/random_gen.py @@ -515,9 +515,11 @@ def gen_pg_numbers_range(number_cast: Callable[[int], Any]) -> Callable: def gen_range(): try: - from psycopg.types.range import Range + from psycopg.types.range import Range # ty: ignore[unresolved-import] except ImportError: - from psycopg2._range import NumericRange as Range + from psycopg2._range import ( # ty: ignore[unresolved-import] + NumericRange as Range, + ) base_num = baker_random.randint(1, 100000) return Range(number_cast(-1 * base_num), number_cast(base_num)) @@ -533,9 +535,9 @@ def gen_date_range(): to avoid empty ranges in tests. """ try: - from psycopg.types.range import DateRange + from psycopg.types.range import DateRange # ty: ignore[unresolved-import] except ImportError: - from psycopg2.extras import DateRange + from psycopg2.extras import DateRange # ty: ignore[unresolved-import] base_date = gen_date() interval = gen_interval(min_interval=24 * 60 * 60 * 1000) @@ -551,9 +553,13 @@ def gen_datetime_range(): to avoid empty ranges in tests. """ try: - from psycopg.types.range import TimestamptzRange + from psycopg.types.range import ( # ty: ignore[unresolved-import] + TimestamptzRange, + ) except ImportError: - from psycopg2.extras import DateTimeTZRange as TimestamptzRange + from psycopg2.extras import ( # ty: ignore[unresolved-import] + DateTimeTZRange as TimestamptzRange, + ) base_datetime = gen_datetime() interval = gen_interval(min_interval=24 * 60 * 60 * 1000) diff --git a/model_bakery/recipe.py b/model_bakery/recipe.py index 30a86068..1b8d99b2 100644 --- a/model_bakery/recipe.py +++ b/model_bakery/recipe.py @@ -5,12 +5,9 @@ Any, Generic, TypeVar, - cast, overload, ) -from django.db.models import Model - from . import baker from ._types import M from .exceptions import RecipeNotFound @@ -178,19 +175,19 @@ def extend(self: _T, **attrs: Any) -> _T: return type(self)(self._model, **attr_mapping) -def _load_recipe_from_calling_module(recipe: str) -> Recipe[Model]: +def _load_recipe_from_calling_module(recipe_name: str) -> Recipe[Any]: """Load `Recipe` from the string attribute given from the calling module. Args: - recipe (str): the name of the recipe attribute within the module from + recipe_name (str): the name of the recipe attribute within the module from which it should be loaded Returns: (Recipe): recipe resolved from calling module """ - recipe = getattr(get_calling_module(2), recipe) + recipe = getattr(get_calling_module(2), recipe_name) if recipe: - return cast(Recipe[Model], recipe) + return recipe else: raise RecipeNotFound @@ -217,16 +214,19 @@ def foreign_key( This resolves recipes supplied as strings from other module paths or from the calling code's module. """ + resolved_recipe: Recipe[M] if isinstance(recipe, str): # Load `Recipe` from string before handing off to `RecipeForeignKey` try: # Try to load from another module - recipe = baker._recipe(recipe) + resolved_recipe = baker._recipe(recipe) except (AttributeError, ImportError, ValueError): # Probably not in another module, so load it from calling module - recipe = _load_recipe_from_calling_module(cast(str, recipe)) + resolved_recipe = _load_recipe_from_calling_module(recipe) + else: + resolved_recipe = recipe - return RecipeForeignKey(cast(Recipe[M], recipe), one_to_one) + return RecipeForeignKey(resolved_recipe, one_to_one) class related(Generic[M]): # FIXME diff --git a/pyproject.toml b/pyproject.toml index a01dcdcd..e7e75df0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dev = [ "pytest-django", "black", "ruff", - "mypy", + "ty", ] [project.urls] @@ -84,9 +84,14 @@ source = [ [tool.coverage.report] show_missing = true -[tool.mypy] -ignore_missing_imports = true -disallow_untyped_calls = true +[tool.ty.rules] +# Django dynamic attributes (.objects, ._meta, etc.) +# TODO: remove these two ignores when ty adds better Django support +# https://github.com/astral-sh/ty/issues/1018 +unresolved-attribute = "ignore" +possibly-missing-attribute = "ignore" +# Allow `# type: ignore` comments (mypy syntax) without warnings +unused-ignore-comment = "ignore" [tool.pytest.ini_options] addopts = "--tb=short -rxs --nomigrations"