From 49e9fa050a610ed588a4b33909d07e854ac336fc Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Fri, 3 Oct 2025 22:20:48 +0100 Subject: [PATCH 1/4] Fix mypy errors: resolve type issues and remove unused type ignores --- ellar/app/lifespan.py | 2 +- ellar/app/main.py | 8 ++++---- ellar/auth/handlers/schemes/http.py | 2 +- ellar/auth/policy/base.py | 2 +- ellar/cache/module.py | 2 +- ellar/cache/service.py | 6 +++--- ellar/common/decorators/controller.py | 6 +++--- ellar/common/decorators/html.py | 6 +++--- ellar/common/decorators/modules.py | 2 +- ellar/common/interfaces/exceptions.py | 12 ++++++------ ellar/common/params/args/base.py | 14 +++++++------- ellar/common/params/args/resolver_generators.py | 2 +- ellar/common/params/params.py | 10 +++++----- ellar/common/params/resolvers/base.py | 8 ++++---- .../resolvers/system_parameters/background.py | 4 +++- ellar/common/responses/models/base.py | 4 +--- ellar/common/responses/models/file.py | 2 +- ellar/common/responses/models/route.py | 6 +++--- ellar/common/serializer/base.py | 2 +- ellar/core/conf/config.py | 2 +- ellar/core/exceptions/callable_exceptions.py | 4 ++-- ellar/core/modules/config.py | 6 +++--- ellar/core/modules/ref/base.py | 6 +++--- ellar/core/modules/ref/factory.py | 6 +++--- ellar/core/modules/ref/plain.py | 6 +++--- ellar/core/modules/ref/template.py | 6 +++--- ellar/core/routing/file_mount.py | 4 ++-- ellar/core/routing/route.py | 2 +- ellar/core/routing/websocket/handler.py | 6 +++--- ellar/core/security/hashers/argon2.py | 2 +- ellar/core/security/hashers/scrypt.py | 6 +++--- ellar/core/templating/service.py | 6 +++--- ellar/core/versioning/resolver.py | 2 +- ellar/di/injector/ellar_injector.py | 2 +- ellar/di/service_config.py | 2 +- ellar/openapi/module.py | 8 ++++++-- ellar/openapi/route_doc_models.py | 6 ++---- ellar/reflect/_reflect.py | 6 ++++-- ellar/socket_io/decorators/gateway.py | 2 +- ellar/threading/sync_worker.py | 9 ++++----- ellar/utils/__init__.py | 2 +- .../db_learning/command.py | 4 +++- tests/test_di/test_tree_manager.py | 6 +++--- tests/test_hashers.py | 2 +- 44 files changed, 109 insertions(+), 104 deletions(-) diff --git a/ellar/app/lifespan.py b/ellar/app/lifespan.py index 5f287f3f..5ddbfe30 100644 --- a/ellar/app/lifespan.py +++ b/ellar/app/lifespan.py @@ -60,7 +60,7 @@ async def lifespan(self, app: "App") -> t.AsyncIterator[t.Any]: logger.debug("Executing Modules Startup Handlers") await self.run_all_startup_actions(app) - async with self._lifespan_context(app) as ctx: # type:ignore[attr-defined] + async with self._lifespan_context(app) as ctx: logger.info("Application is ready.") yield ctx finally: diff --git a/ellar/app/main.py b/ellar/app/main.py index d3352319..8bc7bc80 100644 --- a/ellar/app/main.py +++ b/ellar/app/main.py @@ -53,9 +53,9 @@ def __init__( ): _routes = routes or [] assert isinstance(config, Config), "config must instance of Config" - assert isinstance( - injector, EllarInjector - ), "injector must instance of EllarInjector" + assert isinstance(injector, EllarInjector), ( + "injector must instance of EllarInjector" + ) self._config = config self._injector: EllarInjector = injector @@ -70,7 +70,7 @@ def __init__( redirect_slashes=self.config.REDIRECT_SLASHES, default=self.config.DEFAULT_NOT_FOUND_HANDLER, lifespan=EllarApplicationLifespan( - self.config.DEFAULT_LIFESPAN_HANDLER # type: ignore[arg-type] + self.config.DEFAULT_LIFESPAN_HANDLER ).lifespan, ) diff --git a/ellar/auth/handlers/schemes/http.py b/ellar/auth/handlers/schemes/http.py index 74354d31..110374c5 100644 --- a/ellar/auth/handlers/schemes/http.py +++ b/ellar/auth/handlers/schemes/http.py @@ -94,7 +94,7 @@ def _get_credentials(self, connection: "HTTPConnection") -> HTTPBasicCredentials if not separator: self._not_unauthorized_exception("Invalid authentication credentials") - return HTTPBasicCredentials(username=username, password=password) # type: ignore[arg-type] + return HTTPBasicCredentials(username=username, password=password) class HttpDigestAuth(HttpBearerAuth, ABC): diff --git a/ellar/auth/policy/base.py b/ellar/auth/policy/base.py index fdad6426..2b299b25 100644 --- a/ellar/auth/policy/base.py +++ b/ellar/auth/policy/base.py @@ -19,7 +19,7 @@ class MyPolicyHandler(PolicyWithRequirement): """ def __init__(self, *args: t.Any) -> None: - kwargs_args = {f"arg_{idx+1}": value for idx, value in enumerate(args)} + kwargs_args = {f"arg_{idx + 1}": value for idx, value in enumerate(args)} super().__init__(kwargs_args) diff --git a/ellar/cache/module.py b/ellar/cache/module.py index 495c2338..4f377777 100644 --- a/ellar/cache/module.py +++ b/ellar/cache/module.py @@ -34,7 +34,7 @@ def setup( args = {"default": default} args.update(kwargs) - schema = CacheModuleSchemaSetup(**{"CACHES": args}) # type: ignore[arg-type] + schema = CacheModuleSchemaSetup(**{"CACHES": args}) return cls._create_dynamic_module(schema) @classmethod diff --git a/ellar/cache/service.py b/ellar/cache/service.py index 82fb0947..358d4b1e 100644 --- a/ellar/cache/service.py +++ b/ellar/cache/service.py @@ -84,9 +84,9 @@ def __init__( self, backends: t.Optional[t.Dict[str, BaseCacheBackend]] = None ) -> None: if backends: - assert backends.get( - "default" - ), "CACHES configuration must have a 'default' key." + assert backends.get("default"), ( + "CACHES configuration must have a 'default' key." + ) self._backends = backends or { "default": LocalMemCacheBackend(key_prefix="ellar", version=1, ttl=300) } diff --git a/ellar/common/decorators/controller.py b/ellar/common/decorators/controller.py index 42d6cb16..657b94fa 100644 --- a/ellar/common/decorators/controller.py +++ b/ellar/common/decorators/controller.py @@ -42,9 +42,9 @@ def Controller( _prefix = NOT_SET if _prefix is not NOT_SET: - assert _prefix == "" or str(_prefix).startswith( - "/" - ), "Controller Prefix must start with '/'" + assert _prefix == "" or str(_prefix).startswith("/"), ( + "Controller Prefix must start with '/'" + ) # TODO: replace with a ControllerTypeDict and OpenAPITypeDict kwargs = AttributeDict( path=_prefix, diff --git a/ellar/common/decorators/html.py b/ellar/common/decorators/html.py index fe69d8af..d0496903 100644 --- a/ellar/common/decorators/html.py +++ b/ellar/common/decorators/html.py @@ -41,9 +41,9 @@ def render(template_name: t.Optional[str] = NOT_SET) -> t.Callable: :return: """ if template_name is not NOT_SET: - assert isinstance( - template_name, str - ), "Render Operation must invoked eg. @render()" + assert isinstance(template_name, str), ( + "Render Operation must invoked eg. @render()" + ) template_name = None if template_name is NOT_SET else template_name def _decorator(func: t.Union[t.Callable, t.Any]) -> t.Union[t.Callable, t.Any]: diff --git a/ellar/common/decorators/modules.py b/ellar/common/decorators/modules.py index 4e3cbd8f..76c62a40 100644 --- a/ellar/common/decorators/modules.py +++ b/ellar/common/decorators/modules.py @@ -91,7 +91,7 @@ def Module( :return: t.TYPE[ModuleBase] """ - base_directory = get_main_directory_by_stack(base_directory, stack_level=2) # type:ignore[arg-type] + base_directory = get_main_directory_by_stack(base_directory, stack_level=2) kwargs = AttributeDict( name=name, controllers=list(controllers), diff --git a/ellar/common/interfaces/exceptions.py b/ellar/common/interfaces/exceptions.py index d160b2e6..2ea4470d 100644 --- a/ellar/common/interfaces/exceptions.py +++ b/ellar/common/interfaces/exceptions.py @@ -23,13 +23,13 @@ async def catch(self, ctx: IHostContext, exc: t.Any) -> t.Union[Response, t.Any] """Catch implementation""" def __init_subclass__(cls, **kwargs: t.Any) -> None: - assert ( - cls.exception_type_or_code - ), f"'exception_type_or_code' must be defined. {cls}" + assert cls.exception_type_or_code, ( + f"'exception_type_or_code' must be defined. {cls}" + ) if not isinstance(cls.exception_type_or_code, int): - assert issubclass( - cls.exception_type_or_code, Exception - ), "'exception_type_or_code' is not a valid type" + assert issubclass(cls.exception_type_or_code, Exception), ( + "'exception_type_or_code' is not a valid type" + ) class IExceptionMiddlewareService: diff --git a/ellar/common/params/args/base.py b/ellar/common/params/args/base.py index 1de7fb61..c0c7c0e6 100644 --- a/ellar/common/params/args/base.py +++ b/ellar/common/params/args/base.py @@ -187,9 +187,9 @@ def get_convertor_model_field( cls, param_name: str, convertor: Convertor ) -> ModelField: _converter_signature = inspect.signature(convertor.convert) - assert ( - _converter_signature.return_annotation is not inspect.Parameter.empty - ), f"{convertor.__class__.__name__} Convertor must have return type" + assert _converter_signature.return_annotation is not inspect.Parameter.empty, ( + f"{convertor.__class__.__name__} Convertor must have return type" + ) _type = _converter_signature.return_annotation return get_parameter_field( param_default=params.PathFieldInfo(), @@ -280,9 +280,9 @@ def compute_route_parameter_list( default_field_info=params.PathFieldInfo, ignore_default=ignore_default, ) - assert is_scalar_field( - field=param_field - ), "Path params must be of one of the supported types" + assert is_scalar_field(field=param_field), ( + "Path params must be of one of the supported types" + ) self._add_to_model(field=param_field) else: param_field = process_parameter_file( @@ -302,7 +302,7 @@ def _add_system_parameters_to_dependency( key: t.Optional[str] = None, ) -> t.Optional[bool]: if isinstance(param_default, SystemParameterResolver): - model = param_default(param_name, param_annotation) # type:ignore + model = param_default(param_name, param_annotation) self._computation_models[key or model.in_].append(model) return True return None diff --git a/ellar/common/params/args/resolver_generators.py b/ellar/common/params/args/resolver_generators.py index ef0abec8..2ba5aea7 100644 --- a/ellar/common/params/args/resolver_generators.py +++ b/ellar/common/params/args/resolver_generators.py @@ -84,7 +84,7 @@ def generate_resolvers(self, body_field_class: t.Type[FieldInfo]) -> None: for k, field in self.pydantic_outer_type.model_fields.items(): model_field = create_model_field( name=k, - type_=field.annotation, # type:ignore[arg-type] + type_=field.annotation, default=field.default, alias=field.alias, field_info=field, diff --git a/ellar/common/params/params.py b/ellar/common/params/params.py index db4dafcd..812801a6 100644 --- a/ellar/common/params/params.py +++ b/ellar/common/params/params.py @@ -129,13 +129,13 @@ def create_resolver( Returns: BaseRouteParameterResolver: The created resolver. """ - multiple_resolvers = model_field.field_info.json_schema_extra.pop( # type:ignore[union-attr] + multiple_resolvers = model_field.field_info.json_schema_extra.pop( MULTI_RESOLVER_KEY, None ) if multiple_resolvers: return self.bulk_resolver( model_field=model_field, - resolvers=multiple_resolvers, # type:ignore[arg-type] + resolvers=multiple_resolvers, ) return self.resolver(model_field) @@ -440,17 +440,17 @@ def __init__( def create_resolver( self, model_field: ModelField ) -> t.Union[BaseRouteParameterResolver, IRouteParameterResolver]: - multiple_resolvers = model_field.field_info.json_schema_extra.pop( # type:ignore[union-attr] + multiple_resolvers = model_field.field_info.json_schema_extra.pop( MULTI_RESOLVER_KEY, [] ) - is_grouped = model_field.field_info.json_schema_extra.pop( # type:ignore[union-attr] + is_grouped = model_field.field_info.json_schema_extra.pop( MULTI_RESOLVER_FORM_GROUPED_KEY, False ) if multiple_resolvers: return self.bulk_resolver( model_field=model_field, - resolvers=multiple_resolvers, # type:ignore[arg-type] + resolvers=multiple_resolvers, is_grouped=is_grouped, ) return self.resolver(model_field) diff --git a/ellar/common/params/resolvers/base.py b/ellar/common/params/resolvers/base.py index 5c9829a0..c26ccf1c 100644 --- a/ellar/common/params/resolvers/base.py +++ b/ellar/common/params/resolvers/base.py @@ -76,9 +76,9 @@ def assert_field_info(self) -> None: """ from .. import params - assert isinstance( - self.model_field.field_info, params.ParamFieldInfo - ), "Params must be subclasses of Param" + assert isinstance(self.model_field.field_info, params.ParamFieldInfo), ( + "Params must be subclasses of Param" + ) @classmethod def create_error(cls, loc: t.Any) -> t.Any: @@ -92,7 +92,7 @@ def validate_error_sequence(cls, errors: t.Any) -> t.List[t.Any]: async def resolve(self, *args: t.Any, **kwargs: t.Any) -> ResolverResult: value_ = await self.resolve_handle(*args, **kwargs) - return value_ + return t.cast(ResolverResult, value_) @abstractmethod @t.no_type_check diff --git a/ellar/common/params/resolvers/system_parameters/background.py b/ellar/common/params/resolvers/system_parameters/background.py index 41a72fac..0c275458 100644 --- a/ellar/common/params/resolvers/system_parameters/background.py +++ b/ellar/common/params/resolvers/system_parameters/background.py @@ -23,7 +23,9 @@ async def resolve(self, ctx: IExecutionContext, **kwargs: t.Any) -> ResolverResu background_tasks = BackgroundTasks() if res.background and isinstance(res.background, BackgroundTask): - background_tasks.add_task(res.background.func) + background_tasks.add_task( + res.background.func, *res.background.args, **res.background.kwargs + ) res.background = background_tasks diff --git a/ellar/common/responses/models/base.py b/ellar/common/responses/models/base.py index f7a869bb..29cf1d27 100644 --- a/ellar/common/responses/models/base.py +++ b/ellar/common/responses/models/base.py @@ -33,9 +33,7 @@ def validate_object(self, obj: t.Any) -> t.Any: ) values, error = self.validate(obj, {}, loc=(self.alias,)) if error: - _errors = ( - list(error) if isinstance(error, list) else [error] # type:ignore[list-item] - ) + _errors = list(error) if isinstance(error, list) else [error] return None, _errors return values, [] diff --git a/ellar/common/responses/models/file.py b/ellar/common/responses/models/file.py index 76340104..26ed808c 100644 --- a/ellar/common/responses/models/file.py +++ b/ellar/common/responses/models/file.py @@ -57,7 +57,7 @@ def create_response( ) init_kwargs = self.serialize(response_obj) - response_args.update(init_kwargs) # type:ignore[arg-type] + response_args.update(init_kwargs) response = self._response_type( **response_args, diff --git a/ellar/common/responses/models/route.py b/ellar/common/responses/models/route.py index 96ab7a7f..8e28cb85 100644 --- a/ellar/common/responses/models/route.py +++ b/ellar/common/responses/models/route.py @@ -28,9 +28,9 @@ def convert_route_responses_to_response_models( ) -> None: self.validate_route_response(route_responses) for status_code, response_schema in route_responses.items(): - assert ( - isinstance(status_code, int) or status_code == Ellipsis - ), "status_code must be a number" + assert isinstance(status_code, int) or status_code == Ellipsis, ( + "status_code must be a number" + ) description: str = "Successful Response" if isinstance(response_schema, (tuple, list)): response_schema, description = response_schema diff --git a/ellar/common/serializer/base.py b/ellar/common/serializer/base.py index 56d030ec..72d0f3b9 100644 --- a/ellar/common/serializer/base.py +++ b/ellar/common/serializer/base.py @@ -146,7 +146,7 @@ def serialize_object( return serialize_object(obj_dict, _encoders) if is_dataclass(obj): return serialize_object( - asdict(obj), # type:ignore[call-overload] + asdict(obj), encoders=_encoders, serializer_filter=serializer_filter, ) diff --git a/ellar/core/conf/config.py b/ellar/core/conf/config.py index ff5d7145..cbdfb89c 100644 --- a/ellar/core/conf/config.py +++ b/ellar/core/conf/config.py @@ -58,7 +58,7 @@ def _load_config_module(self, prefix: str) -> dict: return data def __repr__(self) -> str: # pragma: no cover - hidden_values = {key: "..." for key in self._schema.serialize().keys()} + hidden_values = dict.fromkeys(self._schema.serialize().keys(), "...") return f"" def __str__(self) -> str: diff --git a/ellar/core/exceptions/callable_exceptions.py b/ellar/core/exceptions/callable_exceptions.py index 9a5124c5..7bf1135e 100644 --- a/ellar/core/exceptions/callable_exceptions.py +++ b/ellar/core/exceptions/callable_exceptions.py @@ -66,8 +66,8 @@ async def catch( ) -> t.Union[Response, t.Any]: args = tuple(list(self.func_args) + [ctx, exc]) if self.is_async: - return await self.callable_exception_handler(*args) # type:ignore[misc] - return await run_in_threadpool(self.callable_exception_handler, *args) # type:ignore[arg-type] + return await self.callable_exception_handler(*args) # type: ignore[misc] + return await run_in_threadpool(self.callable_exception_handler, *args) def __eq__(self, other: t.Any) -> bool: if isinstance(other, CallableExceptionHandler): diff --git a/ellar/core/modules/config.py b/ellar/core/modules/config.py index 93533750..60168b24 100644 --- a/ellar/core/modules/config.py +++ b/ellar/core/modules/config.py @@ -171,9 +171,9 @@ def get_module_ref(self, config: "Config", container: Container) -> ModuleRefBas self.module, config, container, **self.init_kwargs ) - assert isinstance( - ref, ModuleRefBase - ), f"{ref.module} is not properly configured." + assert isinstance(ref, ModuleRefBase), ( + f"{ref.module} is not properly configured." + ) ref.initiate_module_build() return ref diff --git a/ellar/core/modules/ref/base.py b/ellar/core/modules/ref/base.py index 84483b70..d2897cb4 100644 --- a/ellar/core/modules/ref/base.py +++ b/ellar/core/modules/ref/base.py @@ -222,9 +222,9 @@ def _validate_() -> None: and data.parent == self.module ) ) - assert ( - module - ), f"Unknown Export '{provider_type}' found in '{self.module}'" + assert module, ( + f"Unknown Export '{provider_type}' found in '{self.module}'" + ) _validate_() diff --git a/ellar/core/modules/ref/factory.py b/ellar/core/modules/ref/factory.py index c48126d0..07e3c256 100644 --- a/ellar/core/modules/ref/factory.py +++ b/ellar/core/modules/ref/factory.py @@ -33,9 +33,9 @@ def create_module_ref_factor( ) return module_ref elif type(module_type) is ModuleBaseMeta: - assert ( - container is not None - ), "ModulePlainRef class can't take a nullable 'container'" + assert container is not None, ( + "ModulePlainRef class can't take a nullable 'container'" + ) module_ref = ModulePlainRef( module_type, diff --git a/ellar/core/modules/ref/plain.py b/ellar/core/modules/ref/plain.py index f8b98a3f..6e953003 100644 --- a/ellar/core/modules/ref/plain.py +++ b/ellar/core/modules/ref/plain.py @@ -38,9 +38,9 @@ def __init__( self._register_module() def _validate_module_type(self) -> None: - assert ( - type(self.module) is ModuleBaseMeta - ), f"Module Type must be a subclass of ModuleBase;\n Invalid Type[{self.module}]" + assert type(self.module) is ModuleBaseMeta, ( + f"Module Type must be a subclass of ModuleBase;\n Invalid Type[{self.module}]" + ) def _register_module(self) -> None: if not is_decorated_with_injectable(self.module): diff --git a/ellar/core/modules/ref/template.py b/ellar/core/modules/ref/template.py index bea998ef..a8793355 100644 --- a/ellar/core/modules/ref/template.py +++ b/ellar/core/modules/ref/template.py @@ -77,9 +77,9 @@ def initiate_module_build(self) -> None: def _validate_module_type(self) -> None: res = reflect.get_metadata(MODULE_WATERMARK, self.module) - assert ( - res is True - ), f"Module Type must be decorated with @Module decorator;\n Invalid Module type[{self.module}]" + assert res is True, ( + f"Module Type must be decorated with @Module decorator;\n Invalid Module type[{self.module}]" + ) def _register_module(self) -> None: self.add_provider( diff --git a/ellar/core/routing/file_mount.py b/ellar/core/routing/file_mount.py index 8d7af9f0..bc722eb7 100644 --- a/ellar/core/routing/file_mount.py +++ b/ellar/core/routing/file_mount.py @@ -21,7 +21,7 @@ def __init__( middleware: t.Optional[t.Sequence[Middleware]] = None, base_directory: t.Optional[str] = None, ) -> None: - base_directory = get_main_directory_by_stack(base_directory, stack_level=2) # type: ignore[arg-type] + base_directory = get_main_directory_by_stack(base_directory, stack_level=2) if base_directory: directories = [ str(os.path.join(base_directory, directory)) @@ -30,7 +30,7 @@ def __init__( self._middleware = middleware - _files_app = StaticFiles(directories=directories, packages=packages) # type:ignore[arg-type] + _files_app = StaticFiles(directories=directories, packages=packages) super().__init__( path=path, name=name, app=self._combine_app_with_middleware(_files_app) ) diff --git a/ellar/core/routing/route.py b/ellar/core/routing/route.py index 86990440..48a475d8 100644 --- a/ellar/core/routing/route.py +++ b/ellar/core/routing/route.py @@ -92,7 +92,7 @@ def _load_model(self) -> None: self._defined_responses.update(response_override) self.response_model = RouteResponseModel( - route_responses=self._defined_responses # type: ignore + route_responses=self._defined_responses ) def get_operation_unique_id( diff --git a/ellar/core/routing/websocket/handler.py b/ellar/core/routing/websocket/handler.py index b8b3b018..998785be 100644 --- a/ellar/core/routing/websocket/handler.py +++ b/ellar/core/routing/websocket/handler.py @@ -151,7 +151,7 @@ async def decode(self, websocket: "WebSocket", message: Message) -> t.Any: reason="Malformed JSON data received.", ) from e - assert ( - self.encoding is None - ), f"Unsupported 'encoding' attribute {self.encoding}" + assert self.encoding is None, ( + f"Unsupported 'encoding' attribute {self.encoding}" + ) return message["text"] if message.get("text") else message["bytes"] diff --git a/ellar/core/security/hashers/argon2.py b/ellar/core/security/hashers/argon2.py index f2727544..54a8fae3 100644 --- a/ellar/core/security/hashers/argon2.py +++ b/ellar/core/security/hashers/argon2.py @@ -32,7 +32,7 @@ def _get_using_kwargs(self) -> dict: def encode( self, password: EncodingType, salt: EncodingSalt = None ) -> t.Union[str, t.Any]: - salt = bytes(salt, "utf-8") if salt else salt # type:ignore[arg-type] + salt = bytes(salt, "utf-8") if salt else salt return super().encode(password, salt) def decode(self, encoded: str) -> dict: diff --git a/ellar/core/security/hashers/scrypt.py b/ellar/core/security/hashers/scrypt.py index d8d13b85..7dbff91b 100644 --- a/ellar/core/security/hashers/scrypt.py +++ b/ellar/core/security/hashers/scrypt.py @@ -27,9 +27,9 @@ def _encode_action( p: int, salt: EncodingSalt, ) -> str: - hash_ = self.hasher( # type:ignore[misc] - password.encode(), # type:ignore[union-attr] - salt=salt.encode(), # type:ignore[union-attr] + hash_ = self.hasher( # type: ignore[misc] + password.encode(), + salt=salt.encode(), n=n, r=r, p=p, diff --git a/ellar/core/templating/service.py b/ellar/core/templating/service.py index 00f641e5..a730d6e7 100644 --- a/ellar/core/templating/service.py +++ b/ellar/core/templating/service.py @@ -43,9 +43,9 @@ def _compute_template_context(self, template_context: t.Dict) -> t.Dict: org_context = template_context.copy() for processor in self.config.APP_CONTEXT_PROCESSORS or []: res = processor(request) - assert isinstance( - res, dict - ), f"{processor} is expected to return a dict object" + assert isinstance(res, dict), ( + f"{processor} is expected to return a dict object" + ) template_context.update(res) template_context.update(org_context) diff --git a/ellar/core/versioning/resolver.py b/ellar/core/versioning/resolver.py index 3992963f..44264a1c 100644 --- a/ellar/core/versioning/resolver.py +++ b/ellar/core/versioning/resolver.py @@ -128,7 +128,7 @@ def resolve_version(self) -> str: assert value message[self.header_parameter] = value - accept = dict(message.get_params(header=self.header_parameter)) # type: ignore[arg-type] + accept = dict(message.get_params(header=self.header_parameter)) version = accept.get(self.version_parameter, self.default_version) return str(version) diff --git a/ellar/di/injector/ellar_injector.py b/ellar/di/injector/ellar_injector.py index 11dc9cd2..2bae8297 100644 --- a/ellar/di/injector/ellar_injector.py +++ b/ellar/di/injector/ellar_injector.py @@ -83,7 +83,7 @@ def __init__( @cached_property def tree_manager(self) -> ModuleTreeManager: - return self.get(ModuleTreeManager) + return t.cast(ModuleTreeManager, self.get(ModuleTreeManager)) @property # type: ignore def binder(self) -> Container: diff --git a/ellar/di/service_config.py b/ellar/di/service_config.py index 740b2b90..4e7e0578 100644 --- a/ellar/di/service_config.py +++ b/ellar/di/service_config.py @@ -206,7 +206,7 @@ def _decorator(func_or_class: ConstructorOrClassT) -> ConstructorOrClassT: ): func_ = scope scope = SingletonScope - return _decorator(func_) # type: ignore[arg-type] + return _decorator(func_) return _decorator diff --git a/ellar/openapi/module.py b/ellar/openapi/module.py index 3fc63e95..77adfb81 100644 --- a/ellar/openapi/module.py +++ b/ellar/openapi/module.py @@ -140,7 +140,9 @@ def _setup_document_manager( _path = docs_ui.path.lstrip("/").rstrip("/") if not docs_ui.template_name: - assert docs_ui.template_string, f"`{docs_ui.__class__.__name__}` class requires the `template_string` attribute to be provided." + assert docs_ui.template_string, ( + f"`{docs_ui.__class__.__name__}` class requires the `template_string` attribute to be provided." + ) @t.no_type_check async def _doc(ctx: IExecutionContext) -> HTMLResponse: @@ -151,7 +153,9 @@ async def _doc(ctx: IExecutionContext) -> HTMLResponse: return HTMLResponse(html_str) else: - assert docs_ui.template_name, f"`{docs_ui.__class__.__name__}` class requires the `template_name` attribute to be provided." + assert docs_ui.template_name, ( + f"`{docs_ui.__class__.__name__}` class requires the `template_name` attribute to be provided." + ) @render(docs_ui.template_name) async def _doc() -> t.Any: diff --git a/ellar/openapi/route_doc_models.py b/ellar/openapi/route_doc_models.py index cfb27791..f39762bb 100644 --- a/ellar/openapi/route_doc_models.py +++ b/ellar/openapi/route_doc_models.py @@ -228,7 +228,7 @@ def output_fields(self) -> t.List[ModelField]: _models: t.List[ModelField] = [] for _, model in self.route.response_model.models.items(): if model.get_model_field(): - _models.append(model.get_model_field()) # type: ignore + _models.append(model.get_model_field()) return _models def get_route_models(self) -> t.List[ModelField]: @@ -249,9 +249,7 @@ def _get_openapi_security_scheme( keys = list(security_scheme.keys()) if keys: scheme_name = keys[0] - operation_security.append( - {scheme_name: item.openapi_scope} # type:ignore[union-attr] - ) + operation_security.append({scheme_name: item.openapi_scope}) return security_definitions, operation_security diff --git a/ellar/reflect/_reflect.py b/ellar/reflect/_reflect.py index b402ddb1..69f1d8f1 100644 --- a/ellar/reflect/_reflect.py +++ b/ellar/reflect/_reflect.py @@ -105,8 +105,10 @@ def define_metadata( if target_metadata is not None: existing = target_metadata.get(metadata_key) if existing is not None: - update_callback = self._data_type_update_callbacks.get( - type(existing), self._default_update_callback + update_callback: t.Callable[[t.Any, t.Any], t.Any] = ( + self._data_type_update_callbacks.get( + type(existing), self._default_update_callback + ) ) metadata_value = update_callback(existing, metadata_value) target_metadata[metadata_key] = metadata_value diff --git a/ellar/socket_io/decorators/gateway.py b/ellar/socket_io/decorators/gateway.py index 1dbf3098..a7213653 100644 --- a/ellar/socket_io/decorators/gateway.py +++ b/ellar/socket_io/decorators/gateway.py @@ -69,5 +69,5 @@ def _decorator(cls: t.Type) -> t.Type: if callable(path): func = path path = "/socket.io" - return _decorator(func) # type:ignore[arg-type] + return _decorator(func) return _decorator diff --git a/ellar/threading/sync_worker.py b/ellar/threading/sync_worker.py index e70db5ab..db3f4127 100644 --- a/ellar/threading/sync_worker.py +++ b/ellar/threading/sync_worker.py @@ -75,7 +75,7 @@ def run(self) -> None: break coro, ctx = item if inspect.isasyncgen(coro): - ctx.run(loop.run_until_complete, self.agen_wrapper(coro)) # type: ignore[arg-type] + ctx.run(loop.run_until_complete, self.agen_wrapper(coro)) elif isinstance(coro, t.AsyncContextManager): ctx.run( loop.run_until_complete, @@ -83,8 +83,7 @@ def run(self) -> None: ) else: try: - # FIXME: Once python/mypy#12756 is resolved, remove the type-ignore tag. - result = ctx.run(loop.run_until_complete, coro) # type: ignore[arg-type] + result = ctx.run(loop.run_until_complete, coro) except Exception as e: self.done_queue.put_nowait(e) self.work_queue.task_done() @@ -250,8 +249,8 @@ async def async_gen(): _worker_thread.join() -@contextlib.contextmanager # type:ignore[arg-type] -def execute_async_context_manager( # type:ignore[misc] +@contextlib.contextmanager +def execute_async_context_manager( # type: ignore[misc] async_gen: t.AsyncContextManager, context_update: bool = True ) -> t.ContextManager: """ diff --git a/ellar/utils/__init__.py b/ellar/utils/__init__.py index 35a5d804..3f456134 100644 --- a/ellar/utils/__init__.py +++ b/ellar/utils/__init__.py @@ -24,7 +24,7 @@ def generate_operation_unique_id( if isinstance(controller, type): operation_id += ( - f'__{str(controller.__name__).lower().replace("controller", "")}' + f"__{str(controller.__name__).lower().replace('controller', '')}" ) return operation_id diff --git a/samples/05-ellar-with-sqlalchemy/db_learning/command.py b/samples/05-ellar-with-sqlalchemy/db_learning/command.py index 999c8259..b62583eb 100644 --- a/samples/05-ellar-with-sqlalchemy/db_learning/command.py +++ b/samples/05-ellar-with-sqlalchemy/db_learning/command.py @@ -12,7 +12,9 @@ def seed_user(): session = db_service.session_factory() for i in range(300): - session.add(User(username=f"username-{i+1}", email=f"user{i+1}doe@example.com")) + session.add( + User(username=f"username-{i + 1}", email=f"user{i + 1}doe@example.com") + ) session.commit() db_service.session_factory.remove() diff --git a/tests/test_di/test_tree_manager.py b/tests/test_di/test_tree_manager.py index 18658751..c9124f1e 100644 --- a/tests/test_di/test_tree_manager.py +++ b/tests/test_di/test_tree_manager.py @@ -157,11 +157,11 @@ def test_search_module_tree_works(): tree_manager.add_module(app_module_type, ModuleSetup(app_module_type)) for _ in range(5): - module_type = Module()(get_unique_type(f"ModuleType{_+1}")) + module_type = Module()(get_unique_type(f"ModuleType{_ + 1}")) tree_manager.add_module(module_type, ModuleSetup(module_type), app_module_type) for _ in range(5): - module_type_ = Module()(get_unique_type(f"ModuleType{_+1}")) + module_type_ = Module()(get_unique_type(f"ModuleType{_ + 1}")) tree_manager.add_module(module_type_, ModuleSetup(module_type_), module_type) res = tree_manager.search_module_tree( @@ -182,7 +182,7 @@ def test_find_module_return_list_of_items(): tree_manager.add_module(app_module_type, ModuleSetup(app_module_type)) for _ in range(10): - module_type = Module()(get_unique_type(f"ModuleType{_+1}")) + module_type = Module()(get_unique_type(f"ModuleType{_ + 1}")) tree_manager.add_module(module_type, ModuleSetup(module_type), app_module_type) res = list( diff --git a/tests/test_hashers.py b/tests/test_hashers.py index 05fb3a23..6affe1df 100644 --- a/tests/test_hashers.py +++ b/tests/test_hashers.py @@ -351,7 +351,7 @@ def test_argon2_upgrade(self): def test_argon2_version_upgrade(self): state = {"upgraded": False} encoded = ( - "$argon2id$v=19$m=102400,t=2,p=8$Y041dExhNkljRUUy$TMa6A8fPJh" "CAUXRhJXCXdw" + "$argon2id$v=19$m=102400,t=2,p=8$Y041dExhNkljRUUy$TMa6A8fPJhCAUXRhJXCXdw" ) def setter(password): From 952f4dda747ea44d03f1e2273ad14c45c744f5ef Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Fri, 3 Oct 2025 22:29:15 +0100 Subject: [PATCH 2/4] upgraded to latest starlette == 0.48.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d3cbc411..38118647 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ dependencies = [ "injector == 0.22.0", - "starlette == 0.46.1", + "starlette == 0.48.0", "pydantic >=2.5.1,<3.0.0", "typing-extensions>=4.8.0", "jinja2" From 3b08ba76a308ec6854f28c25aac5372353645460 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Fri, 3 Oct 2025 22:44:20 +0100 Subject: [PATCH 3/4] Fix bcrypt hashers for Python 3.13 compatibility Add fallback handling for Python 3.13's strict 72-byte password limit enforcement in bcrypt. BCryptSHA256Hasher now manually pre-hashes with SHA256 when needed, and BCryptHasher explicitly truncates passwords to 72 bytes. Maintains backward compatibility across all Python versions. --- ellar/core/security/hashers/bcrypt.py | 89 ++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/ellar/core/security/hashers/bcrypt.py b/ellar/core/security/hashers/bcrypt.py index b8f25dc6..82ac9c25 100644 --- a/ellar/core/security/hashers/bcrypt.py +++ b/ellar/core/security/hashers/bcrypt.py @@ -1,6 +1,11 @@ +import hashlib +import math +import typing as t + +from ellar.utils.crypto import RANDOM_STRING_CHARS from passlib.hash import django_bcrypt, django_bcrypt_sha256 -from .base import BaseHasher +from .base import BaseHasher, EncodingSalt, EncodingType class BCryptSHA256Hasher(BaseHasher): @@ -11,6 +16,9 @@ class BCryptSHA256Hasher(BaseHasher): must first install the bcrypt library. Please be warned that this library depends on native C code and might cause portability issues. + + This hasher uses SHA256 to pre-hash passwords, allowing passwords + of any length to be safely hashed without hitting bcrypt's 72-byte limit. """ hasher = django_bcrypt_sha256 @@ -22,6 +30,55 @@ def _get_using_kwargs(self) -> dict: "rounds": self.rounds, } + def _sha256_hash(self, password: EncodingType) -> str: + """ + Hash password with SHA256 and return as hex string. + This matches what passlib's django_bcrypt_sha256 does internally. + """ + if isinstance(password, str): + password_bytes = password.encode("utf-8") + else: + password_bytes = password + + return hashlib.sha256(password_bytes).hexdigest() + + def encode( + self, password: EncodingType, salt: EncodingSalt = None + ) -> t.Union[str, t.Any]: + self._check_encode_args(password, salt) + + default_salt_size = math.ceil( + self.salt_entropy / math.log2(len(RANDOM_STRING_CHARS)) + ) + using_kw = {"default_salt_size": default_salt_size, "salt": salt} + using_kw.update(self._get_using_kwargs()) + + # Try passlib first (works on Python < 3.13) + try: + return self.hasher.using(**using_kw).hash(password) + except ValueError as e: + # Python 3.13+ bcrypt enforces 72-byte limit before passlib can pre-hash + # So we pre-hash manually and use the plain bcrypt hasher + if "password cannot be longer than 72 bytes" in str(e): + hashed = self._sha256_hash(password) + return self.hasher.using(**using_kw).hash(hashed) + raise + + def verify(self, secret: EncodingType, hash_secret: str) -> bool: + """ + Verify secret against an existing hash. + """ + # Try passlib first (works on Python < 3.13) + try: + return self.hasher.verify(secret, hash_secret) # type:ignore[no-any-return] + except ValueError as e: + # Python 3.13+ bcrypt enforces 72-byte limit before passlib can pre-hash + # So we pre-hash manually + if "password cannot be longer than 72 bytes" in str(e): + hashed = self._sha256_hash(secret) + return self.hasher.verify(hashed, hash_secret) # type:ignore[no-any-return] + raise + def decode(self, encoded: str) -> dict: algorithm, empty, algostr, work_factor, data = encoded.split("$", 4) assert algorithm == self.algorithm @@ -54,3 +111,33 @@ class BCryptHasher(BCryptSHA256Hasher): algorithm = "bcrypt" hasher = django_bcrypt + + def encode( + self, password: EncodingType, salt: EncodingSalt = None + ) -> t.Union[str, t.Any]: + self._check_encode_args(password, salt) + + # Truncate password to 72 bytes for bcrypt compatibility (Python 3.13+) + if isinstance(password, str): + password_bytes = password.encode("utf-8")[:72] + else: + password_bytes = password[:72] + + default_salt_size = math.ceil( + self.salt_entropy / math.log2(len(RANDOM_STRING_CHARS)) + ) + using_kw = {"default_salt_size": default_salt_size, "salt": salt} + using_kw.update(self._get_using_kwargs()) + return self.hasher.using(**using_kw).hash(password_bytes) + + def verify(self, secret: EncodingType, hash_secret: str) -> bool: + """ + Verify secret against an existing hash, truncating to 72 bytes. + """ + # Truncate secret to 72 bytes for bcrypt compatibility (Python 3.13+) + if isinstance(secret, str): + secret_bytes = secret.encode("utf-8")[:72] + else: + secret_bytes = secret[:72] + + return self.hasher.verify(secret_bytes, hash_secret) # type:ignore[no-any-return] From b34798c121cac934377e66e03d8fd111c8c23b44 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sat, 4 Oct 2025 07:01:34 +0100 Subject: [PATCH 4/4] Fix mypy type checking issues - Remove unused type ignore comments in bcrypt hasher - Update Makefile - Update type stubs for redis and ujson to latest versions --- Makefile | 10 +-- ellar/core/security/hashers/bcrypt.py | 92 +++++++++++++++------------ requirements-tests.txt | 6 +- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/Makefile b/Makefile index 1774afd0..7d7805fd 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,11 @@ help: @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' clean: ## Removing cached python compiled files - find . -name \*pyc | xargs rm -fv - find . -name \*pyo | xargs rm -fv - find . -name \*~ | xargs rm -fv - find . -name __pycache__ | xargs rm -rfv - find . -name .ruff_cache | xargs rm -rfv + find . -name "*.pyc" -type f -delete + find . -name "*.pyo" -type f -delete + find . -name "*~" -type f -delete + find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + find . -name ".ruff_cache" -type d -exec rm -rf {} + 2>/dev/null || true install: ## Install dependencies pip install -r requirements.txt diff --git a/ellar/core/security/hashers/bcrypt.py b/ellar/core/security/hashers/bcrypt.py index 82ac9c25..f64cf427 100644 --- a/ellar/core/security/hashers/bcrypt.py +++ b/ellar/core/security/hashers/bcrypt.py @@ -2,6 +2,7 @@ import math import typing as t +import bcrypt from ellar.utils.crypto import RANDOM_STRING_CHARS from passlib.hash import django_bcrypt, django_bcrypt_sha256 @@ -30,18 +31,6 @@ def _get_using_kwargs(self) -> dict: "rounds": self.rounds, } - def _sha256_hash(self, password: EncodingType) -> str: - """ - Hash password with SHA256 and return as hex string. - This matches what passlib's django_bcrypt_sha256 does internally. - """ - if isinstance(password, str): - password_bytes = password.encode("utf-8") - else: - password_bytes = password - - return hashlib.sha256(password_bytes).hexdigest() - def encode( self, password: EncodingType, salt: EncodingSalt = None ) -> t.Union[str, t.Any]: @@ -53,31 +42,44 @@ def encode( using_kw = {"default_salt_size": default_salt_size, "salt": salt} using_kw.update(self._get_using_kwargs()) - # Try passlib first (works on Python < 3.13) - try: - return self.hasher.using(**using_kw).hash(password) - except ValueError as e: - # Python 3.13+ bcrypt enforces 72-byte limit before passlib can pre-hash - # So we pre-hash manually and use the plain bcrypt hasher - if "password cannot be longer than 72 bytes" in str(e): - hashed = self._sha256_hash(password) - return self.hasher.using(**using_kw).hash(hashed) - raise + # Avoid passlib's backend long-secret detection which raises on Python 3.13+ + # Pre-hash the secret with SHA256 and then use plain django_bcrypt, + # rewriting the prefix to bcrypt_sha256 for compatibility. + if isinstance(password, str): + secret_bytes = password.encode("utf-8") + else: + secret_bytes = password + + digest_hex = hashlib.sha256(secret_bytes).hexdigest().encode("ascii") + + if salt is not None: + salt_str = ( + salt.decode("ascii") + if isinstance(salt, (bytes, bytearray)) + else str(salt) + ) + salt_full = f"$2b${self.rounds:02d}${salt_str}".encode("ascii") + else: + salt_full = bcrypt.gensalt(self.rounds) + + hashed = bcrypt.hashpw(digest_hex, salt_full) + return f"bcrypt_sha256${hashed.decode('ascii')}" def verify(self, secret: EncodingType, hash_secret: str) -> bool: """ Verify secret against an existing hash. """ - # Try passlib first (works on Python < 3.13) - try: - return self.hasher.verify(secret, hash_secret) # type:ignore[no-any-return] - except ValueError as e: - # Python 3.13+ bcrypt enforces 72-byte limit before passlib can pre-hash - # So we pre-hash manually - if "password cannot be longer than 72 bytes" in str(e): - hashed = self._sha256_hash(secret) - return self.hasher.verify(hashed, hash_secret) # type:ignore[no-any-return] - raise + # Verify by pre-hashing secret and delegating to django_bcrypt + if isinstance(secret, str): + secret_bytes = secret.encode("utf-8") + else: + secret_bytes = secret + + digest_hex = hashlib.sha256(secret_bytes).hexdigest().encode("ascii") + if not hash_secret.startswith("bcrypt_sha256$"): + return False + hashed = hash_secret[len("bcrypt_sha256$") :].encode("ascii") + return bcrypt.checkpw(digest_hex, hashed) def decode(self, encoded: str) -> dict: algorithm, empty, algostr, work_factor, data = encoded.split("$", 4) @@ -117,27 +119,35 @@ def encode( ) -> t.Union[str, t.Any]: self._check_encode_args(password, salt) - # Truncate password to 72 bytes for bcrypt compatibility (Python 3.13+) + # Truncate password to 72 bytes for bcrypt compatibility if isinstance(password, str): password_bytes = password.encode("utf-8")[:72] else: password_bytes = password[:72] - default_salt_size = math.ceil( - self.salt_entropy / math.log2(len(RANDOM_STRING_CHARS)) - ) - using_kw = {"default_salt_size": default_salt_size, "salt": salt} - using_kw.update(self._get_using_kwargs()) - return self.hasher.using(**using_kw).hash(password_bytes) + if salt is not None: + salt_str = ( + salt.decode("ascii") + if isinstance(salt, (bytes, bytearray)) + else str(salt) + ) + salt_full = f"$2b${self.rounds:02d}${salt_str}".encode("ascii") + else: + salt_full = bcrypt.gensalt(self.rounds) + + hashed = bcrypt.hashpw(password_bytes, salt_full) + return f"bcrypt${hashed.decode('ascii')}" def verify(self, secret: EncodingType, hash_secret: str) -> bool: """ Verify secret against an existing hash, truncating to 72 bytes. """ - # Truncate secret to 72 bytes for bcrypt compatibility (Python 3.13+) if isinstance(secret, str): secret_bytes = secret.encode("utf-8")[:72] else: secret_bytes = secret[:72] - return self.hasher.verify(secret_bytes, hash_secret) # type:ignore[no-any-return] + if not hash_secret.startswith("bcrypt$"): + return False + hashed = hash_secret[len("bcrypt$") :].encode("ascii") + return bcrypt.checkpw(secret_bytes, hashed) diff --git a/requirements-tests.txt b/requirements-tests.txt index 39a5c440..2f0517b2 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,7 +2,7 @@ aiohttp == 3.10.5 anyio[trio] >= 3.2.1 argon2-cffi == 25.1.0 autoflake -bcrypt; python_version >= '3.12' +bcrypt; python_version >= '3.9' click >= 8.1.7,<9.0.0, email_validator >=1.1.1 itsdangerous >=1.1.0,<3.0.0 @@ -17,8 +17,8 @@ regex==2025.9.18 ruff ==0.13.3 types-dataclasses ==0.6.6 types-orjson ==3.6.2 -types-redis ==4.6.0.20240903 +types-redis ==4.6.0.20241004 # types -types-ujson ==5.10.0.20250326 +types-ujson ==5.10.0.20250822 ujson >= 4.0.1 uvicorn[standard] == 0.30.6