@@ -408,3 +408,258 @@ def test_same_args_produce_cache_hit(self):
408408 result4 = self .get_data ("r1" , "a" , "b" , kw_only = "k1" )
409409 assert self .call_count == 1
410410 assert result4 == result1
411+
412+
413+ @pytest .mark .parametrize ("backend" , ["pickle" , "memory" ])
414+ class TestFunctoolsPartial :
415+ """Test functools.partial wrapped functions with variadic arguments."""
416+
417+ @pytest .fixture (autouse = True )
418+ def setup (self , backend ):
419+ """Set up the test function for each test."""
420+ import functools
421+
422+ self .call_count = 0
423+ self .backend = backend
424+
425+ def base_function (prefix , * args , suffix = "end" ):
426+ """Base function to be wrapped with partial."""
427+ self .call_count += 1
428+ return f"{ prefix } -{ args } -{ suffix } -{ self .call_count } "
429+
430+ # Create partial with prefix bound
431+ partial_func = functools .partial (base_function , "PREFIX" )
432+
433+ @cachier (backend = backend , stale_after = timedelta (seconds = 500 ))
434+ def wrapped_partial (* args , ** kwargs ):
435+ return partial_func (* args , ** kwargs )
436+
437+ self .get_data = wrapped_partial
438+ wrapped_partial .clear_cache ()
439+ self .call_count = 0
440+ yield
441+ wrapped_partial .clear_cache ()
442+
443+ def test_partial_with_different_varargs (self ):
444+ """Test that partial functions with different *args get unique keys."""
445+ result1 = self .get_data ("a" , "b" )
446+ assert self .call_count == 1
447+
448+ result2 = self .get_data ("a" , "b" , "c" )
449+ assert self .call_count == 2
450+ assert result1 != result2
451+
452+ def test_partial_with_same_varargs_uses_cache (self ):
453+ """Test that partial functions with same *args use cache."""
454+ result1 = self .get_data ("a" , "b" )
455+ assert self .call_count == 1
456+
457+ result2 = self .get_data ("a" , "b" )
458+ assert self .call_count == 1
459+ assert result1 == result2
460+
461+
462+ @pytest .mark .parametrize ("backend" , ["pickle" , "memory" ])
463+ class TestMethodWithVarargs :
464+ """Test instance methods with variadic arguments."""
465+
466+ @pytest .fixture (autouse = True )
467+ def setup (self , backend ):
468+ """Set up the test class for each test."""
469+ self .backend = backend
470+ self .call_count = 0
471+
472+ # Create a simple test class with a cached method
473+ # Note: Methods cache based on ALL arguments including self,
474+ # so we test that different instances don't interfere
475+ @cachier (backend = backend , stale_after = timedelta (seconds = 500 ))
476+ def standalone_func (* args ):
477+ """Standalone function that simulates method behavior."""
478+ self .call_count += 1
479+ return f"Result: { args } , call #{ self .call_count } "
480+
481+ self .cached_func = standalone_func
482+ standalone_func .clear_cache ()
483+ yield
484+ standalone_func .clear_cache ()
485+
486+ def test_method_with_different_varargs (self ):
487+ """Test that functions with different *args get unique keys."""
488+ result1 = self .cached_func ("a" , "b" )
489+ assert self .call_count == 1
490+
491+ result2 = self .cached_func ("x" , "y" , "z" )
492+ assert self .call_count == 2
493+ assert result1 != result2
494+
495+ def test_method_with_same_varargs_uses_cache (self ):
496+ """Test that functions with same *args use cache."""
497+ result1 = self .cached_func ("a" , "b" )
498+ assert self .call_count == 1
499+
500+ result2 = self .cached_func ("a" , "b" )
501+ assert self .call_count == 1
502+ assert result1 == result2
503+
504+
505+ @pytest .mark .parametrize ("backend" , ["pickle" , "memory" ])
506+ class TestPositionalOnlyParams :
507+ """Test functions with positional-only parameters (/) and varargs."""
508+
509+ @pytest .fixture (autouse = True )
510+ def setup (self , backend ):
511+ """Set up the test function for each test."""
512+ self .call_count = 0
513+ self .backend = backend
514+
515+ # Note: POSITIONAL_ONLY requires Python 3.8+
516+ # Using exec to define function with / syntax
517+ exec_globals = {"cachier" : cachier , "timedelta" : timedelta , "backend" : backend }
518+ exec ( # noqa: S102
519+ """
520+ @cachier(backend=backend, stale_after=timedelta(seconds=500))
521+ def get_data(pos_only, /, *args, kw_only=None):
522+ global call_count
523+ call_count += 1
524+ return f"pos_only={pos_only}, args={args}, kw_only={kw_only}, call #{call_count}"
525+ """ ,
526+ exec_globals ,
527+ )
528+
529+ self .get_data = exec_globals ["get_data" ]
530+ exec_globals ["call_count" ] = 0
531+ self .exec_globals = exec_globals
532+ self .get_data .clear_cache ()
533+ yield
534+ self .get_data .clear_cache ()
535+
536+ def test_positional_only_with_varargs (self ):
537+ """Test positional-only params with varargs produce unique keys."""
538+ self .exec_globals ["call_count" ] = 0
539+
540+ result1 = self .get_data ("pos1" , "a" , "b" , kw_only = "k1" )
541+ assert self .exec_globals ["call_count" ] == 1
542+
543+ result2 = self .get_data ("pos2" , "a" , "b" , kw_only = "k1" )
544+ assert self .exec_globals ["call_count" ] == 2
545+ assert result1 != result2
546+
547+ def test_positional_only_different_varargs (self ):
548+ """Test different varargs with positional-only produce unique keys."""
549+ self .exec_globals ["call_count" ] = 0
550+
551+ result1 = self .get_data ("pos1" , "a" , "b" )
552+ assert self .exec_globals ["call_count" ] == 1
553+
554+ result2 = self .get_data ("pos1" , "x" , "y" , "z" )
555+ assert self .exec_globals ["call_count" ] == 2
556+ assert result1 != result2
557+
558+
559+ @pytest .mark .parametrize ("backend" , ["pickle" , "memory" ])
560+ class TestComplexParameterMix :
561+ """Test functions with all parameter types combined."""
562+
563+ @pytest .fixture (autouse = True )
564+ def setup (self , backend ):
565+ """Set up the test function for each test."""
566+ self .call_count = 0
567+ self .backend = backend
568+
569+ @cachier (backend = backend , stale_after = timedelta (seconds = 500 ))
570+ def complex_func (regular1 , regular2 = "default2" , * args , kw_only , kw_default = "kw_def" , ** kwargs ):
571+ """Function with all parameter types."""
572+ self .call_count += 1
573+ return (
574+ f"r1={ regular1 } , r2={ regular2 } , args={ args } , kw_only={ kw_only } , "
575+ f"kw_default={ kw_default } , kwargs={ sorted (kwargs .items ())} , call #{ self .call_count } "
576+ )
577+
578+ self .get_data = complex_func
579+ complex_func .clear_cache ()
580+ self .call_count = 0
581+ yield
582+ complex_func .clear_cache ()
583+
584+ def test_all_params_different_combinations (self ):
585+ """Test various combinations of all parameter types."""
586+ # Combination 1: minimal required params
587+ result1 = self .get_data ("r1" , kw_only = "ko1" )
588+ assert self .call_count == 1
589+
590+ # Combination 2: with varargs
591+ result2 = self .get_data ("r1" , "r2val" , "extra1" , "extra2" , kw_only = "ko1" )
592+ assert self .call_count == 2
593+ assert result1 != result2
594+
595+ # Combination 3: with varkwargs
596+ result3 = self .get_data ("r1" , kw_only = "ko1" , extra_kw = "value" )
597+ assert self .call_count == 3
598+ assert result3 != result1
599+
600+ # Combination 4: full complexity
601+ result4 = self .get_data ("r1" , "r2val" , "e1" , "e2" , kw_only = "ko1" , kw_default = "custom" , x = "a" , y = "b" )
602+ assert self .call_count == 4
603+ assert result4 != result1
604+ assert result4 != result2
605+ assert result4 != result3
606+
607+ def test_cache_hit_with_complex_params (self ):
608+ """Test cache hit with complex parameter mix."""
609+ result1 = self .get_data ("r1" , "r2val" , "e1" , kw_only = "ko1" , extra = "val" )
610+ assert self .call_count == 1
611+
612+ result2 = self .get_data ("r1" , "r2val" , "e1" , kw_only = "ko1" , extra = "val" )
613+ assert self .call_count == 1
614+ assert result1 == result2
615+
616+
617+ @pytest .mark .parametrize ("backend" , ["pickle" , "memory" ])
618+ class TestEdgeCasesEmptyAndNone :
619+ """Test edge cases with empty values and None."""
620+
621+ @pytest .fixture (autouse = True )
622+ def setup (self , backend ):
623+ """Set up the test function for each test."""
624+ self .call_count = 0
625+ self .backend = backend
626+
627+ @cachier (backend = backend , stale_after = timedelta (seconds = 500 ), allow_none = True )
628+ def get_data (* args , ** kwargs ):
629+ """Function that might return None."""
630+ self .call_count += 1
631+ return (args , kwargs , self .call_count ) if args or kwargs else None
632+
633+ self .get_data = get_data
634+ get_data .clear_cache ()
635+ self .call_count = 0
636+ yield
637+ get_data .clear_cache ()
638+
639+ def test_none_in_varargs (self ):
640+ """Test that None values in varargs are handled correctly."""
641+ result1 = self .get_data (None , "value" )
642+ assert self .call_count == 1
643+
644+ result2 = self .get_data ("value" , None )
645+ assert self .call_count == 2
646+ assert result1 != result2
647+
648+ def test_empty_string_in_varargs (self ):
649+ """Test that empty strings in varargs are distinguished."""
650+ result1 = self .get_data ("" , "value" )
651+ assert self .call_count == 1
652+
653+ result2 = self .get_data ("value" , "" )
654+ assert self .call_count == 2
655+ assert result1 != result2
656+
657+ def test_zero_in_varargs (self ):
658+ """Test that zero values in varargs are handled correctly."""
659+ result1 = self .get_data (0 , 1 )
660+ assert self .call_count == 1
661+
662+ result2 = self .get_data (1 , 0 )
663+ assert self .call_count == 2
664+ assert result1 != result2
665+
0 commit comments