Skip to content

Commit

Permalink
[NEW] Singleton Software Design Pattern
Browse files Browse the repository at this point in the history
Release Software Design Patterns v1.3.0
  • Loading branch information
boromir674 committed Jun 10, 2022
2 parents 336d1af + 76b3ca5 commit d357d6f
Show file tree
Hide file tree
Showing 18 changed files with 347 additions and 59 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.rst
@@ -1,6 +1,21 @@
=========
Changelog
=========

1.3.0 (2022-06-10)
==================

Releasing the Singleton Pattern, implemented as a Python Metaclass.

Changes
^^^^^^^

feature
"""""""
- add Singleton Pattern



1.2.1 (2022-05-04)
------------------

Expand Down
4 changes: 2 additions & 2 deletions README.rst
Expand Up @@ -133,9 +133,9 @@ Example code to use the `factory` pattern in the form of a `(sub) class registry
:alt: PyPI - Python Version
:target: https://pypi.org/project/software-patterns

.. |commits_since| image:: https://img.shields.io/github/commits-since/boromir674/software-patterns/v1.2.1/master?color=blue&logo=Github
.. |commits_since| image:: https://img.shields.io/github/commits-since/boromir674/software-patterns/v1.3.0/master?color=blue&logo=Github
:alt: GitHub commits since tagged version (branch)
:target: https://github.com/boromir674/software-patterns/compare/v1.2.1..master
:target: https://github.com/boromir674/software-patterns/compare/v1.3.0..master



Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py 100644 → 100755
Expand Up @@ -22,7 +22,7 @@
author = 'Konstantinos Lampridis'

# The full version, including alpha/beta/rc tags
release = '1.2.1'
release = '1.3.0'

# -- General configuration ---------------------------------------------------

Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
@@ -1,3 +1,20 @@
[build-system]
requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"


## Black formatting/linting

[tool.black]
line-length = 95
include = '\.pyi?$'
extend-exclude = '''
'''


[tool.isort]
profile = 'black'


[tool.software-release]
version_variable = "src/software_patterns/__init__.py:__version__"
2 changes: 1 addition & 1 deletion setup.cfg
@@ -1,7 +1,7 @@
[metadata]
## Setuptools specific information
name = software_patterns
version = 1.2.1
version = 1.3.0
description = Software Design Patterns with types in Python.
long_description = file: README.rst
long_description_content_type = text/x-rst
Expand Down
19 changes: 13 additions & 6 deletions src/software_patterns/__init__.py
@@ -1,10 +1,17 @@
__version__ = '1.2.1'
__version__ = '1.3.0'

from .notification import Observer, Subject
from .memoize import ObjectsPool
from .proxy import ProxySubject, Proxy
from .notification import Observer, Subject
from .proxy import Proxy, ProxySubject
from .singleton import Singleton
from .subclass_registry import SubclassRegistry


__all__ = ['Observer', 'Subject', 'ObjectsPool', 'SubclassRegistry',
'ProxySubject', 'Proxy']
__all__ = [
'Observer',
'Subject',
'ObjectsPool',
'SubclassRegistry',
'ProxySubject',
'Proxy',
'Singleton',
]
42 changes: 42 additions & 0 deletions src/software_patterns/handler.py
@@ -0,0 +1,42 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, Optional


class Handler(ABC):
"""
The Handler interface declares a method for building the chain of handlers.
It also declares a method for executing a request.
"""

@abstractmethod
def set_next(self, handler: Handler) -> Handler:
pass

@abstractmethod
def handle(self, request) -> Optional[str]:
pass


class AbstractHandler(Handler):
"""
The default chaining behavior can be implemented inside a base handler
class.
"""

_next_handler: Optional[Handler] = None

def set_next(self, handler: Handler) -> Handler:
self._next_handler = handler
# Returning a handler from here will let us link handlers in a
# convenient way like this:
# monkey.set_next(squirrel).set_next(dog)
return handler

@abstractmethod
def handle(self, request: Any) -> Optional[str]:
if self._next_handler:
return self._next_handler.handle(request)

return None
23 changes: 17 additions & 6 deletions src/software_patterns/memoize.py
Expand Up @@ -4,14 +4,12 @@
the result of computing a hash given runtime arguments.
"""
from typing import Dict, Generic, TypeVar, Any, Callable, Optional, Union
import types

from typing import Any, Callable, Dict, Generic, Optional, TypeVar, Union

__all__ = ['ObjectsPool']



DictKey = Union[int, str]
ObjectType = TypeVar('ObjectType')

Expand All @@ -21,6 +19,7 @@
def adapt_build_hash(a_callable: RuntimeBuildHashCallable):
def build_hash(_self: ObjectType, *args, **kwargs):
return a_callable(*args, **kwargs)

return build_hash


Expand Down Expand Up @@ -60,23 +59,35 @@ class ObjectsPool(Generic[ObjectType]):
Returns:
[type]: [description]
"""

_objects: Dict[DictKey, ObjectType]

user_supplied_callback: Dict[bool, Callable] = {
True: lambda callback: callback,
False: lambda callback: ObjectsPool.__build_hash,
}

def __init__(self, callback: Callable[..., ObjectType], hash_callback: Optional[RuntimeBuildHashCallable]=None):
def __init__(
self,
callback: Callable[..., ObjectType],
hash_callback: Optional[RuntimeBuildHashCallable] = None,
):
self.constructor = callback
build_hash_callback = self.user_supplied_callback[callable(hash_callback)](hash_callback)
build_hash_callback = self.user_supplied_callback[callable(hash_callback)](
hash_callback
)
self._build_hash = types.MethodType(adapt_build_hash(build_hash_callback), self)
self._objects = {}

@staticmethod
def __build_hash(*args: Any, **kwargs: Any) -> int:
r"""Construct a hash out of the input \*args and \*\*kwargs."""
return hash('-'.join([str(_) for _ in args] + [f'{key}={str(value)}' for key, value in kwargs.items()]))
return hash(
'-'.join(
[str(_) for _ in args]
+ [f'{key}={str(value)}' for key, value in kwargs.items()]
)
)

def get_object(self, *args: Any, **kwargs: Any) -> ObjectType:
r"""Request an object from the pool.
Expand Down
4 changes: 3 additions & 1 deletion src/software_patterns/notification.py
Expand Up @@ -25,7 +25,7 @@
"""

from abc import ABC, abstractmethod
from typing import List, Generic, TypeVar
from typing import Generic, List, TypeVar

__all__ = ['Subject', 'Observer']

Expand All @@ -39,6 +39,7 @@ class ObserverInterface(ABC):
Enables objects to act as "event" listeners; react to "notifications"
by executing specific (event) handling logic.
"""

@abstractmethod
def update(self, *args, **kwargs) -> None:
"""Receive an update (from a subject); handle an event notification."""
Expand Down Expand Up @@ -121,6 +122,7 @@ class Subject(SubjectInterface, Generic[StateType]):
>>> broadcaster.notify()
observer-type-a reacts to event event-object-B
"""

def __init__(self, *args, **kwargs):
self._observers: List[ObserverInterface] = []
self._state = None
Expand Down
49 changes: 48 additions & 1 deletion src/software_patterns/proxy.py
Expand Up @@ -4,21 +4,66 @@
design pattern, to the client code."""

from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Callable
from typing import Callable, Generic, TypeVar

T = TypeVar('T')


__all__ = ['ProxySubject', 'Proxy']


class ProxySubjectInterfaceClass(type, Generic[T]):
"""Interfacing enabling classes to construct classes (instead of instances).
Dynamically creates classes that represent a ProxySubjectInterface.
The created classes automatically gain an abstract method with name given at
creation time. The input name can match the desired named selected to a
proxied object.
For example in a scenario where you proxy a remote web server you might
create ProxySubjectInterface with a 'make_request' abstract method where as
in a scenario where the proxied object is a Tensorflow function you might
name the abstract method as 'tensorflow'.
Dynamically, creating a class (as this class allows) is useful to adjust to
scenarios like the above.
Args:
Generic ([type]): [description]
Raises:
NotImplementedError: [description]
Returns:
[type]: [description]
"""

def __new__(mcs, *args, **kwargs):
def __init__(self, proxied_object):
self._proxy_subject = proxied_object

def object(self, *args, **kwargs) -> T:
return self._proxy_subject

return super().__new__(
mcs,
'ProxySubjectInterface',
(ABC,),
{
'__init__': __init__,
args[0]: object,
},
)


class ProxySubjectInterface(ABC, Generic[T]):
"""Proxy Subject interface holding the important 'request' operation.
Declares common operations for both ProxySubject and
the Proxy. As long as the client uses ProxySubject's interface, a proxy can
be passed pass to it, instead of a real subject.
"""

@abstractmethod
def request(self, *args, **kwargs) -> T:
raise NotImplementedError
Expand All @@ -42,6 +87,7 @@ class ProxySubject(ProxySubjectInterface, Generic[T]):
>>> proxied_object.request(1)
2
"""

def __init__(self, callback: Callable[..., T]):
self._callback = callback

Expand Down Expand Up @@ -74,6 +120,7 @@ class Proxy(ProxySubjectInterface, Generic[T]):
>>> proxy.request(3)
9
"""

def __init__(self, proxy_subject: ProxySubject):
self._proxy_subject = proxy_subject

Expand Down
48 changes: 48 additions & 0 deletions src/software_patterns/singleton.py
@@ -0,0 +1,48 @@
import typing as t


class Singleton(type):
"""Singleton Design Pattern imlemented as a Metaclass.
Use this Metaclass to design your class (constructor) with the Singleton
Pattern. Setting your class's 'metaclass' key to this Metaclass (see
example below), will restrict object instantiation so that it always return
the same (singleton) object.
Example:
>>> class ObjectDict(metaclass=Singleton):
... def __init__(self):
... super().__init__()
... self.objects = {}
>>> reg1 = ObjectDict()
>>> reg1.objects['a'] = 1
>>> reg2 = ObjectRegistry()
>>> reg2.objects['b'] = 2
>>> reg3 = ObjectRegistry()
>>> reg2.objects == {'a': 1}
True
>>> reg3.objects['a'] == 1
True
>>> reg3.objects['b'] == 2
True
>>> id(reg1) == id(reg2) == id(reg3)
True
"""

_instances: t.Mapping[t.Type, t.Any] = {}

def __call__(cls: t.Type, *args, **kwargs) -> t.Any:
instance = cls._instances.get(cls)
if not instance:
instance = super(Singleton, cls).__call__(*args, **kwargs)
cls._instances[cls] = instance
return instance

0 comments on commit d357d6f

Please sign in to comment.