Skip to content

Commit aa6bcbe

Browse files
committed
Add support for concrete methods in interfaces
1 parent 31b1186 commit aa6bcbe

File tree

5 files changed

+161
-56
lines changed

5 files changed

+161
-56
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A Python library that provides explicit interface implementations with compile-t
99
## Features
1010

1111
- **Explicit Interface Declarations**: Define clear interface contracts using abstract methods
12+
- **Concrete Method Support**: Include concrete methods in interfaces with default implementations
1213
- **Flexible Implementation**: Allow partial implementations by default, enforce completeness with `concrete=True`
1314
- **Compile-time Safety**: Catch implementation errors at class definition time, not runtime
1415
- **Multiple Interface Support**: Implement multiple interfaces in a single class
@@ -109,6 +110,42 @@ repository_interface.save({"name": "example"})
109110
repository_interface.load("123")
110111
```
111112

113+
### Concrete Methods in Interfaces
114+
115+
Interfaces can include concrete (non-abstract) methods that provide default implementations. These methods can be accessed directly from implementing classes without explicit implementation:
116+
117+
```python
118+
class IService(Interface):
119+
@abstractmethod
120+
def process(self, data: str) -> str:
121+
...
122+
123+
def log(self, message: str) -> None:
124+
"""Concrete method with default implementation."""
125+
print(f"[LOG] {message}")
126+
127+
@classmethod
128+
def get_version(cls) -> str:
129+
"""Concrete class method."""
130+
return "1.0.0"
131+
132+
class MyService(IService):
133+
@implements(IService.process)
134+
def process_data(self, data: str) -> str:
135+
return f"Processed: {data}"
136+
137+
service = MyService()
138+
139+
# Access concrete methods directly
140+
service.log("Starting process") # Works directly
141+
service.get_version() # Works directly
142+
143+
# Also accessible through interface casting
144+
interface = service.as_interface(IService)
145+
interface.log("Through interface") # Also works
146+
interface.get_version() # Also works
147+
```
148+
112149
### Concrete Classes
113150

114151
By default, partial implementations are allowed at class definition time. To enforce complete implementation:
@@ -302,6 +339,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
302339

303340
## Changelog
304341

342+
### 0.1.1
343+
- **Bug Fix**: Fixed access to concrete (non-abstract) methods when using `as_interface()`
344+
- Concrete methods in interfaces can now be properly accessed both directly and through interface casting
345+
- Added comprehensive test coverage for concrete method access patterns
346+
- Improved documentation with concrete method usage examples
347+
305348
### 0.1.0
306349
- Initial release
307350
- Basic interface and implementation functionality

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "explicit-implementation"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "A Python library for explicit interface implementations with compile-time safety"
55
readme = "README.md"
66
authors = [

src/explicit_implementation/interface.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from abc import ABCMeta
2-
from sys import implementation
3-
from typing import Callable, Dict, Set, Type, TypeVar
2+
from typing import Callable, Dict, FrozenSet, Type, TypeVar, cast
43

54

65
ImplementationMapping = Dict[Callable, Callable]
@@ -9,7 +8,7 @@
98

109
class InterfaceMeta(ABCMeta):
1110
__explicit__implementations__: Dict[Type['Interface'], ImplementationMapping]
12-
__explicit__specifications__: Set[Callable]
11+
__explicit__specifications__: FrozenSet[Callable]
1312

1413
def __new__(mcs, name, bases, namespace, *, concrete: bool = False):
1514
inherited_specifications = []
@@ -108,15 +107,15 @@ def __new__(mcs, name, bases, namespace, *, concrete: bool = False):
108107

109108
return cls
110109

111-
def as_interface_type(cls, interface: Type[T]) -> T:
110+
def as_interface_type(cls, interface: Type[T]) -> Callable[[T], T]:
112111
if not isinstance(interface, type) or not issubclass(interface, Interface):
113112
raise TypeError(f"Expected an interface type, got {interface}")
114113

115114
if not issubclass(cls, interface):
116115
raise TypeError(f"Class {cls.__name__} does not implement interface {interface.__name__}")
117116

118117
if not interface.__explicit__specifications__:
119-
return lambda instance: instance
118+
return lambda instance: cast(T, instance)
120119

121120
if interface not in cls.__explicit__implementations__:
122121
raise TypeError(f"Class {cls.__name__} does not implement interface {interface.__name__}")
@@ -133,14 +132,17 @@ def __getattr__(self, name):
133132
except AttributeError as e:
134133
raise e from None
135134

136-
try:
137-
return implementation_mapping[specification].__get__(self._instance)
138-
except KeyError:
139-
raise TypeError(f"Class {cls.__name__} does not provide an explicit implementation for method '{name}' of interface {interface.__name__}") from None
135+
if hasattr(specification, "__declaring_interface__"):
136+
try:
137+
return implementation_mapping[specification].__get__(self._instance)
138+
except KeyError:
139+
raise TypeError(f"Class {cls.__name__} does not provide an explicit implementation for method '{name}' of interface {interface.__name__}") from None
140+
141+
return getattr(self._instance, name)
140142

141-
return ExplicitImplementation
143+
return cast(Callable[[T], T], ExplicitImplementation)
142144

143145

144146
class Interface(metaclass=InterfaceMeta):
145147
def as_interface(self, interface: Type[T]) -> T:
146-
return type(self).as_interface_type(interface)(self)
148+
return type(self).as_interface_type(interface)(cast(T, self))

tests/test_explicit_implementation.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ def baz(self, z: str) -> float:
3030
...
3131

3232

33+
class IWithConcreteMethod(Interface):
34+
@abstractmethod
35+
def abstract_method(self) -> str:
36+
...
37+
38+
def concrete_method(self) -> str:
39+
"""A normal concrete method that should be accessible directly."""
40+
return "concrete method result"
41+
42+
@classmethod
43+
def class_method(cls) -> str:
44+
"""A class method that should be accessible directly."""
45+
return "class method result"
46+
47+
3348
class TestInterfaceBasics:
3449
"""Test basic interface functionality."""
3550

@@ -316,6 +331,91 @@ def foo_implementation(self, x: int) -> str:
316331
# Missing IBar.bar implementation - should raise TypeError
317332

318333

334+
class TestConcreteMethodAccess:
335+
"""Test accessing concrete methods in interfaces directly from concrete classes."""
336+
337+
def test_concrete_method_direct_access(self):
338+
"""Test that concrete methods in interfaces can be accessed directly without explicit implementation."""
339+
340+
class Concrete(IWithConcreteMethod):
341+
@implements(IWithConcreteMethod.abstract_method)
342+
def abstract_method_impl(self) -> str:
343+
return "abstract implementation"
344+
345+
concrete = Concrete()
346+
347+
# Access concrete method directly - should work without explicit implementation
348+
result = concrete.concrete_method()
349+
assert result == "concrete method result"
350+
351+
# Access class method directly - should work without explicit implementation
352+
class_result = concrete.class_method()
353+
assert class_result == "class method result"
354+
355+
# Also test accessing class method on the class itself
356+
class_result_from_class = Concrete.class_method()
357+
assert class_result_from_class == "class method result"
358+
359+
# Verify that the abstract method works through interface casting
360+
interface_impl = concrete.as_interface(IWithConcreteMethod)
361+
abstract_result = interface_impl.abstract_method()
362+
assert abstract_result == "abstract implementation"
363+
364+
def test_concrete_method_inheritance_behavior(self):
365+
"""Test how concrete methods behave with inheritance and overriding."""
366+
367+
class Concrete(IWithConcreteMethod):
368+
@implements(IWithConcreteMethod.abstract_method)
369+
def abstract_method_impl(self) -> str:
370+
return "abstract implementation"
371+
372+
def concrete_method(self) -> str:
373+
"""Override the concrete method in the concrete class."""
374+
return "overridden concrete method"
375+
376+
concrete = Concrete()
377+
378+
# Direct access should use the overridden version
379+
direct_result = concrete.concrete_method()
380+
assert direct_result == "overridden concrete method"
381+
382+
# Class method should still work the same way since it's not overridden
383+
class_result = concrete.class_method()
384+
assert class_result == "class method result"
385+
386+
# Concrete methods should be accessible through interface casting
387+
# Since there's no explicit implementation, it falls back to the concrete class's method
388+
interface_impl = concrete.as_interface(IWithConcreteMethod)
389+
interface_result = interface_impl.concrete_method()
390+
assert interface_result == "overridden concrete method" # Uses the concrete class's implementation
391+
392+
# Class methods should also be accessible through interface casting
393+
interface_class_result = interface_impl.class_method()
394+
assert interface_class_result == "class method result"
395+
396+
def test_concrete_method_without_override(self):
397+
"""Test concrete method access when not overridden in concrete class."""
398+
399+
class SimpleImplementation(IWithConcreteMethod):
400+
@implements(IWithConcreteMethod.abstract_method)
401+
def abstract_method_impl(self) -> str:
402+
return "simple abstract implementation"
403+
404+
concrete = SimpleImplementation()
405+
406+
# Direct access should use the interface's concrete method
407+
direct_result = concrete.concrete_method()
408+
assert direct_result == "concrete method result"
409+
410+
# Interface casting should also work and return the same result
411+
interface_impl = concrete.as_interface(IWithConcreteMethod)
412+
interface_result = interface_impl.concrete_method()
413+
assert interface_result == "concrete method result"
414+
415+
# Both should be the same since there's no override
416+
assert direct_result == interface_result
417+
418+
319419
class TestEdgeCases:
320420
"""Test edge cases and boundary conditions."""
321421

0 commit comments

Comments
 (0)