From 7948253de16ba4f8501c783ee94855d0eae10c89 Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Wed, 15 Jul 2026 14:39:41 -0400 Subject: [PATCH 1/7] 010 - pystalk: wrap raw connection errors in BeanstalkConnectionError --- docs/source/index.rst | 3 +++ pystalk/__init__.py | 4 ++-- pystalk/client.py | 34 +++++++++++++++++++++++++++++++++- tests/unit/test_pystalk.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 3a1155f..cecba9b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -29,6 +29,9 @@ Members .. autoclass:: pystalk.BeanstalkError :members: +.. autoclass:: pystalk.BeanstalkConnectionError + :members: + .. autoclass:: pystalk.client.Job :members: diff --git a/pystalk/__init__.py b/pystalk/__init__.py index e290c0f..0a2ba9f 100644 --- a/pystalk/__init__.py +++ b/pystalk/__init__.py @@ -1,4 +1,4 @@ -from .client import BeanstalkClient, BeanstalkError +from .client import BeanstalkClient, BeanstalkError, BeanstalkConnectionError from .pool import ProductionPool version_info = (0, 7, 0) @@ -6,4 +6,4 @@ __author__ = 'EasyPost ' -__all__ = ['BeanstalkClient', 'BeanstalkError', 'ProductionPool'] +__all__ = ['BeanstalkClient', 'BeanstalkError', 'BeanstalkConnectionError', 'ProductionPool'] diff --git a/pystalk/client.py b/pystalk/client.py index 07423e8..4ed1e18 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -30,6 +30,31 @@ class BeanstalkError(Exception): message: str = attr.ib(converter=lambda m: m.decode('ascii')) +class BeanstalkConnectionError(BeanstalkError): + """Raised when the underlying socket connection to beanstalkd fails. + + Distinguishes connection-level failures (e.g. connection refused, timeout, + DNS errors) from beanstalk protocol errors. Carries the ``host`` and + ``port`` that were being connected to, and preserves the original socket + error both on :attr:`original` and via ``__cause__`` (``raise ... from``). + + This is a subclass of :class:`BeanstalkError` so that existing + ``except BeanstalkError`` callers continue to catch connection failures. + """ + + def __init__(self, host, port, original): + # BeanstalkError is an attr.s(frozen=True) class whose generated + # __init__ expects a bytes ``message`` (it calls .decode('ascii')). + # We bypass it entirely: set our fields via object.__setattr__ (the + # class is frozen) and initialize the Exception base with a plain str. + object.__setattr__(self, 'host', host) + object.__setattr__(self, 'port', port) + object.__setattr__(self, 'original', original) + msg = 'Failed to connect to beanstalkd at {0}:{1}: {2}'.format(host, port, original) + object.__setattr__(self, 'message', msg) + Exception.__init__(self, msg) + + def yaml_load(fo): # yaml.safe_load will never use the C loader; we have to detect it ourselves if hasattr(yaml, 'CSafeLoader'): @@ -135,7 +160,14 @@ def __str__(self): @property def _socket(self): if self.socket is None: - self.socket = socket.create_connection((self.host, self.port), timeout=self.socket_timeout) + try: + self.socket = socket.create_connection( + (self.host, self.port), timeout=self.socket_timeout + ) + except OSError as e: + # OSError covers ConnectionRefusedError, socket.timeout, + # socket.error, and socket.gaierror in Python 3. + raise BeanstalkConnectionError(self.host, self.port, e) from e self._re_establish_use_watch() return self.socket diff --git a/tests/unit/test_pystalk.py b/tests/unit/test_pystalk.py index db10268..72fc50a 100644 --- a/tests/unit/test_pystalk.py +++ b/tests/unit/test_pystalk.py @@ -58,3 +58,35 @@ def test_from_uri(uri, expected_host, expected_port): def test_invalid_uri_fails(uri): with pytest.raises(ValueError): pystalk.BeanstalkClient.from_uri(uri) + + +def test_connection_error_is_wrapped(monkeypatch): + def boom(*args, **kwargs): + raise ConnectionRefusedError('refused') + monkeypatch.setattr('pystalk.client.socket.create_connection', boom) + + client = pystalk.BeanstalkClient('pystalk.example.com', 11300) + # ensure fresh (no injected mock socket) + client.socket = None + + with pytest.raises(pystalk.BeanstalkConnectionError) as ei: + _ = client._socket + assert 'pystalk.example.com' in str(ei.value) + assert '11300' in str(ei.value) + # host/port context is exposed on the exception + assert ei.value.host == 'pystalk.example.com' + assert ei.value.port == 11300 + # original socket error is preserved via __cause__ + assert isinstance(ei.value.__cause__, ConnectionRefusedError) + assert isinstance(ei.value.original, ConnectionRefusedError) + + +def test_connection_error_is_a_beanstalk_error(monkeypatch): + # Success criterion: existing `except BeanstalkError` still catches it + def boom(*args, **kwargs): + raise OSError('nope') + monkeypatch.setattr('pystalk.client.socket.create_connection', boom) + client = pystalk.BeanstalkClient('h', 1) + client.socket = None + with pytest.raises(pystalk.BeanstalkError): # must still be caught here + _ = client._socket From 38128785e0a8f293d0f74c7b38af2aa3ccbe39ce Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Thu, 16 Jul 2026 11:20:03 -0400 Subject: [PATCH 2/7] address review feedback: use catch_and_raise decorator, un-freeze subclass, rename original to err --- pystalk/client.py | 55 ++++++++++++++++++++++++-------------- tests/unit/test_pystalk.py | 2 +- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/pystalk/client.py b/pystalk/client.py index 4ed1e18..87f5c70 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -1,4 +1,5 @@ from contextlib import contextmanager +from functools import wraps import attr import socket import yaml @@ -36,22 +37,23 @@ class BeanstalkConnectionError(BeanstalkError): Distinguishes connection-level failures (e.g. connection refused, timeout, DNS errors) from beanstalk protocol errors. Carries the ``host`` and ``port`` that were being connected to, and preserves the original socket - error both on :attr:`original` and via ``__cause__`` (``raise ... from``). + error both on :attr:`err` and via ``__cause__`` (``raise ... from``). This is a subclass of :class:`BeanstalkError` so that existing ``except BeanstalkError`` callers continue to catch connection failures. """ - def __init__(self, host, port, original): - # BeanstalkError is an attr.s(frozen=True) class whose generated - # __init__ expects a bytes ``message`` (it calls .decode('ascii')). - # We bypass it entirely: set our fields via object.__setattr__ (the - # class is frozen) and initialize the Exception base with a plain str. - object.__setattr__(self, 'host', host) - object.__setattr__(self, 'port', port) - object.__setattr__(self, 'original', original) - msg = 'Failed to connect to beanstalkd at {0}:{1}: {2}'.format(host, port, original) - object.__setattr__(self, 'message', msg) + # BeanstalkError is an attr.s(frozen=True) class, and frozen __setattr__ + # is inherited here too. Un-freeze this subclass so we can use plain + # attribute assignment below instead of object.__setattr__. + __setattr__ = object.__setattr__ + + def __init__(self, host, port, err): + self.host = host + self.port = port + self.err = err + msg = 'Failed to connect to beanstalkd at {0}:{1}: {2}'.format(host, port, err) + self.message = msg Exception.__init__(self, msg) @@ -63,6 +65,20 @@ def yaml_load(fo): return yaml.safe_load(fo) +def catch_and_raise(*errors, raise_as=BeanstalkConnectionError): + """Catches specified errors on an instance method and re-raises + as ``raise_as(self.host, self.port, err)``, preserving the chain.""" + def decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except errors as e: + raise raise_as(self.host, self.port, e) from e + return wrapper + return decorator + + @attr.s(frozen=True) class BeanstalkInsertingProxy(object): """Proxy object yielded by :func:`BeanstalkClient.using()`""" @@ -157,18 +173,17 @@ def __str__(self): repr(self), self._watchlist, self.current_tube # pragma: no cover ) # pragma: no cover + @catch_and_raise(ConnectionRefusedError, socket.timeout, socket.error, socket.gaierror) + def _connect(self): + self.socket = socket.create_connection( + (self.host, self.port), timeout=self.socket_timeout + ) + self._re_establish_use_watch() + @property def _socket(self): if self.socket is None: - try: - self.socket = socket.create_connection( - (self.host, self.port), timeout=self.socket_timeout - ) - except OSError as e: - # OSError covers ConnectionRefusedError, socket.timeout, - # socket.error, and socket.gaierror in Python 3. - raise BeanstalkConnectionError(self.host, self.port, e) from e - self._re_establish_use_watch() + self._connect() return self.socket def _re_establish_use_watch(self): diff --git a/tests/unit/test_pystalk.py b/tests/unit/test_pystalk.py index 72fc50a..1043af4 100644 --- a/tests/unit/test_pystalk.py +++ b/tests/unit/test_pystalk.py @@ -78,7 +78,7 @@ def boom(*args, **kwargs): assert ei.value.port == 11300 # original socket error is preserved via __cause__ assert isinstance(ei.value.__cause__, ConnectionRefusedError) - assert isinstance(ei.value.original, ConnectionRefusedError) + assert isinstance(ei.value.err, ConnectionRefusedError) def test_connection_error_is_a_beanstalk_error(monkeypatch): From 6adfceaa4ae8327651d5073bf5e89383eb8ef043 Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Thu, 16 Jul 2026 11:25:55 -0400 Subject: [PATCH 3/7] address remaining review feedback: BeanstalkError gets explicit init, use super().__init__, f-string message --- pystalk/client.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pystalk/client.py b/pystalk/client.py index 87f5c70..5ef7110 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -24,11 +24,17 @@ class Job(object): job_data = attr.ib() -@attr.s(frozen=True, hash=True, eq=True) +@attr.s(frozen=True, hash=True, eq=True, init=False) class BeanstalkError(Exception): """Common error raised when something goes wrong with beanstalk""" - message: str = attr.ib(converter=lambda m: m.decode('ascii')) + message: str = attr.ib() + + def __init__(self, message: Union[str, bytes]): + if isinstance(message, bytes): + message = message.decode('ascii') + object.__setattr__(self, 'message', message) + Exception.__init__(self, message) class BeanstalkConnectionError(BeanstalkError): @@ -52,9 +58,8 @@ def __init__(self, host, port, err): self.host = host self.port = port self.err = err - msg = 'Failed to connect to beanstalkd at {0}:{1}: {2}'.format(host, port, err) - self.message = msg - Exception.__init__(self, msg) + msg = f'Failed to connect to beanstalkd at {host}:{port}: {err}' + super().__init__(msg) def yaml_load(fo): From 9df6fe75b3e19696776603c48d13a6a377b7d1e4 Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Tue, 21 Jul 2026 09:36:25 -0400 Subject: [PATCH 4/7] Updated reviewer comments --- pystalk/client.py | 28 ++++++++++------------------ tests/unit/test_pystalk.py | 8 ++++---- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/pystalk/client.py b/pystalk/client.py index 5ef7110..f1bed42 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -24,17 +24,16 @@ class Job(object): job_data = attr.ib() -@attr.s(frozen=True, hash=True, eq=True, init=False) +@attr.s(hash=True, eq=True, init=False) class BeanstalkError(Exception): - """Common error raised when something goes wrong with beanstalk""" message: str = attr.ib() def __init__(self, message: Union[str, bytes]): if isinstance(message, bytes): message = message.decode('ascii') - object.__setattr__(self, 'message', message) - Exception.__init__(self, message) + self.message = message + super().__init__(message) class BeanstalkConnectionError(BeanstalkError): @@ -49,11 +48,7 @@ class BeanstalkConnectionError(BeanstalkError): ``except BeanstalkError`` callers continue to catch connection failures. """ - # BeanstalkError is an attr.s(frozen=True) class, and frozen __setattr__ - # is inherited here too. Un-freeze this subclass so we can use plain - # attribute assignment below instead of object.__setattr__. - __setattr__ = object.__setattr__ - + def __init__(self, host, port, err): self.host = host self.port = port @@ -79,7 +74,7 @@ def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except errors as e: - raise raise_as(self.host, self.port, e) from e + raise raise_as(self.host, self.port, e) return wrapper return decorator @@ -178,17 +173,14 @@ def __str__(self): repr(self), self._watchlist, self.current_tube # pragma: no cover ) # pragma: no cover - @catch_and_raise(ConnectionRefusedError, socket.timeout, socket.error, socket.gaierror) - def _connect(self): - self.socket = socket.create_connection( - (self.host, self.port), timeout=self.socket_timeout - ) - self._re_establish_use_watch() - @property + @catch_and_raise(ConnectionRefusedError, socket.timeout, socket.error, socket.gaierror) def _socket(self): if self.socket is None: - self._connect() + self.socket = socket.create_connection( + (self.host, self.port), timeout=self.socket_timeout + ) + self._re_establish_use_watch() return self.socket def _re_establish_use_watch(self): diff --git a/tests/unit/test_pystalk.py b/tests/unit/test_pystalk.py index 1043af4..dba4645 100644 --- a/tests/unit/test_pystalk.py +++ b/tests/unit/test_pystalk.py @@ -76,17 +76,17 @@ def boom(*args, **kwargs): # host/port context is exposed on the exception assert ei.value.host == 'pystalk.example.com' assert ei.value.port == 11300 - # original socket error is preserved via __cause__ - assert isinstance(ei.value.__cause__, ConnectionRefusedError) + # original socket error is preserved via implicit context chaining + assert isinstance(ei.value.__context__, ConnectionRefusedError) assert isinstance(ei.value.err, ConnectionRefusedError) def test_connection_error_is_a_beanstalk_error(monkeypatch): # Success criterion: existing `except BeanstalkError` still catches it def boom(*args, **kwargs): - raise OSError('nope') + raise pystalk.BeanstalkError('nope') monkeypatch.setattr('pystalk.client.socket.create_connection', boom) client = pystalk.BeanstalkClient('h', 1) client.socket = None - with pytest.raises(pystalk.BeanstalkError): # must still be caught here + with pytest.raises(pystalk.BeanstalkError): _ = client._socket From 7e696dfbf07aae07d1e0ebcc943e4a4c3be722dd Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Tue, 21 Jul 2026 10:59:42 -0400 Subject: [PATCH 5/7] Reviewer comments corrections 2 --- pystalk/client.py | 77 ++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/pystalk/client.py b/pystalk/client.py index f1bed42..7ccd60f 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -11,7 +11,8 @@ @attr.s(frozen=True) class Job(object): - """Structure holding a job returned from Beanstalk + """ + Structure holding a job returned from Beanstalk :ivar job_id: Opaque identifier for the job (to be passed to :func:`BeanstalkClient.release_job()` or :func:`BeanstalkClient.stats_job()`). @@ -33,11 +34,12 @@ def __init__(self, message: Union[str, bytes]): if isinstance(message, bytes): message = message.decode('ascii') self.message = message - super().__init__(message) + super(BeanstalkError, self).__init__(message) class BeanstalkConnectionError(BeanstalkError): - """Raised when the underlying socket connection to beanstalkd fails. + """ + Raised when the underlying socket connection to beanstalkd fails. Distinguishes connection-level failures (e.g. connection refused, timeout, DNS errors) from beanstalk protocol errors. Carries the ``host`` and @@ -53,8 +55,7 @@ def __init__(self, host, port, err): self.host = host self.port = port self.err = err - msg = f'Failed to connect to beanstalkd at {host}:{port}: {err}' - super().__init__(msg) + super(BeanstalkConnectionError, self).__init__(f"Failed to connect to beanstalkd at {host}:{port}: {err}") def yaml_load(fo): @@ -66,8 +67,10 @@ def yaml_load(fo): def catch_and_raise(*errors, raise_as=BeanstalkConnectionError): - """Catches specified errors on an instance method and re-raises - as ``raise_as(self.host, self.port, err)``, preserving the chain.""" + """ + Catches specified errors on an instance method and re-raises + as ``raise_as(self.host, self.port, err)``, preserving the chain. + """ def decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): @@ -87,7 +90,8 @@ class BeanstalkInsertingProxy(object): tube: str = attr.ib() def put_job(self, data: Union[str, bytes], pri: int = 65536, delay: int = 0, ttr: int = 120): - """Method to insert a job into the tube selected with :func:`BeanstalkClient.using`. + """ + Method to insert a job into the tube selected with :func:`BeanstalkClient.using`. :param data: Job body :type data: Text (either str which will be encoded as utf-8, or bytes which are already utf-8 @@ -105,7 +109,8 @@ def put_job(self, data: Union[str, bytes], pri: int = 65536, delay: int = 0, ttr class BeanstalkClient(object): - """Simple wrapper around the Beanstalk API. + """ + Simple wrapper around the Beanstalk API. :param host: Hostname or IP address to connect to :type host: str @@ -126,7 +131,9 @@ class BeanstalkClient(object): """ def __init__(self, host: str, port: int = 11300, socket_timeout: Optional[float] = None, auto_decode: bool = False): - """Construct a synchronous Beanstalk Client. Does not connect!""" + """ + Construct a synchronous Beanstalk Client. Does not connect! + """ self.host = host self.port = port self.socket_timeout = socket_timeout @@ -137,7 +144,8 @@ def __init__(self, host: str, port: int = 11300, socket_timeout: Optional[float] @classmethod def from_uri(cls, uri, socket_timeout=None, auto_decode=False): - """Construct a synchronous Beanstalk Client from a URI. + """ + Construct a synchronous Beanstalk Client from a URI. The URI may be of the form beanstalk://host:port or beanstalkd://host:port @@ -177,14 +185,13 @@ def __str__(self): @catch_and_raise(ConnectionRefusedError, socket.timeout, socket.error, socket.gaierror) def _socket(self): if self.socket is None: - self.socket = socket.create_connection( - (self.host, self.port), timeout=self.socket_timeout - ) + self.socket = socket.create_connection((self.host, self.port), timeout=self.socket_timeout) self._re_establish_use_watch() return self.socket def _re_establish_use_watch(self): - """Call after a close/re-connect. + """ + Call after a close/re-connect. Automatically re-establishes the USE and WATCH configs previously setup. """ @@ -194,7 +201,8 @@ def _re_establish_use_watch(self): self.watchlist = self.desired_watchlist def close(self): - """Close any open connection to the Beanstalk server. + """ + Close any open connection to the Beanstalk server. This object is still safe to use after calling :func:`close()` ; it will automatically reconnect and re-establish any open watches / uses. @@ -290,7 +298,8 @@ def _send_message(self, message, sock): return sock.sendall(message.encode('utf-8')) def list_tubes(self): - """Return a list of tubes that this beanstalk instance knows about + """ + Return a list of tubes that this beanstalk instance knows about :rtype: list of tubes """ @@ -301,7 +310,8 @@ def list_tubes(self): return tubes def stats(self): - """Return a dictionary with a bunch of instance-wide statistics + """ + Return a dictionary with a bunch of instance-wide statistics :rtype: dict """ @@ -312,7 +322,8 @@ def stats(self): return stats def put_job(self, data: Union[str, bytes], pri: int = 65536, delay: int = 0, ttr: int = 120): - """Insert a new job into whatever queue is currently USEd + """ + Insert a new job into whatever queue is currently USEd :param data: Job body :type data: Text (either str which will be encoded as utf-8, or bytes which are already utf-8 @@ -518,7 +529,9 @@ def peek_buried_iter(self): return self._common_iter(self.peek_buried, 'NOT_FOUND') def delete_job(self, job_id): - """Delete the given job id. The job must have been previously reserved by this connection""" + """ + Delete the given job id. The job must have been previously reserved by this connection + """ if hasattr(job_id, 'job_id'): job_id = job_id.job_id with self._sock_ctx() as socket: @@ -526,7 +539,8 @@ def delete_job(self, job_id): self._receive_word(socket, b'DELETED') def bury_job(self, job_id, pri=65536): - """Mark the given job_id as buried. The job must have been previously reserved by this connection + """ + Mark the given job_id as buried. The job must have been previously reserved by this connection :param job_id: Job to bury :param pri: Priority for the newly-buried job. If not passed, will keep its current priority @@ -539,7 +553,8 @@ def bury_job(self, job_id, pri=65536): return self._receive_word(socket, b'BURIED') def release_job(self, job_id, pri=65536, delay=0): - """Put a job back on the queue to be processed (indicating that you've aborted it) + """ + Put a job back on the queue to be processed (indicating that you've aborted it) You can only release a job which you have reserved using :func:`reserve_job()` or :func:`reserve_iter()`. @@ -556,7 +571,8 @@ def release_job(self, job_id, pri=65536, delay=0): return self._receive_word(socket, b'RELEASED', b'BURIED') def kick_job(self, job_id): - """Kick the given job id. The job must either be in the DELAYED or BURIED state and will be immediately moved to + """ + Kick the given job id. The job must either be in the DELAYED or BURIED state and will be immediately moved to the READY state.""" if hasattr(job_id, 'job_id'): job_id = job_id.job_id @@ -565,7 +581,8 @@ def kick_job(self, job_id): self._receive_word(socket, b'KICKED') def use(self, tube): - """Start producing jobs into the given tube. + """ + Start producing jobs into the given tube. :param tube: Name of the tube to USE @@ -580,7 +597,8 @@ def use(self, tube): @contextmanager def using(self, tube): - """Context-manager to insert jobs into a specific tube + """ + Context-manager to insert jobs into a specific tube :param tube: Tube to insert to @@ -605,7 +623,8 @@ def using(self, tube): self.use(current_tube) def kick_jobs(self, num_jobs): - """Kick some number of jobs from the buried queue onto the ready queue. + """ + Kick some number of jobs from the buried queue onto the ready queue. :param num_jobs: Number of jobs to kick :type num_jobs: int @@ -616,7 +635,8 @@ def kick_jobs(self, num_jobs): return self._receive_id(socket) def pause_tube(self, tube, delay=3600): - """Pause a tube for some number of seconds, preventing it from issuing jobs. + """ + Pause a tube for some number of seconds, preventing it from issuing jobs. :param delay: Time to pause for, in seconds :type delay: int @@ -633,7 +653,8 @@ def pause_tube(self, tube, delay=3600): return self._receive_word(socket, b'PAUSED') def unpause_tube(self, tube): - """Unpause a tube which was previously paused with :func:`pause_tube()`. + """ + Unpause a tube which was previously paused with :func:`pause_tube()`. .. seealso:: From a9b2768aa145ceaa565da159000483b433773732 Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Tue, 21 Jul 2026 13:47:12 -0400 Subject: [PATCH 6/7] Version change --- pystalk/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pystalk/__init__.py b/pystalk/__init__.py index 0a2ba9f..580379f 100644 --- a/pystalk/__init__.py +++ b/pystalk/__init__.py @@ -1,8 +1,8 @@ from .client import BeanstalkClient, BeanstalkError, BeanstalkConnectionError from .pool import ProductionPool -version_info = (0, 7, 0) -__version__ = '.'.join(str(s) for s in version_info) +__version__ = '0.8.0' + __author__ = 'EasyPost ' From 00aa6d7bd1e6d9a695db1fd07011861f7e44da0e Mon Sep 17 00:00:00 2001 From: Adonis-Diaz Date: Wed, 22 Jul 2026 10:35:15 -0400 Subject: [PATCH 7/7] Fix mypy/flake8 issues and version mismatch - Refactor _socket property: move connect logic into a decorated _connect() method so @property no longer stacks with @catch_and_raise(...), fixing mypy's 'Decorated property not supported' error. - Remove whitespace-only blank line in BeanstalkConnectionError (flake8 W293/E303). - Remove trailing whitespace in test_pystalk.py (flake8 W291). - Bump setup.py version to 0.8.0 to match pystalk/__init__.py's __version__, which had drifted out of sync. --- pystalk/client.py | 10 ++++++---- setup.py | 2 +- tests/unit/test_pystalk.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pystalk/client.py b/pystalk/client.py index 7ccd60f..4cc178d 100644 --- a/pystalk/client.py +++ b/pystalk/client.py @@ -50,7 +50,6 @@ class BeanstalkConnectionError(BeanstalkError): ``except BeanstalkError`` callers continue to catch connection failures. """ - def __init__(self, host, port, err): self.host = host self.port = port @@ -181,12 +180,15 @@ def __str__(self): repr(self), self._watchlist, self.current_tube # pragma: no cover ) # pragma: no cover - @property @catch_and_raise(ConnectionRefusedError, socket.timeout, socket.error, socket.gaierror) + def _connect(self): + self.socket = socket.create_connection((self.host, self.port), timeout=self.socket_timeout) + self._re_establish_use_watch() + + @property def _socket(self): if self.socket is None: - self.socket = socket.create_connection((self.host, self.port), timeout=self.socket_timeout) - self._re_establish_use_watch() + self._connect() return self.socket def _re_establish_use_watch(self): diff --git a/setup.py b/setup.py index a823f59..b81f006 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( name="pystalk", - version="0.7.0", + version="0.8.0", author="EasyPost", author_email="oss@easypost.com", url="https://github.com/easypost/pystalk", diff --git a/tests/unit/test_pystalk.py b/tests/unit/test_pystalk.py index dba4645..07f9f03 100644 --- a/tests/unit/test_pystalk.py +++ b/tests/unit/test_pystalk.py @@ -88,5 +88,5 @@ def boom(*args, **kwargs): monkeypatch.setattr('pystalk.client.socket.create_connection', boom) client = pystalk.BeanstalkClient('h', 1) client.socket = None - with pytest.raises(pystalk.BeanstalkError): + with pytest.raises(pystalk.BeanstalkError): _ = client._socket