Skip to content

010 - pystalk: wrap raw connection errors in BeanstalkConnectionError#23

Draft
Adonis-Diaz wants to merge 8 commits into
EXP-1217from
010-beanstalk-connection-error
Draft

010 - pystalk: wrap raw connection errors in BeanstalkConnectionError#23
Adonis-Diaz wants to merge 8 commits into
EXP-1217from
010-beanstalk-connection-error

Conversation

@Adonis-Diaz

Copy link
Copy Markdown

No description provided.

Comment thread tests/unit/test_pystalk.py Dismissed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading through these changes I think the best option is to use a decorator. Catching OSError is quite dangerous due to the large number of exceptions that encompasses.

Comment thread pystalk/client.py Outdated
Comment thread pystalk/client.py Outdated
# __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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically correct but way too verbose. The same thing can be done using the following

Suggested change
object.__setattr__(self, 'host', host)
self.host = host
self.port = port
self.original = original

Comment thread pystalk/client.py
Comment thread pystalk/client.py Outdated
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a subclass of BeanstalkError you should be calling super for the parent class not Exception which is another level up. I'd probably add an init function to BeanstalkError since you're calling the implicit init function as well. Its best to use an explicit function rather than an implicit one.

Comment thread pystalk/client.py Outdated
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need from e

Comment thread pystalk/client.py Outdated
``except BeanstalkError`` callers continue to catch connection failures.
"""

def __init__(self, host, port, original):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than original I'd just call it err since its more obvious what that means.

Comment thread pystalk/client.py Outdated
self.socket = socket.create_connection(
(self.host, self.port), timeout=self.socket_timeout
)
except OSError as e:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a better way of handling this implementation. Instead of running a try-except here I'd actually build out a decorator that you can define to catch any number of errors.

The decorator definition would look something like this (you will need to update it):

from functools import wraps

def catch_and_raise(*errors, raise_as=BeanstalkConnectionError):
    """Catches specified errors and raises a specific exception instead."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except errors as e:
                print(f"[Decorator] Intercepted {type(e).__name__}")
                # Use 'from e' to preserve the original traceback (exception chaining)
                raise raise_as(f"Operation failed due to an internal error.") from e
        return wrapper
    return decorator

and using it would look like this:

@property
@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._re_establish_use_watch()
  return self.socket

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a description

Comment thread pystalk/client.py Outdated
def __init__(self, message: Union[str, bytes]):
if isinstance(message, bytes):
message = message.decode('ascii')
object.__setattr__(self, 'message', message)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use

Suggested change
object.__setattr__(self, 'message', message)
self.message = message

Comment thread pystalk/client.py Outdated
# 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__

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get rid of this

Comment thread pystalk/client.py Outdated
try:
return func(self, *args, **kwargs)
except errors as e:
raise raise_as(self.host, self.port, e) from e

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need from e

Suggested change
raise raise_as(self.host, self.port, e) from e
raise raise_as(self.host, self.port, e)

Comment thread pystalk/client.py
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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not add this new function. You can use multiple decorators on a single function.

Comment thread pystalk/client.py Outdated
if isinstance(message, bytes):
message = message.decode('ascii')
object.__setattr__(self, 'message', message)
Exception.__init__(self, message)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use super here

Comment thread pystalk/client.py Outdated


@attr.s(frozen=True, hash=True, eq=True)
@attr.s(frozen=True, hash=True, eq=True, init=False)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this just complicates your life. Maybe we should consider getting rid of it entirely or at least getting rid of frozen=True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor, mostly cosmetic changes

Comment thread pystalk/client.py Outdated
if isinstance(message, bytes):
message = message.decode('ascii')
self.message = message
super().__init__(message)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is technically not required but I like the more verbose way of writing super()

Suggested change
super().__init__(message)
super(BeanstalkError, self).__init__(message)

Comment thread pystalk/client.py Outdated
self.port = port
self.err = err
msg = f'Failed to connect to beanstalkd at {host}:{port}: {err}'
super().__init__(msg)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Same thing here for super()
  • There's no reason to store msg in a separate variable (this is again a stylistic thing for me personally so it doesn't matter if you change it or not).
  • For strings, even though python allows for single or double quotes, its generally better to use double quotes because, historically (in bash), single-quoted strings do not allow for interpolation (variable expansion - i.e., literal strings) whereas double-quoted strings allow for interpolation and therefore using them allows for a easier reading of code for people not familiar with how python works.
Suggested change
super().__init__(msg)
super(BeanstalkConnectionError, self).__init__(f"Failed to connect to beanstalkd at {host}:{port}: {err}")

Comment thread pystalk/client.py Outdated


def catch_and_raise(*errors, raise_as=BeanstalkConnectionError):
"""Catches specified errors on an instance method and re-raises

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a stylistic thing but I'd recommend multiline strings be structured with the opening/closing triple quotes on separate lines like so

"""
Catches specified errors...
"""

Comment thread pystalk/client.py
@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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no change here except the spacing. Did this line change due to a linter? If not, I'd revert this line back to

Suggested change
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you will need to bump the patch version and push a new tag before the merge occurs. I'll defer to Justin Hammond (@Justintime50) on this because I believe the publishing instructions need to be updated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to bump the version in pystalk/__init__.py, the setup.py file, and then add an entry that matches describing your changes in docs/src/CHANGES.rst (I'd love to just move that to the root /CHANGES.rst, I have 0 idea why the indirection exists... but that's a separate issue to be handled later.

Once that's done, this is approved, and the code has been merged, you'll want to then cut a new release on GitHub and push to pypi. I am happy to assist with this, just let me know (it's highly possible we need to do some work around the releasing of this lib.) It would be advantageous for us to automate releasing via github actions like we do our python client library for customers. This should be done (probably first), separately from this PR.

Comment thread pystalk/__init__.py Outdated
Comment on lines 4 to 5
version_info = (0, 7, 0)
__version__ = '.'.join(str(s) for s in version_info)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you'll need to bump the version, please simplify this and bump the version (note that version_info isn't used elsewhere so this is a safe change.

Suggested change
version_info = (0, 7, 0)
__version__ = '.'.join(str(s) for s in version_info)
__version__ = '0.8.0'

- 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.
@Adonis-Diaz
Adonis-Diaz changed the base branch from master to EXP-1217 July 23, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants