-
-
Notifications
You must be signed in to change notification settings - Fork 219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CLI: Re-add command line interface (2024) #549
base: main
Are you sure you want to change the base?
Conversation
Warning Rate limit exceeded@amotl has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 7 minutes and 53 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughThe changes in this pull request involve updates to the Responder framework's package configuration, installation instructions, and documentation. Key modifications include the addition of a console script entry point in Changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
setup.py
Outdated
@@ -20,7 +20,9 @@ | |||
required = [ | |||
"aiofiles", | |||
"apispec>=1.0.0b1", | |||
"asgiref", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's asgiref
for in this context? Can it be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does anyone know why this would be needed?
2b58380
to
5be3422
Compare
6b69a75
to
703720b
Compare
f724117
to
e61876a
Compare
59da080
to
56f0b34
Compare
cd3f238
to
c8d3c36
Compare
Hi again. We had to resolve some woes about Windows support and flakyness, mostly revolving around software testing. Now that CI signalled success a few times in a row, we guess the patch is finally ready now. |
tests/test_cli.py
Outdated
def test_cli_run(capfd): | ||
""" | ||
Verify that `responder run` works as expected. | ||
""" | ||
|
||
# Sanity checks. | ||
target = Path("examples") / "helloworld.py" | ||
if not target.is_file(): | ||
raise FileNotFoundError(f"Example file not found: {target}") | ||
|
||
# Invoke `responder run`. | ||
# Start a Responder service instance in the background, using its CLI. | ||
|
||
# Make it terminate itself after serving one HTTP request. | ||
server = ResponderServer(target=str(target), port=random_port(), limit_max_requests=1) | ||
try: | ||
# Start server and wait until it responds on TCP. | ||
server.start() | ||
wait_server_tcp(server.port) | ||
|
||
# Submit a single probing HTTP request that also will terminate the server. | ||
response = requests.get( | ||
f"http://127.0.0.1:{server.port}{HELLO_ENDPOINT}", timeout=REQUEST_TIMEOUT | ||
) | ||
assert "hello, world!" == response.text | ||
finally: | ||
server.join(timeout=SERVER_TIMEOUT) | ||
|
||
# Capture process output. | ||
time.sleep(SERVER_IDLE_WAIT) | ||
output = capfd.readouterr() | ||
|
||
stdout = output.out.strip() | ||
assert f'"GET {HELLO_ENDPOINT} HTTP/1.1" 200 OK' in stdout | ||
|
||
stderr = output.err.strip() | ||
|
||
# Define expected lifecycle messages in order. | ||
lifecycle_messages = [ | ||
# Startup phase | ||
"Started server process", | ||
"Waiting for application startup", | ||
"Application startup complete", | ||
"Uvicorn running", | ||
# Shutdown phase | ||
"Shutting down", | ||
"Waiting for application shutdown", | ||
"Application shutdown complete", | ||
"Finished server process", | ||
] | ||
|
||
# Verify messages appear in expected order. | ||
last_pos = -1 | ||
for msg in lifecycle_messages: | ||
pos = stderr.find(msg) | ||
assert pos > last_pos, f"Expected '{msg}' to appear after previous message" | ||
last_pos = pos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section might resemble the first actual integration test for Responder, including its CLI interface, end-to-end?
The `debug` argument no longer exists. Let's adjust the `log_level` to `debug` instead.
Install: pip install 'responder[cli]' The CLI is an optional subsystem from now on.
Also, refactor to `responder.ext.cli`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.
Actionable comments posted: 3
🧹 Outside diff range and nitpick comments (4)
tests/util.py (2)
59-101
: Consider adding debug logging for error cases.While the error handling is robust, adding debug logs for specific error cases would aid in troubleshooting.
Consider this enhancement:
if error_number in transient_socket_error_numbers(): + logger.debug(f"Ignoring transient error {error_number} while connecting to {endpoint}") pass # Unexpected error. else: + logger.debug(f"Encountered unexpected error {error_number} while connecting to {endpoint}") raise RuntimeError( f"Unexpected error while connecting to {endpoint}: {error_number}" )
103-135
: Consider adding debug logging for consistency with wait_server_tcp.For consistency with
wait_server_tcp
, consider adding similar debug logging.Consider this enhancement:
url = f"{protocol}://{host}:{port}/" + logger.debug(f"Waiting for endpoint: {url}") for attempt in range(1, attempts + 1): try: requests.get(url, timeout=delay / 2) # Shorter timeout for connection break except requests.exceptions.RequestException: + logger.debug(f"Connection attempt {attempt}/{attempts} failed for {url}") if attempt < attempts: # Don't sleep on last attempt time.sleep(delay)responder/ext/cli.py (1)
33-46
: Consider reorganizing imports for better readability.The imports could be better organized following the common pattern: standard library, third-party packages, and local imports, each group separated by a blank line.
-import logging -import platform -import subprocess -import sys -import typing as t -from pathlib import Path - -import docopt - -from responder.__version__ import __version__ -from responder.util.python import InvalidTarget, load_target +import logging +import platform +import subprocess +import sys +import typing as t +from pathlib import Path + +import docopt + +from responder.__version__ import __version__ +from responder.util.python import InvalidTarget, load_targetresponder/util/cmd.py (1)
170-198
: Consider closing socket explicitly in wait_until_ready.The socket connection test uses a context manager, but it's good practice to explicitly close the socket to ensure immediate resource release, especially in a loop.
- try: - with socket.create_connection( - ("localhost", self.port), timeout=request_timeout - ): - return True + sock = None + try: + sock = socket.create_connection( + ("localhost", self.port), timeout=request_timeout + ) + return True except ( socket.timeout, ConnectionRefusedError, socket.gaierror, OSError, ) as ex: logger.debug(f"Server not ready yet: {ex}") time.sleep(delay) + finally: + if sock: + sock.close()
🛑 Comments failed to post (3)
tests/util.py (1)
21-34: 🛠️ Refactor suggestion
Consider enhancing error handling for socket operations.
While the implementation is solid, it could benefit from more specific error handling.
Consider this enhancement:
def random_port() -> int: sock = socket.socket() try: sock.bind(("", 0)) return sock.getsockname()[1] + except socket.error as e: + raise RuntimeError(f"Failed to allocate random port: {e}") from e finally: sock.close()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.def random_port() -> int: """ Return a random available port by binding to port 0. Returns: int: An available port number that can be used for testing. """ sock = socket.socket() try: sock.bind(("", 0)) return sock.getsockname()[1] except socket.error as e: raise RuntimeError(f"Failed to allocate random port: {e}") from e finally: sock.close()
responder/ext/cli.py (1)
106-111: 🛠️ Refactor suggestion
Consider using a custom exception for CLI errors.
Instead of raising a generic
ValueError
, consider creating a custom exception likeCLIError
that better represents the domain-specific error condition. This would make it easier to handle CLI-specific errors differently from other value errors.+class CLIError(Exception): + """Raised when CLI encounters an error condition.""" + pass - raise ValueError( + raise CLIError( f"{ex}. " "Use either a Python module entrypoint specification, " "a filesystem path, or a remote URL. " "See also https://responder.kennethreitz.org/cli.html." ) from exCommittable suggestion was skipped due to low confidence.
responder/util/cmd.py (1)
25-40: 🛠️ Refactor suggestion
Consider caching the program path.
The
path()
method searches PATH on every call. Consider caching the result to improve performance.class ResponderProgram: + _cached_path = None + @staticmethod def path(): + if ResponderProgram._cached_path is not None: + return ResponderProgram._cached_path + name = "responder" if sys.platform == "win32": name = "responder.exe" program = shutil.which(name) if program is None: paths = os.environ.get("PATH", "").split(os.pathsep) raise RuntimeError( f"Could not find '{name}' executable in PATH. " f"Please ensure Responder is installed correctly. " f"Searched in: {', '.join(paths)}" ) logger.debug(f"Found responder program: {program}") + ResponderProgram._cached_path = program return programCommittable suggestion was skipped due to low confidence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments from a self-review.
- name: Install package and run software tests (Python 3.6) | ||
if: matrix.python-version == '3.6' | ||
run: | | ||
pip install '.[graphql,develop,test]' | ||
pip install '.[cli,graphql,develop,test]' | ||
poe test | ||
|
||
- name: Install and completely validate package (Python >=3.6) | ||
if: matrix.python-version != '3.6' | ||
run: | | ||
uv pip install '.[graphql,develop,test]' --system | ||
uv pip install '.[cli,graphql,develop,test]' --system | ||
poe check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be using the full
extra now.
This module exports the core functionality of the Responder framework, | ||
including the API, Request, Response classes and CLI interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer true?
This module exports the core functionality of the Responder framework, | |
including the API, Request, Response classes and CLI interface. | |
This module exports the core functionality of the Responder framework, | |
including the API, Request, and Response classes. |
Commands: | ||
run Start the application server | ||
build Build frontend assets using npm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kennethreitz: Now after learning it, I think the second subcommand responder build
is debatable. Did you keep and/or evolve it within Dyne, @tabotkevin?
- responder run: Server execution | ||
|
||
Requirements: | ||
- The `docopt` package must be installed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Time flies.
- The `docopt` package must be installed | |
- The `docopt-ng` package must be installed |
About
The CLI interface got lost. This patch intends to bring it back.
After refactoring, the CLI module is living at
responder.ext.cli
now.Setup
From now on, the CLI interface is optional, and its
docopt-ng
dependency needs to be installed explicitly.pip install --upgrade 'responder[cli]'
Usage
The feature will also sport a dedicated documentation page now, see preview at Responder CLI.
Software Tests
Previously, the CLI module apparently had no software tests. Now, the two major subcommand operations
build
vs.run
are covered, mostly on their happy paths.