Skip to content

Commit

Permalink
Linting, self.arguments is a property now, self.clients incrementer, …
Browse files Browse the repository at this point in the history
…max_clients kwarg, update readme
  • Loading branch information
emboiko committed Nov 12, 2020
1 parent 34d18aa commit dd165c9
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 49 deletions.
23 changes: 18 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

**Constructor:**

`Socket_Singleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True)`
`Socket_Singleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True, max_clients=0)`

**Usage:**

Expand Down Expand Up @@ -66,8 +66,7 @@ Now, in another shell, if we try:
The interpreter exits immediately and we end up back at the prompt.

---
We can also get access to **arguments** passed from subsequent attempts to run `python app.py` with the `arguments` attribute.
This attribute is not intended to be accessed directly- it's most likely more convenient to use the `trace()` method. This allows you to **register a callback**, which gets called when `arguments` is appended (as other instances *try* to run).
We can also get access to **arguments** passed from subsequent attempts to run `python app.py` with the `arguments` property, although is not intended to be accessed directly- it's most likely more convenient to use the `trace()` method. This allows you to **register a callback**, which gets called when `arguments` is appended (as other instances *try* to run).

`Socket_Singleton.trace(observer, *args, **kwargs)`

Expand Down Expand Up @@ -159,13 +158,27 @@ with Socket_Singleton() as ss:
```

---
**Timeout**
- `timeout`

A duration in seconds, specifying how long to hold the socket. Defaults to 0 (no timeout, keep-alive). Countdown starts at the end of initialization, immediately after the socket is bound successfully.

---

If we specify the kwarg `strict=False`, we can raise and capture a **custom exception**, `MultipleSingletonsError`, rather than entirely destroying the process which fails to become the singleton.
- `clients`

An integer property describing how many client processes have connected since instantiation.

---

- `max_clients`

A positive integer describing how many client processes to capture before internally calling close() and releasing the port. Defaults to 0 (no maximum)

---

- `strict=False`

We can raise and capture a custom **exception**, `MultipleSingletonsError`, rather than entirely destroying the process which fails to become the singleton.

```
from Socket_Singleton import Socket_Singleton, MultipleSingletonsError
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

setup(
name="Socket_Singleton",
version="0.0.2",
version="1.0.0",
description="Allow a single instance of a Python application to run at once",
py_modules=["Socket_Singleton"],
package_dir={"":"src"},
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
Expand Down
61 changes: 47 additions & 14 deletions src/Socket_Singleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@ class Socket_Singleton:
strict: (bool) This controls how strongly we enforce our singleton status.
If set to False, we'll raise a specific exception instead of a hard,
immediate raise of SystemExit (MultipleSingletonsError).
max_clients: (int) A positive integer describing how many client processes
to capture before internally calling close() and releasing the port. Defaults
to 0 (no maximum)
"""

def __init__(
self,
address:str="127.0.0.1",
port:int=1337,
timeout:int=0,
client:bool=True,
strict:bool=True
strict:bool=True,
max_clients:int=0,
):
"""
Initializer uses exception handler control flow to determine
Expand All @@ -53,17 +58,23 @@ def __init__(
-Silently & immediately close.
-Immediately close after sending their arguments to the host.
-Close after a timeout.
-Close after connecting with a set number of client processes.
"""

self.address = address
self.port = int(port)
self.timeout = int(timeout)
self.max_clients = int(max_clients)

if self.max_clients < 0:
raise ValueError("max_clients must be equal to or greater than 0")

self.arguments = deque([arg for arg in argv[1:]])
self._arguments = deque([arg for arg in argv[1:]])

self._client = client
self._strict = strict
self._observers = dict()
self._clients = 0

self._sock = socket()
try:
Expand Down Expand Up @@ -109,7 +120,7 @@ def __repr__(self):
f"@ {self.address} on port {self.port}\n"\
f"@ {hex(id(self))} "\
f"w/ {len(self._observers.keys())} registered observer(s)\n"\
f"client={self._client}, strict={self._strict}"
f"client={self._client}, strict={self._strict}, max_clients={self.max_clients}"


def __enter__(self):
Expand All @@ -133,17 +144,22 @@ def _create_server(self):
If the socket bound successfully, a threaded server will
continuously listen for & receive connections & data from
potential clients. Arguments passed from clients are
collected into self.arguments
collected into self._arguments
"""

with self._sock as s:
s.listen()
while self._running:
connection, address = s.accept()
connection, _ = s.accept()
with connection:
self._clients += 1

data = connection.recv(1024)
args = data.decode().split("\n")
[self._append_args(arg) for arg in args if arg]
[self._append_arg(arg) for arg in args if arg]

if (self.max_clients) and (self._clients >= self.max_clients):
self.close()


def _create_client(self):
Expand All @@ -160,31 +176,48 @@ def _create_client(self):
s.send("\n".encode())


def _append_args(self, arg):
"""Pseudo-setter for self._arguments"""
@property
def arguments(self):
"""Args shouldn't be dealt with manually, or at least not here"""

return self._arguments

self.arguments.append(arg)

@property
def clients(self):
"""A getter for the clients counter"""

return self._clients


def _append_arg(self, arg):
"""Setter/pusher for self._arguments, should only be called internally"""

self._arguments.append(arg)
self._update_observers()


def _update_observers(self):
"""Call all observers with their respective args, kwargs"""
"""
Call all observers with their respective args, kwargs
(publisher for observer pattern)
"""

[
observer(self.arguments.pop(), *args[0], **args[1])
observer(self._arguments.pop(), *args[0], **args[1])
for observer, args
in self._observers.items()
]


def trace(self, observer, *args, **kwargs):
"""Attach a callback w/ arbitrary # of args & kwargs"""
"""Attach (subscribe) a callback w/ arbitrary # of args & kwargs"""

self._observers[observer] = args, kwargs


def untrace(self, observer):
"""Detach a callback"""
"""Detach (unsubscribe) a callback"""

self._observers.pop(observer)

Expand Down
20 changes: 10 additions & 10 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def setUp(self):
self.traced_args = []


def test_defaults(self):
result = run("test_app.py defaults", shell=True, capture_output=True)
def test_default(self):
result = run("test_app.py default", shell=True, capture_output=True)
self.assertFalse(result.stdout)


Expand All @@ -21,16 +21,16 @@ def test_different_port(self):


def test_no_client(self):
result = run("test_app.py no_client foo bar baz", shell=True, capture_output=True)
run("test_app.py no_client foo bar baz", shell=True, capture_output=True)
self.assertNotIn("noclient", self.app.arguments)
self.assertNotIn("foo", self.app.arguments)
self.assertNotIn("bar", self.app.arguments)
self.assertNotIn("baz", self.app.arguments)


def test_client(self):
run("test_app.py defaults foo bar baz", shell=True, capture_output=True)
self.assertIn("defaults", self.app.arguments)
run("test_app.py default foo bar baz", shell=True, capture_output=True)
self.assertIn("default", self.app.arguments)
self.assertIn("foo", self.app.arguments)
self.assertIn("bar", self.app.arguments)
self.assertIn("baz", self.app.arguments)
Expand All @@ -53,15 +53,15 @@ def test_no_strict(self):

def test_trace(self):
self.app.trace(self.traced)
run("test_app.py defaults foo bar baz", shell=True, capture_output=True)
run("test_app.py default foo bar baz", shell=True, capture_output=True)
self.assertEqual(len(self.traced_args), 4)


def test_untrace(self):
self.app.trace(self.traced)
run("test_app.py defaults foo bar baz", shell=True, capture_output=True)
run("test_app.py default foo bar baz", shell=True, capture_output=True)
self.app.untrace(self.traced)
run("test_app.py defaults foo bar baz", shell=True, capture_output=True)
run("test_app.py default foo bar baz", shell=True, capture_output=True)
self.assertEqual(len(self.traced_args), 4)


Expand All @@ -71,8 +71,8 @@ def traced(self, argument):

def test_slam_args(self):
self.app.arguments.clear()
for i in range(10):
run("test_app.py defaults foo bar bin baz", shell=True)
for _ in range(10):
run("test_app.py default foo bar bin baz", shell=True)

self.assertEqual(len(self.app.arguments), 50)

Expand Down
74 changes: 55 additions & 19 deletions test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,91 @@
from sys import argv


def defaults():
# This file is used for the test cases in test.py and for manual debugging / testing
# Functions defined here are not *all* neccessarily invoked by test.py


def default():
Socket_Singleton()
print("I am the singleton")


def cb(arg):
print(arg)


def trace():
app = Socket_Singleton()
print("I am the singleton")
app.trace(cb)
input()


def different_port():
app = Socket_Singleton(port=1338)
Socket_Singleton(port=400)
print("I am the singleton")


def no_client():
app = Socket_Singleton(client=False)
Socket_Singleton(client=False)


def context():
with Socket_Singleton() as ss:
with Socket_Singleton():
print("I am the singleton")


def context_no_strict():
try:
with Socket_Singleton(strict=False):
print("I am the singleton")
except MultipleSingletonsError as err:
except MultipleSingletonsError:
print("MultipleSingletonsError")


def no_strict():
try:
app = Socket_Singleton(strict=False)
Socket_Singleton(strict=False)
print("I am the singleton")

except MultipleSingletonsError as err:
except MultipleSingletonsError:
print("MultipleSingletonsError")


if argv[1] == "defaults":
defaults()
def max_clients():
app = Socket_Singleton(max_clients=3)
app.trace(cb)
input()


def main():
if not argv[1]:
print("Missing required argument. ex: default")

if argv[1] == "default":
default()

if argv[1] == "trace":
trace()

if argv[1] == "different_port":
different_port()

if argv[1] == "no_client":
no_client()

if argv[1] == "context":
context()

if argv[1] == "context":
context()
if argv[1] == "context_no_strict":
context_no_strict()

if argv[1] == "different_port":
different_port()
if argv[1] == "no_strict":
no_strict()

if argv[1] == "no_client":
no_client()
if argv[1] == "max_clients":
max_clients()

if argv[1] == "no_strict":
no_strict()

if argv[1] == "context_no_strict":
context_no_strict()
if __name__ == "__main__":
main()

0 comments on commit dd165c9

Please sign in to comment.