sending_request
is not a great name for an event; similar to your other signals, it should describe something that happened i.e. request_started
.
Add parameter names and types to your signals where applicable. The failure signals should receive contextual information, and the valid response signal should receive the payload (or perhaps the Response
object).
Provide sane defaults for your __init__
parameters. For instance, get
is overwhelmingly the most common method.
Stop using Dict
. Update Python and use dict
.
Instead of keeping the broken-out request parameters url
, method
etc. on the class, just prepare a request. Don't allow mutation of these parameters. Delete every one of your accessor methods. If you need a different request, construct a different "manager".
Don't use exec
! And especially don't tie this into a non-reentrant global variable. And especially especially don't allow arbitrary code execution by directly concatenating the parameter strings.
Don't silently swallow exceptions. Re-throw them.
Delete your module_is_runned_directly
; it isn't helping.
Consider upgrading from PyQt5 to PyQt6.
You should be using a requests.Session
. Internally, calling any of the top-level methods like requests.get
spins up a single-use session, but you can do better. There should usually be one session per domain name (or, sometimes, set of related domain names within one organization). Separately, you need a session to be able to manually send prepared requests, as I demonstrate in the first code blob.
More broadly: this is in a cursed middle ground. It's a wrapper with some events that might seem useful - but only if the method is asynchronous, and it isn't asynchronous. If you want to keep it synchronous, then... just delete everything and call requests
directly. This is also over-broad boilerplate; it doesn't offer much beyond what requests already offers. It would be more useful if it spoke to the domain-specific HTTP operation, which you have not shown. (There's a decent chance that your code will never use a delete
method.) But in another sense - if you wanted to keep it generic, it isn't broad enough. The list of methods isn't exhaustive; for instance there's also OPTIONS.
Since you're already drinking the Qt cool-aid, consider replacing requests with QNetworkRequest. It has plenty of signals.
Example (requests)
Covering a subset of the above,
from typing import Literal, Any import requests from PyQt5.QtCore import QObject, pyqtSignal, pyqtBoundSignal class NyvoNetHunterRequestManager(QObject): __slots__ = 'request', request_started: pyqtBoundSignal = pyqtSignal() request_sent: pyqtBoundSignal = pyqtSignal() failed_to_send: pyqtBoundSignal = pyqtSignal(str, arguments=('reason',)) received_valid_response: pyqtBoundSignal = pyqtSignal(str, arguments=('content',)) received_invalid_response: pyqtBoundSignal = pyqtSignal(int, str, arguments=('code', 'reason')) def __init__( self, url: str, method: Literal['get', 'post', 'put', 'delete'] = 'get', headers: dict[str, str] | None = None, data: dict[str, Any] | None = None, ) -> None: self.request = requests.Request( method=method, url=url, headers=headers, data=data, ).prepare() super().__init__() def send(self, session: requests.Session) -> str: self.request_started.emit() try: with session.send(request=self.request, stream=True) as response: # Due to stream mode, will be sent before the full body is retrieved self.request_sent.emit() if response.ok: self.received_valid_response.emit(response.text) return response.text self.received_invalid_response.emit(response.status_code, response.reason) response.raise_for_status() except requests.HTTPError: raise except IOError as e: self.failed_to_send.emit(str(e)) raise def demo() -> None: def request_started() -> None: print('Request started.') def request_sent() -> None: print('Request sent.') def failed_to_send(reason: str) -> None: print('Failed to send:', reason) def received_valid_response(response: str) -> None: print('Received valid response:', response[:80], '...') def received_invalid_response(code: int, reason: str) -> None: print('Received invalid response:', code, reason) with requests.Session() as session: manager = NyvoNetHunterRequestManager(url= 'https://google.com' # Valid # 'https://invaliddomainname.wrong', # IOError # 'https://google.com/invalidurlpath', # 404 ) manager.request_started.connect(request_started) manager.request_sent.connect(request_sent) manager.failed_to_send.connect(failed_to_send) manager.received_valid_response.connect(received_valid_response) manager.received_invalid_response.connect(received_invalid_response) manager.send(session) if __name__ == '__main__': demo()
A word on eval
Don't exec
, don't eval
. Forget that those ever existed until about 15 years into your software development career. If there were a gun to your head and you were forced to use eval
for this application (which, again, you absolutely shouldn't), a safer way would look like
import requests def request_eval(method: str, url: str) -> str: if not isinstance(method, str): raise TypeError('method must be str') if method.lower() not in { 'get', 'put', 'post', 'delete', 'head', 'options', 'connect', 'trace', 'patch', }: raise ValueError(f'Invalid method "{method}"') code = compile( source='requests.' + method + '(url=url)', filename='dynamic_requests_call', mode='eval', dont_inherit=1, ) empty_globals = {'__builtins__': {}} req_locals = { 'requests': requests, 'url': url, # ...other request parameters } response = eval(code, empty_globals, req_locals) return response.text print( request_eval( method='get', url='https://google.com', )[:80], '...', )
Observe the differences. We don't allow use of any built-ins or globals, we pass in locals via variable key-value pair and not string formatting, and we ban compiler flag inheritance.
Dynamic import
For completeness, the following is a half-measure that you also shouldn't do, but is still a better idea than eval
. It imports the requests library method based on name.
import requests def request_attr(method: str, url: str) -> str: if not isinstance(method, str): raise TypeError('method must be str') if method.lower() not in { 'get', 'put', 'post', 'delete', 'head', 'options', 'connect', 'trace', 'patch', }: raise ValueError(f'Invalid method "{method}"') fun = getattr(requests, method) response = fun(url=url) return response.text print( request_attr( method='get', url='https://google.com', )[:80], '...', )
exec
. It's practically never the right answer.\$\endgroup\$self.url
,self.data
, orself.headers
contain a'
? What if one of them is equal to'); import os; os.system('malware.exe'); ('
?\$\endgroup\$